-
概述
作為前端開發(fā)技術(shù),動畫是一門前端語言所必須的,在Flutter中的動畫是如何使用的呢?它的設(shè)計(jì)原理又是什么呢?本文就從源碼的角度來分析一下Flutter動畫。
-
從使用開始
當(dāng)然,我們還是從使用API的入口開始,看一下動畫機(jī)制是怎么驅(qū)動的。下面是一個demo:
//需要繼承TickerProvider,如果有多個AnimationController,則應(yīng)該使用TickerProviderStateMixin。 class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin { late Animation<double> animation; late AnimationController controller; initState() { super.initState(); //step1. controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); //勻速 //圖片寬高從0變到300 //step2. final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); //animation = Tween(begin: 0.0, end: 300.0).animate(controller) //Tween可以持有AnimationController也可以持有CurvedAnimation,他們都是Animation類型 //animation = Tween(begin: 0.0, end: 300.0).animate(controller) animation = Tween(begin: 0.0, end: 300.0).animate(curve) ..addListener(() { setState(() => {}); }); //啟動動畫(正向執(zhí)行) //step3. controller.forward(); } @override Widget build(BuildContext context) { return Center( child: Image.asset( "imgs/avatar.png", //Image的寬高被動畫的更新值驅(qū)動 width: animation.value, height: animation.value, ), ); } dispose() { //路由銷毀時需要釋放動畫資源 controller.dispose(); super.dispose(); } }首先我們需要一個AnimationController,不管是什么類型的動畫一定得有這個,它的作用就是控制動畫的開始停止等(原理是被vsync屏幕刷新信號驅(qū)動來控制動畫值的更新,后面會分析到)。
這里給出了兩個必須的參數(shù),一個是duration,表示動畫時長,這個值如果不傳會在執(zhí)行的時候報(bào)錯,另一個參數(shù)vsync是TickerProvider,它被required修飾,所以必傳,這里傳入的是this,這個this指向的并不是State而是SingleTickerProviderStateMixin,SingleTickerProviderStateMixin繼承自TickerProvider。
step2部分的Tween其實(shí)你可以選擇去掉,但是需要在AnimationController構(gòu)造時指定lowerBound和upperBound來確定范圍,同時,你要用AnimationController調(diào)用addListener方法,并在回調(diào)中調(diào)用setState方法,否則動畫不會引起圖片大小的變化。
Curve是用來描述速度變化快慢的,他最終想要驅(qū)動動畫值變化還是要調(diào)用AnimationController,它只不過是AnimationController的進(jìn)一步封裝,相當(dāng)于包裝模式;同理,在這個demo 中,Tween也是完成了對于Curve的封裝調(diào)用。
最后執(zhí)行forward方法來開啟動畫。
總之,在上面的這個demo中,AnimationController和其必須的構(gòu)造參數(shù)對象是必須要有的,中間的Tween就是指定圖片大小的上下限取值,可以用AnimationController全權(quán)受理。
下面我們針對上面用到的這些類來逐個研究。
-
AnimationController
上面我們說到,AnimationController是用來管理動畫的啟動停止等動作的,并且是通過vsync信號驅(qū)動的,我們從源碼角度來驗(yàn)證它。
它的構(gòu)造方法中有這樣一段代碼:
_ticker = vsync.createTicker(_tick); _internalSetValue(value ?? lowerBound);_internalSetValue方法邏輯比較簡單,我們先看 _internalSetValue方法:
void _internalSetValue(double newValue) { _value = newValue.clamp(lowerBound, upperBound); if (_value == lowerBound) { _status = AnimationStatus.dismissed; } else if (_value == upperBound) { _status = AnimationStatus.completed; } else { _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.forward : AnimationStatus.reverse; } }_value 是動畫即時值,未指定時是null,如果它小于lowerBound,clamp方法會把它置為lowerBound,同樣,大于upperBound會把它置為upperBound,這里會根據(jù)是它是等于lowerBound還是等于upperBound而給 _status賦值為AnimationStatus.dismissed或AnimationStatus.completed,前者表示在起點(diǎn),后者表示在終點(diǎn);如果是在規(guī)定區(qū)間內(nèi)的值則會根據(jù) _direction是否為 _AnimationDirection.forward來決定 _status是AnimationStatus.forward還是AnimationStatus.reverse。
總之,_internalSetValue的作用就是初始化 _value和 _status。
回過頭來,我們看到_ticker在這里是調(diào)用的vsync的createTicker方法創(chuàng)建的,假如我們State依賴的mixin是SingleTickerProviderStateMixin,我們?nèi)タ纯此倪@個方法:
@override Ticker createTicker(TickerCallback onTick) { _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null); return _ticker!; }我只接截取了關(guān)鍵代碼,assert等容錯邏輯沒截取。
可以看到,在這里創(chuàng)建了一個Ticker對象,他持有了一個函數(shù)對象onTick,這個onTick就是AnimationController構(gòu)造方法里傳入的 _tick函數(shù):
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(); }可以看到這里會通過 _simulation產(chǎn)生新的 _value值,所以猜測,這里可能是屏幕刷新后會回調(diào)的地方,但是怎么驗(yàn)證呢?我們?nèi)フ艺宜窃趺春推聊凰⑿聶C(jī)制關(guān)聯(lián)上的。
_simulation是什么呢?我們發(fā)現(xiàn)他沒有初始化值,又在什么時候賦值的呢?因?yàn)槠聊凰⑿乱恢痹谶M(jìn)行,動畫的回調(diào)想要被它影響一定會在某個時間點(diǎn)建立關(guān)聯(lián),那在什么時候呢?開啟動畫的時候就是最恰當(dāng)?shù)臅r候,所以我們直接從動畫開啟方法之一的forward方法找起:
TickerFuture forward({ double? from }) { _direction = _AnimationDirection.forward; if (from != null) value = from; return _animateToInternal(upperBound); }可見它內(nèi)部調(diào)用了_animateToInternal方法,這個方法就是開啟動畫的方法,很明顯它是一個受保護(hù)的內(nèi)部方法,調(diào)用它的有四處,也是供外部調(diào)用的四個開啟方法:forward、reverse、animateTo、animateBack。
_animateToInternal方法內(nèi)部主要是做一些對于當(dāng)前動畫的一些停止和對即將開始的新動畫的初始化工作,關(guān)鍵是它最后調(diào)用了 _startSimulation方法:
return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));TickerFuture _startSimulation(Simulation simulation) { _simulation = simulation; _lastElapsedDuration = Duration.zero; _value = simulation.x(0.0).clamp(lowerBound, upperBound); final TickerFuture result = _ticker!.start(); _status = (_direction == _AnimationDirection.forward) ? AnimationStatus.forward : AnimationStatus.reverse; _checkStatusChanged(); return result; }可以發(fā)現(xiàn),_startSimulation的代碼和 _tick中的代碼好相似啊,仔細(xì)看會發(fā)現(xiàn), _startSimulation方法中simulation的x方法的傳參固定是0.0,因?yàn)檫@是動畫開啟;又發(fā)現(xiàn)這里沒有調(diào)用stop方法,因?yàn)閯赢媱傞_啟不需要結(jié)束,結(jié)束的判斷就是要交給上面的 _tick方法。
重要的是我們在這里找到了 _ticker的開啟方法:
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!; }在這里會創(chuàng)建TickerFuture,isActive方法中會根據(jù) _future來判斷動畫是否正在運(yùn)行。然后調(diào)用scheduleTick方法:
@protected void scheduleTick({ bool rescheduling = false }) { assert(!scheduled); assert(shouldScheduleTick); _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling); }看到這里我們需要先明白一個原理,就是<font color=red>Flutter 應(yīng)用在啟動時都會綁定一個SchedulerBinding,通過SchedulerBinding可以給每一次屏幕刷新添加回調(diào),而Ticker就是通過SchedulerBinding來添加屏幕刷新回調(diào)的,這樣一來,每次屏幕刷新都會調(diào)用TickerCallback。使用Ticker(而不是Timer)來驅(qū)動動畫會防止屏幕外動畫(動畫的UI不在當(dāng)前屏幕時,如鎖屏?xí)r)消耗不必要的資源,因?yàn)镕lutter中屏幕刷新時會通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動的,由于鎖屏后屏幕會停止刷新,所以Ticker就不會再觸發(fā)。</font>
所以這里調(diào)用了SchedulerBinding.instance的scheduleFrameCallback方法來和 _tick回調(diào)函數(shù)建立關(guān)聯(lián):
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) { scheduleFrame(); _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling); return _nextFrameCallbackId; }我們在handleBeginFrame方法中找到了使用 _transientCallbacks的地方:
void handleBeginFrame(Duration? rawTimeStamp) { ... callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { if (!_removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack); }); ... }這個方法會確保注冊在window上:
@protected void ensureFrameCallbacksRegistered() { window.onBeginFrame ??= _handleBeginFrame; window.onDrawFrame ??= _handleDrawFrame; }到這里,我們就找出了動畫回調(diào)函數(shù)和屏幕刷新回調(diào)之間的關(guān)聯(lián)邏輯。
而在Ticker的stop方法中我們會找到回調(diào)移除的邏輯:
void stop({ bool canceled = false }) { if (!isActive) return; // We take the _future into a local variable so that isTicking is false // when we actually complete the future (isTicking uses _future to // determine its state). final TickerFuture localFuture = _future!; _future = null; _startTime = null; assert(!isActive); //移除動畫回調(diào) unscheduleTick(); if (canceled) { localFuture._cancel(this); } else { localFuture._complete(); } } -
Simulation
在上面的分析中有一個Simulation類,它有三個方法:
/// The position of the object in the simulation at the given time. double x(double time); /// The velocity of the object in the simulation at the given time. double dx(double time); /// Whether the simulation is "done" at the given time. bool isDone(double time);根據(jù)注釋可知,可以通過x方法、dx方法、isDone方法分別可以得出當(dāng)前動畫的進(jìn)度、速度和是否已完成的標(biāo)志。我們上面?zhèn)魅氲氖撬膶?shí)現(xiàn)類 _InterpolationSimulation,它內(nèi)部持有的屬性有開始值、終點(diǎn)值和執(zhí)行時長,執(zhí)行時長保存在 _durationInSeconds屬性:
_durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;可以看到,這里會將設(shè)置的市場按照scale進(jìn)行壓縮,并最終取微秒單位,因?yàn)閯赢嬕笕庋劭床怀隹D,所以這里使用最細(xì)致的微秒單位。
我們先看看它的x方法:
@override double x(double timeInSeconds) { final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0); if (t == 0.0) return _begin; else if (t == 1.0) return _end; else return _begin + (_end - _begin) * _curve.transform(t); }可見,這里會在初始值的基礎(chǔ)上加上需要前進(jìn)的值,正常的t是自上次屏幕刷新后消逝的時間,這里調(diào)用 _curve的transform方法也正是Curve的原理所在,它對進(jìn)度作了進(jìn)一步的處理。
@override double dx(double timeInSeconds) { final double epsilon = tolerance.time; return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon); }同樣,dx方法中是關(guān)于速度的算法。
isDone最簡單,只是通過是否超出_durationInSeconds來判斷動畫是否結(jié)束:
@override bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;<font color=red>記住這個Simulation所做的事情,在下面對于Curve和Tween的原理分析中,你會看到似曾相識的邏輯</font>。
-
Curve
在上面對于Simulation的分析中我們提到,在x方法獲取最新進(jìn)度值的時候會通過Curve的transform方法處理一下,而這一點(diǎn)也正是在動畫機(jī)制中Curve發(fā)揮作用的關(guān)鍵。
transform方法最終在其父類ParametricCurve中會調(diào)用transformInternal方法,這個方法理應(yīng)由子類實(shí)現(xiàn),Curve的子類有很多,代表著很多中變化曲線算法,這里就不展開講了,Curve的原理在上文中其實(shí)已經(jīng)講完了。
-
Tween
Tween是用來設(shè)置屬性范圍的,可能有人會講,屬性范圍我完全可以通過AnimationController的lowerBound和upperBound來指定,為什么還需要這個多余的類呢?
把屬性的類型不設(shè)限于double,你就能體會到他應(yīng)該有的作用了。
class Tween<T extends Object?> extends Animatable<T> { Tween({ this.begin, this.end, }); ... }可見,Tween持有了一個泛型,表示可以設(shè)置任何屬性,通過Tween,我們動畫最終產(chǎn)生的值就不再只局限于double了,我可以轉(zhuǎn)成任何直接使用的屬性值。
上文我們知道,AnimationController會在vsync的驅(qū)動下執(zhí)行動畫回調(diào)獲取動畫最新值,那么在使用那部分我們看到,我們引用的直接是Tween的animate方法返回的Animation對象的value值,那么這個value值和AnimationController即時獲取的最新值是怎么關(guān)聯(lián)的呢?
我們先看一下Tween的animate方法返回的是什么:
Animation<T> animate(Animation<double> parent) { return _AnimatedEvaluation<T>(parent, this); }可以看到,是一個_AnimatedEvaluation實(shí)例,它的parent指定為AnimationController,我們來看一下它的value:
@override T get value => _evaluatable.evaluate(parent);value通過 _evaluatable的evaluate方法獲取, _evaluatable就是前面?zhèn)魅氲膖his,也就是Tween本身:
T evaluate(Animation<double> animation) => transform(animation.value);可見evaluate又調(diào)用了transform方法:
@override T transform(double t) { if (t == 0.0) return begin as T; if (t == 1.0) return end as T; return lerp(t); }transform方法中如果是中間值的話又會調(diào)用lerp方法:
@protected T lerp(double t) { ... return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T; }怎么樣,熟悉吧,是不是和_InterpolationSimulation中的x方法如出一轍,這就是默認(rèn)的進(jìn)度處理邏輯,不同的是,這里的begin、end和返回值都是泛型指定的,而不是固定的double,t為double是因?yàn)閠是刷新的微秒進(jìn)度值,一定是double。
你可以定義自己的Tween類,繼承并重寫lerp方法即可完成自定義的屬性值轉(zhuǎn)換,比如ColorTween:
@override Color? lerp(double t) => Color.lerp(begin, end, t);Color的lerp方法如下:
static Color? lerp(Color? a, Color? b, double t) { assert(t != null); if (b == null) { if (a == null) { return null; } else { return _scaleAlpha(a, 1.0 - t); } } else { if (a == null) { return _scaleAlpha(b, t); } else { return Color.fromARGB( _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255), _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255), _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255), _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255), ); } } }這就是Tween的原理,可以知道,Tween只不過是內(nèi)部持有了AnimationController,然后通過AnimationController拿到進(jìn)度值然后做自己的處理之后返回給value使用。
Tween的parent換做持有CurvedAnimation也是一樣的原理:
@override double get value { final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve; final double t = parent.value; if (activeCurve == null) return t; if (t == 0.0 || t == 1.0) { return t; } return activeCurve.transform(t); }可見,只不過CurvedAnimation持有的是Curve,然后通過Curve去調(diào)用AnimationController而已。
-
TickerProviderStateMixin
前面講了SingleTickerProviderStateMixin,其實(shí)TickerProvider還有一個子類就是TickerProviderStateMixin:
@override Ticker createTicker(TickerCallback onTick) { _tickers ??= <_WidgetTicker>{}; final _WidgetTicker result = _WidgetTicker(onTick, this, debugLabel: 'created by $this'); _tickers!.add(result); return result; } void _removeTicker(_WidgetTicker ticker) { assert(_tickers != null); assert(_tickers!.contains(ticker)); _tickers!.remove(ticker); }可見,這個mixin是用來出來State中有多個AnimationController的情況,主要是用于Ticker的釋放清理工作。
-
總結(jié)
Flutter的動畫原理的核心主要在于AnimationController和Scheduler關(guān)聯(lián),通過給Scheduler添加回調(diào)函數(shù)的方式和系統(tǒng)的vsync信號建立關(guān)聯(lián),從而由屏幕的刷新信號驅(qū)動到動畫回調(diào)函數(shù),在回調(diào)函數(shù)中調(diào)用setState方法更新界面,而界面控件中又引用了動畫類的value,value是通過直接或者間接的方式從AnimationController中獲取的最新當(dāng)前進(jìn)度值,這就完成了整個動畫的流程。
可以通過CurvedAnimation、Tween等包裝類(當(dāng)然也可以自定義)使用包裝的設(shè)計(jì)模式去包裝AnimationController,使得在使用獲取的進(jìn)度值之前可以對其有更靈活的處理。
Flutter動畫原理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
相關(guān)閱讀更多精彩內(nèi)容
- Flutter的動畫體系是怎么運(yùn)作的,各組件之間的關(guān)聯(lián)關(guān)系及原理什么,隱式動畫、顯式動畫怎么區(qū)分,本文將會進(jìn)行詳細(xì)...
- 概述 動畫API認(rèn)識 動畫案例練習(xí) 其它動畫補(bǔ)充 一、動畫API認(rèn)識 動畫實(shí)際上是我們通過某些方式(某種對象,An...
- 對于一個前端的App來說,添加適當(dāng)?shù)膭赢?,可以給用戶更好的體驗(yàn)和視覺效果。所以無論是原生的iOS或Android,...
- 動畫實(shí)現(xiàn)的方式 Flutter中,我們可以簡單的把調(diào)用this.setState()理解為渲染一幀。那么只要我們不...
- 在任何系統(tǒng)的UI框架中,動畫實(shí)現(xiàn)的原理都是相同的:在一段時間內(nèi),快速地多次改變UI外觀;由于人眼會產(chǎn)生視覺暫留,所...