动画教程
本教程将向你展示如何在 Flutter 中构建显式动画。在介绍了动画库中的一些基本概念、类和方法后,它将引导你完成 5 个动画示例。这些示例相互构建,向你介绍动画库的不同方面。
Flutter SDK 还提供了内置显式动画,例如 FadeTransition
、SizeTransition
和 SlideTransition
。这些简单的动画通过设置起始点和结束点来触发。它们比此处描述的自定义显式动画更容易实现。
基本动画概念和类
Flutter 中的动画系统基于类型化的 动画
对象。小部件可以通过直接读取其当前值并侦听其状态更改来在其构建函数中合并这些动画,或者它们可以使用动画作为传递给其他小部件的更精细动画的基础。
<double>
动画在 Flutter 中,一个 动画
对象不知道屏幕上显示的内容。一个 动画
是一个抽象类,它了解其当前值和状态(已完成或已取消)。一种使用更广泛的动画类型是 动画<double>
。
一个 Animation
对象在两个值之间按顺序生成插值数字,持续时间为一定时间段。Animation
对象的输出可以是线性的、曲线、阶跃函数或任何你能设计出的其他映射。根据 Animation
对象的控制方式,它可以反向运行,甚至在中间转换方向。
动画还可以插值除双精度类型之外的其他类型,例如 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 的数字。例如,此代码创建一个 Animation
对象,但不会开始运行
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
派生自 Animation<double>
,因此可以在需要 Animation
对象的任何地方使用它。但是,AnimationController
具有控制动画的其他方法。例如,你可以使用 .forward()
方法启动动画。数字的生成与屏幕刷新相关,因此通常每秒生成 60 个数字。在生成每个数字后,每个 Animation
对象都会调用附加的 Listener
对象。要为每个子项创建自定义显示列表,请参阅 RepaintBoundary
。
在创建 AnimationController
时,你需要向其传递 vsync
参数。vsync
的存在可防止屏幕外动画消耗不必要的资源。你可以通过向类定义中添加 SingleTickerProviderStateMixin
来使用你的有状态对象作为 vsync。你可以在 GitHub 上的 animate1 中看到一个示例。
补间
默认情况下,AnimationController
对象的范围为 0.0 到 1.0。如果你需要不同的范围或不同的数据类型,你可以使用 Tween
将动画配置为插值到不同的范围或数据类型。例如,以下 Tween
从 -200.0 到 0.0
tween = Tween<double>(begin: -200, end: 0);
Tween
是一个无状态对象,只接受 begin
和 end
。一个 Tween
的唯一工作就是定义从输入范围到输出范围的映射。输入范围通常为 0.0 到 1.0,但这不是必需的。
一个 Tween
继承自 Animatable<T>
,而不是 Animation<T>
。一个 Animatable
,就像 Animation
,不必输出双精度。例如,ColorTween
指定两个颜色之间的进度。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
一个 Tween
对象不会存储任何状态。相反,它提供 evaluate(Animation<double> animation)
方法,该方法使用 transform
函数将动画的当前值(介于 0.0 和 1.0 之间)映射到实际动画值。
可以在 .value
方法中找到 Animation
对象的当前值。evaluate 函数还执行一些清理工作,例如确保在动画值分别为 0.0 和 1.0 时返回开始和结束。
Tween.animate
要使用 Tween
对象,请在 Tween
上调用 animate()
,并传入控制器对象。例如,以下代码在 500 毫秒内生成从 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()
的示例。
动画示例
本部分将指导您完成 5 个动画示例。每个部分都提供指向该示例的源代码的链接。
渲染动画
到目前为止,您已经了解了如何随着时间的推移生成数字序列。尚未向屏幕渲染任何内容。要使用 Animation
对象进行渲染,请将 Animation
对象存储为小组件的成员,然后使用其值来决定如何绘制。
考虑以下在没有动画的情况下绘制 Flutter 徽标的应用
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
以下显示了修改后的相同代码,用于将徽标从无到满大小进行动画处理。在定义 AnimationController
时,您必须传入一个 vsync
对象。vsync
参数在 AnimationController
部分 中进行了描述。
非动画示例中的更改已突出显示
@@ -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
|
+ // 这里改变的状态是动画对象的值。
|
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
生成一个新数字时,当前帧都会被标记为脏,这会强制再次调用 build()
。在 build()
中,容器会改变大小,因为其高度和宽度现在使用 animation.value
而不是硬编码的值。在 State
对象被丢弃时释放控制器,以防止内存泄漏。
通过这几个更改,你已经创建了在 Flutter 中的第一个动画!
使用 AnimatedWidget 简化
AnimatedWidget
基类允许您将核心小组件代码从动画代码中分离出来。AnimatedWidget
无需维护 State
对象来保存动画。添加以下 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
|
导入 'package:flutter/material.dart';
|
2
2
|
void main() => runApp(const LogoApp());
|
3
|
+ 类 AnimatedLogo 扩展 AnimatedWidget {
|
4
|
+ const AnimatedLogo({super.key, required Animation<double> animation})
|
5
|
+ : super(listenable: animation);
|
6
|
+
|
7
|
+ @override
|
8
|
+ 小部件 build(BuildContext context) {
|
9
|
+ final animation = listenable as Animation<double>;
|
10
|
+ 返回 Center(
|
11
|
+ 子项: Container(
|
12
|
+ margin: const EdgeInsets.symmetric(vertical: 10),
|
13
|
+ height: animation.value,
|
14
|
+ width: animation.value,
|
15
|
+ 子项: const FlutterLogo(),
|
16
|
+
),
|
17
|
+
);
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
3
21
|
类 LogoApp 扩展 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
|
- // 此处已更改的状态是动画对象的值。
|
24
|
-
});
|
25
|
-
});
|
38
|
+ animation = Tween<double>(begin: 0, end: 300).animate(controller);
|
26
39
|
controller.forward();
|
27
40
|
}
|
28
41
|
@override
|
29
|
- 小部件 build(BuildContext context)
|
30
|
- 返回 Center(
|
31
|
- 子项: Container(
|
32
|
- margin: const EdgeInsets.symmetric(vertical: 10),
|
33
|
- height: animation.value,
|
34
|
- 宽度:animation.value,
|
35
|
- 子项: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
|
+ 如果 (状态 == 动画状态.已完成) {
|
42
|
+ 控制器.反转();
|
43
|
+ } 否则,如果 (状态 == 动画状态.已取消) {
|
44
|
+ controller.forward();
|
45
|
+
}
|
46
|
+
})
|
47
|
+ ..addStatusListener((状态) => 打印('$状态'));
|
40
48
|
controller.forward();
|
41
49
|
}
|
应用源代码: animate3
使用 AnimatedBuilder 重构
animate3 示例中代码的一个问题是,更改动画需要更改渲染徽标的小部件。更好的解决方案是将职责分隔到不同的类中
- 渲染徽标
- 定义
动画
对象 - 渲染过渡
您可以借助 AnimatedBuilder
类完成此分离。 AnimatedBuilder
是渲染树中的一个单独类。与 AnimatedWidget
一样, AnimatedBuilder
会自动侦听来自 Animation
对象的通知,并根据需要将小组件树标记为脏,因此您无需调用 addListener()
。
用于 animate4 示例的小组件树如下所示
从小组件树的底部开始,用于渲染徽标的代码非常简单
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
小组件本身是无状态的,并且包含定义过渡动画所需的一组最终变量。 build() 函数创建并返回 AnimatedBuilder
,它将 (Anonymous
构建器) 方法和 LogoWidget
对象作为参数。渲染过渡的工作实际上发生在 (Anonymous
构建器) 方法中,该方法创建一个适当大小的 Container
,以强制 LogoWidget
缩小以适应。
下面代码中一个棘手的点是 child 看起来好像被指定了两次。发生的情况是 child 的外部引用被传递给 AnimatedBuilder
,后者将其传递给匿名闭包,然后该闭包将其对象用作其 child。最终结果是 AnimatedBuilder
被插入到渲染树中两个小组件之间。
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()
方法中,该方法返回一个 GrowTransition
对象,其中 LogoWidget
为子级,动画对象用于驱动过渡。这些是上面项目符号列表中列出的三个元素。
@@ -1,27 +1,47 @@
|
|
1
1
|
导入 '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
|
+ // 留出高度和宽度以填充动画父级
|
7
|
+ @override
|
8
|
+ 小部件 build(BuildContext context) {
|
9
|
+ return Container(
|
10
|
+ margin: const EdgeInsets.symmetric(vertical: 10),
|
11
|
+ 子项: 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
|
类 LogoApp 扩展 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
后续步骤
本教程为您提供使用 Tweens
在 Flutter 中创建动画的基础,但还有许多其他类可供探索。您可能会研究专门的 Tween
类、特定于 Material Design 的动画、ReverseAnimation
、共享元素转换(也称为 Hero 动画)、物理模拟和 fling()
方法。请参阅 动画登陆页面 以获取最新可用文档和示例。