美文网首页Flutter初探
Day14 - Flutter - 动画

Day14 - Flutter - 动画

作者: IIronMan | 来源:发表于2020-06-01 12:20 被阅读0次

    概述

    • 动画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,
                       ),
                     ),
                   ),
                ),
            );
         }
      }
      

    相关文章

      网友评论

        本文标题:Day14 - Flutter - 动画

        本文链接:https://www.haomeiwen.com/subject/ujbqzhtx.html