本教程展示如何在 Flutter 中构建显式动画。示例相互递进,向你介绍动画库的不同方面。本教程基于动画库中的基本概念、类和方法,你可以在动画简介中了解这些内容。

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

以下部分将带你了解几个动画示例。每个部分都提供了该示例的源代码链接。

渲染动画

#

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

考虑以下绘制 Flutter logo 但没有动画的应用程序

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

以下代码显示了修改后的相同代码,使 logo 从无到有逐渐变大。定义 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 中创建了你的第一个动画!

使用 AnimatedWidget 简化

#

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(() {
          // 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 示例中的代码存在一个问题,即更改动画需要更改渲染 logo 的 widget。更好的解决方案是将职责分离到不同的类中

  • 渲染 logo
  • 定义 Animation 对象
  • 渲染过渡

你可以借助 AnimatedBuilder 类来实现这种分离。AnimatedBuilder 是渲染树中的一个独立类。与 AnimatedWidget 类似,AnimatedBuilder 会自动监听 Animation 对象的通知,并根据需要将 widget 树标记为“脏”,因此你无需调用 addListener()

animate4 示例的 widget 树如下所示

AnimatedBuilder widget tree

从 widget 树的底部开始,渲染 logo 的代码非常简单

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 widget 本身是无状态的,并保存了定义过渡动画所需的一组最终变量。build() 函数创建并返回 AnimatedBuilder,后者将(匿名 builder)方法和 LogoWidget 对象作为参数。渲染过渡的实际工作发生在(匿名 builder)方法中,该方法创建一个适当大小的 Container 以强制 LogoWidget 缩小以适应。

下面代码中一个棘手的地方是 child 似乎被指定了两次。实际上,child 的外部引用被传递给 AnimatedBuilder,后者又将其传递给匿名闭包,然后该闭包将该对象用作其子项。最终结果是 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});

  // 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) {
    return Center(
      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 对象知道动画的当前状态(例如,它是开始、停止,还是向前或向后移动),但对屏幕上显示的内容一无所知。

下一步

#

本教程为你使用 Tweens 在 Flutter 中创建动画奠定了基础,但还有许多其他类值得探索。你可能会研究专用 Tween 类、特定于你设计系统类型的动画、ReverseAnimation、共享元素过渡(也称为 Hero 动画)、物理模拟和 fling() 方法。