教程 | 在 Flutter 应用里实现动画效果
本教程将讲解如何在 Flutter 中构建显式动画。我们先来介绍一些动画库中的基本概念,类和方法,然后列举五个动画示例。这些示例互相关联,展示了动画库的不同方面。
Flutter SDK 也内置了显式动画,比如
FadeTransition
,SizeTransition
和 SlideTransition
。这些简单的动画可以通过设置起点和终点来触发。它们比下面介绍的显式动画更容易实现。
基本动画概念和类
#Flutter 中的动画系统基于类型化的 Animation
[] 对象。
Widgets 既可以通过读取当前值和监听状态变化直接合并动画到 build 函数,也可以作为传递给其他 widgets
的更精细动画的基础。
Animation<double>
#在 Flutter 中,动画对象无法获取屏幕上显示的内容。
Animation
是一个已知当前值和状态(已完成或已解除)的抽象类。一个比较常见的动画类型是 Animation<double>
。
一个 Animation
对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation
对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation
对象的不同控制方式,它可以反向运行,或者中途切换方向。
动画还可以插入除 double 以外的类型,比如 Animation<Color>
或者 Animation<Size>
。
Animation
对象具有状态。它的当前值在 .value
中始终可用。
Animation
对象与渲染或 build()
函数无关。
CurvedAnimation
#CurvedAnimation
定义动画进程为非线性曲线。
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation
和 AnimationController
(下面将会详细说明)都是 Animation<double>
类型,所以可以互换使用。
CurvedAnimation
封装正在修改的对象 —
不需要将 AnimationController
分解成子类来实现曲线。
AnimationController
#AnimationController
是个特殊的 Animation
对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController
在给定期间内会线性生成从 0.0 到 1.0 的数字。例如,这段代码创建了一个动画对象,但是没有启动运行。
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
源自于 Animation<double>
,所以可以用在任何需要 Animation
对象的地方。但是 AnimationController
还有其他方法控制动画。例如,使用 .forward()
方法启动动画。数字的生成与屏幕刷新关联,所以一般来说每秒钟会生成 60 个数字。数字生成之后,每个动画对象都调用附加 Listener 对象。为每个 child 创建自定义显示列表,请参考 RepaintBoundary
。
创建 AnimationController
的同时,也赋予了一个 vsync
参数。
vsync
的存在防止后台动画消耗不必要的资源。你可以通过添加 SingleTickerProviderStateMixin
到类定义,将有状态的对象用作 vsync。可参考 GitHub 网站 animate1
中的示例。
Tween
#在默认情况下,AnimationController
对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween
配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween
的范围是 -200 到 0.0。
tween = Tween<double>(begin: -200, end: 0);
Tween
是无状态的对象,只有 begin
和 end
。
Tween
的这种单一用途用来定义从输入范围到输出范围的映射。输入范围一般为 0.0-1.0,但这并不是必须的。
Tween
源自 Animatable<T>
,而不是 Animation<T>
。像动画这样的可动画元素不必重复输出。例如,ColorTween
指定了两种颜色之间的过程。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
A Tween
object doesn't store any state. Instead, it provides the
evaluate(Animation<double> animation)
method that uses the
transform
function to map the current value of the animation
(between 0.0 and 1.0), to the actual animation value.
Tween
对象不存储任何状态。而是提供 evaluate(Animation<double> animation)
方法,将映射函数应用于动画当前值。
Animation
对象的当前值可以在 .value
方法中找到。
evaluate 函数还执行一些内部处理内容,比如确保当动画值在 0.0 和1.0 时分别返回起始点和终点。
Tween.animate
#要使用 Tween
对象,请在 Tween
调用 animate()
,传入控制器对象。例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
下面的示例展示了一个控制器,一个曲线,和一个 Tween
。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
动画通知
#一个 Animation
对象可以有不止一个
Listener
和 StatusListener
,用 addListener()
和 addStatusListener()
来定义。当动画值改变时调用 Listener
。
Listener
最常用的操作是调用 setState()
进行重建。当一个动画开始,结束,前进或后退时,会调用 StatusListener
,用 AnimationStatus
来定义。下一部分有关于 addListener()
方法的示例,在 监控动画过程
中也有 addStatusListener()
的示例。
动画示例
#这部分列举了五个动画示例,每个示例都提供了源代码的链接。
渲染动画
#目前为止,我们学习了如何随着时间生成数字序列。但屏幕上并未显示任何内容。要显示一个 Animation
对象,需将 Animation
对象存储为你的 widget 成员,然后用它的值来决定如何绘制。
参考下面的应用程序,它没有使用动画绘制 Flutter logo。
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: const FlutterLogo(),
),
);
}
}
源代码: animate0
下面的代码是加入动画效果的,logo 从无到全屏。当定义 AnimationController
时,必须要使用一个 vsync
对象。在 AnimationController
部分
会具体介绍 vsync
参数。
对比无动画示例,改动部分被突出显示:
@@ -9,16 +9,39 @@
|
|
9
9
|
State<LogoApp> createState() => _LogoAppState();
|
10
10
|
}
|
11
|
-
class _LogoAppState extends State<LogoApp> {
|
11
|
+
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
|
12
|
+
late Animation<double> animation;
|
13
|
+
late AnimationController controller;
|
14
|
+
|
15
|
+
@override
|
16
|
+
void initState() {
|
17
|
+
super.initState();
|
18
|
+
controller =
|
19
|
+
AnimationController(duration: const Duration(seconds: 2), vsync: this);
|
20
|
+
animation = Tween<double>(begin: 0, end: 300).animate(controller)
|
21
|
+
..addListener(() {
|
22
|
+
setState(() {
|
23
|
+
// The state that has changed here is the animation object's value.
|
24
|
+
});
|
25
|
+
});
|
26
|
+
controller.forward();
|
27
|
+
}
|
28
|
+
|
12
29
|
@override
|
13
30
|
Widget build(BuildContext context) {
|
14
31
|
return Center(
|
15
32
|
child: Container(
|
16
33
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
17
|
-
height:
|
18
|
-
width:
|
34
|
+
height: animation.value,
|
35
|
+
width: animation.value,
|
19
36
|
child: const FlutterLogo(),
|
20
37
|
),
|
21
38
|
);
|
22
39
|
}
|
40
|
+
|
41
|
+
@override
|
42
|
+
void dispose() {
|
43
|
+
controller.dispose();
|
44
|
+
super.dispose();
|
45
|
+
}
|
23
46
|
}
|
源代码: animate1
因为addListener()
函数调用 setState()
,所以每次 Animation
生成一个新的数字,当前帧就被标记为 dirty,使得 build()
再次被调用。在 build()
函数中,container 会改变大小,因为它的高和宽都读取 animation.value
,而不是固定编码值。当 State
对象销毁时要清除控制器以防止内存溢出。
经过这些小改动,你成功创建了第一个 Flutter 动画。
使用 AnimatedWidget 进行简化
#AnimatedWidget
基本类可以从动画代码中区分出核心 widget 代码。
AnimatedWidget
不需要保持 State
对象来 hold 动画。可以添加下面的 AnimatedLogo
类:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
在绘制时,AnimatedLogo
会读取 animation
当前值。
LogoApp
持续控制 AnimationController
和 Tween
,并将 Animation
对象传给 AnimatedLogo
:
@@ -1,10 +1,28 @@
|
|
1
1
|
import 'package:flutter/material.dart';
|
2
2
|
void main() => runApp(const LogoApp());
|
3
|
+
class AnimatedLogo extends AnimatedWidget {
|
4
|
+
const AnimatedLogo({super.key, required Animation<double> animation})
|
5
|
+
: super(listenable: animation);
|
6
|
+
|
7
|
+
@override
|
8
|
+
Widget build(BuildContext context) {
|
9
|
+
final animation = listenable as Animation<double>;
|
10
|
+
return Center(
|
11
|
+
child: Container(
|
12
|
+
margin: const EdgeInsets.symmetric(vertical: 10),
|
13
|
+
height: animation.value,
|
14
|
+
width: animation.value,
|
15
|
+
child: const FlutterLogo(),
|
16
|
+
),
|
17
|
+
);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
3
21
|
class LogoApp extends StatefulWidget {
|
4
22
|
const LogoApp({super.key});
|
5
23
|
@override
|
6
24
|
State<LogoApp> createState() => _LogoAppState();
|
7
25
|
}
|
@@ -15,32 +33,18 @@
|
|
15
33
|
@override
|
16
34
|
void initState() {
|
17
35
|
super.initState();
|
18
36
|
controller =
|
19
37
|
AnimationController(duration: const Duration(seconds: 2), vsync: this);
|
20
|
-
animation = Tween<double>(begin: 0, end: 300).animate(controller)
|
21
|
-
..addListener(() {
|
22
|
-
setState(() {
|
23
|
-
// The state that has changed here is the animation object's value.
|
24
|
-
});
|
25
|
-
});
|
38
|
+
animation = Tween<double>(begin: 0, end: 300).animate(controller);
|
26
39
|
controller.forward();
|
27
40
|
}
|
28
41
|
@override
|
29
|
-
Widget build(BuildContext context)
|
30
|
-
return Center(
|
31
|
-
child: Container(
|
32
|
-
margin: const EdgeInsets.symmetric(vertical: 10),
|
33
|
-
height: animation.value,
|
34
|
-
width: animation.value,
|
35
|
-
child: const FlutterLogo(),
|
36
|
-
),
|
37
|
-
);
|
38
|
-
}
|
42
|
+
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
|
39
43
|
@override
|
40
44
|
void dispose() {
|
41
45
|
controller.dispose();
|
42
46
|
super.dispose();
|
43
47
|
}
|
源代码: animate2
监控动画过程
#了解动画何时改变状态通常是很有用的,比如完成,前进或后退。可以通过 addStatusListener()
来获得提示。下面是之前示例修改后的代码,这样就可以监听状态的改变和更新。修改部分会突出显示:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) => print('$status'));
controller.forward();
}
// ...
}
运行这段代码,得到如下结果:
AnimationStatus.forward
AnimationStatus.completed
下一步,在起始或结束时,使用 addStatusListener()
反转动画。制造“呼吸”效果:
@@ -35,7 +35,15 @@
|
|
35
35
|
void initState() {
|
36
36
|
super.initState();
|
37
37
|
controller =
|
38
38
|
AnimationController(duration: const Duration(seconds: 2), vsync: this);
|
39
|
-
animation = Tween<double>(begin: 0, end: 300).animate(controller)
|
39
|
+
animation = Tween<double>(begin: 0, end: 300).animate(controller)
|
40
|
+
..addStatusListener((status) {
|
41
|
+
if (status == AnimationStatus.completed) {
|
42
|
+
controller.reverse();
|
43
|
+
} else if (status == AnimationStatus.dismissed) {
|
44
|
+
controller.forward();
|
45
|
+
}
|
46
|
+
})
|
47
|
+
..addStatusListener((status) => print('$status'));
|
40
48
|
controller.forward();
|
41
49
|
}
|
源代码: animate3
使用 AnimatedBuilder 进行重构
#animate3 示例代码中有个问题,就是改变动画需要改变渲染 logo 的widget。较好的解决办法是,将任务区分到不同类里:
-
渲染 logo
-
定义动画对象
-
渲染过渡效果
你可以使用 AnimatedBuilder
类方法来完成分配。
AnimatedBuilder
作为渲染树的一个单独类。像 AnimatedWidget
,AnimatedBuilder
自动监听动画对象提示,并在必要时在 widget 树中标出,所以这时不需要调用 addListener()
。
应用于 animate4 示例的 widget 树长这样:
从 widget 树底部开始,渲染 logo 的代码很容易:
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
图表中间的三部分都是用 GrowTransition
中的 build()
方法创建的,如下。 GrowTransition
widget 本身是无状态的,而且拥有定义过渡动画所需的一系列最终变量。
build() 函数创建并返回 AnimatedBuilder
,
AnimatedBuilder
使用(Anonymous
builder)方法并将 LogoWidget 对象作为参数。渲染过渡效果实际上是在(Anonymous
builder)方法中完成的,该方法创建一个适当大小 Container
强制 LogoWidget
配合。
在下面这段代码中,一个比较棘手的问题是 child 看起来被指定了两次。其实是 child 的外部参照被传递给了 AnimatedBuilder
,再传递给匿名闭包,然后用作 child 的对象。最终结果就是 AnimatedBuilder
被插入渲染树的两个 widgets 中间。
class GrowTransition extends StatelessWidget {
const GrowTransition(
{required this.child, required this.animation, super.key});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
最后,初始动画的代码看起来很像 animate2 的示例。
initState()
方法创建了 AnimationController
和 Tween
,然后用 animate()
绑定它们。神奇的是 build()
方法,它返回一个以LogoWidget
为 child 的 GrowTransition
对象,和一个驱动过渡的动画对象。上面列出了三个主要因素。
@@ -1,27 +1,47 @@
|
|
1
1
|
import 'package:flutter/material.dart';
|
2
2
|
void main() => runApp(const LogoApp());
|
3
|
-
class
|
4
|
-
const
|
5
|
-
|
3
|
+
class LogoWidget extends StatelessWidget {
|
4
|
+
const LogoWidget({super.key});
|
5
|
+
|
6
|
+
// Leave out the height and width so it fills the animating parent
|
7
|
+
@override
|
8
|
+
Widget build(BuildContext context) {
|
9
|
+
return Container(
|
10
|
+
margin: const EdgeInsets.symmetric(vertical: 10),
|
11
|
+
child: const FlutterLogo(),
|
12
|
+
);
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
class GrowTransition extends StatelessWidget {
|
17
|
+
const GrowTransition(
|
18
|
+
{required this.child, required this.animation, super.key});
|
19
|
+
|
20
|
+
final Widget child;
|
21
|
+
final Animation<double> animation;
|
6
22
|
@override
|
7
23
|
Widget build(BuildContext context) {
|
8
|
-
final animation = listenable as Animation<double>;
|
9
24
|
return Center(
|
10
|
-
child:
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
25
|
+
child: AnimatedBuilder(
|
26
|
+
animation: animation,
|
27
|
+
builder: (context, child) {
|
28
|
+
return SizedBox(
|
29
|
+
height: animation.value,
|
30
|
+
width: animation.value,
|
31
|
+
child: child,
|
32
|
+
);
|
33
|
+
},
|
34
|
+
child: child,
|
15
35
|
),
|
16
36
|
);
|
17
37
|
}
|
18
38
|
}
|
19
39
|
class LogoApp extends StatefulWidget {
|
20
40
|
const LogoApp({super.key});
|
21
41
|
@override
|
22
42
|
State<LogoApp> createState() => _LogoAppState();
|
@@ -34,18 +54,23 @@
|
|
34
54
|
@override
|
35
55
|
void initState() {
|
36
56
|
super.initState();
|
37
57
|
controller =
|
38
58
|
AnimationController(duration: const Duration(seconds: 2), vsync: this);
|
39
59
|
animation = Tween<double>(begin: 0, end: 300).animate(controller);
|
40
60
|
controller.forward();
|
41
61
|
}
|
42
62
|
@override
|
43
|
-
Widget build(BuildContext context)
|
63
|
+
Widget build(BuildContext context) {
|
64
|
+
return GrowTransition(
|
65
|
+
animation: animation,
|
66
|
+
child: const LogoWidget(),
|
67
|
+
);
|
68
|
+
}
|
44
69
|
@override
|
45
70
|
void dispose() {
|
46
71
|
controller.dispose();
|
47
72
|
super.dispose();
|
48
73
|
}
|
49
74
|
}
|
源代码: animate4
同步动画
#在这部分内容中,你会根据 监控动画过程 (animate3) 创建示例,该示例将使用 AnimatedWidget
持续进行动画。可以用在需要对透明度进行从透明到不透明动画处理的情况。
每个补间动画控制一个动画的不同方面,例如:
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);
通过 sizeAnimation.value
我们可以得到尺寸,通过 opacityAnimation.value
可以得到不透明度,但是 AnimatedWidget
的构造函数只读取单一的 Animation
对象。为了解决这个问题,该示例创建了一个 Tween
对象并计算确切值。
修改 AnimatedLogo
来封装其 Tween
对象,以及其 build()
方法在母动画对象上调用
Tween.evaluate()
来计算所需的尺寸和不透明度值。下面的代码中将这些改动突出显示:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({super.key, required Animation<double> animation})
: super(listenable: animation);
// Make the Tweens static because they don't change.
static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
static final _sizeTween = Tween<double>(begin: 0, end: 300);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: const FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
const LogoApp({super.key});
@override
State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late 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
下面的步骤
#本指南是在 Flutter 中应用 Tweens
创建动画的基础介绍,还有很多其他类可供探索。比如指定 Tween
类,Material Design 特有的动画,
ReverseAnimation
,共享元素过渡(也称为 Hero 动画),物理模拟和 fling()
方法。关于最新的文档和示例可参见 动画效果介绍。