動畫教程
本教程展示瞭如何在 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 物件。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 基類允許你將核心 Widget 程式碼與動畫程式碼分離開來。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 重構
#animate3 示例中程式碼的一個問題是,更改動畫需要更改渲染 Logo 的 Widget。更好的解決方案是將職責分離到不同的類中
- 渲染 Logo
- 定義
Animation物件 - 渲染過渡動畫
你可以藉助 AnimatedBuilder 類來實現這種分離。AnimatedBuilder 是渲染樹中的一個獨立類。與 AnimatedWidget 一樣,AnimatedBuilder 會自動監聽來自 Animation 物件的通知,並根據需要標記 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,它將(匿名構建器)方法和 LogoWidget 物件作為引數。渲染過渡的實際工作發生在(匿名構建器)方法中,該方法建立一個適當大小的 Container 來強制 LogoWidget 縮小以適應大小。
以下程式碼的一個棘手之處在於 child 看起來被指定了兩次。實際發生的情況是,外部的 child 引用被傳遞給了 AnimatedBuilder,後者將其傳遞給匿名閉包,然後閉包將該物件用作其子元件。最終結果是 AnimatedBuilder 被插入到了渲染樹中的這兩個 Widget 之間。
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 為子元件的 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() 方法。