Flutter中的动画

动画的概念

什么是动画?

动画实际上就是以一定的速度, 按照一定的变化规律不断更新UI. 速度决定了动画的质量, 速度快就是我们所说的“帧数高”, 动画就更加流畅; 速度慢就是“帧数低”, 动画看起来就比较卡顿. 一般情况下, 播放速度(或者说帧数)是由播放设备的硬件资源决定的, 设备的性能越高, 帧数就越高. 当然, 动画资源也可以影响动画的帧数, 资源越简单, 帧数就越高.

动画的内容是什么?

动画的帧数开发者一般不能决定, 但是动画的内容——或者说动画是如何变化的, 是可以由开发者决定的. 动画有四种基本类型: 平移、伸缩、旋转、透明, 它们分别对应一个区域的四个属性: 位置、缩放比例、角度、透明度. 理论上, 绝大多数动画效果都可以通过这四种基本类型来实现, 这就是开发者可以决定的部分.

如何描述一段动画?

那么, 我们怎么描述一段动画呢? 我们都知道, 动画是由一连串的图片快速播放而形成的. 那么其实我们也可以这么说, 只要知道了这一串图片的第一张和最后一张, 中间的图片我们就可以通过数学语言去描述它们. 比如我们要播放一个杯子自由落体的动画, 我们只要知道杯子的起始位置和最终位置, 中间的位置我们可以通过加速度公式去描述. 我们把杯子的起始位置和结束位置(或者说一连串图片的第一张图片和最后一张图片)称作“关键帧”, 把它们中间的所有位置(或者说一连串图片除去第一张和最后一张图片剩下的图片)称为“过渡帧”. 显然, 过渡帧是可以通过关键帧和变化规律得到的. 这样一来, 我们就知道如何描述一段动画了: 给出动画的起始和结束两个关键帧, 再给出动画的变化规律.

插值器和估值器

前面说到, 描述一段动画只需要给出动画的起始和结束两个关键帧, 再给出动画的变化规律. 那么实际上, 动画的播放过程就是动画的起始帧的四个属性逐渐过渡到结束帧的四个属性的过程. 为了描述这个过程, 我们引入了插值器和估值器两个函数. 插值器用来计算当前动画播放的进度, 估值器用来计算当前进度对应四个属性值应该是多少. 通过插值器和估值器两个函数, 我们就可以计算所有过渡帧的属性. 换句话说, 插值器和估值器两个函数合起来, 就是动画的变化规律.

Flutter中的动画组件

了解了动画的基本知识, 我们来看一下Flutter中的动画组件

Animation

Animation是Flutter中的估值器. 因为Animation是一个抽象类, 在实际中我们只用到AnimationController和Tween两个它的子类, 所以这部分不讲Animation的用法, 只讲它的属性和作用.

如果大家看过Animation的代码, 就会知道Animation的实现有多复杂. 幸运的是, 从开发者的角度看, 我们并不需要记住它的内部实现, 只需要知道它对外提供的属性和接口就好了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
T get value;
enum AnimationStatus {
dismissed,
forward,
reverse,
completed,
}

@override
void addListener(VoidCallback listener);

@override
void removeListener(VoidCallback listener);

void addStatusListener(AnimationStatusListener listener);

void removeStatusListener(AnimationStatusListener listener);

上面是Animation对外提供的属性和接口.

首先我们看一下value这个属性. 简单来说, value这个属性就是估值器得到的最后结果. 我们看到value的类型是一个泛型, 这个泛型对我们来说意义还是很重大的, 至于原因我们放到后面再说.

接着我们再看枚举类AnimationStatus, 它包括四个值: dismissed表示动画是否停止在起始帧, completed表示动画是否停止在结束帧, forward表示动画的播放方向是从起始帧到结束帧, reverse表示动画的播放方向是从结束帧到起始帧. 显然, 在reverse模式下, dissmissed可以表示动画是否播放完; 在forward模式下, completed可以表示动画是否播放完.

然后我们看一下四个接口, 显然, 这四个接口是用来添加或移除回调函数的. 一般来说, Animation的回调函数就是通过调用StatefulWidget的setState()函数来刷新UI的. 那么这两个回调有什么不一样呢? VoidCallback只有当value的值, 也就是估值器得到的结果发生变化后才会被调用. 自然, 要更新页面, 一般就会通过VoidCallback来调用setState()函数. AnimationStatusListener只有当Animation的status发生变化时才会被调用. 一般来说执行到这个方法时, 动画要么处在当前模式下的初始帧, 要么处在当前模式下的结束帧, 所以一般用这个方法来做播放动画前的准备和动画播放完后的收尾工作.

AnimationController

AnimationController是Animation<double<的子类, 它是Flutter实现动画的中心类, 因为它除了承担估值器的作用外, 还承担着控制时间轴的功能. AnimaciontController的value的取值范围是[0.0, 1.0], 代表着动画的播放进度. 理想状态下, Animation一秒钟可以更新60次value, 也就是说Flutter的动画在理想条件下可以达到60帧.

要使用AnimationController, 一般需要设置下面几个值:

SingleTickerProviderStateMixin vsync: 在使用AnimationController时, 我们需要让State类混继承一个SingleTickerProviderStateMixin类. Flutter在实现动画时, 实际上是通过这个类来逐帧令AnimationController计算属性值并最终调用相应的Listener来实现刷新UI的. 所以在创建AnimationController时, 需要为其指定一个SingleTickerProviderStateMixin类的实例.

Duration duration: 这个属性用来设置动画的持续时间, 很好理解, 这里不多说了.

begin、end: 实际上, 通过duration就已经可以确定动画的时间轴了, 但是Flutter为我们提供了更灵活的使用方式. 通过设置begin和end的值, 我们就可以控制动画在什么时候开始播放. 当value等于begin的时候, AnimationController才会通知更新UI; 当value等于end的时候, UI就不再更新了.

在设置完这些属性后, AnimationController就可以使用了. 常见的AnimationController使用方法有下面几种:

AnimationController.forward({from:}): 以forward模式播放动画, 其中可选项from表示从动画的什么地方开始播放, 也就是人为设定AnimationController的value值(或者说人为拨动时间轴), 如果不设置, 那么会从当前的value值开始播放.

AnimationController.reverse({from:}): 以reverse模式播放动画, 同forward.

AnimationController.animateTo(double target, {Duration duration, Curve curve}): 从当前时间点播放到目标时间点, 播放完后的状态是AnimationStatus.completed, 可以通过可选项设置持续时间. Curve我们放到后面讲.

AnimationController.animateBack(double target, {Duration duration, Curve curve}): 和上面一样, 只不过这个目标时间点在当前时间点的前面. 我们可以把这两个函数看成是动画的子集.

AnimationController.reset(): 重置动画, 顾名思义.

AnimationController.stop(): 停止播放动画, 顾名思义.

AnimationController.repeat(): 重复播放动画, 可以通过可选参数设置reverse为true, 从而实现重复反向播放的效果(反复横跳的效果).

Tween

前面提到, AnimationController的value可以理解为动画的时间轴, 它的取值范围只能是[0.0, 1.0]. 如果只用它作为估值器, 那未免也太局限了, 因为不可能所有的起始帧和结束帧的属性都在[0.0, 1.0]之间,

为了解决这个问题, 我们引入了Tween. 还记得Animaton中value是泛型吗? 这就是为Tween准备的. 我们可以将Tween理解为真正的估值器, 将AnimationController理解为只是单纯地管理时间轴和通知更新UI的控制器. Flutter默认实现了多种Tween, 比如IntTween、ColorTween、ReverseTween、SizeTween、RectTween、StepTween、ConstantTween等, 看名字也可以很容易知道它们的value的类型是什么以及value是如何变化的. 同时, 我们也可以继承Tween来创建自己的估值器.

要使用Tween也很简单, 只需要使用下面的一行代码:

1
Animation _animation = RectTween(begin: Rect.fromLTWH(0, 0, 100, 50), end: Rect.fromLTWH(100, 100, 50, 100)).animate(_animationController);

Tween的animate函数用来绑定AnimationController, Tween通过AnimationController的value(也就是时间轴)来对自己的value(也就是真正的估值器结果)进行计算. animate函数的返回值是和Tween类型相同的一个Animation实例, 这个实例存储了Tween每次计算后的结果, 我们可以根据它来更新UI.

CurvedAnimation

Tween和AnimationController使用的插值器都是线性插值器, 这表示它们所实现的动画都是匀速的. 如果要想实现变速的动画呢? 我们引入了CurvedAnimation.

CurvedAnimation和Tween的不同之处只有它们的插值器不同, Tween的插值器是线性的, CurvedAnimation的插值器是非线性的. Flutter默认实现了一些非线性插值器, 它们被封装在Curves类中, 我们也可以通过继承Cubic类来实现自己的非线性插值器.

AnimatedBuilder和AnimatedContainer

AnimatedBuilder和AnimatedContainer是对动画的两个封装类.

AnimatedBuilder相对于纯粹用AnimatedController+Tween或者CurvedAnimation来实现动画, 省略了为Animation设置监听的步骤. 同时, AnimatedBuilder简化了UI的渲染树. 当我们纯粹使用纯粹用AnimatedController+Tween或者CurvedAnimation来实现动画来实现动画时, Flutter会连同与动画无关的控件一同刷新, 这显然是一种浪费. AnimatedBuilder的一个用途在于, 它只刷新builder返回的控件, 也就是与动画有关的控件, 这节约了系统的开销.

AnimatedContainer是另外一种封装, 通过使用AnimtaedContainer, 我们连AnimationController都无需创建, 只需要指定插值器curve(默认为线性)和持续时间duration, 就可以对AnimationContainer的背景图片、背景颜色、宽和高、圆角、透明度等一系列属性添加动画. 当然, 这种高度的封装也丧失了一些灵活性, 比如AnimatedContainer无法为自己的位置添加动画.