動畫教程
本教程向您展示如何在 Flutter 中構建顯式動畫。這些示例相互借鑑,向您介紹了動畫庫的不同方面。本教程基於動畫庫中的基本概念、類和方法構建,您可以在動畫簡介中瞭解它們。
Flutter SDK 還提供了內建的顯式動畫,例如FadeTransition、SizeTransition 和SlideTransition。這些簡單的動畫透過設定起點和終點來觸發。它們比此處描述的自定義顯式動畫更容易實現。
以下部分將引導您完成幾個動畫示例。每個部分都提供了該示例的原始碼連結。
渲染動畫
#到目前為止,您已經學會了如何隨時間生成一系列數字。螢幕上還沒有渲染任何內容。要使用 Animation 物件進行渲染,請將 Animation 物件儲存為小部件的成員,然後使用其值來決定如何繪製。
考慮以下不帶動畫繪製 Flutter 標誌的應用程式
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
以下顯示了修改後的相同程式碼,以動畫方式使標誌從無到完全大小增長。定義 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 重構
#animate3 示例中的程式碼存在一個問題,即更改動畫需要更改渲染標誌的小部件。一個更好的解決方案是將職責分離到不同的類中
- 渲染標誌
- 定義
Animation物件 - 渲染過渡
您可以使用 AnimatedBuilder 類來實現這種分離。AnimatedBuilder 是渲染樹中的一個獨立類。與 AnimatedWidget 一樣,AnimatedBuilder 自動監聽 Animation 物件的通知,並在必要時將小部件樹標記為髒,因此您無需呼叫 addListener()。
animate4 示例的小部件樹如下所示

從小部件樹的底部開始,渲染標誌的程式碼很簡單
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,它將(匿名構建器)方法和 LogoWidget 物件作為引數。渲染過渡的工作實際上發生在(匿名構建器)方法中,該方法建立了一個適當大小的 Container 以強制 LogoWidget 縮小以適應。
以下程式碼中一個棘手的地方是子項看起來被指定了兩次。實際上,子項的外部引用被傳遞給 AnimatedBuilder,AnimatedBuilder 又將其傳遞給匿名閉包,然後閉包將該物件用作其子項。最終結果是 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,
),
);
}
}最後,初始化動畫的程式碼與 animate2 示例非常相似。initState() 方法建立一個 AnimationController 和一個 Tween,然後用 animate() 將它們繫結。魔術發生在 build() 方法中,該方法返回一個 GrowTransition 物件,其中包含一個 LogoWidget 作為子項,以及一個驅動過渡的動畫物件。這些是上面專案符號列表中列出的三個元素。
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 持續進出動畫。考慮一種情況,您希望在不透明度從透明變為不透明的同時進行進出動畫。
每個補間動畫管理動畫的一個方面。例如
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、共享元素過渡(也稱為英雄動畫)、物理模擬和 fling() 方法。