教程 | 在 Flutter 应用里实现动画效果
本教程将讲解如何在 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
对象。在 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 对象知道动画的当前状态(例如,是开始、停止、前进还是后退),但对屏幕上出现的内容一无所知。
-
AnimationController
管理Animation
。 -
CurvedAnimation
将渐进定义为非线性曲线 (non-linear curve)。 -
Tween
会在对象所使用的数据范围之间进行插值(补间动画)
下面的步骤
#本指南是在 Flutter 中应用 Tweens
创建动画的基础介绍,还有很多其他类可供探索。比如指定 Tween
类、针对设计系统特有的动画、
ReverseAnimation
、共享元素过渡(也称为 Hero 动画)、物理模拟和 fling()
方法。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2025-02-13。 查看文档源码 或者 为本页面内容提出建议。