动画教程

本教程将向你展示如何在 Flutter 中构建显式动画。在介绍了动画库中的一些基本概念、类和方法后,它将引导你完成 5 个动画示例。这些示例相互构建,向你介绍动画库的不同方面。

Flutter SDK 还提供了内置显式动画,例如 FadeTransitionSizeTransitionSlideTransition。这些简单的动画通过设置起始点和结束点来触发。它们比此处描述的自定义显式动画更容易实现。

基本动画概念和类

Flutter 中的动画系统基于类型化的 动画 对象。小部件可以通过直接读取其当前值并侦听其状态更改来在其构建函数中合并这些动画,或者它们可以使用动画作为传递给其他小部件的更精细动画的基础。

动画<double>

在 Flutter 中,一个 动画 对象不知道屏幕上显示的内容。一个 动画 是一个抽象类,它了解其当前值和状态(已完成或已取消)。一种使用更广泛的动画类型是 动画<double>

一个 Animation 对象在两个值之间按顺序生成插值数字,持续时间为一定时间段。Animation 对象的输出可以是线性的、曲线、阶跃函数或任何你能设计出的其他映射。根据 Animation 对象的控制方式,它可以反向运行,甚至在中间转换方向。

动画还可以插值除双精度类型之外的其他类型,例如 Animation<Color>Animation<Size>

一个 Animation 对象具有状态。其当前值始终在 .value 成员中可用。

一个 Animation 对象不了解渲染或 build() 函数。

Curved­Animation

一个 CurvedAnimation 将动画的进度定义为非线性曲线。

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationAnimationController(在下一部分中描述)都是 Animation<double> 类型,因此你可以交替传递它们。CurvedAnimation 封装它正在修改的对象——你不需要对 AnimationController 进行子类化来实现曲线。

Animation­Controller

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 是一个无状态对象,只接受 beginend。一个 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 对象可以有 ListenerStatusListener,它们分别使用 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 部分 中进行了描述。

非动画示例中的更改已突出显示

{animate0 → animate1}/lib/main.dart
@@ -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: 300,
18
- width: 300,
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 中的第一个动画!

使用 Animated­Widget 简化

AnimatedWidget 基类允许您将核心小组件代码从动画代码中分离出来。AnimatedWidget 无需维护 State 对象来保存动画。添加以下 AnimatedLogo

lib/main.dart (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 仍然管理 AnimationControllerTween,并将 Animation 对象传递给 AnimatedLogo

{animate1 → animate2}/lib/main.dart
@@ -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() 在开始或结束时反转动画。这会创建“呼吸”效果

{animate2 → animate3}/lib/main.dart
@@ -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 示例的小组件树如下所示

AnimatedBuilder widget tree

从小组件树的底部开始,用于渲染徽标的代码非常简单

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 为子级,动画对象用于驱动过渡。这些是上面项目符号列表中列出的三个元素。

{animate2 → animate4}/lib/main.dart
@@ -1,27 +1,47 @@
1
1
  导入 '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);
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: Container(
11
- margin: const EdgeInsets.symmetric(vertical: 10),
12
- height: animation.value,
13
- width: animation.value,
14
- child: const FlutterLogo(),
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) => AnimatedLogo(animation: animation);
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() 方法。请参阅 动画登陆页面 以获取最新可用文档和示例。