Flutter补间动画

2,394 阅读6分钟

作为一个移动端UI框架,Flutter 也拥有自己的动画体系。

分类

Flutter 动画分为两类:补间动画(Tween)和 基于物理的动画。

本文主要介绍第一类动画。

动画的基本类

Animation<T>

Animation是一个抽象的类,主要保存动画的状态和当前值。最常用的Animation类是Animation<double>

T 有很多类型,如Color、Offset。后面会详细介绍

可以通过Animation中的 value 属性获得当前动画的值。

动画的监听:

  • addListener() 每一帧动画执行的监听
  • addStatusListener() 动画状态改变的监听。有下面四种状态
    在这里插入图片描述
AnimationController

AnimationController 继承Animation<double>,负责控制动画的执行,停止等。

AnimationController 会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。

创建 AnimationController,则需要传入一个 vsync 参数。

Tween

默认情况下,AnimationController对象的范围从0.0到1.0。

如果你需要不同范围或者不同的数据类型,就需要tween来配置动画以生成不同的范围或数据类型的值

Tween的子类如下图所示:

在这里插入图片描述

例子
class PageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController controller;
  //doubler类型动画
  Animation<double> doubleAnimation;
  //颜色动画
  Animation<Color> colorAnimation;
  //位置动画
  Animation<Offset> offsetAnimation;
  //圆角动画
  Animation<BorderRadius> radiusAnimation;
  //装饰动画
  Animation<Decoration> decorationAnimation;
  //字体动画
  Animation<TextStyle> textStyleAnimation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //创建AnimationController
    controller = new AnimationController(
        vsync: this, duration: Duration(milliseconds: 2000));
    //animation第一种创建方式:
    doubleAnimation = new Tween<double>(begin: 0.0, end: 200.0).animate(controller)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((AnimationStatus status) {
      	//执行完成后反向执行
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
        //反向执行完成,正向执行
          controller.forward();
        }
      });
    //animation第二种创建方式:
    offsetAnimation = controller.drive(
      Tween<Offset>(begin: Offset(0.0, 0.0),end: Offset(400.0, 200.0))
    );
   
    colorAnimation =  ColorTween(begin: Colors.yellow,end: Colors.red).animate(controller);
    radiusAnimation = BorderRadiusTween(begin: BorderRadius.circular(0),end: BorderRadius.circular(50)).animate(controller);
    decorationAnimation = DecorationTween(begin: BoxDecoration(color: Colors.purple,borderRadius: BorderRadius.circular(0),),
        end: BoxDecoration(color: Colors.lightBlueAccent,borderRadius: BorderRadius.circular(40))).animate(controller);
    textStyleAnimation = TextStyleTween(begin: TextStyle(color: Colors.black,fontSize: 20,fontWeight: FontWeight.w100),
        end: TextStyle(color: Colors.purple,fontSize: 30,fontWeight: FontWeight.w700)).animate(controller);
    //启动动画
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      appBar: AppBar(title: Text("Tween动画"),),
      body: Container(
        alignment: Alignment.center,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 200,
              child:  Container(
                height: doubleAnimation.value,
                width: doubleAnimation.value,
                child: FlutterLogo(),
              ),
            ),
            Container(
              margin: EdgeInsets.only(left: offsetAnimation.value.dx),
              width: 50,
              height: 50,
              color: Colors.green,
            ),
            Container(
              height: 100,
              width: 100,
              color: colorAnimation.value,
            ),
            SizedBox(height: 10,),
            Container(
              height: 100,
              width: 100,
              decoration: BoxDecoration(borderRadius: radiusAnimation.value,color: Colors.blue),
            ),
            Container(
              height: 60,
              width: 200,
              decoration: decorationAnimation.value,
            ),
            Container(
              height: 100,
              child: Text("TestStyleTween",style: textStyleAnimation.value,),
            ),

          ],
        ),
      )
    );
  }
  
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    controller.dispose();
  }
}
tween动画执行效果图

在这里插入图片描述
这里列举里几种比较简单的Tween动画。在上面的代码中我们通过对animation设置addListener()对每一帧的变化进行监听,当animation 中的value插值发生改变时调用 setState(() {});刷新布局,从而达到动画过度的效果。

当我们state中的布局复杂的时候,我们在每一帧变化的时候都调用setState来刷新widget树,会把state中所有的widget都重新绘制,这样就会造成不必要的性能消耗,我们只需要刷新执行动画的那个widget就行了。Flutter为我们提供了AnimatedWidget。

AnimatedWidget

源码
abstract class AnimatedWidget extends StatefulWidget {
	//创建一个widget,当listenable 发生改变时重构
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);
  //声明一个Listenable ,帧动画监听
  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();
...
}

AnimatedWidget继承自StatefulWidget,拥有自己的状态,并且实例一个listenable用来监听帧动画,当动画方法变化时,刷新AnimatedWidget。

__AnimatedState方法源码如下:

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    //添加监听的回调
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }
	//当帧动画发生改变时触发刷新
  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }
//调用build()方法,重构AnimatedWidget
  @override
  Widget build(BuildContext context) => widget.build(context);
}

当listenable触发刷新的时候,调用 setState重构AnimatedWidget,虽然到最后还是调用setState,但是刷新的对象是不同的。

Listenable

从源码中可以看到在AnimatedWidget声明了一个Listenable,用来监听每一帧的变化。那么Listenable又是啥呢。我们可以看一小截Animation的源码:

在这里插入图片描述
可以看到Animation也是继承自Listenable,Listenable 源码如下:

abstract class Listenable {

  const Listenable();

  factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;

  void addListener(VoidCallback listener);

  void removeListener(VoidCallback listener);
}

所以Animation也是一个Listenable 实现类。源码看完,具体用法如下:

ColorAnimationWidget

声明一个ColorAnimationWidget类,继承自AnimatedWidget。代码如下:

class ColorAnimationWidget extends AnimatedWidget{

  ColorAnimationWidget({Key key, Animation<Color> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    // TODO: implement build
    return Center(
      child: Container(
        width: 200,
        height: 200,
        color: animation.value,
      ),
    );
  }
}
使用ColorAnimationWidget
class PageState extends State<HomePage> with SingleTickerProviderStateMixin{

  AnimationController _controller;
  Animation<Color> _animation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
    _animation = ColorTween(begin: Colors.lightBlueAccent,end: Colors.red).animate(_controller)
      ..addStatusListener((AnimationStatus status){
        if(status == AnimationStatus.completed){
          _controller.reverse();
        }else if(status == AnimationStatus.dismissed){
          _controller.forward();
        }
      });
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      body: ColorAnimationWidget(animation: _animation,),
    );
  }
}

class ColorAnimationWidget extends AnimatedWidget{

  ColorAnimationWidget({Key key, Animation<Color> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    // TODO: implement build
    return Center(
      child: Container(
        width: 200,
        height: 200,
        color: animation.value,
      ),
    );
  }
}

可以看到用法基本和不使用AnimatedWidget一样,唯一的区别就是使用AnimatedWidget时setState是在AnimatedWidget内调用的,只刷新一个widget。

ColorAnimationWidget效果图:

在这里插入图片描述

CurvedAnimation

Tween动画默认为我们提供了区间内线性变化,如果我们需要曲线变化,则需要配合使用CurvedAnimation。如下图弹性效果:

在这里插入图片描述
用法如下:

class HomePageState extends State<HomePage> with TickerProviderStateMixin{

  Animation animation;
  AnimationController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
    CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
      ..addListener((){
        setState(() {
        });
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      alignment: Alignment.topCenter,
      child: Container(
        margin: EdgeInsets.only(top: animation.value),
        width: 100,
        height: 100,
        child: FlutterLogo(),
      ),
    );
  }
}

CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut); 这里我们使用的是bounceOut效果

更多的曲线效果可以查看官网的效果图:Curves

我们看一下bounceOut是如何实现的:

class _BounceOutCurve extends Curve {
  const _BounceOutCurve._();

  @override
  double transformInternal(double t) {
    return _bounce(t);
  }
}

double _bounce(double t) {
  if (t < 1.0 / 2.75) {
    return 7.5625 * t * t;
  } else if (t < 2 / 2.75) {
    t -= 1.5 / 2.75;
    return 7.5625 * t * t + 0.75;
  } else if (t < 2.5 / 2.75) {
    t -= 2.25 / 2.75;
    return 7.5625 * t * t + 0.9375;
  }
  t -= 2.625 / 2.75;
  return 7.5625 * t * t + 0.984375;
}

可以看到bounceOut 是继承 Curve类,实现它的transformInternal方法,在transformInternal实现它的轨迹。

AnimatedBuilder

上面我们实现了一个颜色变化的例子,假如我们现在需要实现一个大小变化的widget呢?是不是要在声明一个SizeAnimationWidget继承AnimationWidget ?

显然这样做是可以的。当然我们也有更好的做法,就是使用AnimatedBuilder来重构我们的widget。

什么是AnimatedBuilder

AnimatedBuilder继承自抽象的AnimationWidget ,目的为了构建通用的AnimationWidget 实现类,不用每次使用AnimationWidget 都要创建一个实现类。

AnimatedBuilder源码:
class AnimatedBuilder extends AnimatedWidget {
  /// Creates an animated builder.
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

使用的时候我们只需要传入animation和builder就行了

用法
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new GrowTransition(child: new LogoWidget(), animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) {
    return new Center(
      child: new AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Container(
                height: animation.value, width: animation.value, child: child);
          },
          child: child),
    );
  }
}

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: new FlutterLogo(),
    );
  }
}
效果图

在这里插入图片描述

总结

tween动画到这里就结束了,Hero动画留下一次在补充吧

参考:Flutter动画教程