Flutter动画详解
效果如下:
分享是每个优秀的程序员所必备的品质
一、基本概念
Animation
抽象类。本身与UI渲染没有任何关系,主要的功能就是保存动画的插值和状态。
可以通过它来监听动画的每一帧并执行状态的变化
addListener()
:为Animation添加“动画绘制”监听器,每一帧都会被调用。常见的行为就是改变状态后调用setState()来触发UI重绘。
addStatesListener()
:为Animation添加“动画状态改变”监听器,如开始、结束等。
注意: 动画期间会不停调用动画widget所在的build方法。
Curve
动画的过程可以是匀速、匀加速、先加速后减速等,Flutter中通过Curve
(曲线)来描述动画过程,将匀速动画称为线性的(Curves.linear
),将非匀速动画称为非线性动画。
一般通过CurveAnimation来指定动画的曲线
// linear:匀速的
// decelerate:匀减速
// ease:开始加速,后面减速
// easeIn:开始慢,后面快
// easeOut:开始快,后面慢
// easeInOut:开始慢,然后加速,最后再减速
final CurveAnimation curve = CurveAnimation(parent: controller, curve: Curve.easeIn);
也可以自定义Curve,例如自定义一个正弦曲线
// 正弦曲线
class ShakeCurve extends Curve{
@override
double transform(double t) {
return sin( t * pi * 2);
}
}
AnimationController
用于控制动画,Animation的子类,包含了
forward()
:正向启动(动画正在从开始处运行到结束处)
reverse()
:反向启动
stop()
:停止
dismissed()
:动画停止在开始处
completed()
:动画结束(停止在结束处)
AnimationController会在动画的每一帧生成一个新的值,默认情况下,会在给定的时间段内,线性生成从0.0-1.0(默认区间)的数字。代码如下:
AnimationController controller = AnimationController(vsync: this,duration: Duration(seconds: 1));
其生成的数字区间可以通过lowerBound和lowerBound来指定,代码如下:
AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
lowerBound: 10.0,
upperBound: 20.0
);
Ticker
从上面我们注意到,当创建一个AnimationController时,需要传递一个vsync参数,它接受一个TickerProvider的对象,主要职责是创建Ticker,代码如下:
abstract class TickerProvider{
// 通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter应用在每次启动时都会绑定一个SchedulerBinding,SchedulerBinding可以为每一次屏幕添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调的,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不再当前屏幕时,如锁屏)消耗不必要的资源,因为在Flutter中,屏幕刷新会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏屏幕hi停止刷新,所以Ticker将不会再触发。
通常我们会将SingleTickerProviderStateMixin
添加到State中然后将State对象作为vsync的值(this)。
Tween
补间动画,默认下AnimationController对象值是[0.0,1.0],可以使用Tween来添加映射以生成不同的范围或是不同的数据类型(double、Color、EdgeInsets...)
代码如下
Tween(begin: 100.0,end: 300.0)
注意:Tween继承自Animatable<T>
,而不是Animation<T>
,Animatable主要定义动画值的映射规则。
Tween对象不存储任何状态,提供了evaluate <Animation<double> animation>
方法来获取动画当前的值(animation.value())
Tween仅仅是映射,动画的控制依然由 AnimationController 控制,因此需要 Tween.animate(controller)
将控制器传递给Tween。
如下代码:在1s时间内生成从0到255 的int值
AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
// 注意:animation()返回的是一个Animation,而不是Animatable
Animation<int> alpha = IntTween(begin: 0,end: 255).animate(controller);
二、示例
下面结合一些示例来深入了解下Flutter动画
1、透明度动画
透明度从1.0变为0.0,代码如下:
//需要混入 SingleTickerProviderStateMixin
class Demo1 extends StatefulWidget {
@override
_Demo1State createState() => _Demo1State();
}
class _Demo1State extends State<Demo1> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation ;
@override
void initState() {
super.initState();
// AnimationController继承于Animation,可以调用addListener
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))..addListener(() {
setState(() {});
});
// Interval : begin 参数 代表 延迟多长时间开始 动画 end 参数 代表 超过多少 直接就是 100% 即直接到动画终点
_animation = Tween(begin: 1.0,end: 0.1).animate(CurvedAnimation(parent: _controller,curve: Interval(0.0,0.5,curve: Curves.linear)));
// _animation有不同的构建方式
// _animation = Tween(begin: 1.0,end: 0.2).chain(CurveTween(curve: Curves.easeIn)).animate(_controller);
// _animation = _controller.drive(Tween(begin: 1.0,end: 0.1)).drive(CurveTween(curve: Curves.linearToEaseOut));
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => _controller.forward(),
child: Opacity(
opacity: _animation.value,
child: Container(
width: 100,height: 100, color: Colors.greenAccent,
child: Center(child: Text("Demo1"),),
),
),
);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
}
2、颜色变化 Color.lerp 两种颜色之间线性插值
红 —> 绿
class Demo2 extends StatefulWidget {
@override
_Demo2State createState() => _Demo2State();
}
class _Demo2State extends State<Demo2> with SingleTickerProviderStateMixin{
AnimationController _controller;
Color _color = Colors.red;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
..addListener(() {
setState(() {
_color = Color.lerp(Colors.red, Colors.green, _controller.value);
});
});
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
height: 100,width: 100,
color: _color,
child: Center(child: Text("Demo2"),),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
3、widget大小变化
class Demo3 extends StatefulWidget {
@override
_Demo3State createState() => _Demo3State();
}
class _Demo3State extends State<Demo3> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1),lowerBound: 100,upperBound: 200)
..addListener(() {
setState(() {
_size = _controller.value;
});
})..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
width: _size,height: _size,
color: Colors.redAccent,
child: Center(child: Text("Demo3"),),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
其实系统提供了大量的Tween动画,大大方便了的使用,常用到的数据类型一般都提供了,需要的时候不妨先敲敲看。

例如常见的位移动画使用
EdgeInsetsTween
,颜色变化可以使用ColorTween来实现(查看源码,其本质上也是使用 Color.lerp 实现的)使用ColorTween代码如下:动画效果同Demo3相同。
class ColorTweenDemo extends StatefulWidget {
@override
_ColorTweenDemoState createState() => _ColorTweenDemoState();
}
class _ColorTweenDemoState extends State<ColorTweenDemo> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation <Color> _animation;
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))
..addListener(() {
setState(() { });
});
_animation = ColorTween(begin: Colors.red,end: Colors.green).animate(_controller);
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: (){
_controller.forward();
},
child: Container(
height: 100,width: 100,
color: _animation.value,
),
);
}
}

4、多种动画组合
混入TickerProviderStateMixin
,
大小变化 + 圆角变化 + 颜色变化
class Demo4 extends StatefulWidget {
@override
_Demo4State createState() => _Demo4State();
}
class _Demo4State extends State<Demo4> with TickerProviderStateMixin{
// 可以每个动画都创建各自的AnimationController来控制和监听不同的状态
AnimationController _controller;
Animation <double> _sizeAnimation;
Animation <BorderRadius> _radiusAnimation;
Animation <Color> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 1))..addListener(() {
setState(() {});
})..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_sizeAnimation = Tween(begin: 100.0,end: 200.0).chain(CurveTween(curve: Curves.linear)).animate(_controller);
_radiusAnimation = BorderRadiusTween(begin: BorderRadius.zero,end: BorderRadius.circular(100)).animate(_controller);
_colorAnimation = ColorTween(begin: Colors.redAccent,end: Colors.yellowAccent).chain(CurveTween(curve: Curves.linear)).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Center(
child: InkWell(
onTap: (){
_controller.forward();
},
child: Container(
width: _sizeAnimation.value,height: _sizeAnimation.value,
decoration: BoxDecoration(
border: Border.all(width: 5,color: Colors.greenAccent),
color: _colorAnimation.value,
borderRadius: _radiusAnimation.value
),
child: Center(child: Text("Demo4"),),
),
)
)
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
效果图:

5、AnimatedWidget 之 位移
现在我们可以发现:
- 每次通过addListener()和setState()来更新UI,代码显得比较冗余;
- setState()意味着整个 State 类中的 build 方法就会被重新执行。
为了解决上面的问题,我们可以使用 AnimatedWidget
将需要动画的Widget分离出来。 ``AnimatedWidget`封装调用了setState()的细节
以位移动画为例:
class Demo5 extends StatefulWidget {
@override
_Demo5State createState() => _Demo5State();
}
class _Demo5State extends State<Demo5> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation <EdgeInsets>_animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2))..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
_animation = _controller.drive(EdgeInsetsTween(begin: EdgeInsets.fromLTRB(50, 50, 0, 0),end: EdgeInsets.only(top: 200,left: 200)));
}
@override
Widget build(BuildContext context) {
Future.delayed(Duration(seconds: 1),(){
_controller.forward();
});
return EdgeInsetsDemo(animation: _animation,);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
// 使用AnimatedWidget分离
class EdgeInsetsDemo extends AnimatedWidget{
EdgeInsetsDemo({Key key,Animation<EdgeInsets> animation}):super(key:key,listenable :animation);
@override
Widget build(BuildContext context) {
final Animation<EdgeInsets> animation = listenable;
return Container(
margin: animation.value,
width: 100,
height: 100,
color: Colors.redAccent,
);
}
}
效果如下:

6、AnimatedBuilder 之 矩阵变化
使用AnimatedWidget
可以从动画分离出Widget,但是动画的渲染过程仍在AnimatedWidget中,如果我们在添加一个Widget动画,需要再实现一个AnimatedWidget,然而这很不优雅,而AnimatedBuilder
可以将渲染逻辑分离出来。
示例使用AnimatedBuilder让图片绕X轴旋转。
代码如下:
class Demo6 extends StatefulWidget {
@override
_Demo6State createState() => _Demo6State();
}
class _Demo6State extends State<Demo6> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation <Matrix4> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2))..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
})..addListener(() {
setState(() {
});
});
// 矩阵变化,应用很广泛,例如时钟或电子书翻页动画等
_animation = Matrix4Tween(begin: Matrix4.identity()..rotateX(0.0),end: Matrix4.identity()..rotateX(pi)).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Center(
child: InkWell(
onTap: (){
_controller.forward();
},
child: AnimatedBuilderDemo(animation: _animation,)
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
// 使用AnimatedBuilder重构
class AnimatedBuilderDemo extends AnimatedWidget{
AnimatedBuilderDemo({Key key,Animation<Matrix4> animation}):super(key :key,listenable:animation);
// 看起来child被指定了两次,但实际上是将外部child传递给AnimationBuilder后AnimationBuilder再将其传递给匿名构造器,
// 然后将该对象用作其子对象,最终会将AnimationBuilder返回的对象插入到Widget树中。
@override
Widget build(BuildContext context) {
final Animation animation = listenable;
return AnimatedBuilder(
animation: animation,
child: Image.asset("assets/g.jpg",),
builder: (BuildContext context,Widget child){
return Container(
width: 120,height: 120,
transform: animation.value,
child: child,
);
},
);
}
}
效果如下:

好处:
- 与AnimatedWidget一样,不用显示调用setSate();
- 动画构建范围缩小了,setSate()会在父组件中调用,会导致父组件的build重新调用,意味着它的子 Widget 也会重新 build 。而有了builder,只会让动画的widget自身的build重新调用,避免不必要的rebuild。
- AnimatedBuilder可以封装常见的过渡效果来复用动画,如FadeTransition、ScaleTransition、Sizetransitin等,很多时候这些预置的过渡类都可以复用。
7、AnimatedContainer :动画过渡组件
前面介绍的动画中,使用者要自己提供一个且需手动管理的AnimationController
,大大增加了使用的复杂性,Flutter SDK中预置了很多动画过渡组件。
AnimatedContainer:当Container属性发生变化时候会执行过渡动画到新的状态
AnimatedPadding:在Padding发生变化会执行过渡动画到新的状态
AnimatedPositioned:配合Stack使用,当定位发生变化时,会执行过渡动画到新的状态
AnimatedOpacity:当透明度发生变化时,会执行过渡动画到新的状态
AnimatedAlign:当alignment发生变化时,会执行过渡动画到新的状态
AnimatedDefaultTextStyle:当字体样式发生变化时,会执行过渡动画到新的状态
。。。
代码如下:
class Demo7 extends StatefulWidget {
@override
_Demo7State createState() => _Demo7State();
}
class _Demo7State extends State<Demo7> {
Duration _duration = Duration(seconds: 1);
bool _animation = false;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Spacer(flex: 2,),
AnimatedContainer(
curve: Curves.linear,
duration: _duration,
width: _animation ? 200 : 100,height: _animation ? 200 : 100,
decoration: BoxDecoration(
borderRadius: _animation ? BorderRadius.circular(100) : BorderRadius.zero,
color: _animation ? Colors.yellowAccent : Colors.redAccent,
border: Border.all(width: 5,color: _animation ? Colors.greenAccent : Colors.black87)
),
onEnd: (){ // 动画结束的回调
setState(() => _animation = !_animation );
},
child: InkWell(
onTap: (){
setState(() => _animation = !_animation );
},
),
),
Spacer(),
AnimatedContainer(
alignment: Alignment.center,
duration: _duration,
width: _animation ? 200 : 150,height: _animation ? 100 : 80,
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: _animation == false ? BorderRadius.circular(40) : BorderRadius.zero,
),
child: AnimatedDefaultTextStyle(
duration: _duration,
child: Text("Hello world"),
style: _animation ? TextStyle(color: Colors.black87,fontSize: 12,fontWeight: FontWeight.w100) : TextStyle(color: Colors.redAccent,fontSize: 20,fontWeight: FontWeight.w800),
),
),
Spacer(),
// AnimatedCrossFade 2个组件在切换时出现交叉渐入的效果,需要设置动画前、动画后2个子控件即可。
AnimatedCrossFade(
duration: _duration,
crossFadeState: _animation ? CrossFadeState.showSecond : CrossFadeState.showFirst,
firstChild: Container(
alignment: Alignment.center,
width: 100,height: 120,
color: Colors.redAccent,
child: Text("第一个",style: TextStyle(color: Colors.greenAccent),),
),
secondChild: Container(
alignment: Alignment.center,
width: 100,height: 100,
decoration: BoxDecoration(
color: Colors.greenAccent,
borderRadius: BorderRadius.circular(50)
),
child: Text("第二个",style: TextStyle(color: Colors.redAccent)),
),
),
Spacer(flex: 2,)
],
),
);
}
}
效果如下:

再如Demo1中的透明度变化的动画代码可以简化成如下代码:
// 效果同上面的Demo1效果相同
class Demo7_1 extends StatefulWidget {
@override
_Demo7_1State createState() => _Demo7_1State();
}
class _Demo7_1State extends State<Demo7_1> {
bool _animation = false;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedOpacity( // 透明度过渡动画组件
duration: Duration(seconds: 1),
opacity: _animation ? 0.0 : 1.0,
onEnd: (){
setState(() => _animation = !_animation );
},
child: Container(
width: 100,height: 100,
color: Colors.redAccent,
child: InkWell(
onTap: (){
setState(() => _animation = !_animation );
},
),
),
),
);
}
}
8、AnimatedIcon
再说一个比较有意思的过渡动画组件:AnimatedIcon
,这是Flutter提供的动画图标。
比如之前做过视频播放的按钮,点击按钮后 ,图标 ▶️ —> ⏸ 的变化,这些Flutter已经内置了,而且Icon在切换动画后默认指定了另一个Icon。图标 ▶️ 动画后就默认指定的Icon是 ⏸ 。
代码如下:
class Demo8 extends StatefulWidget {
@override
_Demo8State createState() => _Demo8State();
}
class _Demo8State extends State<Demo8> with SingleTickerProviderStateMixin{
bool _animation = false;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(seconds: 1),vsync: this,)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse();
} else if (status == AnimationStatus.dismissed) {
_controller.forward();
};
});
}
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
children: <Widget>[
setIcon(AnimatedIcons.play_pause),
setIcon(AnimatedIcons.add_event),
setIcon(AnimatedIcons.arrow_menu),
setIcon(AnimatedIcons.close_menu),
setIcon(AnimatedIcons.ellipsis_search),
setIcon(AnimatedIcons.event_add),
setIcon(AnimatedIcons.home_menu),
setIcon(AnimatedIcons.list_view),
InkWell(onTap: () => _controller.forward())
],
);
}
Widget setIcon (AnimatedIconData iconData) {
return Center(
child: AnimatedIcon(
size: 30,
icon: iconData,
progress: _controller,
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
效果如下:

9、路由转场动画
在路由跳转的时候一般会用到MaterialPageRoute或iOS风格的CupertinoPageRoute,默认的是上下滑动切换和左右滑动切换。
那么如何来自定义路由切换动画呢?答案就是使用PageRouteBuilder。例如以渐隐渐入的动画来实现路由过程:
class Demo9 extends StatefulWidget {
@override
_Demo9State createState() => _Demo9State();
}
class _Demo9State extends State<Demo9> {
@override
Widget build(BuildContext context) {
return CupertinoButton(
padding: EdgeInsets.zero,
onPressed: (){
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){
return FadeTransition(
opacity: animation,
child: NewPage(),
);
},
),
);
},
child: Container(
alignment: Alignment.center,
width: double.infinity,height: double.infinity,
child: Text("点击进入下一页",style: TextStyle(color: Colors.black,fontSize: 30),),
),
);
}
}
效果如下:

我们再将渐入转场封装一下,
class RCFadeRoute extends PageRoute{
final WidgetBuilder builder;
@override
final Duration transitionDuration;
@override
final bool opaque;
@override
final bool barrierDismissible;
@override
final Color barrierColor;
@override
final String barrierLabel;
@override
final bool maintainState;
RCFadeRoute({
@required this.builder,
this.transitionDuration = const Duration(milliseconds: 250),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor,
this.barrierLabel,
this.maintainState = true
});
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => builder(context);
@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}
}
那跳转动画的代码就简化成如下:
Navigator.push(context,
RCFadeRoute(builder: (context){
return NewPage();
}),
);
10、Hero动画
Hero是可以在路由之间“飞行”的Widget。由于共享的widget在新旧看路由页面上的位置、外观有所差异,所以在切换时会从旧路由逐渐过渡到新路由中指定的位置,这样就产生一个Hero动画,只需要用Hero将要共享的widget包装起来,并提供一个相同的tag即可。
具体代码如下:
class Demo10 extends StatefulWidget {
@override
_Demo10State createState() => _Demo10State();
}
class _Demo10State extends State<Demo10> {
@override
Widget build(BuildContext context) {
return GridView.builder(
padding: EdgeInsets.only(top: 10,left: 10,right: 10),
itemCount: 15,
itemBuilder: (BuildContext content,int index){
return Container(
child: InkWell(
child: Hero(
tag: "assets/g.jpg"+"$index", // 唯一的标记,前后两个路由的Hero的tag必须相同
child: Image.asset("assets/g.jpg",fit: BoxFit.cover,),
),
onTap: (){
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 250),
pageBuilder: (BuildContext context,Animation animation,Animation secondAnimation){
return FadeTransition(
opacity: animation,
child: HeroAnimationRoute("assets/g.jpg","assets/g.jpg"+"$index"),
);
},
),
);
},
),
);
},
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,mainAxisSpacing: 10.0,crossAxisSpacing: 10.0,
),
);
}
}
class HeroAnimationRoute extends StatelessWidget {
final String imageName,tag;
HeroAnimationRoute(this.imageName,this.tag);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black54,
body: InkWell(
onTap: (){
Navigator.of(context).pop();
},
child: Center(
child: Hero(
tag: tag,
child: Image.asset(imageName,fit: BoxFit.cover,),
),
),
),
);
}
}
效果如下:
