跳到主內容

動畫教程

本教程展示瞭如何在 Flutter 中構建顯式動畫。

本教程向您展示瞭如何在 Flutter 中構建顯式動畫。示例循序漸進,向您介紹動畫庫的不同方面。本教程基於動畫庫中的基本概念、類和方法,您可以在 動畫簡介 中瞭解更多資訊。

Flutter SDK 還提供了內建的顯式動畫,例如 FadeTransitionSizeTransitionSlideTransition。這些簡單的動畫透過設定起點和終點來觸發。與此處描述的自定義顯式動畫相比,它們更易於實現。

以下部分將引導您完成幾個動畫示例。每個部分都提供指向該示例原始碼的連結。

渲染動畫

#

到目前為止,您已經學習瞭如何隨時間生成數字序列。沒有任何內容呈現在螢幕上。要使用 Animation 物件進行渲染,請將 Animation 物件儲存為小部件的成員,然後使用其值來決定如何繪製。

考慮以下一個沒有動畫的 Flutter logo 應用

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

以下顯示了修改後的相同程式碼,以使 logo 從無到完全大小進行動畫顯示。定義 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 中建立了您的第一個動畫!

使用 AnimatedWidget 簡化

#

AnimatedWidget 基類允許您將核心小部件程式碼與動畫程式碼分離。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 重構

#

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 樹如下所示

AnimatedBuilder widget tree

從 widget 樹的底部開始,渲染 logo 的程式碼很簡單

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(),
    );
  }
}

圖中的中間三個塊都在 GrowTransitionbuild() 方法中建立,如下所示。GrowTransition 小部件本身是無狀態的,並儲存定義過渡動畫所需的一組最終變數。build() 函式建立並返回 AnimatedBuilder,它接受 (Anonymous builder) 方法和 LogoWidget 物件作為引數。過渡的實際渲染髮生在 (Anonymous builder) 方法中,它建立一個大小合適的 Container 以強制 LogoWidget 縮小以適應。

程式碼中的一個難點是 child 看起來被指定了兩次。發生的情況是外部 child 引用被傳遞給 AnimatedBuilder,然後傳遞給匿名閉包,然後該物件用作其 child。最終結果是 AnimatedBuilder 被插入到渲染樹中的兩個小部件之間。

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,
      ),
    );
  }
}

最後,初始化動畫的程式碼與 https://github.com/flutter/website/tree/main/examples/animation/animate2 示例非常相似。initState() 方法建立一個 AnimationController 和一個 Tween,然後使用 animate() 將它們繫結。魔術發生在 build() 方法中,它返回一個帶有 LogoWidget 作為 child 和一個用於驅動過渡的動畫物件的 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) {
    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 管理動畫的某個方面。例如

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 類、特定於你的設計系統型別的動畫、ReverseAnimation、共享元素過渡(也稱為 Hero 動畫)、物理模擬和 fling() 方法。