教程 | 在 Flutter 应用里实现动画效果
本教程将讲解如何在 Flutter 中构建显式动画。我们先来介绍一些动画库中的基本概念,类和方法,然后列举五个动画示例。这些示例互相关联,展示了动画库的不同方面。
Flutter SDK 也内置了显式动画,比如
FadeTransition
,SizeTransition
和 SlideTransition
。这些简单的动画可以通过设置起点和终点来触发。它们比下面介绍的显式动画更容易实现。
基本动画概念和类
#Flutter 中的动画系统基于类型化的 Animation
[] 对象。
Widgets 既可以通过读取当前值和监听状态变化直接合并动画到 build 函数,也可以作为传递给其他 widgets
的更精细动画的基础。
Animation<double>
#在 Flutter 中,动画对象无法获取屏幕上显示的内容。
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 的数字。例如,这段代码创建了一个动画对象,但是没有启动运行。
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
源自于 Animation<double>
,所以可以用在任何需要 Animation
对象的地方。但是 AnimationController
还有其他方法控制动画。例如,使用 .forward()
方法启动动画。数字的生成与屏幕刷新关联,所以一般来说每秒钟会生成 60 个数字。数字生成之后,每个动画对象都调用附加 Listener 对象。为每个 child 创建自定义显示列表,请参考 RepaintBoundary
。
创建 AnimationController
的同时,也赋予了一个 vsync
参数。
vsync
的存在防止后台动画消耗不必要的资源。你可以通过添加 SingleTickerProviderStateMixin
到类定义,将有状态的对象用作 vsync。可参考 GitHub 网站 animate1
中的示例。
Tween
#在默认情况下,AnimationController
对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween
配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween
的范围是 -200 到 0.0。
tween = Tween<double>(begin: -200, end: 0);
Tween
是无状态的对象,只有 begin
和 end
。
Tween
的这种单一用途用来定义从输入范围到输出范围的映射。输入范围一般为 0.0-1.0,但这并不是必须的。
Tween
源自 Animatable<T>
,而不是 Animation<T>
。像动画这样的可动画元素不必重复输出。例如,ColorTween
指定了两种颜色之间的过程。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
A Tween
object doesn't store any state. Instead, it provides the
evaluate(Animation<double> animation)
method that uses the
transform
function to map the current value of the animation
(between 0.0 and 1.0), to the actual animation value.
Tween
对象不存储任何状态。而是提供 evaluate(Animation<double> animation)
方法,将映射函数应用于动画当前值。
Animation
对象的当前值可以在 .value
方法中找到。
evaluate 函数还执行一些内部处理内容,比如确保当动画值在 0.0 和1.0 时分别返回起始点和终点。
Tween.animate
#要使用 Tween
对象,请在 Tween
调用 animate()
,传入控制器对象。例如,下面的代码在 500 ms 的进程中生成 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()
的示例。
动画示例
#这部分列举了五个动画示例,每个示例都提供了源代码的链接。
渲染动画
#目前为止,我们学习了如何随着时间生成数字序列。但屏幕上并未显示任何内容。要显示一个 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
对象。在 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(() {
// 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
生成一个新的数字,当前帧就被标记为 dirty,使得 build()
再次被调用。在 build()
函数中,container 会改变大小,因为它的高和宽都读取 animation.value
,而不是固定编码值。当 State
对象销毁时要清除控制器以防止内存溢出。
经过这些小改动,你成功创建了第一个 Flutter 动画。
使用 AnimatedWidget 进行简化
#AnimatedWidget
基本类可以从动画代码中区分出核心 widget 代码。
AnimatedWidget
不需要保持 State
对象来 hold 动画。可以添加下面的 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
-
定义动画对象
-
渲染过渡效果
你可以使用 AnimatedBuilder
类方法来完成分配。
AnimatedBuilder
作为渲染树的一个单独类。像 AnimatedWidget
,AnimatedBuilder
自动监听动画对象提示,并在必要时在 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
,
AnimatedBuilder
使用(Anonymous
builder)方法并将 LogoWidget 对象作为参数。渲染过渡效果实际上是在(Anonymous
builder)方法中完成的,该方法创建一个适当大小 Container
强制 LogoWidget
配合。
在下面这段代码中,一个比较棘手的问题是 child 看起来被指定了两次。其实是 child 的外部参照被传递给了 AnimatedBuilder
,再传递给匿名闭包,然后用作 child 的对象。最终结果就是 AnimatedBuilder
被插入渲染树的两个 widgets 中间。
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
为 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) {
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 中应用 Tweens
创建动画的基础介绍,还有很多其他类可供探索。比如指定 Tween
类,Material Design 特有的动画,
ReverseAnimation
,共享元素过渡(也称为 Hero 动画),物理模拟和 fling()
方法。关于最新的文档和示例可参见 动画效果介绍。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-09-02。 查看文档源码 或者 为本页面内容提出建议。