本文通過程式碼層面去分析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庫基本上就是插值器的實現部分,這部分比較簡單
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,歡迎大家關注。