動畫教程
本教程展示瞭如何在 Flutter 中構建顯式動畫。
本教程向您展示瞭如何在 Flutter 中構建顯式動畫。示例循序漸進,向您介紹動畫庫的不同方面。本教程基於動畫庫中的基本概念、類和方法,您可以在 動畫簡介 中瞭解更多資訊。
Flutter SDK 還提供了內建的顯式動畫,例如 FadeTransition、SizeTransition 和 SlideTransition。這些簡單的動畫透過設定起點和終點來觸發。與此處描述的自定義顯式動畫相比,它們更易於實現。
以下部分將引導您完成幾個動畫示例。每個部分都提供指向該示例原始碼的連結。
渲染動畫
#到目前為止,您已經學習瞭如何隨時間生成數字序列。沒有任何內容呈現在螢幕上。要使用 Animation 物件進行渲染,請將 Animation 物件儲存為小部件的成員,然後使用其值來決定如何繪製。
考慮以下一個沒有動畫的 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 物件。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 重構
#https://github.com/flutter/website/tree/main/examples/animation/animate3 示例中的程式碼的一個問題是,更改動畫需要更改渲染 logo 的小部件。更好的解決方案是將責任分離到不同的類中
- 渲染 logo
- 定義
Animation物件 - 渲染過渡
您可以使用 AnimatedBuilder 類來實現這種分離。AnimatedBuilder 是渲染樹中的一個單獨類。與 AnimatedWidget 一樣,AnimatedBuilder 會自動偵聽來自 Animation 物件的通知,並根據需要標記小部件樹為髒,因此您無需呼叫 addListener()。
https://github.com/flutter/website/tree/main/examples/animation/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 小部件本身是無狀態的,並儲存定義過渡動畫所需的一組最終變數。build() 函式建立並返回 AnimatedBuilder,它接受 (Anonymous builder) 方法和 LogoWidget 物件作為引數。過渡的實際渲染髮生在 (Anonymous builder) 方法中,它建立一個大小合適的 Container 以強制 LogoWidget 縮小以適應。
程式碼中的一個難點是 child 看起來被指定了兩次。發生的情況是外部 child 引用被傳遞給 AnimatedBuilder,然後傳遞給匿名閉包,然後該物件用作其 child。最終結果是 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,
),
);
}
}
最後,初始化動畫的程式碼與 https://github.com/flutter/website/tree/main/examples/animation/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) {
return Center(
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 來連續地進行動畫。考慮一下,如果您想在不透明度從透明到不透明的同時進行動畫,該怎麼辦。
每個 tween 管理動畫的某個方面。例如
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定義了非線性曲線的進度。 - 一個
Tween在正在動畫的屬性的開始值和結束值之間進行插值。
下一步
#本教程為你使用 Tweens 建立 Flutter 動畫奠定了基礎,但還有許多其他類可以探索。你可能需要研究專門的 Tween 類、特定於你的設計系統型別的動畫、ReverseAnimation、共享元素過渡(也稱為 Hero 動畫)、物理模擬和 fling() 方法。