动画教程
本教程将向你展示如何在 Flutter 中构建显式动画。在介绍动画库中一些基本概念、类和方法之后,它将引导你完成 5 个动画示例。这些示例相互构建,向你介绍动画库的不同方面。
Flutter SDK 还提供内置的显式动画,例如 FadeTransition
、SizeTransition
和 SlideTransition
。这些简单的动画通过设置起始点和结束点来触发。它们比这里描述的自定义显式动画更容易实现。
动画基本概念和类
#Flutter 中的动画系统基于类型化的 Animation
对象。Widget 可以通过直接读取其当前值并监听其状态更改来在其构建函数中直接包含这些动画,或者它们可以使用动画作为更详尽动画的基础,并将这些动画传递给其他 Widget。
Animation<double>
#在 Flutter 中,Animation
对象不知道屏幕上显示的内容。Animation
是一个抽象类,它了解其当前值及其状态(已完成或已关闭)。更常用的动画类型之一是 Animation<double>
。
Animation
对象在特定持续时间内依次生成两个值之间的插值数字。Animation
对象的输出可以是线性的、曲线、阶跃函数或你可以设计的任何其他映射。根据 Animation
对象的控制方式,它可以反向运行,甚至可以在中间切换方向。
动画还可以插值除 double 之外的其他类型,例如 Animation<Color>
或 Animation<Size>
。
Animation
对象具有状态。其当前值始终在 .value
成员中可用。
Animation
对象不知道渲染或 build()
函数。
CurvedAnimation
#CurvedAnimation
将动画的进度定义为非线性曲线。
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation
和 AnimationController
(在下一节中描述)都是 Animation<double>
类型,因此你可以互换使用它们。CurvedAnimation
包装它正在修改的对象——你不需要子类化 AnimationController
来实现曲线。
AnimationController
#AnimationController
是一个特殊的 Animation
对象,它在硬件准备好新帧时生成一个新值。默认情况下,AnimationController
在给定持续时间内线性生成 0.0 到 1.0 之间的数字。例如,这段代码创建了一个 Animation
对象,但没有启动它:
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
派生自 Animation<double>
,因此它可以在需要 Animation
对象的任何地方使用。但是,AnimationController
具有其他方法来控制动画。例如,你可以使用 .forward()
方法启动动画。数字的生成与屏幕刷新绑定,因此通常每秒生成 60 个数字。生成每个数字后,每个 Animation
对象都会调用附加的 Listener
对象。要为每个子项创建自定义显示列表,请参见 RepaintBoundary
。
创建 AnimationController
时,你会向其传递 vsync
参数。vsync
的存在可以防止屏幕外动画消耗不必要的资源。你可以通过向类定义添加 SingleTickerProviderStateMixin
来使用你的状态对象作为 vsync
。你可以在 GitHub 上的 animate1 中看到一个示例。
Tween
#默认情况下,AnimationController
对象的范围是 0.0 到 1.0。如果需要不同的范围或不同的数据类型,可以使用 Tween
将动画配置为插值到不同的范围或数据类型。例如,以下 Tween
从 -200.0 到 0.0:
tween = Tween<double>(begin: -200, end: 0);
Tween
是一个无状态对象,它只接受 begin
和 end
。Tween
的唯一作用是定义从输入范围到输出范围的映射。输入范围通常是 0.0 到 1.0,但这并不是必需的。
Tween
继承自 Animatable<T>
,而不是 Animation<T>
。Animatable
与 Animation
一样,不必输出 double。例如,ColorTween
指定两个颜色之间的进度。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
对象不存储任何状态。相反,它提供了 evaluate(Animation<double> animation)
方法,该方法使用 transform
函数将动画的当前值(在 0.0 和 1.0 之间)映射到实际的动画值。
Animation
对象的当前值可以在 .value
方法中找到。evaluate 函数还执行一些日常工作,例如确保在动画值为 0.0 和 1.0 时分别返回 begin 和 end。
Tween.animate
#要使用 Tween
对象,请在 Tween
上调用 animate()
,并传入控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
以下示例显示了一个控制器、一条曲线和一个 Tween
:
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
动画通知
#Animation
对象可以具有 Listener
和 StatusListener
,它们分别使用 addListener()
和 addStatusListener()
定义。只要动画的值发生变化,就会调用 Listener
。Listener
的最常见行为是调用 setState()
以导致重建。StatusListener
在动画开始、结束、正向移动或反向移动时被调用,这由 AnimationStatus
定义。下一节有一个 addListener()
方法的示例,而监控动画进度 显示了 addStatusListener()
的示例。
动画示例
#本节将引导你完成 5 个动画示例。每个部分都提供指向该示例源代码的链接。
渲染动画
#到目前为止,你已经学习了如何随时间生成一系列数字。屏幕上没有任何内容被渲染。要使用 Animation
对象进行渲染,请将 Animation
对象存储为 Widget 的成员,然后使用其值来决定如何绘制。
考虑以下绘制 Flutter 徽标而无需动画的应用程序:
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
下面显示了修改后的相同代码,该代码使徽标从无到全尺寸动画增长。定义 AnimationController
时,必须传入 vsync
对象。AnimationController
部分 中描述了 vsync
参数。
非动画示例中的更改已突出显示:
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(() {
// 此处已更改的状态是动画对象的 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(() {
// 此处已更改的状态是动画对象的 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 示例中代码的一个问题是,更改动画需要更改渲染徽标的 Widget。更好的解决方案是将职责分离到不同的类中:
- 渲染徽标
- 定义
Animation
对象 - 渲染转换
你可以借助 AnimatedBuilder
类来实现这种分离。AnimatedBuilder
是渲染树中的一个单独的类。与 AnimatedWidget
一样,AnimatedBuilder
会自动监听来自 Animation
对象的通知,并在必要时标记 Widget 树为脏,因此你无需调用 addListener()
。
animate4 示例的 Widget 树如下所示:
从 Widget 树的底部开始,渲染徽标的代码很简单:
class LogoWidget extends StatelessWidget {
const LogoWidget({super.key});
// 省略高度和宽度,以便它填充动画父级。
@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
将其传递给匿名闭包,然后匿名闭包将其对象用作其子项。最终结果是 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});
// 省略高度和宽度,以便它填充动画父级。
@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) {
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(),
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);
// 将 Tweens 设置为静态,因为它们不会更改。
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
下一步
#本教程为你提供了使用 Tween
在 Flutter 中创建动画的基础,但还有许多其他类需要探索。你可以研究专门的 Tween
类、特定于 Material Design 的动画、ReverseAnimation
、共享元素转换(也称为 Hero 动画)、物理模拟和 fling()
方法。请查看动画登录页以获取最新的可用文档和示例。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。