-
概述
作為前端開發(fā)技術(shù)弱匪,動(dòng)畫是一門前端語言所必須的缩赛,在Flutter中的動(dòng)畫是如何使用的呢堡纬?它的設(shè)計(jì)原理又是什么呢?本文就從源碼的角度來分析一下Flutter動(dòng)畫饥悴。
-
從使用開始
當(dāng)然坦喘,我們還是從使用API的入口開始盲再,看一下動(dòng)畫機(jī)制是怎么驅(qū)動(dòng)的。下面是一個(gè)demo:
//需要繼承TickerProvider瓣铣,如果有多個(gè)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(() => {}); }); //啟動(dòng)動(dòng)畫(正向執(zhí)行) //step3. controller.forward(); } @override Widget build(BuildContext context) { return Center( child: Image.asset( "imgs/avatar.png", //Image的寬高被動(dòng)畫的更新值驅(qū)動(dòng) width: animation.value, height: animation.value, ), ); } dispose() { //路由銷毀時(shí)需要釋放動(dòng)畫資源 controller.dispose(); super.dispose(); } }
首先我們需要一個(gè)AnimationController绿映,不管是什么類型的動(dòng)畫一定得有這個(gè),它的作用就是控制動(dòng)畫的開始停止等(原理是被vsync屏幕刷新信號(hào)驅(qū)動(dòng)來控制動(dòng)畫值的更新腐晾,后面會(huì)分析到)叉弦。
這里給出了兩個(gè)必須的參數(shù),一個(gè)是duration藻糖,表示動(dòng)畫時(shí)長淹冰,這個(gè)值如果不傳會(huì)在執(zhí)行的時(shí)候報(bào)錯(cuò),另一個(gè)參數(shù)vsync是TickerProvider巨柒,它被required修飾樱拴,所以必傳,這里傳入的是this洋满,這個(gè)this指向的并不是State而是SingleTickerProviderStateMixin晶乔,SingleTickerProviderStateMixin繼承自TickerProvider。
step2部分的Tween其實(shí)你可以選擇去掉牺勾,但是需要在AnimationController構(gòu)造時(shí)指定lowerBound和upperBound來確定范圍正罢,同時(shí),你要用AnimationController調(diào)用addListener方法驻民,并在回調(diào)中調(diào)用setState方法翻具,否則動(dòng)畫不會(huì)引起圖片大小的變化。
Curve是用來描述速度變化快慢的回还,他最終想要驅(qū)動(dòng)動(dòng)畫值變化還是要調(diào)用AnimationController裆泳,它只不過是AnimationController的進(jìn)一步封裝,相當(dāng)于包裝模式柠硕;同理工禾,在這個(gè)demo 中,Tween也是完成了對(duì)于Curve的封裝調(diào)用仅叫。
最后執(zhí)行forward方法來開啟動(dòng)畫帜篇。
總之,在上面的這個(gè)demo中诫咱,AnimationController和其必須的構(gòu)造參數(shù)對(duì)象是必須要有的笙隙,中間的Tween就是指定圖片大小的上下限取值,可以用AnimationController全權(quán)受理坎缭。
下面我們針對(duì)上面用到的這些類來逐個(gè)研究竟痰。
-
AnimationController
上面我們說到签钩,AnimationController是用來管理動(dòng)畫的啟動(dòng)停止等動(dòng)作的,并且是通過vsync信號(hào)驅(qū)動(dòng)的坏快,我們從源碼角度來驗(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 是動(dòng)畫即時(shí)值莽鸿,未指定時(shí)是null昧旨,如果它小于lowerBound,clamp方法會(huì)把它置為lowerBound祥得,同樣兔沃,大于upperBound會(huì)把它置為upperBound,這里會(huì)根據(jù)是它是等于lowerBound還是等于upperBound而給 _status賦值為AnimationStatus.dismissed或AnimationStatus.completed级及,前者表示在起點(diǎn)乒疏,后者表示在終點(diǎn);如果是在規(guī)定區(qū)間內(nèi)的值則會(huì)根據(jù) _direction是否為 _AnimationDirection.forward來決定 _status是AnimationStatus.forward還是AnimationStatus.reverse饮焦。
總之怕吴,_internalSetValue的作用就是初始化 _value和 _status。
回過頭來县踢,我們看到_ticker在這里是調(diào)用的vsync的createTicker方法創(chuàng)建的转绷,假如我們State依賴的mixin是SingleTickerProviderStateMixin,我們?nèi)タ纯此倪@個(gè)方法:
@override Ticker createTicker(TickerCallback onTick) { _ticker = Ticker(onTick, debugLabel: kDebugMode ? 'created by $this' : null); return _ticker!; }
我只接截取了關(guān)鍵代碼硼啤,assert等容錯(cuò)邏輯沒截取暇咆。
可以看到,在這里創(chuàng)建了一個(gè)Ticker對(duì)象丙曙,他持有了一個(gè)函數(shù)對(duì)象onTick,這個(gè)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(); }
可以看到這里會(huì)通過 _simulation產(chǎn)生新的 _value值其骄,所以猜測亏镰,這里可能是屏幕刷新后會(huì)回調(diào)的地方,但是怎么驗(yàn)證呢拯爽?我們?nèi)フ艺宜窃趺春推聊凰⑿聶C(jī)制關(guān)聯(lián)上的索抓。
_simulation是什么呢?我們發(fā)現(xiàn)他沒有初始化值毯炮,又在什么時(shí)候賦值的呢逼肯?因?yàn)槠聊凰⑿乱恢痹谶M(jìn)行,動(dòng)畫的回調(diào)想要被它影響一定會(huì)在某個(gè)時(shí)間點(diǎn)建立關(guān)聯(lián)桃煎,那在什么時(shí)候呢篮幢?開啟動(dòng)畫的時(shí)候就是最恰當(dāng)?shù)臅r(shí)候,所以我們直接從動(dòng)畫開啟方法之一的forward方法找起:
TickerFuture forward({ double? from }) { _direction = _AnimationDirection.forward; if (from != null) value = from; return _animateToInternal(upperBound); }
可見它內(nèi)部調(diào)用了_animateToInternal方法为迈,這個(gè)方法就是開啟動(dòng)畫的方法三椿,很明顯它是一個(gè)受保護(hù)的內(nèi)部方法缺菌,調(diào)用它的有四處,也是供外部調(diào)用的四個(gè)開啟方法:forward搜锰、reverse伴郁、animateTo、animateBack蛋叼。
_animateToInternal方法內(nèi)部主要是做一些對(duì)于當(dāng)前動(dòng)畫的一些停止和對(duì)即將開始的新動(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ì)看會(huì)發(fā)現(xiàn)狐胎, _startSimulation方法中simulation的x方法的傳參固定是0.0,因?yàn)檫@是動(dòng)畫開啟薯嗤;又發(fā)現(xiàn)這里沒有調(diào)用stop方法顽爹,因?yàn)閯?dòng)畫剛開啟不需要結(jié)束,結(jié)束的判斷就是要交給上面的 _tick方法骆姐。
重要的是我們?cè)谶@里找到了 _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!; }
在這里會(huì)創(chuàng)建TickerFuture镜粤,isActive方法中會(huì)根據(jù) _future來判斷動(dòng)畫是否正在運(yùn)行。然后調(diào)用scheduleTick方法:
@protected void scheduleTick({ bool rescheduling = false }) { assert(!scheduled); assert(shouldScheduleTick); _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick, rescheduling: rescheduling); }
看到這里我們需要先明白一個(gè)原理玻褪,就是<font color=red>Flutter 應(yīng)用在啟動(dòng)時(shí)都會(huì)綁定一個(gè)SchedulerBinding肉渴,通過SchedulerBinding可以給每一次屏幕刷新添加回調(diào),而Ticker就是通過SchedulerBinding來添加屏幕刷新回調(diào)的带射,這樣一來同规,每次屏幕刷新都會(huì)調(diào)用TickerCallback。使用Ticker(而不是Timer)來驅(qū)動(dòng)動(dòng)畫會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí)窟社,如鎖屏?xí)r)消耗不必要的資源券勺,因?yàn)镕lutter中屏幕刷新時(shí)會(huì)通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動(dòng)的灿里,由于鎖屏后屏幕會(huì)停止刷新关炼,所以Ticker就不會(huì)再觸發(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; }
我們?cè)趆andleBeginFrame方法中找到了使用 _transientCallbacks的地方:
void handleBeginFrame(Duration? rawTimeStamp) { ... callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) { if (!_removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack); }); ... }
這個(gè)方法會(huì)確保注冊(cè)在window上:
@protected void ensureFrameCallbacksRegistered() { window.onBeginFrame ??= _handleBeginFrame; window.onDrawFrame ??= _handleDrawFrame; }
到這里匣吊,我們就找出了動(dòng)畫回調(diào)函數(shù)和屏幕刷新回調(diào)之間的關(guān)聯(lián)邏輯儒拂。
而在Ticker的stop方法中我們會(huì)找到回調(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); //移除動(dòng)畫回調(diào) unscheduleTick(); if (canceled) { localFuture._cancel(this); } else { localFuture._complete(); } }
-
Simulation
在上面的分析中有一個(gè)Simulation類,它有三個(gè)方法:
/// 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)前動(dòng)畫的進(jìn)度命雀、速度和是否已完成的標(biāo)志蒜哀。我們上面?zhèn)魅氲氖撬膶?shí)現(xiàn)類 _InterpolationSimulation,它內(nèi)部持有的屬性有開始值咏雌、終點(diǎn)值和執(zhí)行時(shí)長凡怎,執(zhí)行時(shí)長保存在 _durationInSeconds屬性:
_durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
可以看到校焦,這里會(huì)將設(shè)置的市場按照scale進(jìn)行壓縮,并最終取微秒單位统倒,因?yàn)閯?dòng)畫要求肉眼看不出卡頓寨典,所以這里使用最細(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); }
可見房匆,這里會(huì)在初始值的基礎(chǔ)上加上需要前進(jìn)的值耸成,正常的t是自上次屏幕刷新后消逝的時(shí)間,這里調(diào)用 _curve的transform方法也正是Curve的原理所在浴鸿,它對(duì)進(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來判斷動(dòng)畫是否結(jié)束:
@override bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
<font color=red>記住這個(gè)Simulation所做的事情,在下面對(duì)于Curve和Tween的原理分析中掸哑,你會(huì)看到似曾相識(shí)的邏輯</font>约急。
-
Curve
在上面對(duì)于Simulation的分析中我們提到,在x方法獲取最新進(jìn)度值的時(shí)候會(huì)通過Curve的transform方法處理一下苗分,而這一點(diǎn)也正是在動(dòng)畫機(jī)制中Curve發(fā)揮作用的關(guān)鍵厌蔽。
transform方法最終在其父類ParametricCurve中會(huì)調(diào)用transformInternal方法,這個(gè)方法理應(yīng)由子類實(shí)現(xiàn)摔癣,Curve的子類有很多奴饮,代表著很多中變化曲線算法,這里就不展開講了择浊,Curve的原理在上文中其實(shí)已經(jīng)講完了戴卜。
-
Tween
Tween是用來設(shè)置屬性范圍的,可能有人會(huì)講琢岩,屬性范圍我完全可以通過AnimationController的lowerBound和upperBound來指定叉瘩,為什么還需要這個(gè)多余的類呢?
把屬性的類型不設(shè)限于double粘捎,你就能體會(huì)到他應(yīng)該有的作用了。
class Tween<T extends Object?> extends Animatable<T> { Tween({ this.begin, this.end, }); ... }
可見危彩,Tween持有了一個(gè)泛型攒磨,表示可以設(shè)置任何屬性,通過Tween汤徽,我們動(dòng)畫最終產(chǎn)生的值就不再只局限于double了娩缰,我可以轉(zhuǎn)成任何直接使用的屬性值。
上文我們知道谒府,AnimationController會(huì)在vsync的驅(qū)動(dòng)下執(zhí)行動(dòng)畫回調(diào)獲取動(dòng)畫最新值拼坎,那么在使用那部分我們看到浮毯,我們引用的直接是Tween的animate方法返回的Animation對(duì)象的value值,那么這個(gè)value值和AnimationController即時(shí)獲取的最新值是怎么關(guān)聯(lián)的呢泰鸡?
我們先看一下Tween的animate方法返回的是什么:
Animation<T> animate(Animation<double> parent) { return _AnimatedEvaluation<T>(parent, this); }
可以看到债蓝,是一個(gè)_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方法中如果是中間值的話又會(huì)調(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還有一個(gè)子類就是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); }
可見,這個(gè)mixin是用來出來State中有多個(gè)AnimationController的情況叽讳,主要是用于Ticker的釋放清理工作追他。
-
總結(jié)
Flutter的動(dòng)畫原理的核心主要在于AnimationController和Scheduler關(guān)聯(lián),通過給Scheduler添加回調(diào)函數(shù)的方式和系統(tǒng)的vsync信號(hào)建立關(guān)聯(lián)岛蚤,從而由屏幕的刷新信號(hào)驅(qū)動(dòng)到動(dòng)畫回調(diào)函數(shù)邑狸,在回調(diào)函數(shù)中調(diào)用setState方法更新界面,而界面控件中又引用了動(dòng)畫類的value涤妒,value是通過直接或者間接的方式從AnimationController中獲取的最新當(dāng)前進(jìn)度值单雾,這就完成了整個(gè)動(dòng)畫的流程。
可以通過CurvedAnimation、Tween等包裝類(當(dāng)然也可以自定義)使用包裝的設(shè)計(jì)模式去包裝AnimationController硅堆,使得在使用獲取的進(jìn)度值之前可以對(duì)其有更靈活的處理屿储。
Flutter動(dòng)畫原理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
- 文/潘曉璐 我一進(jìn)店門户辱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腌紧,“玉大人,你說我怎么就攤上這事敌完〕蕴簦” “怎么了钝荡?”我有些...
- 文/不壞的土叔 我叫張陵,是天一觀的道長舶衬。 經(jīng)常有香客問我埠通,道長,這世上最難降的妖魔是什么逛犹? 我笑而不...
- 正文 為了忘掉前任端辱,我火速辦了婚禮,結(jié)果婚禮上虽画,老公的妹妹穿的比我還像新娘舞蔽。我一直安慰自己,他們只是感情好码撰,可當(dāng)我...
- 文/花漫 我一把揭開白布渗柿。 她就那樣靜靜地躺著,像睡著了一般脖岛。 火紅的嫁衣襯著肌膚如雪朵栖。 梳的紋絲不亂的頭發(fā)上,一...
- 文/蒼蘭香墨 我猛地睜開眼悯嗓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卸察?” 一聲冷哼從身側(cè)響起脯厨,我...
- 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坑质,沒想到半個(gè)月后合武,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
- 正文 獨(dú)居荒郊野嶺守林人離奇死亡涡扼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
- 正文 我和宋清朗相戀三年稼跳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃沪。...
- 正文 年R本政府宣布降铸,位于F島的核電站在旱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏推掸。R本人自食惡果不足惜桶蝎,卻給世界環(huán)境...
- 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望终佛。 院中可真熱鬧俊嗽,春花似錦、人聲如沸铃彰。這莊子的主人今日做“春日...
- 文/蒼蘭香墨 我抬頭看了看天上的太陽牙捉。三九已至竹揍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邪铲,已是汗流浹背芬位。 一陣腳步聲響...
- 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親被饿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子四康,可洞房花燭夜當(dāng)晚...
推薦閱讀更多精彩內(nèi)容
- Flutter的動(dòng)畫體系是怎么運(yùn)作的,各組件之間的關(guān)聯(lián)關(guān)系及原理什么狭握,隱式動(dòng)畫闪金、顯式動(dòng)畫怎么區(qū)分,本文將會(huì)進(jìn)行詳細(xì)...
- 概述 動(dòng)畫API認(rèn)識(shí) 動(dòng)畫案例練習(xí) 其它動(dòng)畫補(bǔ)充 一论颅、動(dòng)畫API認(rèn)識(shí) 動(dòng)畫實(shí)際上是我們通過某些方式(某種對(duì)象哎垦,An...
- 對(duì)于一個(gè)前端的App來說,添加適當(dāng)?shù)膭?dòng)畫恃疯,可以給用戶更好的體驗(yàn)和視覺效果漏设。所以無論是原生的iOS或Android,...
- 動(dòng)畫實(shí)現(xiàn)的方式 Flutter中澡谭,我們可以簡單的把調(diào)用this.setState()理解為渲染一幀愿题。那么只要我們不...
- 在任何系統(tǒng)的UI框架中,動(dòng)畫實(shí)現(xiàn)的原理都是相同的:在一段時(shí)間內(nèi)蛙奖,快速地多次改變UI外觀潘酗;由于人眼會(huì)產(chǎn)生視覺暫留,所...