Flutter 動畫
本文主要介紹 Flutter 動畫相關的內容,對相關的知識點進行了梳理,並從實際例子出發,進一步分析了動畫是如果實現的。
一個簡單的動畫效果
程式碼如下
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> animation
和 AnimationController 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” 關鍵詞即可。