跳到主内容

动画教程

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

本教程向您展示了如何在 Flutter 中构建显式动画。示例循序渐进,向您介绍动画库的不同方面。本教程基于动画库中的基本概念、类和方法,您可以在 动画简介 中了解更多信息。

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

以下部分将引导您完成几个动画示例。每个部分都提供指向该示例源代码的链接。

渲染动画

#

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

考虑以下一个没有动画的 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 基类允许您将核心小部件代码与动画代码分离。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 重构

#

https://github.com/flutter/website/tree/main/examples/animation/animate3 示例中的代码的一个问题是,更改动画需要更改渲染 logo 的小部件。更好的解决方案是将责任分离到不同的类中

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

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

https://github.com/flutter/website/tree/main/examples/animation/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 小部件本身是无状态的,并保存定义过渡动画所需的一组最终变量。build() 函数创建并返回 AnimatedBuilder,它接受 (Anonymous builder) 方法和 LogoWidget 对象作为参数。过渡的实际渲染发生在 (Anonymous builder) 方法中,它创建一个大小合适的 Container 以强制 LogoWidget 缩小以适应。

代码中的一个难点是 child 看起来被指定了两次。发生的情况是外部 child 引用被传递给 AnimatedBuilder,然后传递给匿名闭包,然后该对象用作其 child。最终结果是 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,
      ),
    );
  }
}

最后,初始化动画的代码与 https://github.com/flutter/website/tree/main/examples/animation/animate2 示例非常相似。initState() 方法创建一个 AnimationController 和一个 Tween,然后使用 animate() 将它们绑定。魔术发生在 build() 方法中,它返回一个带有 LogoWidget 作为 child 和一个用于驱动过渡的动画对象的 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() 方法。