Skip to main content

动画教程

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

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

动画基本概念和类

#

Flutter 中的动画系统基于类型化的 Animation 对象。Widget 可以通过直接读取其当前值并监听其状态更改来在其构建函数中直接包含这些动画,或者它们可以使用动画作为更详尽动画的基础,并将这些动画传递给其他 Widget。

Animation<double>

#

在 Flutter 中,Animation 对象不知道屏幕上显示的内容。Animation 是一个抽象类,它了解其当前值及其状态(已完成或已关闭)。更常用的动画类型之一是 Animation<double>

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

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

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

Animation 对象不知道渲染或 build() 函数。

Curved­Animation

#

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

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

CurvedAnimationAnimationController(在下一节中描述)都是 Animation<double> 类型,因此你可以互换使用它们。CurvedAnimation 包装它正在修改的对象——你不需要子类化 AnimationController 来实现曲线。

Animation­Controller

#

AnimationController 是一个特殊的 Animation 对象,它在硬件准备好新帧时生成一个新值。默认情况下,AnimationController 在给定持续时间内线性生成 0.0 到 1.0 之间的数字。例如,这段代码创建了一个 Animation 对象,但没有启动它:

dart
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 中看到一个示例。

Tween

#

默认情况下,AnimationController 对象的范围是 0.0 到 1.0。如果需要不同的范围或不同的数据类型,可以使用 Tween 将动画配置为插值到不同的范围或数据类型。例如,以下 Tween 从 -200.0 到 0.0:

dart
tween = Tween<double>(begin: -200, end: 0);

Tween 是一个无状态对象,它只接受 beginendTween 的唯一作用是定义从输入范围到输出范围的映射。输入范围通常是 0.0 到 1.0,但这并不是必需的。

Tween 继承自 Animatable<T>,而不是 Animation<T>AnimatableAnimation 一样,不必输出 double。例如,ColorTween 指定两个颜色之间的进度。

dart
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 对象不存储任何状态。相反,它提供了 evaluate(Animation<double> animation) 方法,该方法使用 transform 函数将动画的当前值(在 0.0 和 1.0 之间)映射到实际的动画值。

Animation 对象的当前值可以在 .value 方法中找到。evaluate 函数还执行一些日常工作,例如确保在动画值为 0.0 和 1.0 时分别返回 begin 和 end。

Tween.animate

#

要使用 Tween 对象,请在 Tween 上调用 animate(),并传入控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。

dart
AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

以下示例显示了一个控制器、一条曲线和一个 Tween

dart
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() 定义。只要动画的值发生变化,就会调用 ListenerListener 的最常见行为是调用 setState() 以导致重建。StatusListener 在动画开始、结束、正向移动或反向移动时被调用,这由 AnimationStatus 定义。下一节有一个 addListener() 方法的示例,而监控动画进度 显示了 addStatusListener() 的示例。


动画示例

#

本节将引导你完成 5 个动画示例。每个部分都提供指向该示例源代码的链接。

渲染动画

#

到目前为止,你已经学习了如何随时间生成一系列数字。屏幕上没有任何内容被渲染。要使用 Animation 对象进行渲染,请将 Animation 对象存储为 Widget 的成员,然后使用其值来决定如何绘制。

考虑以下绘制 Flutter 徽标而无需动画的应用程序:

dart
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 对象。AnimationController 部分 中描述了 vsync 参数。

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

dart
class _LogoAppState extends State<LogoApp> {
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)
      ..addListener(() {
        setState(() {
          // 此处已更改的状态是动画对象的 value。
        });
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

应用源代码: animate1

addListener() 函数调用 setState(),因此每次 Animation 生成一个新数字时,当前帧都会被标记为脏帧,这会强制再次调用 build()。在 build() 中,容器的大小会发生变化,因为它的高度和宽度现在使用 animation.value 而不是硬编码的值。当 State 对象被丢弃时,释放控制器以防止内存泄漏。

通过这几个简单的更改,你就在 Flutter 中创建了你的第一个动画!

使用 Animated­Widget 简化

#

AnimatedWidget 基类允许你将核心 Widget 代码与动画代码分离。AnimatedWidget 不需要维护一个 State 对象来保存动画。添加以下 AnimatedLogo 类:

dart
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

dart
void main() => runApp(const LogoApp());

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(),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  // ...

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {
          // 此处已更改的状态是动画对象的 value。
        });
      });
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);
  
  // ...
}

应用源代码: animate2

监控动画进度

#

了解动画何时更改状态(例如,完成、正向移动或反向移动)通常很有帮助。你可以使用 addStatusListener() 获取此方面的通知。以下代码修改了前面的示例,使其监听状态更改并打印更新。突出显示的行显示了更改:

dart
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() 在开头或结尾反转动画。这会创建一个“呼吸”效果:

dart
void initState() {
  super.initState();
  controller =
      AnimationController(duration: const Duration(seconds: 2), vsync: this);
  animation = Tween<double>(begin: 0, end: 300).animate(controller);
  animation = Tween<double>(begin: 0, end: 300).animate(controller)
    ..addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    })
    ..addStatusListener((status) => print('$status'));
  controller.forward();
}

应用源代码: animate3

使用 AnimatedBuilder 重构

#

animate3 示例中代码的一个问题是,更改动画需要更改渲染徽标的 Widget。更好的解决方案是将职责分离到不同的类中:

  • 渲染徽标
  • 定义 Animation 对象
  • 渲染转换

你可以借助 AnimatedBuilder 类来实现这种分离。AnimatedBuilder 是渲染树中的一个单独的类。与 AnimatedWidget 一样,AnimatedBuilder 会自动监听来自 Animation 对象的通知,并在必要时标记 Widget 树为脏,因此你无需调用 addListener()

animate4 示例的 Widget 树如下所示:

AnimatedBuilder widget tree

从 Widget 树的底部开始,渲染徽标的代码很简单:

dart
class LogoWidget extends StatelessWidget {
  const LogoWidget({super.key});

  // 省略高度和宽度,以便它填充动画父级。
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}

图中的中间三个块都在下面显示的 GrowTransitionbuild() 方法中创建。GrowTransition Widget 本身是无状态的,它包含定义转换动画所需的最终变量集。build() 函数创建并返回 AnimatedBuilder,它将(匿名构建器)方法和 LogoWidget 对象作为参数。渲染转换的工作实际上发生在(匿名构建器)方法中,该方法创建一个适当大小的 Container 以强制 LogoWidget 缩小以适应。

下面代码中一个棘手的地方是 child 看起来像是两次指定的。发生的情况是 child 的外部引用传递给 AnimatedBuilderAnimatedBuilder 将其传递给匿名闭包,然后匿名闭包将其对象用作其子项。最终结果是 AnimatedBuilder 被插入到渲染树中这两个 Widget 之间。

dart
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 作为子项,以及一个用于驱动转换的动画对象。这些是在上面项目符号中列出的三个元素。

dart
void main() => runApp(const LogoApp());

class LogoWidget extends StatelessWidget {
  const LogoWidget({super.key});

  // 省略高度和宽度,以便它填充动画父级。
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}

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) {
    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(),
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  // ...

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);
  Widget build(BuildContext context) {
    return GrowTransition(
      animation: animation,
      child: const LogoWidget(),
    );
  }

  // ...
}

应用源代码: animate4

同时进行动画

#

在本节中,你将基于监控动画进度 (animate3)中的示例进行构建,该示例使用 AnimatedWidget 来连续地进行动画的淡入和淡出。考虑一下你想要在不透明度从透明动画到不透明的同时进行淡入和淡出的情况。

每个 tween 管理动画的一个方面。例如:

dart
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() 来计算所需的大小和不透明度值。以下代码显示了带有突出显示的更改:

dart
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  // 将 Tweens 设置为静态,因为它们不会更改。
  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

下一步

#

本教程为你提供了使用 Tween 在 Flutter 中创建动画的基础,但还有许多其他类需要探索。你可以研究专门的 Tween 类、特定于 Material Design 的动画、ReverseAnimation、共享元素转换(也称为 Hero 动画)、物理模拟和 fling() 方法。请查看动画登录页以获取最新的可用文档和示例。