动画教程
本教程展示了如何在 Flutter 中构建显式动画。
本教程向您展示了如何在 Flutter 中构建显式动画。示例循序渐进,向您介绍动画库的不同方面。本教程基于动画库中的基本概念、类和方法,您可以在 动画简介 中了解更多信息。
Flutter SDK 还提供了内置的显式动画,例如 FadeTransition、SizeTransition 和 SlideTransition。这些简单的动画通过设置起点和终点来触发。与此处描述的自定义显式动画相比,它们更易于实现。
以下部分将引导您完成几个动画示例。每个部分都提供指向该示例源代码的链接。
渲染动画
#到目前为止,您已经学习了如何随时间生成数字序列。没有任何内容呈现在屏幕上。要使用 Animation 对象进行渲染,请将 Animation 对象存储为小部件的成员,然后使用其值来决定如何绘制。
考虑以下一个没有动画的 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 基类允许您将核心小部件代码与动画代码分离。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 重构
#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 树如下所示

从 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 小部件本身是无状态的,并保存定义过渡动画所需的一组最终变量。build() 函数创建并返回 AnimatedBuilder,它接受 (Anonymous builder) 方法和 LogoWidget 对象作为参数。过渡的实际渲染发生在 (Anonymous builder) 方法中,它创建一个大小合适的 Container 以强制 LogoWidget 缩小以适应。
代码中的一个难点是 child 看起来被指定了两次。发生的情况是外部 child 引用被传递给 AnimatedBuilder,然后传递给匿名闭包,然后该对象用作其 child。最终结果是 AnimatedBuilder 被插入到渲染树中的两个小部件之间。
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 对象。这些是上面列出的三个元素。
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() 方法。