动画¶
什么是动画¶
什么是动画?这个问题非常重要。但是答案却很简单,动起来就是动画。那么什么是动起来呢?
- 屏幕刷新的新一帧界面和上一帧界面不一样,画面就动了起来,可以说这是广义的动画。
- 现在的设备普遍支持 60 帧每秒的刷新率,由于人眼的视觉暂留效果,当屏幕以 60 帧每秒的刷新率刷新,人看起来感觉很流畅。这可以说是狭义的动画,重要是流畅。
StatefulWidget 刷新¶
在第四课入门中,我们其实已经讲过,Flutter 为声明式的 UI 框架,声明式的核心是下面的这个公式:
Text Only | |
---|---|
1 |
|
对于动画来说,这个公式中有用的点为:states
改变时,UI
改变。简单来说,我们想要让画面动起来,只需要让 states
动起来即可。所以,在 Flutter 中做动画,使用 StatefulWidget
是完全足够的。
钟表秒针案例¶
我们来制作一个钟表来感受通过改变数值来改变对动画的刷新。案例中的钟表只显示秒针,每当用户点击,秒数加一,秒针往前走一格。
注:本节课的所有钟表秒针案例代码可以在 GitHub | thu-mobile-dev/running_clock 的 codes/
文件夹中获取,复制粘贴至 lib/main.dart
中即可运行查看效果。
无状态¶
我们先构建出来 ClockView
,其接受一个参数 seconds
,根据秒数来决定屏幕上秒针的位置,如 ClockView(seconds: 45)
。
Dart | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
|
ClockBorderView
使用DecoratedBox
的边框绘制一个圆形ClockHandView
使用VerticalDivider
绘制秒针ClockView
中使用Stack(alignment: Alignment.topCenter, ...)
来将秒针指向正上方ClockHandView
使用Transform.rotate
来确定秒针位置- 查看布局情况,可以将
main()
中的debugPaintSizeEnabled = true;
取消注释。
有状态¶
要想让界面动起来,我们需要有一个状态来存储秒数,还需要一种方式来改变秒数的值。我们需要一个 StatefulWidget
。
注:下面的代码省去 ClockView
ClockHandView
ClockBorderView
。
Dart | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
ClickableClockView
有一个状态seconds
,在GestureDetector.onTap()
中我们在setState()
中对其进行修改,这样ClockView
会得到刷新。
这样我们就实现了一个最简单的动画,用户点击,界面变化。
但是现在的界面有一个问题,相邻的秒之间是跳变的,也就是说,每次用户点击秒针旋转的时候,我们都觉得不流畅。要想让这种变化流畅起来,我们需要每秒 60 帧的动画,下面我们就会提到。
流畅刷新(Explicit Animation)¶
注:这里我们在标题中引入 Explicit Animation,但是后面讲到 Implicit Animation 是才会对比说明意思。这里暂时先只考虑「流畅刷新」。
这一部分的三个重点是 Animation<double>
AnimationController
AnimatedWidget
。可以先查看它们的继承关系:
Object
>Listenable
>Animation<double>
>AnimationController
Object
>Widget
>StatefulWidget
>AnimatedWidget
Animation¶
可以看到,Animation<double>
继承自 Listenable
。
Listenable
(abstract class Listenable
)是一个抽象类,存储着「接收者(listeners
)」列表,方法有addListener()
和removeListener()
。ValueListenable<T>
(abstract class ValueListenable<T> extends Listenable
)是Listenable
的一个子类,添加了一个值为T value
,当value
发生改变的时候,listeners
得到通知。Animation<T>
(abstract class Animation<T> extends Listenable implements ValueListenable<T>
)结合两者,添加了一个属性AnimationStatus status
,其取值有dismissed
(动画未开始)forward
(动画正在前进)reverse
(动画正在后退)completed
(动画已完成)。当T value
或者AnimationStatus status
发生改变时,订阅的listeners
会收到通知。
可以说,Animation<T>
类似一个状态,当这个状态(value
和 status
)的值发生改变时,使用这个状态的类(如 Widget
)会收到改变的通知,从而可以重新渲染刷新界面。
一般动画随着时间变化,所以最常用的动画是 Animation<double>
,其中 value
的取值为 [0.0, 1.0]
。这相当于一个时间轴,0.0
表示动画还没开始,1.0
表示动画已经结束。
AnimationController¶
在实际使用动画时,我们会使用 Animation<T>
的子类 AnimationController
,因为其中包含了驱动 Animation<T>
。如果说 Animation<double>
是时间轴,那么 AnimationController
就有着让时间前进或者后退的控制器。我们之后会重点关注 AnimationController
,其重要的属性和方法如下:
- 属性
double value
动画的值AnimationStatus status
动画的状态double lowerBound
value
的最小值,默认 0.0double upperBound
value
的最大值,默认 1.0Duration? duration
动画的持续时间
- 方法
forward({double? from}) → TickerFuture
开始正向的动画animateTo(double target, {Duration? duration, Curve curve = Curves.linear}) → TickerFuture
开始正向的动画reverse({double? from}) → TickerFuture
开始反向的动画animateBack(double target, {Duration? duration, Curve curve = Curves.linear}) → TickerFuture
开始正向的动画repeat({double? min, double? max, bool reverse = false, Duration? period}) → TickerFuture
循环播放动画stop({bool canceled = true}) → void
reset() → void
注:这里所说的「开始动画」,不是我们一般说的屏幕动起来,这里还没有提到任何屏幕刷新的事情。「动画」指的是 Animation<T>
也就是从 0.0 到 1.0 变化的一个值。「开始动画」指的是这个值开始变化(增加或减少)。
AnimationController
的构造函数如下:
Dart | |
---|---|
1 |
|
可以看到必须要传入的参数有 TickerProvider vsync
。对于这个参数的解释如下:对于连续刷新的动画,每秒要做到刷新 60 帧、或者 120 帧,这取决于设备屏幕的情况。我们需要有一个触发器来在每帧实际呈现在屏幕上之前、还在渲染的时候,告诉 AnimationController
让它根据当前的 value
和 设置的 duration
来计算出下一帧的 value
。这个触发器就是 vsync
。(Ticker
的作用是每一帧都会进行一次触发。)
关于 vsync
的获取,官方提供的方法是:
If you are creating an
AnimationController
from a State, then you can use theTickerProviderStateMixin
andSingleTickerProviderStateMixin
classes to obtain a suitableTickerProvider
.
也就是说,我们可以在 StatefulWidget
中直接拿到这个值,下面的案例会进行演示。
AnimatedWidget¶
刚刚我们也提到,AnimationController
控制一个值增大或减小,但是我们怎么将这个值呈现在屏幕上呢?当然是需要使用一个 StatefulWidget
,不过我们这里可以方便的使用 AnimatedWidget
。构造函数如下:
Dart | |
---|---|
1 |
|
注:通过继承关系 Object
> Widget
> StatefulWidget
> AnimatedWidget
和 Object
> Widget
> StatefulWidget
> AnimatedBuilder
可以看到,AnimatedWidget
和 AnimatedBuilder
的地位相近,作用是相似的,只是写法不同。
可以看到我们需要传入一个 Lisenable
,当 Lisenable
改变时,AnimatedWidget
就可以接收到,从而刷新界面。
结合上面所说的继承关系:Object
> Listenable
> Animation<double>
> AnimationController
,我们可以将 AnimationController
传入 AnimatedWidget
。
流畅的钟表秒针案例¶
刚刚的案例中,相邻两秒的秒针渲染很生硬。我们接下来会在相邻两秒的秒针旋转之间添加一秒的动画,来模拟实际旋转的情况。
注:下面的代码省去 ClockView
ClockHandView
ClockBorderView
。
Dart | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
|
我们从上往下看:
AnimatedClockView
继承了AnimatedWidget
。- 接受参数
AnimationController controller
用于初始化AnimatedWidget.listenable
,接受参数intSeconds
用于初始化自身的intSeconds
。每当lisenable
改变时(controller
发出通知),AnimatedWidget
重新绘制界面。 - 在
build()
中,向ClockView
传递的seconds
是由intSeconds
和controller.value
相加得到的值。
- 接受参数
ClickableAnimatedClockView
是一个StatefulWidget
,状态有int seconds
和AnimationController controller
。class _ClickableAnimatedClockViewState extends State<ClickableAnimatedClockView> with TickerProviderStateMixin
使得在AnimationController
初始化时可以直接将this
传入vsync
,这是官方推荐的获取TickerProvider
的做法。使用switch-case
语句对controller.status
进行判断,只有当状态为AnimationStatus.dismissed
时用户的点击才有效:使用controller.forward()
让动画开始,当动画结束时,对seconds
加一,将动画reset()
。
这样我们再进行点击,可以看到相邻两秒内的动画非常流畅,时间间隔也确实是一秒。
FooTransition¶
查看继承关系:
Object
> DiagnosticableTree
> Widget
> StatefulWidget
> AnimatedWidget
> FooTransition
可以看到,FooTransition
与 AnimatedWidget
的作用基本一致,但使用起来更方便,代码的层级关系和功能也更容易看出。
你可以在 AnimatedWidget
的文档 中找到 Flutter 内置的所有 FooTransition
。
我们完全可以用 RotationTransition
(RotationTransition({Key? key, required Animation<double> turns, Alignment alignment = Alignment.center, FilterQuality? filterQuality, Widget? child})
)改写上面的案例,将 ClockHandView
用 RotationTransition
包裹,将 AnimationController
转为 Animation
传入 turns
参数。
这里不提供具体的代码实现,在当前的案例中 RotationTransition
的意义不大,需要逐层传递 animation
或 controller
与直接传递 seconds
的思路是一样的。
Implicit Animation¶
上面讲的所有内容,其实只是 Flutter 中众多动画的一种:Explicit Animation。那么它明确(explicit)在哪里呢?我们使用 AnimationController
来使动画前进或停止,这是由开发者指定的。另一种动画,Implicit Animation,则通过 Flutter 框架本身来操作动画的进展,也就是说,并不再需要开发者手动创建管理 AnimationController
了。
自动的钟表秒针案例¶
注:下面的代码省去 ClockView
ClockHandView
ClockBorderView
。
Dart | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
- 使用
TweenAnimationBuilder
,其中Tween
可以理解开头和结尾的两个值,TweenAnimationBuilder
负责将这两个值插值,将得到的值传递给builder
的double
,案例中命名为tweenSeconds
,将这个插值得到的值直接传给ClockView
。
运行看到效果和上面的 Explicit Animation 一模一样,但是代码量急剧减少。事实上,当需要对动画精确控制时,我们才会需要考虑 Explicit Animation,大多数的动画,只是给出一个初值、一个结束值、动画的持续时间,然后框架自动触发这个动画就好了。Implicit Animation 对平凡的开发者来说是应用动画的一大福音。
AnimatedFoo¶
基于下面的继承关系:
Object
> Widget
> StatefulWidget
> ImplicitlyAnimatedWidget
> AnimatedFoo
完全可以用 AnimatedRotation
来对上面的案例进行改写,课程不呈现这部分代码。在大多数的时间里中,使用 Flutter 已有的 AnimatedFoo
能够使代码更加简洁清晰。
你可以在 ImplicitlyAnimatedWidget
的文档 中找到 Flutter 内置的所有 AnimatedFoo
。
Curve¶
我们之前提到 Animation<double>
从 0.0 到 1.0,使用 AnimationController
让其前进,通过 animation.value
取出其值。在这个过程中,从 0.0 到 1.0 的过程是均匀的,也就是说,变化的速度为 (1.0 - 0.0) / controller.duration
。这被成为线性动画。
现实中的动画从 0.0 到 1.0 则有着多种变化方式,在 Flutter 中,Curve
表述这种变化,你可以在 Curves
中看到多种变化的曲线,在代码中可以进行添加。
你也可以自定义 Curve
,比如下面的代码(来自 YouTube | Animation Basics with Implicit Animations):
Dart | |
---|---|
1 2 3 4 5 6 7 8 |
|
其他动画¶
也有一些比较常用的动画,Flutter 框架做了适当的封装。比如 Hero animations 和 Staggered Animations,同学可以查看 Flutter Animations | Common animation patterns 获取更多信息。
Hero Animation¶
Hero Animation 从效果上来说,就是 macOS Keynote(如果有同学用过) 的 Magic Move。当两个场景有相同元素的时候,使用 Hero Animation 可以在两个场景之间流畅转换。
一个简单的使用场景是,用户点击图库中的一张图片,然后图片放大铺满全屏幕。在这个场景中,图库中的图片需要用 Hero
包裹起来,全屏幕的图片也用 Hero
包裹起来,给它们添加相同的 tag
,在页面切换时 Flutter 会帮你完成转换的动画。
Staggered Animation¶
直观来说,Staggered Animation 类似分段函数,在 Animation<double>
0.0 到 1.0 的时间段中使用不同的曲线对不同的属性进行修改。感兴趣的同学可以查看 Flutter Animations | Staggered animations
绘制类动画¶
对于 Explicit Animation 和 Implicit Animation,它们都是针对 Widget 的一些属性进行插值得到流畅的动画,但是如果一些动画很难通过 Widget 进行表达,那么在它就脱离了 Flutter 动画的框架。不过 Flutter 也提供了底层的动画接口,开发者可以通过使用一些第三方包来得到绘制类动画。课程不对此类动画做要求。
学习资源¶
这一节课讲动画比较抽象,同学可以查看 Flutter 官方推出的系列视频教程 来复习课程中提到的内容。里面还提到了动画选取的方法。
官方文档的动画部分,同学们也可以阅读,加深对动画的理解。