1. 簡介
動畫是改善應用程式使用者體驗、向使用者傳達重要資訊,以及讓應用程式更精緻、使用起來更愉快的好方法。
Flutter 動畫架構簡介
Flutter 會在每個影格上重建小工具樹的一部分,藉此顯示動畫效果。它提供預先建構的動畫效果和其他 API,可簡化動畫的建立和組合作業。
- 隱含動畫是預先建構的動畫特效,可自動執行整個動畫。當動畫的目標值變更時,動畫會從目前值執行至目標值,並在兩者之間顯示每個值,讓小工具的動畫流暢播放。隱含動畫的範例包括
AnimatedSize
、AnimatedScale
和AnimatedPositioned
。 - 明確動畫也是預先建構的動畫效果,但需要
Animation
物件才能運作。例如SizeTransition
、ScaleTransition
或PositionedTransition
。 - Animation 是代表執行中或已停止的動畫的類別,由值和狀態組成,前者代表動畫執行的目標值,後者則代表動畫在任何特定時間點在螢幕上顯示的目前值。這是
Listenable
的子類別,會在動畫執行期間狀態變更時通知其監聽器。 - AnimationController 是一種建立動畫並控制其狀態的方式。其方法 (例如
forward()
、reset()
、stop()
和repeat()
) 可用於控制動畫,而不需要定義要顯示的動畫效果,例如縮放、大小或位置。 - Tweens 可用於內插起始值和結束值之間的值,且可代表任何類型,例如雙精度、
Offset
或Color
。 - 曲線可用於調整參數隨時間變化的速率。動畫執行時,通常會套用緩和曲線,讓動畫開始或結束時的變化速率變快或變慢。曲線會接收介於 0.0 和 1.0 之間的輸入值,並傳回介於 0.0 和 1.0 之間的輸出值。
建構項目
在本程式碼研究室中,您將建構一個多項選擇測驗遊戲,其中包含各種動畫效果和技巧。
您將瞭解如何...
- 建構可設定大小和顏色動畫的小工具
- 建構 3D 資訊卡翻轉效果
- 使用動畫套件中的精美預先建構動畫效果
- 新增最新版 Android 的預測返回手勢支援功能
課程內容
在本程式碼研究室中,您將學到:
- 如何使用隱含動畫效果,在不需編寫大量程式碼的情況下,製作出精美的動畫。
- 如何使用明確的動畫效果,透過預先建立的動畫小工具 (例如
AnimatedSwitcher
或AnimationController
) 設定自己的效果。 - 如何使用
AnimationController
定義可顯示 3D 效果的小工具。 - 如何使用
animations
套件,在設定最少的情況下顯示精美的動畫效果。
軟硬體需求
- Flutter SDK
- IDE,例如 VSCode 或 Android Studio / IntelliJ
2. 設定 Flutter 開發環境
您需要兩個軟體才能完成這個實驗室活動,分別是 Flutter SDK 和編輯器。
您可以使用下列任一裝置執行程式碼研究室:
- 實體 Android (建議在步驟 7 中實作預測返回功能) 或 iOS 裝置,並連接至電腦並設為開發人員模式。
- iOS 模擬器 (需要安裝 Xcode 工具)。
- Android Emulator (需要在 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:顯示含有「New Game」按鈕的畫面
- main.dart:設定
MaterialApp
以使用 Material 3 並顯示主畫面 - model.dart:定義整個應用程式所使用的核心類別
- question_screen.dart:顯示猜謎遊戲的 UI
- view_model.dart:儲存測驗遊戲的狀態和邏輯,由
QuestionScreen
顯示
應用程式目前不支援任何動畫效果,但當使用者按下「New Game」按鈕時,Flutter 的 Navigator
類別會顯示預設的檢視畫面轉場效果。
4. 使用隱含的動畫效果
隱含動畫不需要任何特殊設定,因此在許多情況下都是不錯的選擇。在本節中,您將更新 StatusBar
小工具,以便顯示動畫版面。如要查看常見的隱含動畫效果,請瀏覽 ImplicitlyAnimatedWidget API 說明文件。
建立不含動畫的計分板小工具
建立新檔案 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
小工具的子項中新增 Scoreboard
小工具,取代先前顯示分數和總題數的 Text
小工具。編輯器應會自動在檔案頂端新增必要的 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
),
],
),
),
);
}
}
這個小工具會為每個問題顯示星號圖示。當學生回答正確時,另一顆星星會立即亮起,但沒有任何動畫效果。在後續步驟中,您將透過動畫效果呈現分數大小和顏色,以便通知使用者分數已變更。
使用隱含動畫效果
建立名為 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( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
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
小工具會使用隱含動畫更新大小。Icon
的 color
在這裡不會顯示動畫,只有 scale
會顯示,而 scale
是由 AnimatedScale
小工具顯示。
使用 Tween 在兩個值之間進行內插
請注意,isActive
欄位變更為 true 後,AnimatedStar
小工具的顏色會立即變更。
如要呈現動畫顏色效果,您可以嘗試使用 AnimatedContainer
小工具 (這是 ImplicitlyAnimatedWidget
的另一個子類別),因為它可以自動為所有屬性 (包括顏色) 設定動畫效果。很抱歉,我們的小工具必須顯示圖示,而非容器。
您也可以試試 AnimatedIcon
,這個元素會在圖示形狀之間實作轉場效果。不過,AnimatedIcons
類別中並未提供星號圖示的預設實作方式。
我們會改用另一個 ImplicitlyAnimatedWidget
子類別,稱為 TweenAnimationBuilder
,這個子類別會將 Tween
做為參數。轉場是一種類別,會採用兩個值 (begin
和 end
) 並計算中間值,以便動畫顯示這些值。在本例中,我們會使用 ColorTween
,這個類別可滿足建構動畫效果所需的 Tween<Color>
介面。
選取 Icon
小工具,然後在 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, // Modify from here...
);
}, // To here.
),
);
}
}
現在,請熱載應用程式,查看新的動畫。
請注意,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
小工具。
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 的「Debug」工具列中選取「Widget Inspector」,或在 IntelliJ / Android Studio 的「Debug」工具視窗中選取「Open Flutter DevTools」按鈕。
5. 使用明確的動畫效果
與隱含動畫一樣,明確動畫也是預先建構的動畫效果,但它們不會採用目標值,而是採用 Animation
物件做為參數。因此,如果動畫已由導覽轉場 AnimatedSwitcher
或 AnimationController
定義,這類函式就非常實用。
使用明確的動畫特效
如要開始使用明確的動畫效果,請使用 AnimatedSwitcher
包裝 Card
小工具。
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
的子項小工具,以及 Animation
物件。這正是使用明確動畫的絕佳時機。
在本程式碼研究室中,我們將使用第一個明確的動畫 SlideTransition
,該動畫會使用 Animation<Offset>
定義輸入和輸出小工具之間的開始和結束偏移量。
轉場效果有輔助函式 animate()
,可將任何 Animation
轉換為套用轉場效果的另一個 Animation
。也就是說,您可以使用 Tween<Offset>
將 AnimatedSwitcher
提供的 Animation<double>
轉換為 Animation<Offset>
,再提供給 SlideTransition
小工具。
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
,然後將其從 Tween<double>
(範圍為 0.0 到 1.0) 轉換為 Tween<Offset>
(在 x 軸上從 -0.1 轉換至 0.0)。
或者,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);
},
使用明確動畫的另一個優點是,您可以輕鬆將這些動畫組合在一起。新增另一個明確的動畫:透過包裝 SlideTransition 小工具,使用相同的曲線動畫 FadeTransition。
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
},
自訂版面配置建構工具
您可能會發現 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 物件的動畫效果 (與 ImplicitlyAnimatedWidget 相反,後者會採用目標值和時間長度)
- Animation 類別代表執行中的動畫,但不會定義特定效果。
- 使用 Tween().animate 或 Animation.drive(),將 Tween 和曲線 (使用 CurveTween) 套用至動畫。
- 使用 AnimatedSwitcher 的 layoutBuilder 參數,調整子項的版面配置方式。
6. 控制動畫狀態
到目前為止,每個動畫都會由架構自動執行。隱含動畫會自動執行,而明確的動畫效果則需要動畫才能正常運作。在本節中,您將瞭解如何使用 AnimationController 建立 Animation 物件,並使用 TweenSequence 將 Tween 組合在一起。
使用 AnimationController 執行動畫
如要使用 AnimationController 建立動畫,請按照下列步驟操作:
- 建立 StatefulWidget
- 在 State 類別中使用 SingleTickerProviderStateMixin 混合元件,為 AnimationController 提供 Ticker
- 在 initState 生命週期方法中初始化 AnimationController,為
vsync
(TickerProvider) 參數提供目前的 State 物件。 - 請確認 AnimationController 通知其監聽器時,小工具會重新建構,方法是使用 AnimatedBuilder 或手動呼叫 listen() 和 setState。
建立新檔案 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 通知其監聽器時,小工具樹會重新建構,並使用 Transform 小工具套用 3D 旋轉效果,模擬翻轉卡片的動作。
如要使用這個小工具,請將每張答案資訊卡包裝在 CardFlipEffect 小工具中。請務必為資訊卡小工具提供 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 小工具翻轉。
您可能會注意到,這個類別看起來很像明確的動畫效果。事實上,直接擴充 AnimatedWidget 類別,以便實作您自己的版本,通常是個不錯的做法。不過,由於這個類別需要在其狀態中儲存先前的 Widget,因此必須使用 StatefulWidget。如要進一步瞭解如何建立自己的明確動畫效果,請參閱 AnimatedWidget 的 API 說明文件。
使用 TweenSequence 新增延遲時間
在本節中,您將為 CardFlipEffect 小工具新增延遲,讓每張資訊卡逐一翻轉。首先,新增名為 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
建構方法。
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
套用延遲時間。請注意,這並不使用 dart:async
程式庫中的任何公用程式,例如 Future.delayed
。這是因為延遲時間是動畫的一部分,並非小工具使用 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>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
最後,在建構方法中將 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
小工具提供的 3D 效果視角。
7. 使用自訂導覽轉場效果
到目前為止,我們已瞭解如何在單一畫面上自訂效果,但動畫的另一種用途是用於在不同畫面之間轉場。在本節中,您將瞭解如何使用內建動畫效果和 pub.dev 上的官方動畫套件提供的精美預先建構動畫效果,將動畫效果套用至畫面轉場效果。
為導覽轉場加上動畫效果
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( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
動畫套件提供精美的預先建構動畫效果,例如 FadeThroughTransition。匯入動畫套件,並將 FadeTransition 替換為 FadeThroughTransition 小工具:
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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
自訂預測返回動畫
預測返回是 Android 的新功能,可讓使用者在前往下一個路徑或應用程式前,先查看目前路徑或應用程式後方有哪些內容。Peek 動畫會根據使用者在螢幕上拖曳手指的位置而運作。
當 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),
useMaterial3: true,
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(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
使用 FadeThroughTransition 變更目前的問題
AnimatedSwitcher 小工具只會在建構函式回呼中提供一個動畫。為解決這個問題,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( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
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 小工具提供容器轉換動畫效果,可在兩個小工具之間建立視覺連結。
系統會一開始顯示 closedBuilder
傳回的小工具,然後在輕觸容器或呼叫 openContainer
回呼時,將其展開為 openBuilder
傳回的小工具。
如要將 openContainer
回呼連結至檢視畫面模型,請將新的傳遞檢視畫面模型新增至 QuestionCard 小工具,並儲存用於顯示「Game Over」畫面的回呼:
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
}
新增「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 小工具中,將 Card 替換為來自動畫套件的 OpenContainer 小工具,並為 viewModel 和 OpenContainer 回呼新增兩個新欄位:
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
- 如何將曲線和補間效果套用至動畫
- 如何使用預先建構的轉場小工具,例如 AnimatedSwitcher 或 PageRouteBuilder
- 如何使用
animations
套件中的精緻預先建構動畫效果,例如 FadeThroughTransition 和 OpenContainer - 如何自訂預設轉場動畫,包括新增 Android 上的預測返回支援。
後續步驟
請查看以下程式碼研究室:
或者下載動畫範例應用程式,瞭解各種動畫技巧
其他資訊
您可以在 flutter.dev 上找到更多動畫資源:
- 動畫簡介
- 動畫教學課程 (教學課程)
- 隱含動畫 (教學課程)
- 為容器的屬性設定動畫 (食譜)
- 淡入淡出小工具 (食譜)
- 主頁橫幅動畫
- 為頁面路徑轉換加上動畫效果 (食譜)
- 使用物理模擬功能為小工具製作動畫 (食譜)
- 動畫間隔
- 動畫和動態效果小工具 (小工具目錄)
或者,請參閱 Medium 上的以下文章:
- 動畫深入探索
- 在 Flutter 中自訂隱含動畫
- 使用 Flutter 和 Flux / Redux 管理動畫
- 如何選擇適合您的 Flutter 動畫小工具?
- 方向動畫,內含內建明確動畫
- 使用隱含動畫的 Flutter 動畫基本概念
- 何時該使用 AnimatedBuilder 或 AnimatedWidget?