深入動畫


在〈AnimatedBuilder〉中,接觸了更多動畫的細節,其中也留下了一個問題,如果想套用緩動曲線怎麼辦?

AnimationController 其實是 Animation 的實現,Animation 還有其他的子類,例如 CurvedAnimationReverseAnimation 等,這些子類的實例可以組成階層關係,作為 child 的 Animation,可以取得 parent 的 value 套用進一步的處理,從而組合出各式的動畫。

通常 AnimationController 會作為最上層的 parent,然後套用其他的 Animation,例如,想要使用緩動曲線的話,可以如下:

  AnimationController animationController;
  Animation curve;

  @override
  void initState() {
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..stop();
    curve = CurvedAnimation(parent: animationController, curve: Curves.easeOut);
    ...
  }

AnimationControllervalue 會傳入 Curves.easeOuttransform 方法,那麼怎麼整合進〈AnimatedBuilder〉的範例呢?可以進一步地將 curve 指定給 Tweenanimate 方法:

Animation<Color> tween = ColorTween(begin: Colors.white, end: Colors.red).animate(curve);

animate 會取得傳入的 Animationvalue,進一步套用使用了 beginend 的運算,然後傳回 Animation,也就是說,如果你願意的話,Animation 增加更多的套用層次。

因此單就〈AnimatedBuilder〉中的範例,可以改寫如下:

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 start = false;

  Animation<double> animationController;
  Animation<double> curve;
  Animation<Color> tween;

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

    // 建立 Animation 階層
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..stop();
    curve = CurvedAnimation(parent: animationController, curve: Curves.easeOut);
    tween = ColorTween(begin: Colors.white, end: Colors.red).animate(curve);
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: animationController,
          builder: (_, __) {
            return Image(
              image: NetworkImage('https://openhome.cc/Gossip/images/caterpillar.jpg'),
              // 取得 Animation 的 value
              color: tween.value, 
              colorBlendMode: BlendMode.colorBurn,
            );
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {
          start = !start;
          if(start) {
            animationController.repeat(reverse: true);
          }
          else {
            animationController.stop();
          }
        }),
        tooltip: start ? 'Stop' : 'Start',
        child: Icon(start ? Icons.stop : Icons.play_arrow),
      ),
    );
  }
}

讓我們更進一步地深入動畫的細節,Flutter 是怎麼完成動畫的呢?令人驚奇地,Flutter 是不斷地重建 Widget 樹,舉個來說,上例如果不使用 AnimatedBuilder 的話,該怎麼寫呢?可以利用 AnimationaddListeneraddStatusListener 來監聽事件,例如,最基本就是透過 addListener 來監聽每次 value 的改變,然後呼叫 setState

import 'package:flutter/material.dart';

...略

class _CaterpillarState extends State<Caterpillar> with SingleTickerProviderStateMixin {
  var start = false;

  AnimationController animationController;
  Animation<double> curve;
  Animation<Color> tween;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..stop();
    curve = CurvedAnimation(parent: animationController, curve: Curves.easeOut);
    tween = ColorTween(begin: Colors.white, end: Colors.red).animate(curve);

    // 在每次 value 改變時呼叫 setState
    tween.addListener(() => setState(() {

    }));
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('openhome.cc'),
      ),
      body: Center(
        // 不使用 AnimatedBulider,直接使用 tween.value
        child: Image(
          image: NetworkImage('https://openhome.cc/Gossip/images/caterpillar.jpg'),
          color: tween.value,
          colorBlendMode: BlendMode.colorBurn,
        ),
      ),
      floatingActionButton: FloatingActionButton(
          ...略
      ),
    );
  }
}

那麼那個 SingleTickerProviderStateMixin 是做什麼的?它是 TickerProvider 的實作,顧名思義,可以提供 Ticker,Single 的意思是,它的 createTicker 方法只能被呼叫一次,之後只使用這唯一的 Ticker 來進行計時,可以實作個簡單的 TickerProvider 來仿效:

import 'package:flutter/material.dart';
import 'package:flutter/src/scheduler/ticker.dart';

...略

// 簡單的 TickerProvider 實作
class MyTickProvider implements TickerProvider {
  Ticker _ticker;

  @override
  Ticker createTicker(onTick) {
    assert(() {
      if (_ticker == null)
        return true;
      throw FlutterError('MyTickProvider 的 createTicker 只能被呼叫一次');
    }());

    _ticker = Ticker(onTick);

    return _ticker;
  }
}

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

  AnimationController animationController;
  Animation<double> curve;
  Animation<Color> tween;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      duration: const Duration(seconds: 2),
      // 使用 MyTickProvider
      vsync: MyTickProvider(),
    )..stop();
    curve = CurvedAnimation(parent: animationController, curve: Curves.easeOut);
    tween = ColorTween(begin: Colors.white, end: Colors.red).animate(curve);
    tween.addListener(() => setState(() {

    }));
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        ...略
    );
  }
}

Ticker 的目的,是在每次的計時呼叫指定的回呼函式,一開始建立時 Ticker 是停止狀態,可呼叫 start 來啟動,Ticker 建立時指定的回呼函式,必須接受 Duration,在 AnimationController 內部會管理 Ticker,回呼函式會用來變動 value,若要簡單的示意會像是:

var value = 0;
Ticker((_)  {
  print(value);
  value++;
}).start();

這會不斷地顯示 value 的遞增。基本上,你只需要知道 Ticker 的存在,透過 AnimationController 來控制 value 就可以了。

AnimationController 內部只會使用一個 Ticker,會呼叫 TickerProvidercreateTicker 一次,因此使用 SingleTickerProviderStateMixin 就可以了。

如果要操作多個 AnimationController,可以使用 TickerProviderStateMixin,你可以多次呼叫它的 createTicker,在內部,TickerProviderStateMixin 會使用 Set 來管理建立的 Ticker