Day14 - Flutter - 动画

概述

  • 动画API认识
  • 动画案例练习
  • 其它动画补充
一、动画API认识

动画实际上是我们通过某些方式(某种对象,Animation对象)给Flutter引擎提供不同的值,而Flutter可以根据我们提供的值,给对应的小部件添加顺滑的动画效果。

  • 1.1、Animation
    在Flutter中,实现动画的核心类是动画,小部件可以直接将这些动画合并到自己的构建方法中来读取它们的当前值或监听其状态变化。
    我们一起来看一下Animation这个类,它是一个抽象类

    • addListener方法(监听动画值的概念)
      • 建立动画的状态值发生变化时,动画都会通知所有通过addListener添加的监听器。
      • 通常,一个正在监听的动画的state对象会调用自身的setState方法,将自身本身作为这些监听器的插入函数来通知小部件,系统需要根据新状态值进行重新生成。
    • addStatusListener(监听动画状态的改变)
      • 当动画的状态发生变化时,会通知所有通过addStatusListener添加的监听器。

      • 通常情况下,动画会从dismissed状态开始,表示它处于变化区间的开始点。
        举例来说,从0.0到1.0的动画在dismissed状态时的值应该是0.0。

      • 动画进行的下一状态可能是forward(例如从0.0到1.0)或者reverse(例如从1.0到0.0)。

      • 最终,如果动画到达其区间的结束点(例如1.0),则动画会变成completed状态。

        abstractclass Animation<T> extends Listenable implements ValueListenable<T> {
            const Animation();
        
            // 添加动画监听器
            @override
            void addListener(VoidCallback listener);
        
            // 移除动画监听器
            @override
            void removeListener(VoidCallback listener);
        
            // 添加动画状态监听器
            void addStatusListener(AnimationStatusListener listener);
        
            // 移除动画状态监听器
            void removeStatusListener(AnimationStatusListener listener);
        
            // 获取动画当前状态
            AnimationStatus get status;
        
            // 获取动画当前的值
            @override
            T get value;
        }
        
  • 1.2、AnimationController
    Animation是一个抽象类,并不能直接创建对象实现动画的使用。
    AnimationController是Animation的一个子类,实现动画通常我们需要创建AnimationController对象。

    • AnimationController会生成一系列的值,交替情况下值是0.0到1.0区间的值;

    除了上面的监听器,获取动画的状态,值之外,AnimationController还提供了对动画的控制:

    • forward:向前执行动画
    • 反向:方向播放动画
    • stop:停止动画

    AnimationController的源码:

    class AnimationController extends Animation<double>  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
       AnimationController({
           // 初始化值
           double value,
           // 动画执行的时间
           this.duration,
           // 反向动画执行的时间
           this.reverseDuration,
           // 最小值
           this.lowerBound = 0.0,
           // 最大值
           this.upperBound = 1.0,
           // 刷新率ticker的回调(看下面详细解析)
           @required TickerProvider vsync,
       })
    }
    
    • AnimationController有一个必传的参数vsync,它是什么呢?
      • Flutter的渲染闭环,Flutter每次渲染一帧画面之前都需要等待一个vsync信号。
      • 这里也是为了监听vsync信号,当Flutter开发的应用程序不再接受同步信号时(比如锁屏或退到后台),那么继续执行动画会消耗性能。
      • 这个时候我们设置了Ticker,就不会再出发动画了。
      • 开发中比较常见的是将 SingleTickerProviderStateMixin 混入到State的定义中。
  • 1.3、CurvedAnimation(设置动画执行的速率-速率曲线)
    CurvedAnimation也是Animation的一个实现类,它的目的是为了给AnimationController增加动画曲线:
    CurvedAnimation可以将AnimationControllerCurve结合起来,生成一个新的Animation对象

    class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
        CurvedAnimation({
            // 通常传入一个AnimationController
            @requiredthis.parent,
            // Curve类型的对象
            @requiredthis.curve,
            this.reverseCurve,
        });
    }
    

    官方也发表了自己的定义Curse的一个示例

    import'dart:math';
    
    class ShakeCurve extends Curve {
        @override
        double transform(double t) => sin(t * pi * 2);
    }
    
  • 1.4、Tween
    默认情况下,AnimationController动画生成的值所在区间是0.0到1.0
    如果希望使用这个以外的值,或者其他的数据类型,就需要使用Tween
    Tween的源码:源码非常简单,预设两个值即可,可以定义一个范围。

    class Tween<T extends dynamic> extends Animatable<T> {
       // begin 开始值,end 结束值
       Tween({ this.begin, this.end });
    }
    

    Tween也有一些子类,比如ColorTween、BorderTween,可以针对动画或者边框来设置动画的值。
    Tween.animate
    要使用Tween对象,需要调用Tween的animate()方法,传入一个Animation对象。

二、动画案例练习
  • 2.1. 动画的基本使用(不可取,优缺点)
    我们来完成一个案例:

    • 点击案例后执行一个心跳动画,可以反复执行

    • 再次点击可以暂停和重新开始动画


      import 'package:flutter/material.dart';
      
      void main() => runApp(MyApp());
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
           return MaterialApp(
               title: 'Flutter Demo',
               theme: ThemeData(
                   primarySwatch: Colors.blue, splashColor: Colors.transparent),
                   home: HYHomePage(),
               );
           }
      }
      
      class HYHomePage extends StatefulWidget {
         @override
          _HYHomePageState createState() => _HYHomePageState();
      }
      
      class _HYHomePageState extends State<HYHomePage>  with SingleTickerProviderStateMixin {
          // 创建AnimationController
          AnimationController _controller;
          Animation _animation;
          Animation _sizeAnim;
      
          @override
          void initState() {
              super.initState();
      
              // 1.创建AnimationController
              _controller = AnimationController(
                   vsync: this,
                   duration: Duration(seconds: 2)
              );
      
              // 2.动画添加Curve效果
              _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
      
              // 3.Tween 设置值的范围
              _sizeAnim = Tween(begin: 50.0, end: 150.0).animate(_animation);
      
              // 4.监听动画值的改变
              _controller.addListener(() {
                 setState(() {});
              });
      
              // 5.监听动画的状态改变
              _controller.addStatusListener((status) {
                 if (status == AnimationStatus.completed) {
                      _controller.reverse();
                 } else if (status == AnimationStatus.dismissed) {
                      _controller.forward();
                 }
              });
          }
      
          @override
          Widget build(BuildContext context) {
             print("执行_HYHomePageState的build方法");
             return Scaffold(
                appBar: AppBar(
                   title: Text("首页"),
                ),
                body: return Center(
                   child: Icon(Icons.favorite, color: Colors.red, size: _sizeAnim.value,),
                );
                floatingActionButton: FloatingActionButton(
                  child: Icon(Icons.play_arrow),
                  onPressed: () {
                      if (_controller.isAnimating) {
                          _controller.stop();
                          print(_controller.status);
                      } 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();
          }
      }
      
  • 2.2、AnimatedWidget(不可取,优缺点)
    在上面的代码中,我们必须监听动画值的改变,并且改变后需要调用setState(也就是上面的第4步),这会带来两个问题:

    • 1.执行动画必须包含这部分代码,代码比较冗余
    • 2.调用setState意味着整个State类中的build方法就会被重新build

    如何可以优化上面的操作:创建一个Widget继承自AnimatedWidget:

    class IconAnimation extends AnimatedWidget {
       IconAnimation(Animation animation): super(listenable: animation);
    
       @override
       Widget build(BuildContext context) {
           Animation animation = listenable;
           return Icon(Icons.favorite, color: Colors.red, size: animation.value,);
       }
    }
    

    那么2.1中的 的 第四步就可以去掉了,在Icon调用的地方直接:IconAnimation(_animation)

    • 缺点是:1、每次都需要创建一个类,类里面的build也会打印;2、如果创建的Widget有子类,那么子类依然会重复的build
  • 2.3、AnimatedBuilder(优解)
    AnimatedBuilder 可以解决上面 AnimatedWidget 产生的两个问题,代码如下

    class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin {
       // 创建AnimationController
       AnimationController _controller;
       Animation _animation;
    
       @override
       void initState() {
           super.initState();
    
           // 1.创建AnimationController
           _controller = AnimationController(
               vsync: this,
               duration: Duration(seconds: 2)
           );
    
           // 2.设置Curve的值
           _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
    
           // 3.Tween
           _animation = Tween(begin: 50.0, end: 150.0).animate(_animation);
    
           // 监听动画的状态改变
           _controller.addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                    _controller.reverse();
              } else if (status == AnimationStatus.dismissed) {
                    _controller.forward();
              }
           });
        }
    
        @override
         Widget build(BuildContext context) {
            print("执行_HYHomePageState的build方法");
            return Scaffold(
                appBar: AppBar(
                    title: Text("首页"),
                ),
                body: Center(
                    child: AnimatedBuilder(
                       animation: _controller,
                       builder: (ctx, child) {
                           return Icon(Icons.favorite, color: Colors.red, size: _animation.value,);
                       },
                    ),
                ),
                floatingActionButton: FloatingActionButton(
                    child: Icon(Icons.play_arrow),
                    onPressed: () {
                        if (_controller.isAnimating) {
                            _controller.stop();
                            print(_controller.status);
                        } 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();
         }
    }
    
三、其它动画补充
  • 3.1、交织动画(多个动画同时执行)
    案例说明:点击floatingActionButton执行动画
    动画集合了透明度变化大小变化颜色变化旋转动画等;
    我们这里是通过多个Tween生成了多个Animation对象;
    代码如下

    class HYHomePage extends StatefulWidget {
       @override
       _HYHomePageState createState() => _HYHomePageState();
    }
    
    class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin {
       // 创建AnimationController
       AnimationController _controller;
       Animation _animation;
    
       // 大小
       Animation<double> _sizeAnim;
       // 颜色
       Animation _colorAnim;
       // 透明度
       Animation<double> _opactiyAnim;
       // 角度
       Animation<double> _radiansAnim;
    
       @override
       void initState() {
         super.initState();
    
         // 1.创建AnimationController
         _controller = AnimationController(
            vsync: this,
            duration: Duration(seconds: 2)
         );
    
         // 2.设置Curve的值
         _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
    
         // 3.Tween
         _sizeAnim = Tween(begin: 10.0, end: 150.0).animate(_controller);
         _colorAnim = ColorTween(begin: Colors.brown, end: Colors.green).animate(_controller);
         _opactiyAnim = Tween(begin: 0.0, end: 1.0).animate(_controller);
         _radiansAnim = Tween(begin: 0.0, end: 2 * pi).animate(_controller);
    
         // 监听动画的状态改变
         _controller.addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                 _controller.reverse();
              } else if (status == AnimationStatus.dismissed) {
                 _controller.forward();
              }
         });
        }
    
        @override
        Widget build(BuildContext context) {
           print("执行_HYHomePageState的build方法");
           return Scaffold(
               appBar: AppBar(
                  title: Text("首页"),
               ),
               body: Center(
                  child: AnimatedBuilder(
                     animation: _controller,
                     builder: (ctx, child) {
                        return Opacity(
                          opacity: _opactiyAnim.value,
                          child: Transform(
                              transform: Matrix4.rotationZ(_radiansAnim.value),
                              alignment: Alignment.center,
                              child: Container(
                                  width: _sizeAnim.value,
                                  height: _sizeAnim.value,
                                  color: _colorAnim.value,
                              ),
                          ),
                       );
                     },
                  )
               ),
               floatingActionButton: FloatingActionButton(
                  child: Icon(Icons.play_arrow),
                  onPressed: () {
                       if (_controller.isAnimating) {
                           _controller.stop();
                           print(_controller.status);
                       } 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();
        }
    }
    
  • 3.2、Hero动画
    移动端开发会经常遇到类似这样的需求:

    • 点击一个头像,显示头像的大图,并且从原来图像的Rect到大图的Rect
    • 点击一个商品的图片,可以展示商品的大图,并且从原来图像的Rect到大图的Rect
      这种跨页面共享的动画被称之为享元动画(Shared Element Transition)

    在Flutter中,有一个专门的Widget可以来实现这种动画效果:Hero
    实现Hero动画,需要如下步骤:

    • 1.在第一个Page1中,定义一个起始的Hero Widget,被称之为source hero,并且绑定一个tag;
    • 2.在第二个Page2中,定义一个终点的Hero Widget,被称之为 destination hero,并且绑定相同的tag;
    • 3.可以通过Navigator来实现第一个页面Page1到第二个页面Page2的跳转过程;

    Flutter会设置Tween来界定Hero从起点到终端的大小和位置,并且在图层上执行动画效果。
    首页Page的核心代码:

    GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 6,
        mainAxisSpacing: 6,
        childAspectRatio: 16/9
      ),
      children: List.generate(20, (index) {
        String imageURL = "https://picsum.photos/200/300?random=$index";
        return GestureDetector(
          onTap: () {
            Navigator.of(context).push(PageRouteBuilder(
              pageBuilder: (ctx, animation1, animation2) {
                return FadeTransition(
                  opacity: animation1,
                  child: JKImageDeyail(imageURL),
                );
              },
            ));
          },
          child: Hero(tag: imageURL, child: Image.network(imageURL, fit: BoxFit.cover,)),
        );
      }),
    ),
    

    提示:外层包裹了一个:手势 GestureDetector,跳转用的带动画的 PageRouteBuilder,对于跳转的页面包裹了渐变 FadeTransition
    对于展示的 Image 我们包裹了一个 Hero ,对于 Hero 下个页面也要有 Hero,并且和当前的 Hero 的 tag 保持一致

    图片展示Page

    import 'package:flutter/material.dart';
    
    class JKImageDeyail extends StatelessWidget {
    
        final String _imageUrl;
    
        JKImageDeyail(this._imageUrl);
    
        @override
        Widget build(BuildContext context) {
           return Scaffold(
              backgroundColor: Colors.black,
              appBar: AppBar(
                 title: Text('图片详情'),
              ),
              body: Center(
                 child: GestureDetector(
                    onTap: () {
                      Navigator.of(context).pop();
                    },
                   child: Hero(
                     tag: _imageUrl,
                     child: Image.network(
                        _imageUrl,
                        width: double.infinity,
                        fit: BoxFit.cover,
                     ),
                   ),
                 ),
              ),
          );
       }
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342