动画
理解原理
人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”,又称“余晖效应”。
静态的画面之所以能够运动,正是基于这一原理。具体应用则是电影放映,动画片播放等场景。

每张画面帧的快速切换

相关概念
帧
帧是影像动画中单幅画面,一帧就是一幅静止的画面。例如电影胶片中的每一格即为一帧。
FPS
FPS(Frame Per Second)指每秒显示的帧数。例如电影每秒播放24帧,即帧率为24FPS。帧率越大显示的画面越流畅。
刷新频率
通常指显示器的刷新频率,市面上的显示器刷新频率主要为60Hz或120Hz。这里60Hz表示屏幕一秒钟可以刷新60张画面。
FPS帧率是由GPU决定,刷新频率是由显示器决定,显示器的刷新频率在物理上约束了帧数的表现上限。
Flutter 动画
详见Flutter官方文档 Flutter中的动画
Flutter中的动画可以分为两种类型
- 补间动画(Tween Animation)
- 基于物理的动画(Physics-based animation)
基于物理的动画是一种遵循物理学定律的动画形式。例如弹簧、阻尼效果等等
三种动画模式
- 列表或网格动画(Animated list or grid)。指item的添加或删除操作,见
AnimatedList示例 共享元素转换(Shared element transition)。实现路由(页面)之间的共享元素过渡动画,通常称为Hero动画
交错动画(Staggered animation)。动画被分解为较小的动作,它们可以是连续的,或者可以部分或完全重叠。
插值器
主要描述值的变化规律(匀速、加速),决定变化的趋势,能更细腻的表达运动的物理特性。插值器通常是一个数学函数。简单说,插值器就是根据时间流失的百分比 计算当前属性改变的百分比
估值器
插值器描述了变化规律,接下来则需要交给估值器计算出具体的数值。因此估值器的主要作用就是协助插值器实现非线性运动的动画。
简单说,估值器就是根据当前属性改变的百分比来计算改变后的属性值
总结,插值器决定属性值随时间变化的规律;而具体变化属性数值则交给估值器去计算
相关对象
Ticker
可以被应用在 Flutter 的每个对象中,当对象实现了 Ticker 的功能后,每次动画帧改变便会通知该对象。Flutter 提供了 TickerProvider 类型,但它是一个抽象类,它有两个具体子类可快速实现该功能,SingleTickerProviderStateMixin和TickerProviderStateMixin。前者只适用包含单个AnimationController的情况,如果你有多个AnimationController,则应使用后者。
用法:如在有状态控件下使用动画时,通常在 State 对象下混入 TickerProviderStateMixin。
AnimationController
即动画控制器。用来控制动画,如动画的启动、暂停等。它接受两个参数,第一个是 vsync,必须是一个 Ticker 对象,其作用是当接受到来自 tweens 和 curves 的新值后通知对应对象,第二个参数是 duration, 为动画持续的时长。
要创建一个动画,首先要创建一个AnimationController。一旦创建了它,你就可以开始基于它构建其他动画。
常用方法:
forward()从头向尾正向启动动画reverse()从尾到头逆向执行动画fling()使用阻尼效果驱动动画repeat()正向运行此动画,并在完成后重复执行此过程reset()将控制器的值重置为开始值(如果正在进行则停止动画)stop()停止运行动画dispose()释放资源
Animation
是Flutter动画库中的一个核心类,也是一个抽象类,表示一个特定类型的值。其实它封装了动画过程中的值和状态。大多数执行动画的widgets都会收到一个Animation类型对象作为参数,它们从这个对象中读取动画的当前值,并监听该值的变化。
简单说,它生成指导动画的值,知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但它不知道屏幕上显示的内容。
主要是四种状态类型
AnimationStatus.forward动画从头到尾执行AnimationStatus.reverse动画从尾到头执行AnimationStatus.completed动画已执行完成。注意,此状态指动画在终点停止AnimationStatus.dismissed动画处于停止状态。注意,此状态指动画回到起始点停止,即调用了reverse()方法
它的具体实现类:
AlwaysStoppedAnimation始终以给定值停止的动画。该状态始终为AnimationStatus.forwardCompoundAnimation一个用于组合多个动画的接口。子类只需要实现取值器来控制子动画的组合方式。可以链式组合2个以上的动画CurvedAnimation将曲线应用于另一个动画的动画。当你想对动画对象应用非线性曲线时,它很有用ProxyAnimation作为另一个动画的代理的动画ReverseAnimation与另一个动画相反的动画(反向动画)TrainHoppingAnimation接收两个父类,并在它们的值交叉时在它们之间切换AnimationController动画的控制器
需要注意,AnimationController 也是它的一个实现类。大多数 Animation 子类都采用明确的 “父级提供的” Animation<double>。可以说它们是由父级驱动的。
CurvedAnimation 子类接收一个 Animation<double>类(父级)和几个 Curve 类(正向和反向曲线)作为输入,并使用父级的值作为输入提供给曲线来确定它的输出。CurvedAnimation 是不可变和无状态的。
ReverseAnimation 子类接收一个 Animation<double> 类作为它的父级,但反转动画所有的值。父级动画的状态和方向也会被反转。ReverseAnimation 是不可变和无状态的。
ProxyAnimation 子类接收一个 Animation<double> 类作为其父级,并仅转发该父级的当前状态。然而,父级是可变的。
Tween
继承自Animatable,实际上它是一个补间值生成器。相当于在一个固定的时间内,生成一系列从begin到end的数值。对应到前面所述的概念,可以将之理解为一个线性的估值器。
默认情况下,Flutter 中的动画将任何给定时刻的值映射到介于 0.0 和 1.0 之间的 double 值。
Flutter 框架也为我们封装了许多用于具体属性动画的Tween类实现:
- AlignmentGeometryTween
- AlignmentTween
- BorderRadiusTween
- BorderTween
- BoxConstraintsTween
- ColorTween
- ConstantTween
- DecorationTween
- EdgeInsetsGeometryTween
- EdgeInsetsTween
- FractionalOffsetTween
- IntTween
- MaterialPointArcTween
- Matrix4Tween
- RectTween
- RelativeRectTween
- ReverseTween
- ShapeBorderTween
- SizeTween
- StepTween
- TextStyleTween
- ThemeDataTween
Curve(动画曲线)
用来调整动画过程中随时间的变化率,默认情况下,动画以均匀的线性模型变化。我们可以通过继承 Curve 类来定义动画的变化率,比如创建加速、减速或者先加速后减速等曲线模型。可以将之理解为一个插值器。
Flutter 内部也提供了一系列实现相应变化率的 Curves 对象,见文档Curves 文档
同时,我们还可以通过继承Curve类重写transform方法来自定义自己的非线性插值器。
class MyCurve extends Curve{
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
动画流程
总的来说,Flutter中的动画系统基于Animation对象,widget应该在build函数中读取Animation对象的当前值, 并且可以监听动画的状态改变。代码中我们使用AnimationController管理Animation,并设置监听。

动画监听
上图解析了Flutter整个动画的流程。但中间还缺了一个环节,即补间值不断生成,如何通知UI页面刷新呢?
这里,Animation 对象分别可以设置两种监听器
- 动画帧监听器。用于根据生成的补间值不断更新动画帧。
- 动画状态监听器。用于动画执行过程中的控制。
/// 注册帧监听器
_animation.addListener(() {
setState(() {});
});
/// 注册动画状态监听器
_animation.addStatusListener((status) {
print(status);
});
代码示例
以下是一个简单的匀速线性补间值生成示例
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
Animation animation = Tween<double>(begin: 0.0, end: 10.0).animate(_controller);
animation.addListener(() {
// 在回调中输出生成的值
print(animation.value);
});
// 向前开始
_controller.forward();
请注意,获取补间值有两种方式,以上调用Tween的animate方法返回一个animation对象,然后通过value获取值,这是最常用的一种方式。除此外,还可以使用如下方式获取
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
Tween tween = Tween<double>(begin: 0.0, end: 10.0);
_controller.addListener(() {
// 输出补间值
print(tween.evaluate(_controller));
});
// 向前开始
_controller.forward();
直接使用匀速线性的插值器,动画看起来会生硬死板,缺乏细腻。为此我们需要添加非线性插值器。
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 600));
/// 给动画添加非线性插值器
CurvedAnimation curvedAnimation = CurvedAnimation(parent: _controller,curve:Curves.decelerate);
_animation = Tween<double>(begin: 100.0, end: 150.0).animate(curvedAnimation);
_animation.addListener(() {
setState(() {});
});
_animation.addStatusListener((status) {
if(AnimationStatus.completed == status){
_controller.reverse();
}else if(AnimationStatus.dismissed == status){
_controller.forward();
}
});
// 向前开始
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: _animation.value,
height: _animation.value,
decoration: BoxDecoration(
image: DecorationImage(image: NetworkImage("https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/heartshaped.png"))
),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: ()=>_controller?.stop(),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
添加非线性插值器,还有另一种写法,这里使用CurveTween而不是CurvedAnimation
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 600));
/// 给动画添加非线性插值器
Animatable tween = Tween<double>(begin: 100.0, end: 150.0).chain(CurveTween(curve: Curves.decelerate));
_animation = _controller.drive(tween);
_animation.addListener(() {
setState(() {});
});
_animation.addStatusListener((status) {
if(AnimationStatus.completed == status){
_controller.reverse();
}else if(AnimationStatus.dismissed == status){
_controller.forward();
}
});
// 向前开始
_controller.forward();
动画控件
为了简化动画代码的编写,提高开发效率,Flutter为我们提供了许多动画控件。
AlignTransition,它是Align的动画版本DecoratedBoxTransition,它是DecoratedBox的动画版本DefaultTextStyleTransition,它是DefaultTextStyle的动画版本PositionedTransition,它是Positioned的动画版本RelativePositionedTransition,这是Positioned的动画版本RotationTransition,它是RotationTransition的动画版ScaleTransition,可对小控件的缩放进行动画处理SizeTransition,可对其自身的大小进行动画处理SlideTransition,可使小控件的位置相对于其正常位置进行动画处理FadeTransition,这是Opacity的动画版本AnimatedModalBarrier,阻止用户与其自身背后的小部件进行交互的控件,并可以配置一个动画颜色值
自定义动画控件
主要是根据自己的业务逻辑来封装动画控件,以到达复用代码、简化逻辑的目的。
AnimatedWidget要做动画的Widget必须继承自AnimatedWidget。该类主要封装了动画状态监听和页面的刷新逻辑。AnimatedBuilder它将显示内容和动画逻辑分离(职责分离),更加方便的为特定的显示内容添加具体的动画。简单说,它的作用就是将Animation和要作用的Widget关联起来。
隐式动画控件
利用AnimatedWidget已经可以方便地封装出一系列动画控件,但这种实现方式还需要我们自己提供 Animation 对象,然后通过提供的接口方法来启动我们的动画,控件的属性由 Animation 对象提供并在动画过程中改变而达到动画的效果。为了使动画更加简单方便,Flutter 框架还提供了一种更简单的方式实现了动画效果,即隐式动画控件(ImplicitlyAnimatedWidget)。
通过隐式动画控件,我们不需要手动实现补间值生成器、曲线等对象,也不需要使用 AnimationController 来控制动画,它的使用体验更接近普通控件,我们只需要通过 setState 方法改变隐式动画控件的属性值,其内部自行为我们实现动画过程的过渡效果,即隐藏了所有动画实现的细节。当然,它的缺陷也很明显,除了动画控件的属性值,开发人员只能为动画设置duration和curve,无法做到精细的控制。
同样的,Flutter 内部也为我们提供了多个实用的隐式动画控件:
- TweenAnimationBuilder,将Tween表示的任何属性动画化为指定的目标值
- AnimatedAlign,这是Align的隐式动画版本
- AnimatedContainer,它是Container的隐式动画版本
- AnimatedDefaultTextStyle,它是DefaultTextStyle的隐式动画版本
- AnimatedOpacity,它是Opacity的隐式动画版本
- AnimatedPadding,这是Padding的隐式动画版本
- AnimatedPhysicalModel,它是PhysicalModel的隐式动画版本
- AnimatedPositioned,这是Positioned的隐式动画版本
- AnimatedPositionedDirectional,这是PositionedDirectional的隐式动画版本
- AnimatedTheme,这是主题的隐式动画版本
- AnimatedCrossFade,它在两个给定子项之间进行淡入淡出,并在其大小之间进行动画处理
- AnimatedSize,它会在给定的持续时间内自动转换其大小
- AnimatedSwitcher,从一个小控件淡入另一个小控件
动画拓展
Interval用于延缓动画。例如,一个时长6秒的动画,如果使用Interval,其开始时间设置为0.5,结束时间设置为1.0,那么基本上就会变成一个时长3秒的动画,且在3秒后开始。TweenSequence用于定义补间序列。创建一个动画,其值由Tweens序列定义,每个TweenSequenceItem都有一个权重,定义其占动画持续时间的百分比。
交错动画
详见官方示例 交错动画

路由过度动画
其中secondaryAnimation表示,当导航器将新路由推入其堆栈的顶部时,旧的顶层路由的secondaryAnimation从0.0到1.0运行。
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return FadeTransition(
//使用渐隐渐入动画,
opacity: animation,
child: PageB(),
);
},
),
);
Hero动画
使用Hero控件包装需要共享的元素,并设置tag属性,一个用于唯一标识的字符串,在另一个路由中也需要指定该tag。
FirstPage页
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'second_page.dart';
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 此属性用于调试动画,这里以5倍慢放动画
timeDilation = 5.0;
return Scaffold(
appBar: AppBar(
title: Text("第一页"),
),
body: Container(
alignment: Alignment.topLeft,
child: InkWell(
child: Hero(
tag: "sprite",
child: Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/levitation_sprite.jpg",
width: 100,height: 100,),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder:(c)=>SecondPage()));
},
),
),
);
}
}
SecondPage页
import 'package:flutter/material.dart';
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Container(
alignment: Alignment.bottomRight,
child: Hero(
tag: "sprite",
child: Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/levitation_sprite.jpg",
width: 150,height: 150,),
),
),
);
}
}
动画库的使用
animations
官方在Flutter 1.17版本时,为我们提供了一个高质量的动画库 animations,其中提供了一些过度动画。
Flare 动画

Flare为应用程序和游戏设计师提供了强大的实时矢量设计和动画。Flare的主要目标是让设计师直接使用在最终产品中运行的资产,而无需在代码中重做这些工作。
关于Flare与Rive,官方公告如下
最近,我们将旧的公司名称(2Dimensions)和旧的产品名称(Flare)合并为一个新的品牌(Rive)。
一段时间以来,我们一直希望将我们以前的公司名称2Dimensions与主要产品Flare整合在一起。我们不到一年前就推出了Flare,今年已经非常清楚地表明,我们公司的未来就是这种产品。我们希望向用户传达一个强烈的信息,即我们正在认真投资产品并壮大团队。
合并公司和产品的名称也是简化我们的消息传递和展示我们的关注点的明确方法。
起初,合乎逻辑的解决方案似乎是将我们的公司名称改为Flare。我们喜欢这个简单的名字和文字游戏(在你的应用程序中添加 "Flare")。但我们很快意识到,这是一个无处不在的词,被许多其他公司、品牌和产品使用。
简而言之,我们需要一个既能代表我们喜爱的公司及其个性特征的品牌,又能成长,保护和称呼自己的品牌。
那Nima呢?
Nima是我们的第一个产品。它是Rive的基础。您仍然可以创建 Nima 文件,但我们很快就会添加支持在 Rive 中打开您的 Nima 文件。你仍然可以在Rive中做你目前能用Nima做的一切事情。届时,当Nima的所有功能都将被添加到Rive中时,我们将从网站中移除Nima,进一步简化我们的用户体验和消息传递。
Rive是一款实时交互式设计工具,它允许你设计、制作动画,并立即将你的资产整合到任何平台上。 Rive有两个核心部分:编辑器和运行时。编辑器是你创建设计和制作动画的地方。运行时是开源库,允许你用Swift、Flutter、Android、JavaScript/WebGL、React、C++等语言加载和操作Rive文件(我们正在开发更多的语言)。Rive格式和运行时都是开源的,并通过MIT许可提供。
目前Rive 是以工程形式管理、创建动画项目,支持2种工程类型:
Flare:为App和Web构建实时、快速的动画Nima:主要是为游戏引擎和应用构建2D动画
需要注意,Flare 项目导出的是 .flr 格式文件,依赖于flare_flutter库解析。而Nima 项目导出文件有2个:一个是png,一个.nma 文件,需手动把 .nma改成 .nima,然后把这2个文件都放到 asste 资源文件夹中,并依赖nima库解析。
SVGA 动画
动画设计师专注动画设计,通过工具输出 svga 动画文件,提供给开发工程师在集成 svga player 之后直接使用。
Flutter 解析库 svgaplayer_flutter
公众号“编程之路从0到1”