Animation
- 在Flutter中,实现动画的核心类是
Animation
,Widget可以直接将这些动画合并到自己的build方法中来读取它们当前值或监听它们的状态变化;
-
Animation
是一个抽象类,常见的方法如下:
-
addListener
方法
- 每当动画的状态值发生变化时,动画都会通知所有通过 addListener 添加的监听器。
- 通常,一个正在监听动画的state对象会调用自身的setState方法,将自身传入这些监听器的回调函数来通知 widget 系统需要根据新状态值进行重新构建。
-
addStatusListener
方法
- 当动画的状态发生变化时,会通知所有通过 addStatusListener 添加的监听器。
- 通常情况下,动画会从
dismissed
状态开始,表示它处于变化区间的开始点。
- 举例来说,从 0.0 到 1.0 的动画在
dismissed
状态时的值应该是 0.0。
- 动画进行的下一状态可能是
forward
(比如从 0.0 到 1.0)或者 reverse
(比如从 1.0 到 0.0)。
- 最终,如果动画到达其区间的结束点(比如 1.0),则动画会变成
completed
状态。
AnimationController
- AnimationController是Animation的一个子类,通常我们会创建一个 AnimationController实例对象来实现动画效果;
- AnimationController会生成一系列的值,默认情况下值是0.0到1.0区间的值;
- AnimationController提供了对
动画的控制
:
- forward:向前执行动画
- reverse:方向播放动画
- stop:停止动画
- 其构造函数如下:
AnimationController({
double value,
this.duration,
this.reverseDuration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
})
- value:初始化值;
- duration:动画执行的时间;
- reverseDuration:反向动画执行的时间;
- lowerBound:最小值;
- upperBound:最大值;
- vsync:垂直信号,必传参数;
CurvedAnimation
Tween
- 默认情况下,AnimationController动画生成的值所在区间是0.0到1.0,如果需要使用这个以外的值,或者其他的数据类型,就需要使用Tween;
- Tween构造函数如下:
Tween({ this.begin, this.end })
- 传参味初始值与结束值,可确定一段区间;
- Tween也有一些子类,比如ColorTween、BorderTween,可以针对动画或者边框来设置动画的值;
案例代码:
- 需求:目标组件心形图标,由小到大,由大到小 无限循环 ,点击可暂停动画,再次点击在原来的状态基础上继续执行动画;
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatefulWidget {
@override
_SFHomePageState createState() => _SFHomePageState();
}
class _SFHomePageState extends State<SFHomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation;
Animation _animationSize;
@override
void initState() {
// TODO: implement initState
super.initState();
//1.创建Animation Controller
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
lowerBound: 0.0,
upperBound: 1.0
);
//2.设置Curve值 动画的执行速率
_animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
//3.Tween
_animationSize = Tween(begin: 50.0,end: 150.0).animate(_animation);
//3.监听动画值的改变 刷新界面
_controller.addListener(() {
setState(() {
});
});
//4.监听动画状态的改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
}else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: Center(
child: Icon(Icons.favorite,color: Colors.red,size: _animationSize.value),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else if (_controller.status == AnimationStatus.forward) {
_controller.forward();
} else if (_controller.status == AnimationStatus.reverse) {
_controller.reverse();
} else {
_controller.forward();
}
},
),
);
}
@override
void dispose() {
//销毁 回收内存
_controller.dispose();
super.dispose();
}
}
AnimatedWidget
- 上述实现的动画效果,存在一个弊端:
- 监听动画值的改变,然后调用setState方法,就会导致State类build的方法重新执行,里面包含的所有组件都会重新构建,这样效率十分低下;
- 可通过
AnimatedWidget
来解决这个弊端,执行动画时,仅仅让心形组件重建即可,自定义组件SFAnimationIcon
继承自AnimatedWidget
, 实现如下:
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatefulWidget {
@override
_SFHomePageState createState() => _SFHomePageState();
}
class _SFHomePageState extends State<SFHomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation;
Animation _animationSize;
@override
void initState() {
// TODO: implement initState
super.initState();
//1.创建Animation Controller
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
lowerBound: 0.0,
upperBound: 1.0
);
//2.设置Curve值 动画的执行速率
_animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
//3.Tween
_animationSize = Tween(begin: 50.0,end: 150.0).animate(_animation);
//3.监听动画值的改变 刷新界面
// _controller.addListener(() {
// setState(() {
//
// });
// });
//4.监听动画状态的改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
}else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
print("_SFHomePageState build");
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: Center(
child: SFAnimationIcon(_animationSize),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else if (_controller.status == AnimationStatus.forward) {
_controller.forward();
} else if (_controller.status == AnimationStatus.reverse) {
_controller.reverse();
} else {
_controller.forward();
}
},
),
);
}
@override
void dispose() {
//销毁 回收内存
_controller.dispose();
super.dispose();
}
}
class SFAnimationIcon extends AnimatedWidget {
final Animation _animationSize;
SFAnimationIcon(this._animationSize): super(listenable: _animationSize);
@override
Widget build(BuildContext context) {
return Icon(Icons.favorite,color: Colors.red,size: _animationSize.value);
}
}
AnimatedBuilder
-
AnimatedWidget
解决了上述动画的执行效率问题,但其也存在弊端:
- 我们每次都要新建一个类来继承自AnimatedWidget;
- 如果我们的动画Widget有子Widget,那么意味着它的子Widget也会重新build;
- 可通过
AnimatedBuilder
来解决上面的两个问题,代码如下:
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatefulWidget {
@override
_SFHomePageState createState() => _SFHomePageState();
}
class _SFHomePageState extends State<SFHomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation;
Animation _animationSize;
@override
void initState() {
// TODO: implement initState
super.initState();
//1.创建Animation Controller
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
lowerBound: 0.0,
upperBound: 1.0
);
//2.设置Curve值 动画的执行速率
_animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
//3.Tween
_animationSize = Tween(begin: 50.0,end: 150.0).animate(_animation);
//3.监听动画值的改变 刷新界面
// _controller.addListener(() {
// setState(() {
//
// });
// });
//4.监听动画状态的改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
}else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
print("_SFHomePageState build");
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (ctx,child) {
return Icon(Icons.favorite,color: Colors.red,size: _animationSize.value);
},
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else if (_controller.status == AnimationStatus.forward) {
_controller.forward();
} else if (_controller.status == AnimationStatus.reverse) {
_controller.reverse();
} else {
_controller.forward();
}
},
),
);
}
@override
void dispose() {
//销毁 回收内存
_controller.dispose();
super.dispose();
}
}
交织动画
- 动画集合了透明度变化、大小变化、颜色变化、旋转动画等;
- 通过多个Tween生成了多个Animation对象;
- 代码如下所示:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatefulWidget {
@override
_SFHomePageState createState() => _SFHomePageState();
}
class _SFHomePageState extends State<SFHomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation;
Animation _animationSize;
Animation _animationColor;
Animation _animationOpacity;
Animation _animationRotation;
@override
void initState() {
// TODO: implement initState
super.initState();
//1.创建Animation Controller
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 2),
lowerBound: 0.0,
upperBound: 1.0
);
//2.设置Curve值 动画的执行速率
_animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
//3.Tween
_animationSize = Tween(begin: 10.0,end: 200.0).animate(_controller);
_animationColor = ColorTween(begin: Colors.orange,end: Colors.pink).animate(_controller);
_animationOpacity = Tween(begin: 0.0,end: 1.0).animate(_controller);
_animationRotation = Tween(begin: 0.0,end: 2 * pi).animate(_controller);
//4.监听动画状态的改变
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
}else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
print("_SFHomePageState build");
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (ctx,child) {
return Opacity(
opacity: _animationOpacity.value,
child: Transform(
transform: Matrix4.rotationZ(_animationRotation.value),
alignment: Alignment.center,
child: Container(
width: _animationSize.value,
height: _animationSize.value,
color: _animationColor.value,
),
),
);
},
)
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () {
if (_controller.isAnimating) {
_controller.stop();
} else if (_controller.status == AnimationStatus.forward) {
_controller.forward();
} else if (_controller.status == AnimationStatus.reverse) {
_controller.reverse();
} else {
_controller.forward();
}
},
),
);
}
@override
void dispose() {
//销毁 回收内存
_controller.dispose();
super.dispose();
}
}
转场动画
- 界面切换时的动画效果;
- 新建一个界面
ModalPage
,代码如下:
import 'package:flutter/material.dart';
class ModalPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.red,
appBar: AppBar(
title: Text("Modal Page"),
),
body: Center(
child: Text("Modal Page"),
),
);
}
}
import 'package:Fluter01/day01/pages/ModalPage.dart';
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: Center(
child: Text("Hello world!!"),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.pool),
onPressed: () {
//iOS Modal界面切换
// Navigator.of(context).push(MaterialPageRoute(
// builder: (ctx) {
// return ModalPage();
// },
// fullscreenDialog: true
// ));
Navigator.of(context).push(PageRouteBuilder(
transitionDuration: Duration(seconds: 3),
pageBuilder: (ctx,animation1,animation2) {
return FadeTransition(
opacity: animation1,
child: ModalPage()
);
}
));
},
),
);
}
}
- 通过
PageRouteBuilder
实现转场动画效果;
Hero动画
- 需求:图片列表展示图片,当点击一张图片时,切换到图片详情页面,显示点击的图片,切换有动画效果;
- 图片详情页面,代码如下:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class ImageDetailPage extends StatelessWidget {
final String imageUrl;
ImageDetailPage(this.imageUrl);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Hero(
tag: imageUrl,
child: Image.network(imageUrl)
)
),
),
);
}
}
import 'package:Fluter01/day01/pages/ImageDetailPage.dart';
import 'package:Fluter01/day01/pages/ModalPage.dart';
import 'package:flutter/material.dart';
void main() => runApp(SFMyApp());
class SFMyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: SFHomePage());
}
}
class SFHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("基础widget")),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 16/9
),
children: List.generate(20, (index) {
final String imageUrl = "https://picsum/photos/500/500?random=$index";
return GestureDetector(
onTap: () {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (ctx,animation1,animation2) {
return FadeTransition(opacity: animation1, child: ImageDetailPage(imageUrl));
}
));
},
child: Hero(
tag: imageUrl,
child: Image.network(
"https://picsum/photos/500/500?random=$index",
fit: BoxFit.cover,
),
),
);
}),
),
);
}
}