Flutter框架分析分析系列文章:
《Flutter框架分析(三)-- Widget,Element和RenderObject》
《Flutter框架分析(四)-- Flutter框架的執行》
前言
前四篇文章介紹了Flutter框架的全貌,相信大家對Flutter框架有了個整體的瞭解。這一系列文章始終是圍繞著渲染流水線的的執行的各個階段加以說明。我們知道在Vsync訊號到來以後首先執行的是動畫(Animate)階段。而這個階段是在從engine回撥window
的onBeginFrame
函式開始執行的。那麼這篇文章我們就來介紹一下Flutter框架的動畫基本原理。
例子
所謂動畫其實就是一系列連續變化的圖片在極短的時間逐幀顯示,在人眼看來就是動畫了。這裡我們舉一個簡單的例子先說明一下在Flutter中怎麼執行一個動畫:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: LogoAnim()));
}
class LogoAnim extends StatefulWidget {
_LogoAnimState createState() => _LogoAnimState();
}
class _LogoAnimState extends State<LogoAnim> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {
});
});
controller.forward(from: 0);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
void dispose() {
controller.dispose();
super.dispose();
}
}
複製程式碼
這個動畫是在手機螢幕上由小到大漸變的顯示一個Flutter標誌。從上述程式碼中我們可以看到在Flutter中實現一個動畫要做這麼幾件事。
- 首先施加動畫的
Widget
是個StatefulWidget
。其State
要混入(mixin
)SingleTickerProviderStateMixin
。 - 在
initState()
裡要加入和動畫相關的初始化,這裡我們例項化了兩個類AnimationController
和Animation
。例項化AnimationController
的時候我們傳入了兩個引數,一個是動畫的時長,另一個是State
自己,這裡其實是利用到了混入的SingleTickerProviderStateMixin
。例項化另一個Animation
的時候,我們首先例項化的是一個Tween
。這個類其實代表了從最小值到最大值的一個線性變化。所以例項化的時候要傳入開始和結束值。然後呼叫animate()
並傳入之前的controller
。這個呼叫會返回我們需要的Animation
例項。顯然我們需要知道動畫的屬性變化的時候的訊息,所以這裡會通過..addListener()
給Animation
例項註冊回撥。這個回撥只做一件事,那就是呼叫setState()
來更新UI。最後就是呼叫controller.forward()
來啟動動畫。 - 注意在
build()
函式裡我們構建widget
的時候用到了animation.value
。所以這裡的鏈條就是動畫在收到回撥後會呼叫setState()
,而從我們上篇文章知道setState
之後在渲染流水線的構建階段會走到build()
來重建Widget
。重建的時候就用到了發生變化以後的animation.value
。這個一幀一幀的迴圈,我們的動畫就動起來了。 - 最後在
dispose()
的時候要記得呼叫controller.dispose()
釋放資源。
接下來我們就深入Flutter原始碼來看一下動畫是如何執行的。
分析
首先我們來看一下混入到State
中的SingleTickerProviderStateMixin
。
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
_ticker = Ticker(onTick, debugLabel: 'created by $this');
return _ticker;
}
@override
void didChangeDependencies() {
if (_ticker != null)
_ticker.muted = !TickerMode.of(context);
super.didChangeDependencies();
}
}
複製程式碼
這個混入其實就做了一件事,實現createTicker()
來例項化一個Ticker
類。在另一個函式didChangeDependencies()
裡,有這樣一行程式碼_ticker.muted = !TickerMode.of(context);
。這行程式碼的意思是在這個帶有動畫的State
的在element tree中的依賴發生變化的時候是否mute
自己的_ticker
。一個場景就是當前頁的動畫還在播放的時候,使用者導航到另外一個頁面,當前頁的動畫就沒有必要再播放了,反之在頁面切換回來的時候動畫有可能還要繼續播放,控制的地方就在這裡,注意TickerMode.of(context)
這種方式,我們在Flutter框架中很多地方都會見到,基本上就是從element tree的祖先裡找到對應那個InheritedWidget
的方式。
Ticker
顧名思義,就是給動畫提供vsync訊號的吧。我們來看下原始碼一探究竟。
class Ticker {
TickerFuture _future;
bool get muted => _muted;
bool _muted = false;
set muted(bool value) {
if (value == muted)
return;
_muted = value;
if (value) {
unscheduleTick();
} else if (shouldScheduleTick) {
scheduleTick();
}
}
bool get isTicking {
if (_future == null)
return false;
if (muted)
return false;
if (SchedulerBinding.instance.framesEnabled)
return true;
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
return true;
return false;
}
bool get isActive => _future != null;
Duration _startTime;
TickerFuture start() {
_future = TickerFuture._();
if (shouldScheduleTick) {
scheduleTick();
}
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _future;
}
void stop({ bool canceled = false }) {
if (!isActive)
return;
final TickerFuture localFuture = _future;
_future = null;
_startTime = null;
unscheduleTick();
if (canceled) {
localFuture._cancel(this);
} else {
localFuture._complete();
}
}
final TickerCallback _onTick;
int _animationId;
@protected
bool get scheduled => _animationId != null;
@protected
bool get shouldScheduleTick => !muted && isActive && !scheduled;
void _tick(Duration timeStamp) {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
@protected
void unscheduleTick() {
if (scheduled) {
SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
_animationId = null;
}
}
}
複製程式碼
可以看到Ticker
主要在做的有點像控制一個計時器,有start()
和stop()
和mute
。還記錄當前自己的狀態isTicking
。我們需要關注的的是scheduleTick()
這個函式:
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
複製程式碼
你看,這裡就跑到了我們之前文章說的SchedulerBinding
裡面去了。這裡排程的時候會傳入Ticker
的回撥函式_tick
。
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
scheduleFrame();
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
複製程式碼
在排程一幀的時候Ticker
的回撥函式_tick
被加入了transientCallbacks
。從之前對渲染流水線的分析,我們知道transientCallbacks
會在vsync訊號到來以後window
的onBeginFrame
回撥裡被執行一次。也就是說此時就進入到渲染流水線的動畫Animate
階段了。
接著我們就看下Ticker
的回撥函式_tick
做了什麼:
void _tick(Duration timeStamp) {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
複製程式碼
這裡的_onTick
是在例項化Ticker
時候傳入的。_onTick
被呼叫之後,Ticker
如果發現自己的任務還沒有完成,還要接著跳動,那就再來排程新一幀。所以你看動畫的動力其實還是來自vsync訊號的。
那麼這個_onTick
又是啥樣的呢?這個函式是在例項化Ticker
的時候傳入的。而從上述分析我們又知道,Ticker
的例項化是在呼叫TickerProvider.createTicker()
的時候完成的。誰來呼叫這個函式呢?是AnimationController
。
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
}) : _direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
複製程式碼
可見在其建構函式裡就呼叫createTicker()
了,傳入的引數是_ticker
。
接著看_ticker
。
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}
複製程式碼
這個回撥裡做這幾件事,根據vsync到來以後的時間戳來計算更新一下新的值,這裡計算用的是個_simulation
。為啥叫這名?因為這是用來模擬一個物體在外力作用下在不同的時間點的運動狀態的變化,這也算是動畫的本質吧。
算出來新的值以後就呼叫notifyListeners()
來通知各位觀察者。還記的在開始的例子裡我們例項化animation
以後會通過..addListener()
新增的回撥嗎?在這裡這個回撥就會被呼叫,也就是setState()
會被呼叫了。接下來就是渲染流水線的構建(build)階段了。
看到這裡你可能會有疑問,事情都讓AnimationController
做了,那那個例子裡的Tween
是用來幹啥的?
從AnimationController
的建構函式裡我們可以看出來,它只管[0.0, 1.0]之間的模擬,也就是說不管動畫怎麼動,它任何時候只輸出0.0到1.0之間的值,但是我們的動畫有旋轉角度,顏色漸變,圖形變化以及更復雜的組合,顯然我們得想辦法把0.0到1.0之間的值轉換為我們需要的角度,位置,顏色,透明度等等,這個轉化就是由各種Animation
來完成的,像例子裡說的Tween
,它的任務在動畫期間把值從0漸變到300。怎麼做呢?在例項化Tween
以後我們會呼叫animate()
,傳入AnimationController
例項。
Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}
複製程式碼
你看,入參是個Animation<double>
,這裡也就是AnimationController
。出參則是個Animation<T>
。這樣就完成了從[0.0, 1.0]到任意型別的變化。
具體怎麼變呢?這個變化其實是在用到這個值得時候發生的,上面的例子裡在State.build()
函式裡構造widget
的時候會呼叫到animation.value
這個getter
。這其實呼叫的是_AnimatedEvaluation.value
。
@override
T get value => _evaluatable.evaluate(parent);
複製程式碼
_evaluatable
就是Tween
了,parent
就是AnimationController
了。所以呢,這個轉換是Tween
自己完成的,也是,只有它自己知道需要什麼樣的輸出。
T evaluate(Animation<double> animation) => transform(animation.value);
複製程式碼
又到了transform()
裡了
@override
T transform(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
複製程式碼
看到範圍限制了嗎?真正的轉換又是在lerp()
裡完成的。
@protected
T lerp(double t) {
return begin + (end - begin) * t;
}
複製程式碼
很簡單的線性插值。
所裡你要理解Flutter中的Tween
動畫是幹什麼的只要把握住它在自己的transform()
函式中做了什麼事情就知道了,從上可知Tween
其實就是在做線性插值的動畫而已。Tween
是線性插值的,那如果我想搞非線性插值的動畫呢?那就用CurvedAnimation
。Flutter裡有一大票各種各樣的線性插值動畫和非線性插值的動畫,你甚至可以自己定義自己的非線性動畫,只要重寫變換函式就行了:
import 'dart:math';
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
複製程式碼
好了,關於Flutter框架裡的動畫就先分析到這裡。
總結
本篇文章是Flutter框架分析系列文章的第五篇,本系列文章主要是以Flutter的渲染流水線為線索來分析其執行的。本篇主要針對的是渲染流水線的動畫階段,從底層的角度對Flutter的動畫機制做了一個簡要的分析。期望大家對Flutter的動畫有一個基礎的認識。在此之上的各種眼花繚亂的動畫相關widgets
都是在此基礎上衍生出來的,所謂道生一,一生二,二生三,三生萬物。掌握了道,就不會被萬物所迷惑。