如何使用动画库中的基本类为 widget 添加动画。
AnimatedWidget
和 AnimatedBuilder
的应用区别。
本教程将讲解如何在 Flutter 中构建显式动画。我们先来介绍一些动画库中的基本概念,类和方法,然后列举五个动画示例。这些示例互相关联,展示了动画库的不同方面。
Flutter SDK 也提供过渡动画,比如 FadeTransition,SizeTransition 和 SlideTransition。这些简单的动画可以通过设置起点和终点来触发。它们比下面介绍的显式动画更容易实现。
1. 基本动画概念和类
Animation
,Flutter 动画库中的核心类,插入用于指导动画的值。
Animation
对象知道动画目前的状态(例如,是否开始,暂停,前进或倒退),但是对屏幕上显示的内容一无所知。
AnimationController
管理 Animation
。
CurvedAnimation
定义进程为非线性曲线。
Tween
为动画对象插入一个范围值。例如,Tween
可以定义插入值由红到蓝,或从 0 到 255。
使用 Listeners 和 StatusListeners 监视动画状态变化。
Flutter 中的动画系统基于类型化的 Animation 对象。Widgets 既可以通过读取当前值和监听状态变化直接合并动画到 build 函数,也可以作为传递给其他 widgets 的更精细动画的基础。
1.1 Animation
在 Flutter 中,动画对象无法获取屏幕上显示的内容。Animation
是一个已知当前值和状态(已完成或已解除)的抽象类。一个比较常见的动画类型是 Animation
。
一个 Animation
对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation
对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation
对象的不同控制方式,它可以反向运行,或者中途切换方向。
动画还可以插入除 double 以外的类型,比如 Animation
或者 Animation
。
Animation
对象具有状态。它的当前值在 .value
中始终可用。
Animation
对象与渲染或 build()
函数无关。
1.2 CurvedAnimation
CurvedAnimation 定义动画进程为非线性曲线。
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
import 'dart:math';
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
CurvedAnimation
和
AnimationController
(下面将会详细说明)都是
Animation
类型,所以可以互换使用。
CurvedAnimation
封装正在修改的对象 — 不需要将
AnimationController
分解成子类来实现曲线。
AnimationController
是个特殊的
Animation
对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,
AnimationController
在给定期间内会线性生成从 0.0 到 1.0 的数字。例如,这段代码创建了一个动画对象,但是没有启动运行。
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
源自于
Animation
,所以可以用在任何需要
Animation
对象的地方。但是
AnimationController
还有其他方法控制动画。例如,使用
.forward()
方法启动动画。数字的生成与屏幕刷新关联,所以一般来说每秒钟会生成 60 个数字。数字生成之后,每个动画对象都调用附加 Listener 对象。为每个 child 创建自定义显示列表,请参考
RepaintBoundary
。
AnimationController
的同时,也赋予了一个
vsync
参数。
vsync
的存在防止后台动画消耗不必要的资源。您可以通过添加
SingleTickerProviderStateMixin
到类定义,将有状态的对象用作 vsync。可参考 GitHub 网站 animate1 中的示例。
AnimationController
的 0.0-1.0 的范围。例如,
fling()
函数允许提供速度,力和位置(通过 Force 对象)。这个位置可以是任意的,所以可能会超出 0.0-1.0 的范围。
AnimationController
在范围内,
CurvedAnimation
也可能会出现超出 0.0-1.0 范围的情况。根据所选曲线的不同,
CurvedAnimation
的输出范围可能会超过输入。举个例子,弹性曲线(比如Curves.elasticIn)会明显超出或低于默认范围。
AnimationController
对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用
Tween
配置动画来插入不同的范围或数据类型。例如下面的示例中,
Tween
的范围是 -200 到 0.0。
tween = Tween(begin: -200, end: 0);
Tween
是无状态的对象,只有
begin
和
end
。
Tween
的这种单一用途用来定义从输入范围到输出范围的映射。输入范围一般为 0.0-1.0,但这并不是必须的。
Tween
源自
Animatable
,而不是
Animation
。像动画这样的可动画元素不必重复输出。例如,
ColorTween
指定了两种颜色之间的过程。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
对象不存储任何状态。而是提供
evaluate(Animation animation)
方法,将映射函数应用于动画当前值。
Animation
对象的当前值可以在
.value
方法中找到。evaluate 函数还执行一些内部处理内容,比如确保当动画值在 0.0 和1.0 时分别返回起始点和终点。
1.4.1 Tween.animate
Tween
对象,请在
Tween
调用
animate()
,传入控制器对象。例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation alpha = IntTween(begin: 0, end: 255).animate(controller);
animate()
方法会返回一个
Animation
,而不是
Animatable
。
Tween
。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation alpha = IntTween(begin: 0, end: 255).animate(curve);
Animation
对象可以有不止一个
Listener
和
StatusListener
,用
addListener()
和
addStatusListener()
来定义。当动画值改变时调用
Listener
。
Listener
最常用的操作是调用
setState()
进行重建。当一个动画开始,结束,前进或后退时,会调用
StatusListener
,用
AnimationStatus
来定义。下一部分有关于
addListener()
方法的示例,在 2.3章节
监控动画过程
中也有
addStatusListener()
的示例。
addListener()
和
setState()
为 widget 添加基础动画。
addListener()
函数就会调用
setState()
。
AnimatedController
with the required
vsync
parameter. 如何使用所需的
vsync
参数定义一个
AnimatedController
。
..addListener
” 中的 “
..
” 语法,也称为 Dart 的 cascade notation。
_
)。
Animation
对象,需将
Animation
对象存储为您的 widget 成员,然后用它的值来决定如何绘制。
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: FlutterLogo(),
),
);
}
}
源代码 animate0:
https://github.com/cfug/flutter.cn/tree/master/examples/animation/animate0
AnimationController
时,必须要使用一个
vsync
对象。在
AnimationController
部分会具体介绍
vsync
参数。
源代码 animate1:
addListener()
函数调用
setState()
,所以每次
Animation
生成一个新的数字,当前帧就被标记为 dirty,使得
build()
再次被调用。在
build()
函数中,container 会改变大小,因为它的高和宽都读取
animation.value
,而不是固定编码值。当动画结束时要清除控制器以防止内存溢出。
..addListener()
中的两点)不太熟悉。这个语法意思是使用
animate()
的返回值调用
addListener()
方法。参考下面示例:
animation = Tween(begin: 0, end: 300).animate(controller)
..addListener(() {
// ···
});
animation = Tween(begin: 0, end: 300).animate(controller);
animation.addListener(() {
// ···
});
AnimatedWidget
帮助类(代替
addListener()
和
setState()
)创建动画
widget
。
AnimatedWidget
创建一个可以运行重复使用动画的 widget。如需区分 widget 过渡,可以使用
AnimatedBuilder
。
AnimatedWidget
:AnimatedBuilder, AnimatedModalBarrier, DecoratedBoxTransition, FadeTransition, PositionedTransition, RelativePositionedTransition, RotationTransition, ScaleTransition, SizeTransition, SlideTransition。
AnimatedWidget
基本类可以从动画代码中区分出核心 widget 代码。
AnimatedWidget
不需要保持
State
对象来 hold 动画。可以添加下面的
AnimatedLogo
类:
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation animation = listenable;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
在绘制时,AnimatedLogo
会读取 animation
当前值。
LogoApp
持续控制
AnimationController
和
Tween
,并将
Animation
对象传给
AnimatedLogo
:
addStatusListener()
作为动画状态的变更提示,比如开始,结束,或改变方向。
addStatusListener()
来获得提示。下面是之前示例修改后的代码,这样就可以监听状态的改变和更新:
class _LogoAppState extends State with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween(begin: 0, end: 300).animate(controller)
..addStatusListener((state) => print('$state'));
controller.forward();
}
// ...
}
AnimationStatus.forward
AnimationStatus.completed
下一步,在起始或结束时,使用 addStatusListener()
反转动画。制造“呼吸”效果:
源代码 animate3:
https://github.com/cfug/flutter.cn/tree/master/examples/animation/animate3
2.4 使用 AnimatedBuilder 进行重构
AnimatedBuilder
知道如何渲染过渡效果
AnimatedBuilder
不会渲染 widget,也不会控制动画对象。
AnimatedBuilder
描述一个动画是其他 widget 构建方法的一部分。
如果只是单纯需要用可重复使用的动画定义一个 widget,可参考
AnimatedWidget.
。
AnimatedBuilders
:
BottomSheet
,
ExpansionTile
,
PopupMenu
,
ProgressIndicator
,
RefreshIndicator
,
Scaffold
,
SnackBar
,
TabBar
,
TextField
。
animate3
示例代码中有个问题,就是改变动画需要改变渲染 logo 的widget。
较好的解决办法是,将任务区分到不同类里:
AnimatedBuilder
类方法来完成分配。
AnimatedBuilder
作为渲染树的一个单独类。
像
AnimatedWidget
,
AnimatedBuilder
自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用
addListener()
。
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
Widget build(BuildContext context) => Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
GrowTransition
中的
build()
方法创建的,如下。
GrowTransition
widget 本身是无状态的,而且拥有定义过渡动画所需的一系列最终变量。
build() 函数创建并返回
AnimatedBuilder
,
AnimatedBuilder
使用(
Anonymous
builder)方法并将 LogoWidget 对象作为参数。
渲染过渡效果实际上是在(
Anonymous
builder)方法中完成的,该方法创建一个适当大小
Container
强制
LogoWidget
配合。
AnimatedBuilder
,再传递给匿名闭包,然后用作 child 的对象。
最终结果就是
AnimatedBuilder
被插入渲染树的两个 widgets 中间。
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
animate2
的示例。
initState()
方法创建了
AnimationController
和
Tween
,然后用
animate()
绑定它们。
神奇的是
build()
方法,它返回一个以
LogoWidget
为 child 的
GrowTransition
对象,和一个驱动过渡的动画对象。
上面列出了三个主要因素。
AnimatedWidget
持续进行动画。
可以用在需要对透明度进行从透明到不透明动画处理的情况。
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween(begin: 0.1, end: 1).animate(controller);
通过 sizeAnimation.value
我们可以得到尺寸,通过 opacityAnimation.value
可以得到不透明度,但是 AnimatedWidget
的构造函数只读取单一的 Animation
对象。为了解决这个问题,该示例创建了一个 Tween
对象并计算确切值。
AnimatedLogo
来封装其
Tween
对象,以及其
build()
方法在母动画对象上调用
Tween.evaluate()
来计算所需的尺寸和不透明度值。
下面的代码中将这些改动突出显示。
class AnimatedLogo extends AnimatedWidget {
// Make the Tweens static because they don't change.
static final _opacityTween = Tween(begin: 0.1, end: 1);
static final _sizeTween = Tween(begin: 0, end: 300);
AnimatedLogo({Key key, Animation animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation animation = listenable;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
源代码 animate5:
https://github.com/cfug/flutter.cn/tree/master/examples/animation/animate5
3. 下面的步骤
Tweens
创建动画的基础介绍,还有很多其他类可供探索。比如指定
Tween
类,Material Design 特有的动画,
ReverseAnimation
,共享元素过渡(也称为 Hero 动画),物理模拟和
fling()
方法。