动画教程
本教程将向您展示如何在 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>
。与 Animation
一样,Animatable
不必输出 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
对象。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 重构动画
#animate3 示例中代码的一个问题是,更改动画需要更改呈现徽标的小部件。更好的解决方案是将职责分离到不同的类中
- 渲染徽标
- 定义
Animation
对象 - 渲染转换
您可以借助 AnimatedBuilder
类实现此分离。AnimatedBuilder
是渲染树中的一个单独的类。与 AnimatedWidget
一样,AnimatedBuilder
自动侦听来自 Animation
对象的通知,并在必要时标记小部件树为脏,因此您无需调用 addListener()
。
animate4 示例的小部件树如下所示
从小部件树的底部开始,渲染徽标的代码很简单
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
,它将 (匿名
构建器) 方法和 LogoWidget
对象作为参数。渲染转换的工作实际上发生在 (匿名
构建器) 方法中,该方法创建一个适当大小的 Container
以强制 LogoWidget
缩小以适应。
下面代码中一个棘手的点是,子项看起来像是指定了两次。发生的情况是,child 的外部引用传递给 AnimatedBuilder
,后者将其传递给匿名闭包,然后该闭包使用该对象作为其子项。最终结果是 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,
),
);
}
}
最后,初始化动画的代码看起来与 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) {
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
不断地进行淡入和淡出。考虑您希望在不透明度从透明淡入到不透明的同时进行淡入和淡出的情况。
每个补间管理动画的一个方面。例如
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
后续步骤
#本教程为您提供了在 Flutter 中使用 Tween
创建动画的基础,但还有许多其他类需要探索。您可以研究专门的 Tween
类、特定于 Material Design 的动画、ReverseAnimation
、共享元素转换(也称为 Hero 动画)、物理模拟和 fling()
方法。请参阅 动画登录页面 以获取最新的可用文档和示例。
除非另有说明,否则本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-08-16。 查看源代码 或 报告问题。