跳到主内容

动画教程

本教程展示了如何在 Flutter 中构建显式动画。

本教程向你展示如何在 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,它将(匿名构建器)方法和 LogoWidget 对象作为参数。渲染过渡的实际工作发生在(匿名构建器)方法中,该方法创建一个适当大小的 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() 方法中,它返回一个以 LogoWidget 为子组件的 GrowTransition 对象,以及一个用于驱动过渡的动画对象。这些就是上面要点中列出的三个要素。

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 来实现持续的入场和出场动画。考虑这样一种情况:你想要在透明度从透明变为不透明的同时,进行入场和出场动画。

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

  // 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() 方法。