1. 简介
动画是改善应用用户体验、向用户传达重要信息以及让应用更精致、更易于使用的绝佳方式。
Flutter 动画框架概览
Flutter 通过在每一帧上重建部分 widget 树来显示动画效果。它提供预建的动画效果和其他 API,可让您更轻松地创建和组合动画。
- 隐式动画是预建的动画效果,可自动运行整个动画。当动画的目标值发生变化时,它会从当前值运行动画到目标值,并显示中间的每个值,以便微件平稳地添加动画效果。隐式动画的示例包括
AnimatedSize、AnimatedScale和AnimatedPositioned。 - 显式动画也是预建的动画效果,但需要
Animation对象才能正常运行。例如SizeTransition、ScaleTransition或PositionedTransition。 - 动画是一个类,表示正在运行或已停止的动画,由一个表示动画正在运行的目标值的值和一个表示动画在任意给定时间在屏幕上显示的当前值的状态组成。它是
Listenable的子类,可在动画运行时在状态发生变化时通知其监听器。 - AnimationController 是一种创建动画并控制其状态的方式。其方法(例如
forward()、reset()、stop()和repeat())可用于控制动画,而无需定义正在显示的动画效果(例如缩放、大小或位置)。 - 补间动画 用于在起始值和结束值之间插值,并且可以表示任何类型,例如 double、
Offset或Color。 - 曲线用于调整参数随时间的变化率。在动画运行时,通常会应用缓动曲线,以便在动画开始或结束时加快或减慢变化率。曲线接受介于 0.0 和 1.0 之间的输入值,并返回介于 0.0 和 1.0 之间的输出值。
构建内容
在此 Codelab 中,您将构建一个包含各种动画效果和技巧的选择题问答游戏。

您将了解如何...
- 构建可为其大小和颜色添加动画效果的小组件
- 打造 3D 卡片翻转效果
- 使用动画软件包中的精美预构建动画效果
- 添加了对最新版 Android 中提供的预测性返回手势的支持
学习内容
在此 Codelab 中,您将学习:
- 如何使用隐式动画效果来实现美观的动画,而无需编写大量代码。
- 如何使用显式动画效果,通过预构建的动画 widget(例如
AnimatedSwitcher或AnimationController)配置自己的效果。 - 如何使用
AnimationController定义显示 3D 效果的自定义 widget。 - 如何使用
animations软件包以最少的设置显示精美的动画效果。
所需条件
- Flutter SDK
- IDE,例如 VSCode 或 Android Studio / IntelliJ
2. 设置您的 Flutter 开发环境
您需要使用两款软件才能完成此 Codelab:Flutter SDK 和一款编辑器。
您可使用以下任一设备学习此 Codelab:
- 一台连接到计算机并设置为开发者模式的实体 Android(建议使用此设备在第 7 步中实现预测性返回)或 iOS 设备。
- iOS 模拟器(需要安装 Xcode 工具)。
- Android 模拟器(需要在 Android Studio 中设置)。
- 浏览器(需要使用 Chrome,以便进行调试)。
- Windows、Linux 或 macOS 桌面计算机。您必须在打算部署到的平台上进行开发。因此,如果您要开发 Windows 桌面应用,则必须在 Windows 上进行开发,才能使用相应的构建链。如需详细了解针对各种操作系统的具体要求,请访问 docs.flutter.dev/desktop。
验证安装
如需验证 Flutter SDK 是否已正确配置,以及您是否已安装上述至少一个目标平台,请使用 Flutter Doctor 工具:
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources
• No issues found!
3. 运行起始应用
下载起始应用
使用 git 从 GitHub 上的 flutter/samples 代码库克隆起始应用。
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
或者,您也可以下载源代码的 ZIP 文件。
运行应用
如需运行应用,请使用 flutter run 命令并指定目标设备,例如 android、ios 或 chrome。如需查看支持的平台的完整列表,请参阅支持的平台页面。
flutter run -d android
您还可以使用自己惯用的 IDE 运行和调试应用。如需了解详情,请参阅官方 Flutter 文档。
浏览代码
起始应用是一款选择题问答游戏,包含两个界面,遵循模型-视图-视图模型 (MVVM) 设计模式。QuestionScreen(视图)使用 QuizViewModel(视图模型)类向用户提出 QuestionBank(模型)类中的多项选择题。
- home_screen.dart - 显示包含新游戏按钮的界面
- main.dart - 配置
MaterialApp以使用 Material 3 并显示主屏幕 - model.dart - 定义了整个应用中使用的核心类
- question_screen.dart - 显示知识问答游戏的界面
- view_model.dart - 存储测验游戏的状态和逻辑,由
QuestionScreen显示

该应用目前不支持任何动画效果,但当用户按下新游戏按钮时,Flutter 的 Navigator 类显示的默认视图过渡效果除外。
4. 使用隐式动画效果
在许多情况下,隐式动画都是不错的选择,因为它们不需要任何特殊配置。在本部分中,您将更新 StatusBar widget,使其显示动画记分牌。如需查找常见的隐式动画效果,请浏览 ImplicitlyAnimatedWidget API 文档。

创建无动画效果的比分牌 widget
创建一个新文件 lib/scoreboard.dart,其中包含以下代码:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color: score < i + 1
? Colors.grey.shade400
: Colors.yellow.shade700,
),
],
),
);
}
}
然后,在 StatusBar widget 的子元素中添加 Scoreboard widget,替换之前显示得分和问题总数的 Text widget。编辑器应会自动在文件顶部添加所需的 import "scoreboard.dart"。
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
此 widget 会为每个问题显示一个星标图标。当问题回答正确时,另一颗星星会立即亮起,没有任何动画效果。在后续步骤中,您将通过为分数添加大小和颜色动画效果,帮助用户了解分数的变化。
使用隐式动画效果
创建一个名为 AnimatedStar 的新微件,该微件使用 AnimatedScale 微件在星标变为有效时将 scale 金额从 0.5 更改为 1.0:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar(isActive: score > i), // Edit this line.
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
现在,当用户正确回答问题时,AnimatedStar widget 会使用隐式动画更新其大小。Icon 的 color 在这里没有动画效果,只有 scale 有动画效果,这是通过 AnimatedScale widget 实现的。

使用 Tween 在两个值之间进行插值
请注意,在 isActive 字段更改为 true 后,AnimatedStar widget 的颜色会立即发生变化。
为了实现动画颜色效果,您可以尝试使用 AnimatedContainer widget(它是 ImplicitlyAnimatedWidget 的另一个子类),因为它可以自动为所有属性(包括颜色)添加动画效果。遗憾的是,我们的 widget 需要显示图标,而不是容器。
您还可以尝试 AnimatedIcon,该功能可在图标形状之间实现过渡效果。不过,AnimatedIcons 类中没有星形图标的默认实现。
我们将使用 ImplicitlyAnimatedWidget 的另一个子类 TweenAnimationBuilder,它接受 Tween 作为参数。Tween 是一种类,它接受两个值(begin 和 end)并计算中间值,以便动画可以显示这些值。在此示例中,我们将使用 ColorTween,它满足构建动画效果所需的 Tween 接口。
选择 Icon widget,然后在 IDE 中使用“Wrap with Builder”快速操作,将名称更改为 TweenAnimationBuilder。然后提供时长和 ColorTween。
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(Icons.star, size: 50, color: value); // And modify this line.
},
),
);
}
}
现在,热重载应用以查看新动画。

请注意,我们的 ColorTween 的 end 值会根据 isActive 参数的值而变化。这是因为,每当 Tween.end 值发生变化时,TweenAnimationBuilder 都会重新运行其动画。发生这种情况时,新动画会从当前动画值运行到新的结束值,这样您就可以随时(即使在动画运行时)更改颜色,并显示具有正确中间值的平滑动画效果。
应用曲线
这两种动画效果均以恒定速率运行,但如果动画速度加快或减慢,通常会更具视觉趣味性和信息性。
Curve 会应用缓动函数,该函数用于定义参数随时间的变化率。Flutter 在 Curves 类中提供了一系列预建的缓动曲线,例如 easeIn 或 easeOut。


这些图表(可在 Curves API 文档页面上找到)可帮助您了解曲线的运作方式。曲线会将介于 0.0 和 1.0 之间的输入值(显示在 x 轴上)转换为介于 0.0 和 1.0 之间的输出值(显示在 y 轴上)。这些图表还显示了各种动画效果在使用缓动曲线时的预览效果。
在 AnimatedStar 中创建一个名为 _curve 的新字段,并将其作为参数传递给 AnimatedScale 和 TweenAnimationBuilder widget。
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(Icons.star, size: 50, color: value);
},
),
);
}
}
在此示例中,elasticOut 曲线提供了一种夸张的弹簧效果,该效果以弹簧运动开始,并在结束时达到平衡。

热重载应用,以查看此曲线是否已应用于 AnimatedSize 和 TweenAnimationBuilder。

使用开发者工具启用慢速动画
为了调试任何动画效果,Flutter 开发者工具提供了一种减慢应用中所有动画的方法,以便您更清楚地看到动画。
如需打开 DevTools,请确保应用以调试模式运行,然后在 VSCode 中选择调试工具栏中的控件检查器,或在 IntelliJ / Android Studio 中选择调试工具窗口中的 Open Flutter DevTools 按钮,以打开控件检查器。


打开 widget 检查器后,点击工具栏中的减缓动画按钮。

5. 使用显式动画效果
与隐式动画类似,显式动画也是预建的动画效果,但它们不是采用目标值,而是采用 Animation 对象作为形参。因此,在动画已由导航过渡 AnimatedSwitcher 或 AnimationController 定义的情况下,它们非常有用。
使用显式动画效果
如需开始使用显式动画效果,请使用 AnimatedSwitcher 封装 Card widget。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher 默认使用淡入淡出效果,但您可以使用 transitionBuilder 参数替换此设置。过渡构建器提供传递给 AnimatedSwitcher 的子 widget 和一个 Animation 对象。这是一个使用显式动画的绝佳机会。
在此 Codelab 中,我们将使用的第一个显式动画是 SlideTransition,它接受一个 Animation<Offset>,用于定义传入和传出微件将在此之间移动的起始和结束偏移量。
Tween 有一个辅助函数 animate(),可将任何 Animation 转换为应用了 Tween 的另一个 Animation。这意味着,可以使用 Tween 将 AnimatedSwitcher 提供的 Animation 转换为 Animation,以提供给 SlideTransition widget。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
请注意,此函数使用 Tween.animate 将 Curve 应用于 Animation,然后将其从介于 0.0 和 1.0 之间的 Tween 转换为 x 轴上从 -0.1 过渡到 0.0 的 Tween。
或者,Animation 类有一个 drive() 函数,该函数接受任何 Tween(或 Animatable)并将其转换为新的 Animation。这样就可以“链接”补间,从而使生成的代码更简洁:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
使用显式动画的另一个优势是,它们可以组合在一起。添加另一个显式动画 FadeTransition,该动画通过封装 SlideTransition widget 使用相同的曲线动画。
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
自定义 layoutBuilder
您可能会注意到 AnimationSwitcher 存在一个小问题。当 QuestionCard 切换到新问题时,会在动画运行时将其布局在可用空间的中心,但当动画停止时,该微件会贴靠到屏幕顶部。这会导致动画卡顿,因为问题卡的最终位置与动画运行时的位置不一致。

为了解决这个问题,AnimatedSwitcher 还提供了一个 layoutBuilder 参数,可用于定义布局。使用此函数可将布局构建器配置为将卡片与屏幕顶部对齐:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
此代码是 AnimatedSwitcher 类中 defaultLayoutBuilder 的修改版本,但使用 Alignment.topCenter 而不是 Alignment.center。
摘要
- 显式动画是指采用
Animation对象的动画效果(与采用目标value和duration的ImplicitlyAnimatedWidgets相对) Animation类表示正在运行的动画,但不定义具体效果。- 使用
Tween().animate或Animation.drive()将Tweens和Curves(使用CurveTween)应用于动画。 - 使用
AnimatedSwitcher的layoutBuilder参数来调整其子项的布局方式。
6. 控制动画的状态
到目前为止,所有动画都是由框架自动运行的。隐式动画会自动运行,而显式动画效果需要 Animation 才能正常运行。在本部分中,您将学习如何使用 AnimationController 创建自己的 Animation 对象,以及如何使用 TweenSequence 将多个 Tween 组合在一起。
使用 AnimationController 运行动画
如需使用 AnimationController 创建动画,您需要按以下步骤操作:
- 创建
StatefulWidget - 在
State类中使用SingleTickerProviderStateMixinmixin,为AnimationController提供Ticker - 在
initState生命周期方法中初始化AnimationController,并向vsync(TickerProvider) 参数提供当前的State对象。 - 确保每当
AnimationController通过使用AnimatedBuilder或手动调用listen()和setState通知其监听器时,您的 widget 都会重新构建。
创建一个新文件 flip_effect.dart,然后复制并粘贴以下代码:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
此类会设置 AnimationController,并在框架调用 didUpdateWidget 以通知它微件配置已更改且可能存在新的子微件时重新运行动画。
AnimatedBuilder 可确保在 AnimationController 通知其监听器时重新构建 widget 树,而 Transform widget 用于应用 3D 旋转效果,以模拟卡片翻转。
如需使用此 widget,请使用 CardFlipEffect widget 将每个答案卡片封装起来。请务必为 Card widget 提供 key:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
现在,对应用执行热重载,以查看使用 CardFlipEffect widget 翻转的答案卡片。

您可能会注意到,此类与显式动画效果非常相似。事实上,直接扩展 AnimatedWidget 类来实现您自己的版本通常是一个不错的做法。遗憾的是,由于此类需要在其 State 中存储之前的 widget,因此需要使用 StatefulWidget。如需详细了解如何创建自己的显式动画效果,请参阅 AnimatedWidget 的 API 文档。
使用 TweenSequence 添加延迟
在本部分中,您将向 CardFlipEffect widget 添加延迟,以便每张卡片一次翻转一张。首先,添加一个名为 delayAmount 的新字段。
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
然后,将 delayAmount 添加到 AnswerCards build 方法中。
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
然后,在 _CardFlipEffectState 中,创建一个使用 TweenSequence 应用延迟的新 Animation。请注意,此示例不使用 dart:async 库中的任何实用程序,例如 Future.delayed。这是因为延迟是动画的一部分,而不是 widget 在使用 AnimationController 时明确控制的。这样一来,在开发者工具中启用慢速动画时,由于它使用相同的 TickerProvider,因此可以更轻松地调试动画效果。
如需使用 TweenSequence,请创建两个 TweenSequenceItem,一个包含 ConstantTween(用于在相对时长内将动画保持在 0),另一个包含从 0.0 到 1.0 的常规 Tween。
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration * (widget.delayAmount + 1),
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // Add from here...
if (widget.delayAmount > 0)
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: widget.delayAmount,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
]).animate(_animationController); // To here.
}
最后,在 build 方法中,将 AnimationController 的动画替换为新的延迟动画。
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
现在,热重载应用,并观看卡片逐个翻转。不妨尝试更改 Transform widget 提供的 3D 效果的透视,挑战一下自己。

7. 使用自定义导航过渡效果
到目前为止,我们已经了解了如何自定义单个屏幕上的效果,但使用动画的另一种方式是使用动画在屏幕之间进行过渡。在本部分中,您将学习如何使用内置动画效果和 pub.dev 上官方 animations 软件包提供的精美预建动画效果,将动画效果应用于屏幕转场。
为导航过渡添加动画效果
PageRouteBuilder 类是一个 Route,可用于自定义过渡动画。您可以替换其 transitionBuilder 回调,该回调提供两个 Animation 对象,分别表示由 Navigator 运行的传入动画和传出动画。
如需自定义过渡动画,请将 MaterialPageRoute 替换为 PageRouteBuilder,以便在用户从 HomeScreen 导航到 QuestionScreen 时自定义过渡动画。使用 FadeTransition(一种明确的动画微件)使新界面在旧界面之上淡入。
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // Add from here...
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
), // To here.
);
},
child: Text('New Game'),
),
动画软件包提供精美的预建动画效果,例如 FadeThroughTransition。导入动画软件包,并将 FadeTransition 替换为 FadeThroughTransition widget:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
child: Text('New Game'),
),
自定义预测性返回动画

预测性返回是一项新的 Android 功能,可让用户在导航之前查看当前路线或应用后面的内容。当用户在屏幕上向后拖动手指时,预览动画会根据手指的位置而变化。
当 Flutter 的导航堆栈上没有要弹出的路由时,或者换句话说,当返回会退出应用时,Flutter 会通过在系统级启用该功能来支持系统预测性返回。此动画由系统处理,而不是由 Flutter 本身处理。
在 Flutter 应用中,当在不同路由之间导航时,Flutter 也支持预测性返回。一个名为 PredictiveBackPageTransitionsBuilder 的特殊 PageTransitionsBuilder 会监听系统预测性返回手势,并根据手势的进度驱动页面过渡。
预测性返回仅在 Android U 及更高版本中受支持,但 Flutter 会优雅地回退到原始的返回手势行为和 ZoomPageTransitionBuilder。如需了解详情,请参阅我们的博文,其中包含有关如何在您自己的应用中设置该功能的部分。
在应用的 ThemeData 配置中,将 PageTransitionsTheme 配置为在 Android 上使用 PredictiveBack,并在其他平台上使用动画软件包中的淡入淡出过渡效果:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
现在,您可以将 Navigator.push() 回调更改为 MaterialPageRoute。
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
使用 FadeThroughTransition 更改当前问题
AnimatedSwitcher widget 仅在其 builder 回调中提供一个 Animation。为解决此问题,animations 软件包提供了一个 PageTransitionSwitcher。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // Add from here...
layoutBuilder: (entries) {
return Stack(alignment: Alignment.topCenter, children: entries);
},
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
使用 OpenContainer

animations 软件包中的 OpenContainer widget 提供了一种容器转换动画效果,该效果会展开即可在两个 widget 之间创建视觉连接。
closedBuilder 返回的 widget 最初会显示,当容器被点按或 openContainer 回调被调用时,该 widget 会展开为 openBuilder 返回的 widget。
如需将 openContainer 回调连接到视图模型,请将 viewModel 传递到 QuestionCard 微件中,并存储将用于显示“游戏结束”屏幕的回调:
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel = QuizViewModel(
onGameOver: _handleGameOver,
);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
),
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
添加新 widget GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(automaticallyImplyLeading: false),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
在 QuestionCard widget 中,将 Card 替换为 animations 软件包中的 OpenContainer widget,并添加两个新字段,分别用于 viewModel 和打开容器回调:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}

8. 恭喜
恭喜,您已成功向 Flutter 应用添加了动画效果,并了解了 Flutter 动画系统的核心组件。具体来说,您学习了以下内容:
- 如何使用
ImplicitlyAnimatedWidget - 如何使用
ExplicitlyAnimatedWidget - 如何将
Curves和Tweens应用于动画 - 如何使用预构建的转场 widget,例如
AnimatedSwitcher或PageRouteBuilder - 如何使用
animations软件包中的精美预建动画效果,例如FadeThroughTransition和OpenContainer - 如何自定义默认过渡动画,包括在 Android 上添加对预测性返回的支持。

后续操作
查看以下 Codelab:
您也可以下载动画示例应用,其中展示了各种动画技巧。
深入阅读
您可以在 flutter.dev 上找到更多动画资源:
- 动画简介
- 动画教程(教程)
- 隐式动画(教程)
- 为容器的属性添加动画效果(食谱)
- 淡入和淡出微件(食谱)
- Hero 动画
- 为页面路由过渡添加动画(食谱)
- 使用物理模拟为微件添加动画(食谱)
- 交错动画
- 动画和运动 widget(widget 目录)
您也可以在 Medium 上查看以下文章:
- 动画深入解析
- Flutter 中的自定义隐式动画
- 使用 Flutter 和 Flux / Redux 管理动画
- 如何选择适合您的 Flutter 动画 widget?
- 使用内置的显式动画实现方向性动画
- 使用隐式动画的 Flutter 动画基础知识
- 我应在何时使用 AnimatedBuilder 或 AnimatedWidget?