顯式動畫 Widget


到目前為止建立的動畫,只是單個方向,如果想建立往復式的動畫呢?像是自動地重複放大縮小?透過隱式動畫 Widget,也不是辦不到,就是比較麻煩。例如:

import 'package:flutter/material.dart';

void main() => runApp(
    MaterialApp(
      home: Caterpillar(),
    )
);

class Caterpillar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CaterpillarState();
}

class _CaterpillarState extends State<Caterpillar> {
  var zoom_in = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
        child: GestureDetector(
          // 點一下播放動畫
          onTap: () {
            setState(() {
              zoom_in = !zoom_in;
            });
          },
          child: AnimatedContainer(
            width: zoom_in ? 300 : 100,
            child: Image.network('https://openhome.cc/Gossip/images/caterpillar.jpg'),
            duration: Duration(seconds: 1),
            // 在動畫播放完畢後重置狀態
            onEnd: () {
              setState(() {
                zoom_in = !zoom_in;
              });
            },
          ),
        )
      ),
    );
  }
}

在這個範例中,藉由 zoom_in 來改變 AnimatedContainer 的寬度,若在 AnimatedContainer 播放動畫結束後,改變 zoom_in 並重置狀態,那麼就會寬度就會相反,這時就會再度播放動畫,效果如下:

顯式動畫 Widget

那麼…怎麼停止動畫呢?呃…或許再加個 isForward 的變數,判斷是否在 onEnd 時改變 zoom_in 之類的…不過…很麻煩…

如果你想要的動畫,不只是單向播放,而想要能控制正向、反向、停止、播放等細節時,可以改用隱式動畫 Widget,具體來說,這些 Widget 是 AnimatedWidget 的子類,在 API 文件中可以看到,Flutter 也內建了一些隱式動畫 Widget,例如這邊來使用 ScaleTransition 處理縮放:

import 'package:flutter/material.dart';

void main() => runApp(
  MaterialApp(
    home: Caterpillar(),
  )
);

class Caterpillar extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _CaterpillarState();
}

class _CaterpillarState extends State<Caterpillar> with SingleTickerProviderStateMixin {
  var beat = false;
  AnimationController scaleController;

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

    // 建立 AnimationController
    scaleController = AnimationController(
        lowerBound: 0.75,
        upperBound: 1.0,
        duration: Duration(seconds: 1),
        vsync: this
    );

    scaleController.stop();
  }

  // 記得釋放 `AnimationController`
  @override
  void dispose() {
    scaleController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
          child: ScaleTransition(
            child: Image.network('https://openhome.cc/Gossip/images/caterpillar.jpg'),
            alignment: Alignment.center,
            scale: scaleController,
          )
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          beat = !beat;
          if(beat) {
            scaleController.repeat();
          }
          else {
            scaleController.stop();
          }
        }),
        tooltip: beat ? 'Stop' : 'Beat',
        child: Icon(beat ? Icons.stop : Icons.favorite),
      ),
    );
  }
}

這邊有幾個重點,首先是 AnimationController,其實在〈分頁工具列 TabBar〉有稍微提過,TabBar 的動畫在內部就是由它來控制,顯式動畫 Widget 之所以為顯式,就在於你必須顯式地處理 AnimationController

動畫相關的邏輯依 AnimationControllervalue 等資訊來產生,value 預設會是 0.0 到 1.0,對 ScaleTransition 來說,這就是它的縮放值,也就是兩者預設的組合下,圖片會從無到有地放大,你可以自行設定 lowerBoundupperBound,作為 value 的變化範圍。

duration 就是動畫持續時間,接著是 vsync,Flutter 基本上希望能在畫面顯示時,提供每秒 60 個畫框(frame)的更新,而 TickerProvider 可以提供 Ticker 實例,每次 Flutter 在更新畫框(Frame),會呼叫指定給 Ticker 的函式,這個函式中可以操作 AnimationController,例如更新它的 value,動畫相關的邏輯依 AnimationControllervalue 等資訊來產生動畫。

簡單來說,AnimationController 需要 TickerProvider,這邊暫時不接觸 TickerProvider 等細節,只 mixin 了 SingleTickerProviderStateMixin,如此之來,_CaterpillarState 就實作了 TickerProvider,將這個實例(this)指定給 AnimationControllervsync 就可以了。

最後記得,AnimationController 不會自動釋放,因此重新實作了 dispose,在 _CaterpillarState 釋放資源時也釋放 AnimationController

ScaleTransitionscale 特性,需要 Animation<double>AnimationController 實作了 Animation<double>,如前描述,這是為了取得它的 value 來作為縮放依據,alignment 指定了縮放的原點,這邊是以圖片中心縮放。

AnimationController 本身可以有 forwardreversestoprepeat 等方法來操控 value 的計算,從而操作動畫的進行,這就是為什麼想要單向播放以外的動畫控制時,透過顯式控制比較方便的原因。

來看一下執行效果:

顯式動畫 Widget

呃…是可以控制停止,看來也是不停地播放啦!不過好像不是往復式的?每次都是由小至大?說好的由小至大再由大至小呢? AnimationControllerrepeat 有個 reverse 可以用,預設是 false,若設為 true 就是往復式的了:

scaleController.repeat(reverse: true);

效果如下:

顯式動畫 Widget