动画教程
本教程展示如何在 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
,后者将(匿名
builder)方法和 LogoWidget
对象作为参数。渲染过渡的实际工作发生在(匿名
builder)方法中,该方法创建一个适当大小的 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()
方法中,该方法返回一个 GrowTransition
对象,其子项为 LogoWidget
,以及一个用于驱动过渡的动画对象。这些就是上面列出的三要素。
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
连续进出动画。考虑一种情况,你希望在动画进出的同时,不透明度从透明变为不透明。
每个补间动画管理动画的一个方面。例如
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()
方法。