什么是动画,从数学上来说,动画指的是一个属性的变换过程,实际上,就是一个函数,将一个属性值变成另一个属性值的过程。

从现实上来说,动画实际上就是将一系列静态的图片,在一定时间内快速切换,从而利用人眼的视觉暂留效应形成动态的画面。

对于现代移动设备来说,保持流畅体验的标准是60帧每秒,即每秒要切换60张静态图,这每一帧,在Flutter中被称之为Tick,也叫Vsync,在使用动画的时候,需要mixin的SingleTickerProviderStateMixin,就是Tick的一种,开发者可以监听Animation的每一帧回调,在回调中去刷新UI,从而实现动画的播放。

在Flutter中,包含两种动画类型,分别是Tween动画和Physics动画。

一个Tween实际上就是定义的属性值的变化区间,而基于Physics的动画,实际上也是一个变化区间,只不过它的变化区间是根据物理引擎计算出来的,更加模拟真实的物理效果。

Physics动画的相关类如下所示。

https://api.flutter.dev/flutter/animation/AnimationController/animateWith.html
https://api.flutter.dev/flutter/physics/SpringSimulation-class.html

这两种动画类型,实际上是描述动画的两种方式,一种是通过区间值,另一种是通过模拟物理环境(胡克定律)

首先来思考下,如何来设计一个动画框架——动画,简单来说,就是描述一个属性,将其起始值,经过多长时间,由怎样的变化速率,变成目标值,这样就完成了一次动画。

在Flutter中,上面的这些步骤是如何实现的呢?

  • Tween,负责起始值到目标值的数据生成,可以是0-1,也可以是1-100,也可以是Red-Blue,总之就是数据的变化
  • Curve,负责动画的变化速率,即作用在Tween的中间值上的函数f(x),避免生硬的动画过程
  • AnimationController,负责整个动画的行进过程,即控制动画的开始、结束、循环,以及时长

那么有了这三个核心概念,在Flutter中描述动画就很简单了,通过Tween来描述动画的变化区间,在用AnimationController来管理Tween和Curve,就完成了动画的控制。

不过这里要注意的是,AnimationController是Animation的实现类,所以理论上来说,Flutter Animation的核心应该是Animation、Tween和Curve。

Animation在Flutter中与实际的UI渲染是没有任何关系的,它仅仅是一个数值发生器,和Android中的属性动画ValueAnimator非常类似。

下面,针对前面提到的几个名词来进行下分析,这些东西频繁出现在Flutter动画中,理清它们之间的关系和作用,才能为实现动画打好基础。

Animation与AnimationController

Animation是Flutter中实现动画的核心类,但它更像是一个数值发生器,用于产生指定泛型的数据,或者是根据Tween来生成变换后的区间数值,而整个Widget树,根据数值的更新来重绘,从而产生动画效果。

这也是申明式编程,实现动画更加简单方便的原因,因为它更符合动画的语义效果。

Animation是具有状态的对象,它保存了当前的映射值和当前的运行状态(动画完成、中断)等。所以,当动画Stop后继续播放,这个值是有状态的,它会从Stop的地方继续执行,除非你指定了from: 0。

Animation有两个重要的实现,分别是AnimationController和CurvedAnimation。

  • AnimationController
    AnimationController是Animation的实现,在屏幕刷新的每一帧,AnimationController都会产生一个对应的数值,当不使用Tween时,AnimationController最基础的实现,就是连续的、线性的依次产生一个[0,1]的数值。如果需要改变AnimationController的输出范围,可以通过修改AnimationController的lowerBound、upperBound参数,并在调用时设置controller.forward(from: xxx)函数。
  • CurvedAnimation
    CurvedAnimation也是Animation的实现,只不过它可以根据Curve曲线来生成非线性的区间值。CurvedAnimation并不能驱动动画,它只是一个数值的转换器。

前面提到了AnimationController是一个线性的数值发生器,因为它产生的数值的速率是相同的,但是很多情况下,动画的发生速率是变化的,例如加速或者减速效果,这在Android原生中,是通过Interpolator来实现的,而在Flutter中,可以通过CurvedAnimation来实现。

CurvedAnimation同样继承自Animation,所以它和AnimationController是同等地位的,但是通常情况下,CurvedAnimation的创建是需要AnimationController的。创建一个CurvedAnimation最核心的代码如下所示。

  1. _curvedAnimation = CurvedAnimation(parent: _controller, curve: Curves.bounceIn);

其中parent参数就是需要指定的AnimationController,而curve参数,就是需要指定的插值器,在Curves中,已经内置了很多不同类型的插值器,基本覆盖了常用的使用场景,当然,开发者也可以自定义自己的插值器,只需要继承Curve类并实现transform函数即可。

Tween

Tween,是Animatable的子类,它的作用类似一个函数f(x),它将一个输入x(x的取值范围是[0,1]),经过f的变换,产生新的数值。

那么前面讲解了AnimationController,通过设置AnimationController的一些参数,就可以获得动画的值,那这里为什么还要讲解Tween呢?

Tween与Animation不同,Tween是一个无状态的对象,它只包含begin和end值。当AnimationController默认产生的[0,1]不能满足需求时,就可以通过Tween来生成不同的区间范围值,Tween不保存任何状态,它只是起始值的变换函数。

  • Tween
    Tween不仅仅可以返回double类型的数据,在Flutter SDK中,系统定义了很多内置的Tween,当然,开发者也可以自定义自己的Tween。
  • CurvedTween
    与CurvedAnimation一样,Tween也有类似的CurvedTween

另外,这也是Flutter的一种解耦的方式,AnimationController的职责是产生原始的、统一的[0,1]数值,而具体的动画效果,通过交给相应的Tween-Animation来创建不同的动画类型,将AnimationController中的设置分配到了不同的Tween中,而AnimationController则负责管理这些Tween。

一个最简单的Tween动画,就是将[0,1]变换为[begin,end]。

Tween的类型

Tween有很多不同类型的实现,它们都继承自Animatable,如下图所示。

FlutterComponent最佳实践之动画那些词儿 - 图1

这里通过一个例子来介绍下Tween和ColorTween的使用方法,其它类型的Tween的使用基本类似,可以类推。核心代码如下所示。

_animation = Tween(begin: 0.0, end: 100.0).animate(_controller);  
_animationColor = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);

Tween的使用非常简单,创建Tween对象并设置相应参数后,通过animate函数关联相应的AnimationController即可(animate函数即将Animatable转换成了Animation)。

可以发现,实际上Tween的使用只是参数上的区别,不同类型的Tween,作用的属性不同,产生的值的类型也不同,这个值可以是具体数字、可以是Color、Offset,甚至是对象。

那么除了通过animate函数以外,还可以通过AnimationController.drive函数来加载一个Tween。从效果上来看AnimationController.drive == Tween.animate,是等效的,其实drive函数本身,也是通过animate来实现的。

Tween与chain

chain函数可以将多个Tween进行复合,一个常用的场景就是与CurveTween复合,给Tween添加插值曲线,这样就不用使用CurvedAnimation来创建Curve动画了,示例代码如下所示。

tween1.chain(CurveTween(curve: Curves.easeIn)).animate(animation)

Tween中叠加chain的原理,实际上就是函数的叠加,因为有时候,一个Tween是很难描述一个复杂的动画的,这个时候,就需要进行叠加了,类似数学中的复合函数g(f(h(x)))。

通过chain,就可以很方便的将复杂动画拆解成多个单一属性的简单动画的叠加,这样就会让动画开发的思路更加清晰。

自定义Tween

Tween表示的是动画的变换函数,Flutter预设了很多种不同的Tween来帮助开发者完成动画的创建,同时也给出了创建自定义Tween的方法,下面的代码就演示了如何创建一个自定义的Tween,来实现打字机的特效。

首先,需要定义一个Tween,用来不断的截取字符串,模拟出文字逐渐出来的效果,代码如下所示。

class TypewriterTween extends Tween<String> {
  TypewriterTween({String begin = '', String end}) : super(begin: begin, end: end);

  String lerp(double t) {
    var cutoff = (end.length * t).round();
    return end.substring(0, cutoff);
  }
}

代码很简单,实际上就是根据动画的进度参数t来对String进行截取。

所以,对Tween的理解不要局限于数值的改变上,任何类型的「改变」,都可以作为Tween。

本质

让我们从Tween的实现上来看下Tween到底是什么东西。

FlutterComponent最佳实践之动画那些词儿 - 图2

可以发现,Tween实际上就是一个lerp函数作用之后的数值,也就是说,我们通常使用:

Tween().animate(parent) .value

来获取动画值的方式,本质上和下面这种方式是一样的:

tween.evaluate(_controller)

从源码中,其实也能看出,如下所示。

FlutterComponent最佳实践之动画那些词儿 - 图3

所以,Tween实际上是一个函数,用来告诉系统,begin到end,中间的每一个值的产生是如何进行的。

Curves

在动画的函数中,duration和curve两个非常重要的参数,duration控制的是动画的响应时长,而curve控制的是动画的响应曲线。

duration很好理解,就是动画持续时间,而curve,则是描述动画行进的曲线,默认情况下,动画以线性曲线行进,所以,动画的变换过程是线性的,以恒定不变的速率进行变换,这个过程,在动画中,被称为Interpolator,即插值器,插值器的变换过程,可以用一个函数图像来表示,所以默认的线性变换过程,用图来表示,就如下图所示。

FlutterComponent最佳实践之动画那些词儿 - 图4

在Flutter中,SDK定义了很多常用的插值器,具体的插值器可以在下面的网站上找到,如下所示。

https://api.flutter.dev/flutter/animation/Curves-class.html

FlutterComponent最佳实践之动画那些词儿 - 图5

当一个线性插值器被替换成其它非线性插值器时,动画的行进过程就不是恒定不变的了,而是类似插值器的函数图像来进行变换。

运用插值器,可以让动画的实现过程变的更加符合自然规律、设计规范,让动画更加真实、更加具有美感。

CurvedAnimation是使用Curve的方法之一,另一个方法就是使用CurveTween。这两个方法也是等效的。

本质

我们来看下一个Curve是怎么定义的。

FlutterComponent最佳实践之动画那些词儿 - 图6

可以发现,其实Curve和动画没有什么直接关系,它的作用就是让原本线性的值,根据某些函数进行转换,从而产生一些不是那么线性的值。

所以,不仅仅是在动画中可以使用Curve,在其它需要的计算场景下,也可以直接使用Curve来进行计算,代码如下所示。

Curves.elasticIn.transform(_controller.value)

上面的这些名词,就是Flutter动画的基石,只有深刻理解这些名词背后的含义,才能对动画的实现了如指掌。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

FlutterComponent最佳实践之动画那些词儿 - 图7