Flutter 動畫詳解(二)

吹個大氣球發表於2018-10-29

本文通過程式碼層面去分析Flutter動畫的實現過程,介紹了Flutter中的Animation庫以及Physics庫。

1. 介紹

本文會從程式碼層面去介紹Flutter動畫,因此不會涉及到Flutter動畫的具體使用。

1.1 Animation庫

Flutter的animation庫只依賴兩個庫,Dart庫以及physics庫。animation是採用Dart編寫的,所以依賴Dart庫是很正常的。physics庫是什麼呢?

Simple one-dimensional physics simulations, such as springs, friction, and gravity, for use in user interface animations.

physics庫是一個簡單的物理模擬的庫,包含彈簧、阻尼、重力等物理效果。前篇文章介紹過Flutter動畫,Flutter動畫兩個分類中的一個就是基於物理的動畫(Physics-based animation)。所以可以猜測出animation庫中有一部分程式碼,是實現了另一種動畫--補間動畫(Tween Animation)。

通過這種庫的劃分,也可以大致猜測出,基於物理動畫的庫是後續新增的。這說明了什麼呢?

  • 補間動畫是現代移動端相對基礎的動畫型別,這個是必須的;
  • 基於物理動畫是在體驗上的改善新增上去的,大致可以猜測出為iOS端的體驗優化;

1.2 Physics庫

Flutter基於物理的動畫,實際上是相當簡單的。目前實現了彈簧、阻尼、重力三種物理效果,整個庫的程式碼量也不多。詳細的程式碼在下面的部分介紹,在此處,我們先來說下基於物理的動畫庫的原理。

基於物理的動畫,給我們的感覺會更真實,這是因為其更符合人們日常生活的感官。例如做一個球體下落的動畫,如果是勻速的下落,給人的感覺會不夠真實,實際的生活經驗告訴我們,球體自由下落應該是會有先慢後快的一個過程。如果讓我們自己去實現這麼一個動畫效果,我們會怎麼去處理呢?

高中物理我們學習過自由落體相關的概念,其中的位移計算公式:

s = 1/2 * g * t * t

從公式中我們知道,自由落體的位移跟時間不是線性關係。我們可以根據這個公式,來實時的計算出位移來。

如果是摩擦阻尼或者彈簧呢,也都有相關的物理公式,我們所謂的基於物理的動畫庫,也就是基於此類公式來實現的,本質上還是補間動畫,只不過過程遵循物理規律比較複雜罷了。

2. Animation庫

講解這一部分,也考慮過將Flutter的動畫原理先介紹一下。想了想,前一篇文章介紹過這些普世的動畫原理,Flutter只不過是特定平臺的實現,無非是實現手段的不同,因此,Flutter動畫原理的解析,放到本文最後的小節部分,在程式碼的基礎上去解釋,筆者覺得更加好理解。

2.1 animation.dart

animation.dart定義了動畫的四種狀態,以及核心的抽象類Animation。

2.1.1 動畫的四種狀態

這個檔案中定義了Animation的四種狀態:

  • dismissed:動畫的初始狀態
  • forward:從頭到尾播放動畫
  • reverse:從尾到頭播放動畫
  • completed:動畫完成的狀態

2.1.2 Animation類

Animation類是Flutter動畫中核心的抽象類,它包含動畫的當前值和狀態兩個屬性。定義了動畫的一系列回撥,

  • 動畫過程中值變化的回撥:
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
複製程式碼
  • 狀態的回撥函式:
void addStatusListener(AnimationStatusListener listener);
void removeStatusListener(AnimationStatusListener listener);
複製程式碼

2.2 curve.dart

A curve must map t=0.0 to 0.0 and t=1.0 to 1.0.

看到這段英文,首先會想到什麼?沒錯,插值器。Curve也是一個抽象類,定義了時間與數值的一個介面。

double transform(double t);
複製程式碼

例如一個線性的插值器,實現程式碼如下。

class _Linear extends Curve {
  const _Linear._();

  @override
  double transform(double t) => t;
}
複製程式碼

該檔案下面定義了非常多型別的插值器,具體的實現不一一展開了。Flutter定義了一系列的插值器,封裝在Curves類中,有下面13種效果。

  • linear
  • decelerate
  • ease
  • easeIn
  • easeOut
  • easeInOut
  • fastOutSlowIn
  • bounceIn
  • bounceOut
  • bounceInOut
  • elasticIn
  • elasticOut
  • elasticInOut

如果上面的13種還不滿足需求的話,還可以使用Cubic類來進行自定義的構造。可以看出這塊兒實現參考了web中的相關實現。

2.3 tween.dart

該檔案定義了一系列的估值器,Flutter通過抽象類Animatable來實現估值器。關於Animatable,我們可以先看下其定義。

An object that can produce a value of type T given an [Animation] as input.

可以根據不同的輸入,產出不同的數值。通過過載下面的函式來產生不同的估值器。

T transform(double t);
複製程式碼

它的最主要的子類是Tween,一個線性的估值器,實現如下,非常的簡單,就是一個線性函式。

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

在Tween的基礎上實現了不同型別的估值器。

  • ReverseTween
  • ColorTween
  • SizeTween
  • RectTween
  • IntTween
  • StepTween
  • ConstantTween

還可以通過自定義的插值器去實現估值器,例如通過curve實現的估值器CurveTween。

2.4 animation_controller.dart

動畫的控制,就在這個檔案下面實現,其中最重要的部分是AnimationController,它派生自Animation類。

AnimationController的功能有如下幾種:

  • 播放一個動畫(forwaed或者reverse),或者停止一個動畫;
  • 設定動畫的值;
  • 設定動畫的邊界值;
  • 建立基於物理的動畫效果。

預設情況下,AnimationController是線性的產生0.0到1.0之間的值,每重新整理一幀就產出一個數值。AnimationController在不使用的時候需要dispose,否則會造成資源的洩漏。

2.4.1 TickerProvider

提到AnimationController必須要先說一下TickerProvider。

An interface implemented by classes that can vend Ticker objects.

TickerProvider定義了可以傳送Ticker物件的介面,

Ticker createTicker(TickerCallback onTick);
複製程式碼

Ticker能幹什麼呢?

Tickers can be used by any object that wants to be notified whenever a frame triggers.

它的主要作用是獲取每一幀重新整理的通知,作用就顯而易見了,相當於給動畫新增了一個動起來的引擎。

2.4.2 AnimationController

現在再次回到AnimationController。上面為什麼要先說一下TickerProvider呢,這是因為AnimationController的建構函式中需要一個TickerProvider引數。

結合上面介紹的插值器、估值器以及Ticker回撥,AnimationController大致的工作流程,我相信很多人都可以理出來了。

隨著時間的流逝,插值器根據時間產生的值作為輸入,提供給估值器,產生動畫的實際效果值,結合Ticker的回撥,渲染出當前動畫值的影像。這也是補間動畫的工作原理。

補間動畫

AnimationController具體的原始碼不做分析了,可以看到Flutter的動畫實現的其實是相當的原始,AnimationController需要一個觸發重新整理的回撥,輸出也是值的改變,並不像成熟平臺裡面的配合View去做動畫。

3. Physics庫

Physics庫基本上就是插值器的實現部分,這部分比較簡單

Physics動畫庫

Simulation定義了基於物理動畫的相關介面,具體有位置、速度、是否完成以及公差(Tolerance)

double x(double time);
double dx(double time);
複製程式碼

GravitySimulation的實現如下,其中_a加速度,_x是初始距離,_v是初始速度:

@override
double x(double time) => _x + _v * time + 0.5 * _a * time * time;

@override
double dx(double time) => _v + time * _a;
複製程式碼

相信學過高中物理的讀者,對這公式不會陌生。其他幾種具體實現不在此處一一展開了哈。如果擴充套件這個物理動畫庫的話,也很好去擴充套件,掌握一些物理公式,就可以去仿照實現了。

4. Ticker

關於動畫的驅動,在此簡單的說一下,Ticker是被SchedulerBinding所驅動。SchedulerBinding則是監聽著Window.onBeginFrame回撥。

Window.onBeginFrame的作用是什麼呢,是告訴應用該提供一個scene了,它是被硬體的VSync訊號所驅動的。

具體可以檢視sky_engine下面的window.dart的實現,不做展開了。

5. 小節

本篇文章簡單的從程式碼的層面解析了一下Flutter的動畫,更深入的Ticker這塊兒,感興趣的讀者可以自行去了解,這塊兒涉及到sky_engine下面的程式碼。

本篇文章,筆者依然試圖繞過程式碼去講解一些普適性的東西,但是Flutter這塊兒程式碼實現的確實挺簡單的,造成的問題是呼叫起來費勁。

基於物理的動畫,我們要知道深層次的是物理公式,有這個基礎,我們才可以製作出符合感官的動畫效果。其本質也是補間動畫,過程可以被計算出來。

可以說的寬泛一些,一般的動畫,大部分都是補間動畫,如果我們自行去設計一套動畫系統,插值器、估值器、驅動部分以及動畫的管理部分,這四個模組之間相互協調輸出一幀一幀的動畫過場。絕大部分平臺的動畫設計,也都逃不過這些因素,只不過實現的方式各不相同。

如果文中有錯誤的地方,煩請指正,筆者水平有限,再次感謝。

6. 後話

筆者建了一個Flutter學習相關的專案,Github地址,裡面包含了筆者寫的關於Flutter學習相關的一些文章,會定期更新,也會上傳一些學習Demo,歡迎大家關注。

7. 參考

  1. Animations in Flutter
  2. Tutorial: Animations in Flutter
  3. TickerProvider class
  4. Ticker class
  5. SchedulerBinding class
  6. onBeginFrame property

相關文章