动画原理
任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的是一个连续的动画,和电影的原理是一样的。将UI的一次改变称为一个动画帧,对应一次屏幕的刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Sceond),即每秒的动画帧数。很明显,帧率越高则动画就会越流畅。一般情况下,对于人眼来说,动画帧超过16FPS,就基本可以看了,超过32FPS则会感觉相对平滑,超过32FPS,大多数人眼感受不到差别。由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗费资源的,对设备的软硬件系统要求都很高,所以在UI系统中,动画的平均帧率是重要的性能指标,在Flutter中,理想情况下是可以实现60FPS的,这和原生应用性能达到的帧率基本是持平的。
Flutter中动画抽象
Flutter中对动画进行了抽象,主要涉及Animation、Curve、Contriller、Tween这四个角色。
Animation
- Animation是一个抽象类,本身和UI渲染没有任何关系,它主要的功能是保存动画的差值和状态;其中比较常用的Animation类是Animation<double>。
- Animation对象是在一段时间内一次生成一个区间(Tween)之间值的类。
- Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者其他曲线函数等,这由Curve决定。
- 根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。Animation还可以生成除double之外其他类型的值,如:Animation<Color>或Animation<Size>。在动画的每一帧中,可以通过Animation对象的value属性获取动画的当前状态值。
动画通知
可以通过Animation来监听动画每一帧以及执行状态的变化:
- addListener():可以用于给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState来触发UI重建。
- addStatusListener:可以给Animation添加动画状态改变监听器;动画开始、结束、正向、反向时会调用状态改变监听器。
Curve
动画过程可以是匀速、匀加速或者先加速后减速等。Flutter中通过Curve(曲线)来描绘动画过程。把匀速动画称为线性的(Curve.linear),而非匀速动画称为非线性的。
可以通过CurvedAnimation来指定动画的曲线:
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation和AnimatinController都是Animation<double>类型。 CurvedAnimation可以通过包装AnimationController和Curve生成一个新的动画对象,正式通过这种方式来将动画和动画执行的曲线关联起来。指定动画的曲线为Curves.easeIn,它表示动画开始时比较慢,结束时比较快。Curves类是一个预置的枚举类,定义了很多常用的曲线:
Curves曲线 | 动画过程 |
---|---|
linear | 匀速的 |
decelerate | 匀减速的 |
ease | 开始加速,后面减速 |
easeIn | 开始慢,后面快 |
easeOut | 开始快,后面慢 |
easeInOut | 开始慢,然后加速,最后再减速 |
除了上面,Curves类中还定义了一些其他的曲线,可在Curves类源码中查看
当然,也可以自己创建Curve,比如定义一个正弦曲线:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController
AnimationController用于控制动画,它包含动画的启动forward、停止stop、反向播放reverse等方法。AnimationController会在动画的每一帧就生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成0-1(默认区间)的数字。
下面代码创建一个Animation对象,但动画不会启动:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
AnimationController生成数字的区间可以通过lowerBound和upperBound来指定:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
AnimatinController派生自Animatin<double>,因此可以在需要Animation对象的任何地方使用,但是AnimationController具有控制动画的其他方法(forward、reverse等)。在动画开始执行后开始生成动画帧屏幕每刷行一次就是一个动画帧,在动画的每一帧会随着根据动画的曲线来生成当前的动画值(Animation.value),然后根据当前的动画值去构建UI,当所有动画帧依次触发时,动画值会依次改变,所以构建的UI也会依次改变,所以最终可以看到一个完整的动画。另外在动画的每一帧,Animation对象会调用其帧监听器,等待动画状态发生变化时(如动画结束)会调用状态改变监听器。
duration表示动画执行的时长,通过它可以控制动画的速度。
注意:在某些情况下,动画值可能会超出AnimationController的【0-1】范围,这取决于具体的曲线,例如fling()函数可以根据我们手指滑动(甩出)的速度(velocity)、力量(force)等来模拟一个手指甩出动画,因此它的动画值可以在[0-1]之外。也就是根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如Curves.elesticIn等弹性曲线会生成大于或小于默认范围的值。
Ticker
当创建一个AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker:
abstract class TickerProvider {
//通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。
通常会将SingleTickerProviderStateMixin添加到State定义中,然后将State对象作为vsync的值。
Tween
默认情况下,ANimationController对象的范围值是[0-1],如果需要构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween来添加映射以生成不同的范围或数据类型的值,例如:
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
Tween构造函数需要begin和end两个参数。tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0-1,但这不是必须的,可以自定义需要的范围。
Tween继承自Animatable<T>,而不是继承自Animation<T>,Animatable中主要定义动画值的映射规则。
看一个ColorTween将动画输入范围映射为两种颜色值之间过渡输入的例子:
final Tween colorTween =
ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween对象不存储任何状态,相反,它提供了evaluate(Animation<double>animation)方法,它可以获取动画当前的映射值。Animation对象的当前值可以通过value方法取到。evaluate函数还执行一些其他处理,例如分别确保在动画值0和1时返回开始和结束状态。
Tween.animate
要使用Tween对象,需要调用其animate()方法,然后传入一个控制器对象。例如:在500毫秒内生成从0-255的整数值。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
注意animate()返回的是一个Animation,而不是一个Animatable。
示例:构建一个控制器、一条曲线、一个Tween。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
线性插值lerp函数
动画的原理其实就是每一帧绘制不同的内容,一般都是指定起始和结束状态,然后在一段时间内从开始状态逐渐变为结束状态,而具体某一帧的状态值会根据动画进度来算出,因此,Flutter中给有可能会做出动画的一些状态属性都定义了静态的lerp方法(线性插值),比如:
//a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
lert的计算一般遵循返回值= a + (b-a)*t,其他拥有lerp方法的类:
// Size.lerp(a, b, t)
// Rect.lerp(a, b, t)
// Offset.lerp(a, b, t)
// Decoration.lerp(a, b, t)
// Tween.lerp(t) //起始状态和终止状态在构建 Tween 的时候已经指定了
...
需要注意,lert是线性插值,意思是返回值和动画进度t是成一次函数(y=kx+b)关系,因为一次函数的图像是一条直线,所以叫线性插值。如果想让动画按照一个曲线来执行,可以对t进行映射,比如要实现匀加速效果,则t = at²+bt+c,然后指定加速度a和b即可(大多数情况下保证t的取值范围在0-1,当然也有一些情况可能会超出该取值范围,比如弹簧(bounce)效果),而不同的Curve可以按照不同的曲线执行动画的原理本质上就是对t按照不同映射公式进行映射实现的。