本教程向您展示如何在 Flutter 中構建顯式動畫。這些示例相互借鑑,向您介紹了動畫庫的不同方面。本教程基於動畫庫中的基本概念、類和方法構建,您可以在動畫簡介中瞭解它們。

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

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

渲染動畫

#

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

考慮以下不帶動畫繪製 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 中建立了您的第一個動畫!

使用 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 重構

#

animate3 示例中的程式碼存在一個問題,即更改動畫需要更改渲染標誌的小部件。一個更好的解決方案是將職責分離到不同的類中

  • 渲染標誌
  • 定義 Animation 物件
  • 渲染過渡

您可以使用 AnimatedBuilder 類來實現這種分離。AnimatedBuilder 是渲染樹中的一個獨立類。與 AnimatedWidget 一樣,AnimatedBuilder 自動監聽 Animation 物件的通知,並在必要時將小部件樹標記為髒,因此您無需呼叫 addListener()

animate4 示例的小部件樹如下所示

AnimatedBuilder widget tree

從小部件樹的底部開始,渲染標誌的程式碼很簡單

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,它將(匿名構建器)方法和 LogoWidget 物件作為引數。渲染過渡的工作實際上發生在(匿名構建器)方法中,該方法建立了一個適當大小的 Container 以強制 LogoWidget 縮小以適應。

以下程式碼中一個棘手的地方是子項看起來被指定了兩次。實際上,子項的外部引用被傳遞給 AnimatedBuilderAnimatedBuilder 又將其傳遞給匿名閉包,然後閉包將該物件用作其子項。最終結果是 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,
      ),
    );
  }
}

最後,初始化動畫的程式碼與 animate2 示例非常相似。initState() 方法建立一個 AnimationController 和一個 Tween,然後用 animate() 將它們繫結。魔術發生在 build() 方法中,該方法返回一個 GrowTransition 物件,其中包含一個 LogoWidget 作為子項,以及一個驅動過渡的動畫物件。這些是上面專案符號列表中列出的三個元素。

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 持續進出動畫。考慮一種情況,您希望在不透明度從透明變為不透明的同時進行進出動畫。

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

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、共享元素過渡(也稱為英雄動畫)、物理模擬和 fling() 方法。