交織動畫

交織動畫是一個簡單的概念:視覺變化是隨著一系列的動作發生,而不是一次性的動作。動畫可能是純粹順序的,一個改變隨著一個改變發生,動畫也可能是部分或者全部重疊的。動畫也可能有間隙,沒有變化發生。

本指南展示如何在Flutter中建構交織動畫。

以下影片示範了 basic_staggered_animation 所執行的動畫:

在這個影片中,你可以看到一個獨立的 widget 的以下動畫,以一個帶邊框的略微有圓角的藍色矩形開始,這個矩形會按照以下順序變化:

  1. 淡出

  2. 擴大

  3. 向上移動同時變得更高

  4. 變為一個有邊框的圓圈

  5. 顏色變為橙色

向前執行之後,動畫將反向執行。

一個交織動畫的基礎結構

下圖展示了在 basic_staggered_animation 使用間隔的例子。你會注意到有以下特點:

  • 透明度在時間軸的前 10% 發生變化。

  • 透明度的變化和寬度的變化之間有一個很小的間隔。

  • 在時間軸的最後 25% 沒有動畫。

  • 增加填充使 widget 看起來向上上升。

  • 將圓角半徑增加到 0.5,將圓角正方形變成一個圓。

  • 填充和高度的變化發生在相同的時間間隔內,但它們不必這麼做。

Diagram showing the interval specified for each motion

設定這個動畫:

  • 建立一個 AnimationController 管理所有的 Animations

  • 為每一個有動畫的屬性建立一個 Tween

    • Tween 定義一個值的範圍。

    • Tween 的 animate 方法需要 parent 控制器。同時產生一個動畫為這個屬性。

  • 指定動畫的 “curve” 屬性的間隔

當控制動畫的值發生變化時,新動畫的值也隨之變化值更改,觸發 UI 更新。

下面的程式碼為 width 屬性建立了一個 tween。

它建立了一個 CurvedAnimation, 指定一個 eased curve。其他更多的預定的動畫曲線請看 Curves

width = Tween<double>(
  begin: 50.0,
  end: 150.0,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.125, 0.250,
      curve: Curves.ease,
    ),
  ),
),

beginend 的值不一定是 doubles。

下面的程式碼為 borderRadius 屬性建立一個 tween(控制矩形的圓角半徑),使用 BorderRadius.circular()

borderRadius = BorderRadiusTween(
  begin: BorderRadius.circular(4.0),
  end: BorderRadius.circular(75.0),
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Interval(
      0.375, 0.500,
      curve: Curves.ease,
    ),
  ),
),

完整的交織動畫

像所有可互動的 widgets 一樣,完整的動畫包括一對 widget:一個無狀態 widget 和一個有狀態的 widget。

無狀態 widget 指定 Tweens,定義動畫物件,提供一個 build() 方法,負責建構 widget 樹的動畫部分。

有狀態 widget 建立控制器,播放動畫,同時建構 widget 樹的非動畫部分。當在螢幕上檢測到一個點選時,動畫開始。

Full code for basic_staggered_animation’s main.dart

無狀態的 widget: StaggerAnimation

在無狀態 widget 中,StaggerAnimation,the build() 函式例項化了一個 AnimatedBuilder—一個用於建構動畫的通用 widget。 AnimatedBuilder 建構一個 widget 並使用 Tweens 的當前值配置它。這個例子建立一個名為 _buildAnimation() (實際更新 UI)的方法,並將其分配給其 builder 屬性。AnimatedBuilder 監聽來自動畫控制器的通知,當值發生更改時,將 widget 樹標記為 dirty。對於動畫的每一個標記,值都會更新,導致呼叫 _buildAnimation()

class StaggerAnimation extends StatelessWidget {
  StaggerAnimation({ Key key, this.controller }) :

    // Each animation defined here transforms its value during the subset
    // of the controller's duration defined by the animation's interval.
    // For example the opacity animation transforms its value during
    // the first 10% of the controller's duration.

    opacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.0, 0.100,
          curve: Curves.ease,
        ),
      ),
    ),

    // ... Other tween definitions ...

    super(key: key);

  final AnimationController controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius> borderRadius;
  final Animation<Color> color;

  // This function is called each time the controller "ticks" a new frame.
  // When it runs, all of the animation's values will have been
  // updated to reflect the controller's current value.
  Widget _buildAnimation(BuildContext context, Widget child) {
    return Container(
      padding: padding.value,
      alignment: Alignment.bottomCenter,
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: width.value,
          height: height.value,
          decoration: BoxDecoration(
            color: color.value,
            border: Border.all(
              color: Colors.indigo[300],
              width: 3.0,
            ),
            borderRadius: borderRadius.value,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

有狀態的 widget: StaggerDemo

有狀態的 widget, StaggerDemo,建立 AnimationController(控制所有動畫的控制器),設定一個 2000 毫秒的週期。控制器播放一個動畫,然後在 widget 樹上建立一個無動畫的部分。當在螢幕上檢測到一個點選時,動畫開始。動畫向前執行,然後向後執行。

class StaggerDemo extends StatefulWidget {
  @override
  _StaggerDemoState createState() => _StaggerDemoState();
}

class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(milliseconds: 2000),
      vsync: this
    );
  }

  // ...Boilerplate...

  Future<void> _playAnimation() async {
    try {
      await _controller.forward().orCancel;
      await _controller.reverse().orCancel;
    } on TickerCanceled {
      // the animation got canceled, probably because it was disposed of
    }
  }

  @override
  Widget build(BuildContext context) {
    timeDilation = 10.0; // 1.0 is normal animation speed.
    return Scaffold(
      appBar: AppBar(
        title: const Text('Staggered Animation'),
      ),
      body: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: Center(
          child: Container(
            width: 300.0,
            height: 300.0,
            decoration: BoxDecoration(
              color: Colors.black.withOpacity(0.1),
              border: Border.all(
                color:  Colors.black.withOpacity(0.5),
              ),
            ),
            child: StaggerAnimation(
              controller: _controller.view
            ),
          ),
        ),
      ),
    );
  }
}