跳至主要内容

动画教程

本教程将向您展示如何在 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>。与 Animation 一样,Animatable 不必输出 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 对象。vsync 参数在 AnimationController 部分 中进行了描述。

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

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(() {
          // The state that has changed here is the animation object's 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 基类允许您将核心小部件代码与动画代码分离。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(() {
          // The state that has changed here is the animation object's 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 示例中代码的一个问题是,更改动画需要更改呈现徽标的小部件。更好的解决方案是将职责分离到不同的类中

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

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

animate4 示例的小部件树如下所示

AnimatedBuilder widget tree

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

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

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

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

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() 方法创建 AnimationControllerTween,然后使用 animate() 将它们绑定。魔法发生在 build() 方法中,该方法返回一个 GrowTransition 对象,该对象具有 LogoWidget 作为子项,以及一个驱动转换的动画对象。这些是上面要点中列出的三个元素。

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

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

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 不断地进行淡入和淡出。考虑您希望在不透明度从透明淡入到不透明的同时进行淡入和淡出的情况。

每个补间管理动画的一个方面。例如

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);

  // 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 中使用 Tween 创建动画的基础,但还有许多其他类需要探索。您可以研究专门的 Tween 类、特定于 Material Design 的动画、ReverseAnimation、共享元素转换(也称为 Hero 动画)、物理模拟和 fling() 方法。请参阅 动画登录页面 以获取最新的可用文档和示例。