跳至主要內容

動畫教學

本教學課程示範如何在 Flutter 中建構顯式動畫。在介紹動畫程式庫中的一些基本概念、類別和方法之後,它會引導您完成 5 個動畫範例。這些範例相互建立,向您介紹動畫程式庫的不同面向。

Flutter SDK 也提供內建的顯式動畫,例如 FadeTransitionSizeTransitionSlideTransition。這些簡單的動畫是透過設定開始點和結束點來觸發。它們比此處描述的自訂顯式動畫更容易實作。

基本的動畫概念和類別

#

Flutter 中的動畫系統是以類型化的 Animation 物件為基礎。Widget 可以直接在它們的建構函式中透過讀取它們的目前值並監聽它們的狀態變更來合併這些動畫,或者它們可以使用動畫作為更精細動畫的基礎,並將其傳遞給其他 widget。

Animation<double>

#

在 Flutter 中,Animation 物件不知道螢幕上的任何內容。Animation 是一個抽象類別,它會了解其目前值及其狀態(已完成或已關閉)。比較常用的動畫類型之一是 Animation<double>

Animation 物件會在特定持續時間內,依序產生兩個值之間的內插數字。Animation 物件的輸出可能是線性的、曲線、步階函式或您可以設計的任何其他對應。根據 Animation 物件的控制方式,它可以反向執行,甚至可以在中間切換方向。

動畫也可以內插 double 以外的類型,例如 Animation<Color>Animation<Size>

Animation 物件具有狀態。其目前值始終可在 .value 成員中使用。

Animation 物件不知道呈現或 build() 函式。

Curved­Animation

#

CurvedAnimation 將動畫的進度定義為非線性曲線。

dart
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationAnimationController(在下一節中說明)都是 Animation<double> 類型,因此您可以互換傳遞它們。CurvedAnimation 會包裝它正在修改的物件,您不會對 AnimationController 進行子類別化以實作曲線。

Animation­Controller

#

AnimationController 是一個特殊的 Animation 物件,它會在硬體準備好處理新的影格時產生新值。依預設,AnimationController 會在給定持續時間內線性產生從 0.0 到 1.0 的數字。例如,此程式碼會建立 Animation 物件,但不會開始執行

dart
controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

AnimationController 衍生自 Animation<double>,因此可以在需要 Animation 物件的任何地方使用。但是,AnimationController 具有其他方法來控制動畫。例如,您可以使用 .forward() 方法開始動畫。數字的產生與螢幕重新整理相關聯,因此通常每秒產生 60 個數字。在產生每個數字之後,每個 Animation 物件都會呼叫附加的 Listener 物件。若要為每個子系建立自訂顯示清單,請參閱 RepaintBoundary

建立 AnimationController 時,您會傳遞 vsync 引數。vsync 的存在可防止螢幕外動畫耗用不必要的資源。您可以透過將 SingleTickerProviderStateMixin 新增至類別定義,來使用您的具狀態物件作為 vsync。您可以在 GitHub 上的 animate1 中看到此範例。

Tween

#

依預設,AnimationController 物件的範圍從 0.0 到 1.0。如果您需要不同的範圍或不同的資料類型,您可以使用 Tween 來設定動畫,以內插到不同的範圍或資料類型。例如,下列 Tween 會從 -200.0 到 0.0

dart
tween = Tween<double>(begin: -200, end: 0);

Tween 是一個無狀態物件,它只會採用 beginendTween 的唯一工作是定義從輸入範圍到輸出範圍的對應。輸入範圍通常是 0.0 到 1.0,但這不是必要條件。

Tween 繼承自 Animatable<T>,而不是繼承自 Animation<T>AnimatableAnimation 一樣,不必輸出 double。例如,ColorTween 會指定兩種顏色之間的進度。

dart
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 物件不會儲存任何狀態。相反地,它會提供 evaluate(Animation<double> animation) 方法,該方法會使用 transform 函式將動畫的目前值(介於 0.0 和 1.0 之間)對應到實際的動畫值。

Animation 物件的目前值可以在 .value 方法中找到。evaluate 函式也會執行一些內部管理作業,例如確保在動畫值分別為 0.0 和 1.0 時,會傳回 begin 和 end。

Tween.animate

#

若要使用 Tween 物件,請在 Tween 上呼叫 animate(),並傳遞控制器物件。例如,下列程式碼會在 500 毫秒的過程中產生從 0 到 255 的整數值。

dart
AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

下列範例顯示了控制器、曲線和 Tween

dart
AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

動畫通知

#

Animation 物件可以具有 ListenerStatusListener,它們是使用 addListener()addStatusListener() 定義的。每當動畫的值變更時,就會呼叫 ListenerListener 最常見的行為是呼叫 setState() 以導致重新建構。當動畫開始、結束、向前移動或向後移動(如 AnimationStatus 所定義)時,會呼叫 StatusListener。下一節將提供 addListener() 方法的範例,而 監控動畫的進度將示範 addStatusListener() 的範例。


動畫範例

#

本節將引導您完成 5 個動畫範例。每個章節都會提供該範例的原始程式碼連結。

渲染動畫

#

到目前為止,您已學習如何在一段時間內產生一系列數字。螢幕上尚未呈現任何內容。要使用 Animation 物件呈現,請將 Animation 物件儲存為您 Widget 的成員,然後使用其值來決定如何繪製。

請考慮以下繪製沒有動畫的 Flutter 標誌的應用程式

dart
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 區段中說明。

非動畫範例的變更已標示醒目顯示

dart
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 中建立您的第一個動畫!

使用 Animated­Widget 簡化

#

AnimatedWidget 基底類別可讓您將核心 Widget 程式碼與動畫程式碼分隔開。AnimatedWidget 不需要維護 State 物件來保存動畫。新增以下 AnimatedLogo 類別

dart
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 仍管理 AnimationControllerTween,並將 Animation 物件傳遞給 AnimatedLogo

dart
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() 取得此通知。以下程式碼會修改先前的範例,使其監聽狀態變更並列印更新。醒目顯示的行會顯示變更

dart
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() 在開始或結束時反轉動畫。這會產生「呼吸」效果

dart
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範例中的程式碼有一個問題,就是變更動畫需要變更呈現標誌的 Widget。更好的解決方案是將職責分隔到不同的類別中

  • 呈現標誌
  • 定義 Animation 物件
  • 呈現轉換

您可以使用 AnimatedBuilder 類別來達成此分隔。AnimatedBuilder 是呈現樹狀結構中的獨立類別。與 AnimatedWidget 類似,AnimatedBuilder 會自動監聽來自 Animation 物件的通知,並在必要時將 Widget 樹狀結構標記為髒污,因此您不需要呼叫 addListener()

animate4範例的 Widget 樹狀結構如下所示

AnimatedBuilder widget tree

從 Widget 樹狀結構的底部開始,呈現標誌的程式碼很簡單

dart
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,該函數會採用(Anonymous 建構器)方法和 LogoWidget 物件作為參數。呈現轉換的工作實際上是在(Anonymous 建構器)方法中進行,該方法會建立適當大小的 Container 以強制 LogoWidget 縮小以符合。

以下程式碼中一個棘手的地方是子系看起來指定了兩次。發生的是,子系的外部參考會傳遞給 AnimatedBuilder,然後 AnimatedBuilder 將其傳遞給匿名閉包,然後該匿名閉包會使用該物件作為其子系。最終結果是 AnimatedBuilder 會插入呈現樹狀結構中兩個 Widget 之間。

dart
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() 方法會建立 AnimationControllerTween,然後使用 animate() 將其繫結。神奇之處發生在 build() 方法中,該方法會傳回帶有 LogoWidget 作為子系,以及一個動畫物件來驅動轉換的 GrowTransition 物件。這些是上面項目符號清單中列出的三個元素。

dart
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) {
    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(),
      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 來持續加入和退出動畫。請考慮您想要在不透明度從透明動畫化為不透明時加入和退出動畫的情況。

每個補間都會管理動畫的一個方面。例如

dart
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(),以計算所需的大小和不透明度值。以下程式碼會顯示帶有醒目顯示的變更

dart
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

下一步

#

本教學課程為您提供了使用 Tweens 在 Flutter 中建立動畫的基礎,但還有許多其他類別可以探索。您可能會研究專門的 Tween 類別、Material Design 特有的動畫、ReverseAnimation、共享元素轉換(也稱為英雄動畫)、物理模擬和 fling() 方法。請參閱動畫登陸頁面,以取得最新的可用文件和範例。