动画教程
本教程展示了如何在 Flutter 中构建显式动画。
本教程向你展示如何在 Flutter 中构建显式动画。这些示例由浅入深,介绍了动画库的不同方面。本教程建立在动画库的核心概念、类和方法之上,你可以在 动画简介 中了解这些内容。
Flutter SDK 还提供了内置的显式动画,例如 FadeTransition、SizeTransition 和 SlideTransition。这些简单的动画通过设置起始点和结束点来触发。与此处描述的自定义显式动画相比,它们实现起来更为简单。
以下章节将通过几个动画示例为你进行演示。每一节都提供了该示例的源代码链接。
渲染动画
#到目前为止,你已经了解了如何随时间生成数字序列。此时屏幕上还没有渲染任何内容。要使用 Animation 对象进行渲染,请将 Animation 对象存储为 Widget 的成员变量,然后根据其值决定如何绘制。
考虑以下不带动画绘制 Flutter Logo 的应用程序
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 章节 中有详细说明。
与非动画示例相比,所做的更改已高亮显示
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 类
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 仍然管理 AnimationController 和 Tween,并将 Animation 对象传递给 AnimatedLogo
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() 获取这些通知。以下代码修改了上一个示例,以便它监听状态变化并打印更新。高亮行显示了更改之处
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() 在开头或结尾反转动画。这会产生一种“呼吸”效果
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 树结构如下

从 Widget 树的底部开始,渲染 Logo 的代码非常直观
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(),
);
}
}
图中中间的三个块都是在 GrowTransition 的 build() 方法中创建的,如下所示。GrowTransition Widget 本身是无状态的,并持有定义过渡动画所需的最终变量集合。build() 函数创建并返回 AnimatedBuilder,它将(匿名构建器)方法和 LogoWidget 对象作为参数。渲染过渡的实际工作发生在(匿名构建器)方法中,该方法创建一个适当大小的 Container 来强制 LogoWidget 缩小以适应大小。
以下代码的一个棘手之处在于 child 看起来被指定了两次。实际发生的情况是,外部的 child 引用被传递给了 AnimatedBuilder,后者将其传递给匿名闭包,然后闭包将该对象用作其子组件。最终结果是 AnimatedBuilder 被插入到了渲染树中的这两个 Widget 之间。
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 对象,以及一个用于驱动过渡的动画对象。这些就是上面要点中列出的三个要素。
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 管理动画的一个方面。例如
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() 来计算所需的大小和透明度值。以下代码显示了更改的部分,并进行了高亮
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 对象知道动画的当前状态(例如,它是开始、停止、正向还是反向运行),但对屏幕上显示的内容一无所知。
AnimationController管理Animation。CurvedAnimation将过程定义为非线性曲线。Tween在被动画化的属性的开始值和结束值之间进行插值。
下一步
#本教程为你使用 Tweens 在 Flutter 中创建动画奠定了基础,但还有许多其他类值得探索。你可以研究专门的 Tween 类、针对特定设计系统的动画、ReverseAnimation、共享元素过渡(也称为 Hero 动画)、物理模拟和 fling() 方法。