开发者社区> JoanKing> 正文

Day14 - Flutter - 动画

简介: 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、AnimationControllerAnimation是一个抽象类,并不能直接创建对象实现动画的使用。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,
    });
}
    • Curve类型的对象的有一些常量Curves(和Color类型有一些Colors是一样的),可以供我们直接使用:
  • 官方也发表了自己的定义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. 动画的基本使用(不可取,优缺点)我们来完成一个案例:
    • 点击案例后执行一个心跳动画,可以反复执行
    • 再次点击可以暂停和重新开始动画


image.png


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


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
14852 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
15291 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
18581 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
36338 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
27727 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
16589 0
+关注
433
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载