乐趣区

Flutter开发之动画

动画作为产品的重要组成部分,是提升用户体验的重要方式,一个恰当的动画不仅能够缓解用户因为等待而带来的情绪焦躁,还会增加应用的整体用户体验。因此,在应用中增加动画的相关功能,可以增强用户的粘性。

动画的原理

不管是 Android 平台还是 iOS 平台,我们在使用应用时都能看到一些炫酷的动画效果。作为移动应用的重要组成部分,动画是提高用户体验的重要手段,一个恰当的动画,不仅能够缓解用户因为等待而带来的情绪问题,还会提升用户使用的体验。
事实上,不管是什么视图框架,动画的实现原理都是相同的,即在一段有限的时间内,多次快速地改变视图外观来实现一个连续播放的效果。视图的一次改变即称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率 FPS(Frame Per Second 缩写),即每秒的动画帧数。很明显,帧率越高则动画就会越流畅。
目前,大多数设备的屏幕刷新频率可以到达 60Hz,而对于人眼来说,动画帧率超过 16FPS 就认为是流畅的,超过 32FPS 基本就感受不到任何卡顿。由于动画的每一帧都需要改变视图的输出,所以在一个时间段内连续的改变视图输出是比较耗费资源的,对设备的软硬件系统要求也比较高。作为衡量一个视图框架优劣的标准,Flutter 框架在理想情况下是可以实现 60FPS 的,这和原生应用的帧率标准是基本是持平的。
同时,为了方便开发者创建并使用动画,不同的视图框架对动画都进行了高度的抽象和封装,比如在 Android 开发中,可以使用 XML 来描述一个动画然后再设置给一个视图对象。同样,Flutter 也对动画进行了高度的抽象,并且提供了 Animation、Curve、Controller、Tween 等四个动画对象。
Animation 是 Flutter 动画的核心抽象类,包含动画的当前值和状态两个属性。AnimationController 是 Animation 的控制器,动画的开始、结束、停止、反向均由它控制,可以通过 Listener 和 StatusListener 来管理动画状态的改变。

动画 API

在 Flutter 中,学习动画相关的开发,其实就是围绕 Animation、Curve、Controller、Tween 等四个动画对象来展开的。

Animation

在 Flutter 中,Animation 是实现动画的核心类,Animation 的主要作用就是保存动画的插值和状态,它本身与视图渲染没有任何关系。Animation 对象则是一个可以在一段时间内依次生成一个区间值的类,其输出值可以是线性的、曲线的,可以是一个步进函数或者任何其他曲线函数等,由 Curve 来决定。Animation 的核心源码如下:

abstract class 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;

Animation 是一个抽象类,Widget 可以直接将这些动画合并到自己的 build 方法中来读取它们的当前值或者监听它们的状态变化。Animation 提供了 addListener 和 addStatusListener 两个方法来监听动画帧的变化。

addListener
addListener 方法用于给 Animation 对象添加帧监听器,每一帧都会被调用,当帧监听器监听到状态发生改变后会调用 setState() 来触发视图的重建。这意味着:

  • 每当动画的状态值发生变化时,动画都会通知所有通过 addListener 添加的监听器。
  • 一个正在监听动画的 state 对象会调用自身的 setState 方法,将自身传入这些监听器的回调函数来通知 widget 系统需要根据新状态值进行重新构建。

addStatusListener
addStatusListener 方法用于给 Animation 对象添加动画状态改变监听器,动画开始、结束、正向或反向时会调用状态改变的监听器。这意味着:

  • 当动画的状态发生变化时,会通知所有通过 addStatusListener 添加的监听器。
  • 动画会从 dismissed 状态开始,表示它处于变化区间的开始点。
  • 举例来说,从 0.0 到 1.0 的动画在 dismissed 状态时的值应该是 0.0。
  • 动画进行的下一状态可能是 forward(比如从 0.0 到 1.0)或者 reverse(比如从 1.0 到 0.0)。
  • 最终,如果动画到达其区间的结束点(比如 1.0),则动画会变成 completed 状态

AnimationController

AnimationController,即动画控制器,Animation 是一个抽象类,并不能用来直接创建对象并实现动画,它的主要用于控制动画的开始、结束、停止、反向等操作。AnimationController 是 Animation 的一个子类,默认情况下,AnimationController 会在给定的时间段内以线性的方式生成从 0.0 到 1.0 的数字。它的源码如下:

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,那么 AnimationController 有什么作用呢?之前我讲过关于 Flutter 的渲染闭环,Flutter 每次渲染一帧画面之前都需要等待一个 vsync 信号。这里也是为了监听 vsync 信号,当 Flutter 开发的应用程序不再接受同步信号时(比如锁屏或退到后台),那么继续执行动画会消耗性能,开发中比较常见的解决方法是将 SingleTickerProviderStateMixin 混入到 State 的定义中。例如,下面是一个比较简单的数字自动增加动画的示例。

import 'package:flutter/material.dart';
import 'package:gc_data_app/utils/utils.dart';

class AnimText extends StatefulWidget {

  final int number;
  final int duration;
  final Color fontColor;
  final double fontSize;

  const AnimText({
    Key key,
    this.number,
    this.duration,
    this.fontColor,
    this.fontSize,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {return AnimState();
  }
}

class AnimState extends State<AnimText> with SingleTickerProviderStateMixin {

  AnimationController controller;
  Animation animation;
  var begin=0;

  @override
  void initState() {super.initState();
    controller = AnimationController(vsync: this, duration: Duration(milliseconds: widget.duration));
    final Animation curve=CurvedAnimation(parent: controller,curve: Curves.linear);
    animation = IntTween(begin: begin, end: widget.number).animate(curve)..addStatusListener((status) {if(status==AnimationStatus.completed){//         controller.reverse();
       }
    });
  }

  @override
  Widget build(BuildContext context) {controller.forward();
    return AnimatedBuilder(
        animation: controller,
        builder: (context,child){
          return Container(child:Text(Utils.formatMoney(animation.value),
              style: TextStyle(fontSize: widget.fontSize, color: widget.fontColor,fontWeight: FontWeight.bold)),
          );
        } ,
    );
  }

  @override
  void dispose() {controller.dispose();
    super.dispose();}
}

CurvedAnimation

CurvedAnimation 是 Animation 的一个实现类,它的目的是为了给 AnimationController 增加动画曲线。通常,动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter 通过 Curve 来描述动画过程,我们可以把匀速动画称为线性动画,把非匀速动画称为非线性动画。

CurvedAnimation 可以将 AnimationController 和 Curve 结合起来,生成一个新的 Animation 对象。例如:

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

Curve 类型的对象的有一些常量 Curves 可以直接使用,常用的有如下一些:

  • linear:匀速动画
  • decelerate:匀减速动画
  • ease:先加速后减速
  • easeIn:先快后慢动画
  • easeOut:先慢后快动画
  • easeInOut:先慢,然后加速,最后减速

Tween

默认情况下,AnimationController 对象的取值范围是 [0.0,1.0],如果需要给动画设置不同的范围或者类型的值时,可以使用 Tween 来定义并生成不同范围或类型的值。Tween 的源码非常简单,传入两个值即可,如下所示。

class Tween<T extends dynamic> extends Animatable<T> {Tween({ this.begin, this.end});
}

Tween 继承自 Animatable<T>,而不是继承自 Animation<T>,Animatable 是一个控制动画类型的类,主要定义了动画值的映射规则。虽然,Animatable 和 Animation 有很多相似之处,但它的类型可以是除 double 的其他类型。例如,下面是使用 ColorTween 实现颜色渐变的过渡动画的例子。

Tween colorTween =new ColorTween(begin: Colors.transparent, end: Colors.black54);      

Tween 也有一些子类,比如 ColorTween、BorderTween,可以针对动画或者边框来设置动画的值。

动画示例

和原生平台的动画开发一样,Flutter 的动画开发也有一定的规则,实际使用时,只需要按照遵循步骤即可。通用的步骤如下:
1. 创建 AnimationController 和 Animation;
2. 设置动画的类型,监听动画执行
3. 销毁动画

例如,下面是 Flutter 动画的基本使用示例,代码如下:

import 'package:demos/page/anim_page.dart';
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,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(),);
  }
}

class MyHomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter 动画'),
      ),
      body: HeartAnimationWidget(key: animKey),
      floatingActionButton: FloatingActionButton(child: Icon(Icons.add),
        onPressed: () {if (!animKey.currentState.controller.isAnimating) {animKey.currentState.controller.forward();
          } else {animKey.currentState.controller.stop();
          }
        },
      ),
    );
  }
}



GlobalKey<_HeartAnimationPageState> animKey = GlobalKey();

class HeartAnimationPage extends StatefulWidget {HeartAnimationPage({Key key}): super(key: key);
  @override
  _HeartAnimationPageState createState() => _HeartAnimationPageState();
}

class _HeartAnimationPageState extends State<HeartAnimationPage> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {super.initState();
    // 1. 创建 AnimationController
    controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
    // 2. 动画添加 Curve 效果
    animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);
    // 3. 监听动画
    animation.addListener(() {setState(() {});
    });
    // 4. 控制动画的翻转
    animation.addStatusListener((status) {if (status == AnimationStatus.completed) {controller.reverse();
      } else if (status == AnimationStatus.dismissed) {controller.forward();
      }
    });
    // 5. 设置值的范围
    animation = Tween(begin: 50.0, end: 120.0).animate(controller);
  }

  @override
  Widget build(BuildContext context) {
    return Center(child: Icon(Icons.favorite, color: Colors.red, size: animation.value,),
    );
  }

  @override
  void dispose() {controller.dispose();
    super.dispose();}
}

运行上面的代码,当点击案例后执行一个心跳动画,可以反复执行,再次点击可以暂停和重新开始动画,运行效果如下图所示。

AnimatedWidget

通过 addListener() 和 setState() 来更新视图是动画实现的通用的做法,但缺点是需要在每个动画中都添加监听函数,代码比较冗余,并且调用 setState() 方法意味着整个 State 类中的 build 方法就会被重新构建,性能损耗比较严重。因此,官方推荐使用 AnimatedWidget 类来实现同样的动画效果,因为 AnimatedWidget 类简化了 addListener() 和 setState() 的调用流程,并隐藏了底层额实现细节。

对于上面的示例,我们西安创建一个继承自 AnimatedWidget 的 Widget,如下所示。

class HeatAnimationWidget extends AnimatedWidget {HeatAnimationWidget(Animation animation): super(listenable: animation);

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

然后,我们对 HeartAnimationPage 的代码进行如下修改。

class HeartAnimationPage extends StatefulWidget {HeartAnimationPage({Key key}): super(key: key);
  @override
  _HeartAnimationPageState createState() => _HeartAnimationPageState();
}

class _HeartAnimationPageState extends State<HeartAnimationPage> with SingleTickerProviderStateMixin {

  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {super.initState();
    // 1. 创建 AnimationController
    controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
    // 2. 动画添加 Curve 效果
    animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);
    // 3. 监听动画
    // 4. 控制动画的翻转
    animation.addStatusListener((status) {if (status == AnimationStatus.completed) {controller.reverse();
      } else if (status == AnimationStatus.dismissed) {controller.forward();
      }
    });
    // 5. 设置值的范围
    animation = Tween(begin: 50.0, end: 120.0).animate(controller);
  }

  @override
  Widget build(BuildContext context) {
    return Center(child: HeatAnimationWidget(animation),
    );
  }

  @override
  void dispose() {controller.dispose();
    super.dispose();}
}

AnimatedBuilder

通过 AnimatedWidget 类,我们可以从动画中分离出组件,从而将动画和组件分离开来,不过动画的渲染过程仍然在 AnimatedWidget 中执行。如果想要将动画的渲染过程分离出来,可以使用 AnimatedBuilder 类,与 AnimatedWidget 的作用类似,AnimatedBuilder 可以自动监听 Animation 的变化,然后根据需要自动刷新视图。

因此,在上面的示例中,我们可以使用 AnimatedBuilder 进行如下的优化:

class HeartAnimationPage extends StatefulWidget {HeartAnimationPage({Key key}): super(key: key);
  @override
  _HeartAnimationPageState createState() => _HeartAnimationPageState();
}

class _HeartAnimationPageState extends State<HeartAnimationPage> with SingleTickerProviderStateMixin {

  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {super.initState();
    // 1. 创建 AnimationController
    controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
    // 2. 动画添加 Curve 效果
    animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);
    // 3. 监听动画
    // 4. 控制动画的翻转
    animation.addStatusListener((status) {if (status == AnimationStatus.completed) {controller.reverse();
      } else if (status == AnimationStatus.dismissed) {controller.forward();
      }
    });
    // 5. 设置值的范围
    animation = Tween(begin: 50.0, end: 120.0).animate(controller);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (ctx, child) {return Icon(Icons.favorite, color: Colors.red, size: animation.value,);
        },
      )
    );
  }

  @override
  void dispose() {controller.dispose();
    super.dispose();}
}

除了上面介绍的动画外,Flutter 还提供了交错动画和 Hero 动画。

退出移动版