Flutter動畫實現原理淺析

銅板街科技發表於2019-05-16

Flutter 動畫

本文主要介紹 Flutter 動畫相關的內容,對相關的知識點進行了梳理,並從實際例子出發,進一步分析了動畫是如果實現的。

一個簡單的動畫效果

這是一個簡單的 Flutter Logo 動畫

程式碼如下

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        setState(() {
          // Animation 物件的值改變了
        });
      });
    controller.forward();
  }
  Widget build(BuildContext context) {
  
        // ....
        height: animation.value,
        width: animation.value,
        // ....
  }
複製程式碼

這部分原始碼錶示的就是在 2 秒內將 logo 圖片從 0x0 繪製為 300x300 大小的動畫。 類 _LogoAppState 是一個 StatusfulWidget的State,可以呼叫 setStatus() 方法進行更新 weight。 我們可以看到這個例子有兩個成員變數 Animation<double> animationAnimationController controller, 這兩個物件是動畫能夠執行的關鍵。

我們看到在 animation 變數的 addListener() 回撥方法裡面呼叫了 State 的重繪方法 setState() , 而在 State 的 build() 方法裡使用了 animation 物件的值作為 weight 的寬度和高度使用。 從這裡我們能夠推測出:animation 物件通過監聽器註冊將 animation 需要更新的變動通知給監聽器, 而監聽器又呼叫 setStatus() 方法讓 widget 去重新繪製, 而在繪製時,widget 又將 animation 的 value 值當作新繪製圖形的引數, 通過這樣的機制不斷地重繪這個 weight 實現了動畫的效果。

Animation

Animation 是 Flutter 動畫庫中的核心類,它會插入指導動畫生成的值。 Animation 物件知道一個動畫當前的狀態(例如開始、 停止、 播放、 回放), 但它不知道螢幕上繪製的是什麼, 因為 Animation 物件只是提供一個值表示當前需要展示的動畫, UI 如何繪製出圖形完全取決於 UI 自身如何在渲染和 build() 方法裡處理這個值, 當然也可以不做處理。 Animation<double> 是一個比較常用的Animation類, 泛型也可以支援其它的型別,比如: Animation<Color>Animation<Size>。 Animation 物件就是會在一段時間內依次生成一個區間之間值的類, 它的輸出可以是線性的、曲線的、一個步進函式或者任何其他可以設計的對映 比如:CurvedAnimation。

AnimationController

AnimationController 是一個動畫控制器, 它控制動畫的播放狀態, 如例子裡面的: controller.forward() 就是控制動畫"向前"播放。 所以構建 AnimationController 物件之後動畫並沒有立刻開始執行。 在預設情況下, AnimationController 會在給定的時間內線性地生成從 0.0 到 1.0 之間的數字。 AnimationController 是一種特殊的 Animation 物件了, 它父類其實是一個 Animation<double>, 當硬體準備好需要一個新的幀的時候它就會產生一個新的值。 由於 AnimationController 派生自 Animation <double>,因此可以在需要 Animation 物件的任何地方使用它。 但是 AnimationController 還有其他的方法來控制動畫的播放, 例如前面提到的 .forward() 方法啟動動畫。

AnimationController 生成的數字(預設是從 0.0 到 1.0) 是和螢幕重新整理有關, 前面也提到它會在硬體需要一個新幀的時候產生新值。 因為螢幕一般都是 60 幀/秒, 所以它也通常一秒內生成 60 個數字。 每個數字生成之後, 每個 Animation 物件都會呼叫繫結的監聽器物件。

Tween

Tween 本身表示的就是一個 Animation 物件的取值範圍, 只需要設定開始和結束的邊界值(值也支援泛型)。 它唯一的工作就是定義輸入範圍到輸出範圍的對映, 輸入一般是 AnimationController 給出的值 0.0~1.0。 看下面的例子, 我們就能知道 animation 的 value 是怎麼樣通過 AnimationController 生成的值對映到 Tween 定義的取值範圍裡面的。

1、 Tween.animation 通過傳入 aniamtionController 獲得一個_AnimatedEvaluation 型別的 animation 物件(基類為 Animation), 並且將 aniamtionController 和 Tween 物件傳入了 _AnimatedEvaluation 物件。

  animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
    ...
  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }
複製程式碼

2、 animation.value 方法即是呼叫 _evaluatable.evaluate(parent) 方法, 而 _evaluatable 和 parent 分別為 Tween 物件和 AnimationController 物件。

  T get value => _evaluatable.evaluate(parent);
     ....
  class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> {
     _AnimatedEvaluation(this.parent, this._evaluatable);
     ....
複製程式碼

3、 這裡的 animation 其實就是前面的 AnimationController 物件, transform 方法裡面的 animation.value 則就是 AnimationController 線性生成的 0.0~1.0 直接的值。 在 lerp 方法裡面我們可以看到這個 0.0~1.0 的值被對映到了 begin 和 end 範圍內了。

  T evaluate(Animation<double> animation) => transform(animation.value);

    T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }

    T lerp(double t) {
    assert(begin != null);
    assert(end != null);
    return begin + (end - begin) * t;
  }
複製程式碼

Flutter 的"時鐘"

那麼 Flutter 是怎麼樣讓這個動畫在規定時間不斷地繪製的呢?

首先看 Widget 引入的 SingleTickerProviderStateMixin 類。SingleTickerProviderStateMixin 是以 with 關鍵字引入的, 這是 dart 語言的 mixin 特性, 可以理解成"繼承", 所以 widget 相當於是繼承了SingleTickerProviderStateMixin。 所以在 AnimationController 物件的構造方法引數 vsync: this, 我們看到了這個類的使用。 從 "vsync" 引數名意為"垂直幀同步"可以看出, 這個是繪製動畫幀的"節奏器"。

  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  }) : assert(lowerBound != null),
       assert(upperBound != null),
       assert(upperBound >= lowerBound),
       assert(vsync != null),
       _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }
複製程式碼

在 AnimationController 的構造方法中, SingleTickerProviderStateMixin 的父類 TickerProvider 會建立一個 Ticker, 並將_tick(TickerCallback 型別)回撥方法繫結到了 這個 Ticker, 這樣 AnimationController 就將回撥方法 _tick 和 Ticker 繫結了。

@protected
void scheduleTick({ bool rescheduling = false }) {
	assert(!scheduled);
	assert(shouldScheduleTick);
	_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
複製程式碼

而 Ticker 會在 start 函式內將_tick 被繫結到 SchedulerBinding 的幀回撥方法內。 返回的_animationId 是 SchedulerBinding 給定的下一個動作回撥的 ID, 可以根據_animationId 來取消 SchedulerBinding 上繫結的回撥。

SchedulerBinding 則是在構造方法中將自己的 _handleBeginFrame 函式和 window 的 onBeginFrame 繫結了回撥。 這個回撥會在螢幕需要準備顯示幀之前回撥。

再回到 AnimationController 看它是如何控制 Animation 的值的。

   void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    assert(elapsedInSeconds >= 0.0);
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }
複製程式碼

在 AnimationController 的回撥當中, 會有一個 Simulation 根據動畫執行了的時間(elapsed) 來計算當前的的_value 值, 而且這個值還需要處於 Animation 設定的區間之內。 除了計算_value 值之外, 該方法還會更新 Animation Status 的狀態, 判斷是否動畫已經結束。 最後通過 notifyListeners 和_checkStatusChanged 方法通知給監聽器 value 和 AnimationStatus 的變化。 監聽 AnimationStatus 值的變化有一個專門的註冊方法 addStatusListener。

通過監聽 AnimationStatus, 在動畫開始或者結束的時候反轉動畫, 就達到了動畫迴圈播放的效果。

   ...
   animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    });
    controller.forward();

    ...
複製程式碼

回顧一下這個動畫繪製呼叫的順序就是, window 呼叫 SchedulerBinding 的_handleBeginFrame 方法, SchedulerBinding 呼叫 Ticker 的_tick 方法, Ticker 呼叫 AnimationController 的_tick 的方法, AnimationContoller 通知監聽器, 而監聽器呼叫 widget 的 setStatus 方法來呼叫 build 更新, 最後 build 使用了 Animation 物件當前的值來繪製動畫幀。

看到這裡會有一個疑惑, 為什麼監聽器是註冊在 Animation 上的, 監聽通知反而由 AnimationController 傳送?

還是看原始碼吧。

  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }

class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> {
  _AnimatedEvaluation(this.parent, this._evaluatable);
}

mixin AnimationWithParentMixin<T> {
  Animation<T> get parent;
  /// Listeners can be removed with [removeListener].
  void addListener(VoidCallback listener) => parent.addListener(listener);
}

複製程式碼

首先 Animation 物件是由 Tween 的 animate 方法生成的, 它傳入了 AnimationController(Animation 的子類) 引數 作為 parent 引數, 然後我們發現返回的 _AnimatedEvaluation<T> 泛型物件 使用 mixin "繼承" 了 AnimationWithParentMixin<double>, 最後我們看到 Animation 作為 AnimationWithParentMixin 的"子類"實現的 addListener 方法其實是將監聽器註冊到 parent 物件上了, 也就是 AnimationController。

總結

本篇文章從簡單的例子出發, 並且結合了原始碼, 分析了 Flutter 動畫實現的原理。Flutter 以硬體裝置重新整理為驅動, 驅使 widget 依據給定的值生成新動畫幀, 從而實現了動畫效果。

作者簡介

瑞恩,銅板街客戶端開發工程師,2017年11月加入團隊,目前主要負責APP端專案開發。

想要獲取更多有關 Flutter 相關內容,請掃描以下二維碼關注“銅板街科技”公眾號,並在對話方塊內回覆 “Flutter” 關鍵詞即可。

Flutter動畫實現原理淺析

相關文章