动画基本结构

我们通过实现一个图片逐渐放大的示例来演示一下Flutter中动画的基本结构:

  1. class ScaleAnimationRoute extends StatefulWidget {
  2. @override
  3. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  4. }
  5. //需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
  6. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin{
  7. Animation<double> animation;
  8. AnimationController controller;
  9. initState() {
  10. super.initState();
  11. controller = new AnimationController(
  12. duration: const Duration(seconds: 3), vsync: this);
  13. //图片宽高从0变到300
  14. animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
  15. ..addListener(() {
  16. setState(()=>{});
  17. });
  18. //启动动画(正向执行)
  19. controller.forward();
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. return new Center(
  24. child: Image.asset("images/avatar.png",
  25. width: animation.value,
  26. height: animation.value
  27. ),
  28. );
  29. }
  30. dispose() {
  31. //路由销毁时需要释放动画资源
  32. controller.dispose();
  33. super.dispose();
  34. }
  35. }

上面代码中addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用,而在build()中,改变Image的宽高,因为它的高度和宽度现在使用的是animation.value ,所以就会逐渐放大。值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。

上面的例子中并没有指定Curve,所以放大的过程是线性的(匀速),下面我们指定一个Curve,来实现一个类似于弹簧效果的动画过程,我们只需要将initState中的代码改为下面这样即可:

  1. initState() {
  2. super.initState();
  3. controller = new AnimationController(
  4. duration: const Duration(seconds: 3), vsync: this);
  5. //使用弹性曲线
  6. animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
  7. //图片宽高从0变到300
  8. animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
  9. ..addListener(() {
  10. setState(() {
  11. });
  12. });
  13. //启动动画
  14. controller.forward();
  15. }

使用AnimatedWidget简化

细心的读者可能已经发现上面示例中通过addListener()setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,并允许我们将Widget分离出来,重构后的代码如下:

  1. class AnimatedImage extends AnimatedWidget {
  2. AnimatedImage({Key key, Animation<double> animation})
  3. : super(key: key, listenable: animation);
  4. Widget build(BuildContext context) {
  5. final Animation<double> animation = listenable;
  6. return new Center(
  7. child: Image.asset("images/avatar.png",
  8. width: animation.value,
  9. height: animation.value
  10. ),
  11. );
  12. }
  13. }
  14. class ScaleAnimationRoute extends StatefulWidget {
  15. @override
  16. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  17. }
  18. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
  19. with SingleTickerProviderStateMixin {
  20. Animation<double> animation;
  21. AnimationController controller;
  22. initState() {
  23. super.initState();
  24. controller = new AnimationController(
  25. duration: const Duration(seconds: 3), vsync: this);
  26. //图片宽高从0变到300
  27. animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
  28. //启动动画
  29. controller.forward();
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. return AnimatedImage(animation: animation,);
  34. }
  35. dispose() {
  36. //路由销毁时需要释放动画资源
  37. controller.dispose();
  38. super.dispose();
  39. }
  40. }

用AnimatedBuilder重构

用AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中,假设如果我们再添加一个widget透明度变化的动画,那么我们需要再实现一个AnimatedWidget,这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来, 上面的build方法中的代码可以改为:

  1. @override
  2. Widget build(BuildContext context) {
  3. //return AnimatedImage(animation: animation,);
  4. return AnimatedBuilder(
  5. animation: animation,
  6. child: Image.asset("images/avatar.png"),
  7. builder: (BuildContext ctx, Widget child) {
  8. return new Center(
  9. child: Container(
  10. height: animation.value,
  11. width: animation.value,
  12. child: child,
  13. ),
  14. );
  15. },
  16. );
  17. }

上面的代码中有一个迷惑的问题是,child看起来像被指定了两次。但实际发生的事情是:将外部引用child传递给AnimatedBuilder后AnimatedBuilder再将其传递给匿名构造器, 然后将该对象用作其子对象。最终的结果是AnimatedBuilder返回的对象插入到Widget树中。

也许你会说这和我们刚开始的示例差不了多少,其实它会带来三个好处:

  1. 不用显式的去添加帧监听器,然后再调用setState() 了,这个好处和AnimatedWidget是一样的。

  2. 动画构建的范围缩小了,如果没有builder,setState()将会在父widget上下文调用,这将会导致父widget的build方法重新调用,而有了builder之后,只会导致动画widget的build重新调用,这在复杂布局下性能会提高。

  3. 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。下面我们通过封装一个GrowTransition来说明,它可以对子widget实现放大动画:

    1. class GrowTransition extends StatelessWidget {
    2. GrowTransition({this.child, this.animation});
    3. final Widget child;
    4. final Animation<double> animation;
    5. Widget build(BuildContext context) {
    6. return new Center(
    7. child: new AnimatedBuilder(
    8. animation: animation,
    9. builder: (BuildContext context, Widget child) {
    10. return new Container(
    11. height: animation.value,
    12. width: animation.value,
    13. child: child
    14. );
    15. },
    16. child: child
    17. ),
    18. );
    19. }
    20. }

    这样,最初的示例就可以改为:

    1. ...
    2. Widget build(BuildContext context) {
    3. return GrowTransition(
    4. child: Image.asset("images/avatar.png"),
    5. animation: animation
    6. );
    7. }

    Flutter中正是通过这种方式封装了很多动画,如:FadeTransition、ScaleTransition、SizeTransition、FractionalTranslation等,很多时候都可以复用这些预置的过渡类。

动画状态监听

上面说过,我们可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:

枚举值 含义
dismissed 动画在起始点停止
forward 动画正在正向执行
reverse 动画正在反向执行
completed 动画在终点停止

示例

我们将上面图片放大的示例改为先放大再缩小再放大……这样的循环动画。要实现这种效果,我们只需要监听动画状态的改变即可,即:在动画正向执行结束时反转动画,在动画反向执行结束时再正向执行动画。代码如下:

  1. initState() {
  2. super.initState();
  3. controller = new AnimationController(
  4. duration: const Duration(seconds: 1), vsync: this);
  5. //图片宽高从0变到300
  6. animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
  7. animation.addStatusListener((status) {
  8. if (status == AnimationStatus.completed) {
  9. //动画执行结束时反向执行动画
  10. controller.reverse();
  11. } else if (status == AnimationStatus.dismissed) {
  12. //动画恢复到初始状态时执行动画(正向)
  13. controller.forward();
  14. }
  15. });
  16. //启动动画(正向)
  17. controller.forward();
  18. }