摘要:如果沒有碰撞結(jié)果,那么通過(guò)將事件分發(fā)到全局處理。提供手勢(shì)事件競(jìng)技信息的實(shí)體,內(nèi)封裝參與事件競(jìng)技的成員。這樣勝利的會(huì)通過(guò)回調(diào)到中,設(shè)置為標(biāo)志為勝利區(qū)域,然后執(zhí)行和發(fā)出事件響應(yīng)觸發(fā)給這個(gè)控件。
本篇將帶你深入了解 Flutter 中的手勢(shì)事件傳遞、事件分發(fā)、事件沖突競(jìng)爭(zhēng),滑動(dòng)流暢等等的原理,幫你構(gòu)建一個(gè)完整的 Flutter 閉環(huán)手勢(shì)知識(shí)體系,這也許是目前最全面的手勢(shì)事件和滑動(dòng)源碼的深入文章了。
前文:
一、 Dart語(yǔ)言和Flutter基礎(chǔ)
二、 快速開發(fā)實(shí)戰(zhàn)篇
三、 打包與填坑篇
四、 Redux、主題、國(guó)際化
五、 深入探索
六、 深入Widget原理
七、 深入布局原理
八、 實(shí)用技巧與填坑
九、 深入繪制原理
十、 深入圖片加載流程
十一、全面深入理解Stream
十二、全面深入理解狀態(tài)管理設(shè)計(jì)
Flutter 中默認(rèn)情況下,以 Android 為例,所有的事件都是起原生源于 io.flutter.view.FlutterView 這個(gè) SurfaceView 的子類,整個(gè)觸摸手勢(shì)事件實(shí)質(zhì)上經(jīng)歷了 JAVA => C++ => Dart 的一個(gè)流程,整個(gè)流程如下圖所示,無(wú)論是 Android 還是 IOS ,原生層都只是將所有事件打包下發(fā),比如在 Android 中,手勢(shì)信息被打包成 ByteBuffer 進(jìn)行傳遞,最后在 Dart 層的 _dispatchPointerDataPacket 方法中,通過(guò) _unpackPointerDataPacket 方法解析成可用的 PointerDataPacket 對(duì)象使用。
那么具體在 Flutter 中是如何分發(fā)使用手勢(shì)事件的呢?
1、事件流程在前面的流程圖中我們知道,在 Dart 層中手勢(shì)事件都是從 _dispatchPointerDataPacket 開始的,之后會(huì)通過(guò) Zone 判斷環(huán)境回調(diào),會(huì)執(zhí)行 GestureBinding 這個(gè)膠水類中的 _handlePointerEvent 方法。(如果對(duì) Zone 或者 GestureBinding 有疑問可以翻閱前面的篇章)
如下代碼所示, GestureBinding 的 _handlePointerEvent 方法中主要是 hitTest 和 dispatchEvent: 通過(guò) hitTest 碰撞,得到一個(gè)包含控件的待處理成員列表 HitTestResult,然后通過(guò) dispatchEvent 分發(fā)事件并產(chǎn)生競(jìng)爭(zhēng),得到勝利者相應(yīng)。
void _handlePointerEvent(PointerEvent event) {
assert(!locked);
HitTestResult hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
hitTestResult = HitTestResult();
///開始碰撞測(cè)試了,會(huì)添加各個(gè)控件,得到一個(gè)需要處理的控件成員列表
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
///復(fù)用機(jī)制,抬起和取消,不用hitTest,移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
///復(fù)用機(jī)制,手指處于滑動(dòng)中,不用hitTest
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
///開始分發(fā)事件
dispatchEvent(event, hitTestResult);
}
}
了解了結(jié)果后,接下來(lái)深入分析這兩個(gè)關(guān)鍵方法:
hitTest 方法主要為了得到一個(gè) HitTestResult ,這個(gè) HitTestResult 內(nèi)有一個(gè) List
因?yàn)?RenderObject 默認(rèn)都實(shí)現(xiàn)了 HitTestTarget 接口,所以可以理解為: HitTestTarget 大部分時(shí)候都是 RenderObject ,而 HitTestResult 就是一個(gè)帶著碰撞測(cè)試后的控件列表。
事實(shí)上 hitTest 是 HitTestable 抽象類的方法,而 Flutter 中所有實(shí)現(xiàn) HitTestable 的類有 GestureBinding 和 RendererBinding ,它們都是 mixins 在 WidgetsFlutterBinding 這個(gè)入口類上,并且因?yàn)樗鼈兊?mixins 順序的關(guān)系,所以 RendererBinding 的 hitTest 會(huì)先被調(diào)用,之后才調(diào)用 GestureBinding 的 hitTest 。
那么這兩個(gè) hitTest 又分別干了什么事呢?
在 RendererBinding.hitTest 中會(huì)執(zhí)行 renderView.hitTest(result, position: position); ,如下代碼所示,renderView.hitTest 方法內(nèi)會(huì)執(zhí)行 child.hitTest ,它將嘗試將符合條件的 child 控件添加到 HitTestResult 里,最后把自己添加進(jìn)去。
///RendererBinding
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position);
result.add(HitTestEntry(this));
return true;
}
而查看 child.hitTest 方法源碼,如下所示,RenderObjcet 中的hitTest ,會(huì)通過(guò) _size.contains 判斷自己是否屬于響應(yīng)區(qū)域,確認(rèn)響應(yīng)后執(zhí)行 hitTestChildren 和 hitTestSelf ,嘗試添加下級(jí)的 child 和自己添加進(jìn)去,這樣的遞歸就讓我們自下而上的得到了一個(gè) HitTestResult 的相應(yīng)控件列表了,最底下的 Child 在最上面。
///RenderObjcet
bool hitTest(HitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
最后 GestureBinding.hitTest 方法不過(guò)最后把 GestureBinding 自己也添加到 HitTestResult 里,最后因?yàn)楹竺嫖覀兊牧鞒踢€會(huì)需要回到 GestureBinding 中去處理。
dispatchEvent 中主要是對(duì)事件進(jìn)行分發(fā),并且通過(guò)上述添加進(jìn)去的 target.handleEvent 處理事件,如下代碼所示,在存在碰撞結(jié)果的時(shí)候,是會(huì)通過(guò)循環(huán)對(duì)每個(gè)控件內(nèi)部的handleEvent 進(jìn)行執(zhí)行。
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
///如果沒有碰撞結(jié)果,那么通過(guò) `pointerRouter.route` 將事件分發(fā)到全局處理。
if (hitTestResult == null) {
try {
pointerRouter.route(event);
} catch (exception, stack) {
return;
}
///上面我們知道 HitTestEntry 中的 target 是一系自下而上的控件
///還有 renderView 和 GestureBinding
///循環(huán)執(zhí)行每一個(gè)的 handleEvent 方法
for (HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event, entry);
} catch (exception, stack) {
}
}
}
事實(shí)上并不是所有的控件的 RenderObject 子類都會(huì)處理 handleEvent ,大部分時(shí)候,只有帶有 RenderPointerListener (RenderObject) / Listener (Widget) 的才會(huì)處理 handleEvent 事件,并且從上述源碼可以看出,handleEvent 的執(zhí)行是不會(huì)被攔截打斷的。
那么問題來(lái)了,如果同一個(gè)區(qū)域內(nèi)有多個(gè)控件都實(shí)現(xiàn)了 handleEvent 時(shí),那最后事件應(yīng)該交給誰(shuí)消耗呢?
更具體為一個(gè)場(chǎng)景問題就是:比如一個(gè)列表頁(yè)面內(nèi),存在上下滑動(dòng)和 Item 點(diǎn)擊時(shí),F(xiàn)lutter 要怎么分配手勢(shì)事件? 這就涉及到事件的競(jìng)爭(zhēng)了。
2、事件競(jìng)爭(zhēng)核心要來(lái)了,高能預(yù)警?。。?/strong>
Flutter 在設(shè)計(jì)事件競(jìng)爭(zhēng)的時(shí)候,定義了一個(gè)很有趣的概念:通過(guò)一個(gè)競(jìng)技場(chǎng),各個(gè)控件參與競(jìng)爭(zhēng),直接勝利的或者活到最后的第一位,你就獲勝得到了勝利。 那么為了分析接下來(lái)的“戰(zhàn)爭(zhēng)”,我們需要先看幾個(gè)概念:
GestureRecognizer :手勢(shì)識(shí)別器基類,基本上 RenderPointerListener 中需要處理的手勢(shì)事件,都會(huì)分發(fā)到它對(duì)應(yīng)的 GestureRecognizer,并經(jīng)過(guò)它處理和競(jìng)技后再分發(fā)出去,常見有 :OneSequenceGestureRecognizer 、 MultiTapGestureRecognizer 、VerticalDragGestureRecognizer 、TapGestureRecognizer 等等。
GestureArenaManagerr :手勢(shì)競(jìng)技管理,它管理了整個(gè)“戰(zhàn)爭(zhēng)”的過(guò)程,原則上競(jìng)技勝出的條件是 :第一個(gè)競(jìng)技獲勝的成員或最后一個(gè)不被拒絕的成員。
GestureArenaEntry :提供手勢(shì)事件競(jìng)技信息的實(shí)體,內(nèi)封裝參與事件競(jìng)技的成員。
GestureArenaMember:參與競(jìng)技的成員抽象對(duì)象,內(nèi)部有 acceptGesture 和 rejectGesture 方法,它代表手勢(shì)競(jìng)技的成員,默認(rèn) GestureRecognizer 都實(shí)現(xiàn)了它,所有競(jìng)技的成員可以理解為就是 GestureRecognizer 之間的競(jìng)爭(zhēng)。
_GestureArena:GestureArenaManager 內(nèi)的競(jìng)技場(chǎng),內(nèi)部持參與競(jìng)技的 members 列表,官方對(duì)這個(gè)競(jìng)技場(chǎng)的解釋是: 如果一個(gè)手勢(shì)試圖在競(jìng)技場(chǎng)開放時(shí)(isOpen=true)獲勝,它將成為一個(gè)帶有“渴望獲勝”的屬性的對(duì)象。當(dāng)競(jìng)技場(chǎng)關(guān)閉(isOpen=false)時(shí),競(jìng)技場(chǎng)將尋找一個(gè)“渴望獲勝”的對(duì)象成為新的參與者,如果這時(shí)候剛好只有一個(gè),那這一個(gè)參與者將成為這次競(jìng)技場(chǎng)勝利的青睞存在。
好了,知道這些概念之后我們開始分析流程,我們知道 GestureBinding 在 dispatchEvent 時(shí)會(huì)先判斷是否有 HitTestResult 是否有結(jié)果,一般情況下是存在的,所以直接執(zhí)行循環(huán) entry.target.handleEvent 。
循環(huán)執(zhí)行過(guò)程中,我們知道 entry.target.handleEvent 會(huì)觸發(fā)RenderPointerListener 的 handleEvent ,而事件流程中第一個(gè)事件一般都會(huì)是 PointerDownEvent。
PointerDownEvent 的流程在事件競(jìng)技流程中相當(dāng)關(guān)鍵,因?yàn)樗鼤?huì)觸發(fā) GestureRecognizer.addPointer。
GestureRecognizer 只有通過(guò) addPointer 方法將 PointerDownEvent 事件和自己綁定,并添加到 GestureBinding 的 PointerRouter 事件路由和 GestureArenaManager 事件競(jìng)技中,后續(xù)的事件這個(gè)控件的 GestureRecognizer 才能響應(yīng)和參與競(jìng)爭(zhēng)。
事實(shí)上 Down 事件在 Flutter 中一般都是用來(lái)做添加判斷的,如果存在競(jìng)爭(zhēng)時(shí),大部分時(shí)候是不會(huì)直接出結(jié)果的,而 Move 事件在不同 GestureRecognizer 中會(huì)表現(xiàn)不同,而 UP 事件之后,一般會(huì)強(qiáng)制得到一個(gè)結(jié)果。
所以我們知道了事件在 GestureBinding 開始分發(fā)的時(shí)候,在 PointerDownEvent 時(shí)需要響應(yīng)事件的 GestureRecognizer 們,會(huì)調(diào)用 addPointer 將自己添加到競(jìng)爭(zhēng)中。之后流程中如果沒有特殊情況,一般會(huì)執(zhí)行到參與競(jìng)爭(zhēng)成員列表的 last,也就是 GestureBinding 自己這個(gè) handleEvent 。
如下代碼所示,走到 GestureBinding 的 handleEvent ,在 Down 事件的流程中,一般 pointerRouter.route 不會(huì)怎么處理邏輯,然后就是 gestureArena.close 關(guān)閉競(jìng)技場(chǎng)了,嘗試得到勝利者。
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
/// 導(dǎo)航事件去觸發(fā) `GestureRecognizer` 的 handleEvent
/// 一般 PointerDownEvent 在 route 執(zhí)行中不怎么處理。
pointerRouter.route(event);
///gestureArena 就是 GestureArenaManager
if (event is PointerDownEvent) {
///關(guān)閉這個(gè) Down 事件的競(jìng)技,嘗試得到勝利
/// 如果沒有的話就留到 MOVE 或者 UP。
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
///已經(jīng)到 UP 了,強(qiáng)行得到結(jié)果。
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
讓我們看 GestureArenaManager 的 close 方法,下面代碼我們可以看到,如果前面 Down 事件中沒有通過(guò) addPointer 添加成員到 _arenas 中,那會(huì)連參加的機(jī)會(huì)都沒有,而進(jìn)入 _tryToResolveArena 之后,如果 state.members.length == 1 ,說(shuō)明只有一個(gè)成員了,那就不競(jìng)爭(zhēng)了,直接它就是勝利者,直接響應(yīng)后續(xù)所有事件。 那么如果是多個(gè)的話,就需要后續(xù)的競(jìng)爭(zhēng)了。
void close(int pointer) {
/// 拿到我們上面 addPointer 時(shí)添加的成員封裝
final _GestureArena state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
state.isOpen = false;
///開始打起來(lái)吧
_tryToResolveArena(pointer, state);
}
void _tryToResolveArena(int pointer, _GestureArena state) {
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
_arenas.remove(pointer);
} else if (state.eagerWinner != null) {
_resolveInFavorOf(pointer, state, state.eagerWinner);
}
}
那競(jìng)爭(zhēng)呢?接下來(lái)我們以 TapGestureRecognizer 為例子,如果控件區(qū)域內(nèi)存在兩個(gè) TapGestureRecognizer ,那么在 PointerDownEvent 流程是不會(huì)產(chǎn)生勝利者的,這時(shí)候如果沒有 MOVE 打斷的話,到了 UP 事件時(shí),就會(huì)執(zhí)行 gestureArena.sweep(event.pointer); 強(qiáng)行選取一個(gè)。
而選擇的方式也是很簡(jiǎn)單,就是 state.members.first ,從我們之前 hitTest 的結(jié)果上理解的話,就是控件樹的最里面 Child 了。 這樣勝利的 member 會(huì)通過(guò) members.first.acceptGesture(pointer) 回調(diào)到 TapGestureRecognizer.acceptGesture 中,設(shè)置 _wonArenaForPrimaryPointer 為 ture 標(biāo)志為勝利區(qū)域,然后執(zhí)行 _checkDown 和 _checkUp 發(fā)出事件響應(yīng)觸發(fā)給這個(gè)控件。
而這里有個(gè)有意思的就是 ,Down 流程的 acceptGesture 中的 _checkUp 因?yàn)闆]有 _finalPosition 此時(shí)是不會(huì)被執(zhí)行的,_finalPosition 會(huì)在 handlePrimaryPointer 方法中,獲得_finalPosition 并判斷 _wonArenaForPrimaryPointer 標(biāo)志為,再次執(zhí)行 _checkUp 才會(huì)成功。
handlePrimaryPointer 是在 UP 流程中 pointerRouter.route 觸發(fā) TapGestureRecognizer 的 handleEvent 觸發(fā)的。
那么問題來(lái)了,_checkDown 和 _checkUp 時(shí)在 UP 事件一次性被執(zhí)行,那么如果我長(zhǎng)按住的話,_checkDown 不是沒辦法正確回調(diào)了?
當(dāng)然不會(huì),在 TapGestureRecognizer 中有一個(gè) didExceedDeadline 的機(jī)制,在前面 Down 流程中,在 addPointer 時(shí) TapGestureRecognizer 會(huì)創(chuàng)建一個(gè)定時(shí)器,這個(gè)定時(shí)器的時(shí)間時(shí) kPressTimeout = 100毫秒 ,如果我們長(zhǎng)按住的話,就會(huì)等待到觸發(fā) didExceedDeadline 去執(zhí)行 _checkDown 發(fā)出 onTabDown 事件了。
_checkDown 執(zhí)行發(fā)送過(guò)程中,會(huì)有一個(gè)標(biāo)志為 _sentTapDown 判斷是否已經(jīng)發(fā)送過(guò),如果發(fā)送過(guò)了也不會(huì)在重發(fā),之后回到原本流程去競(jìng)爭(zhēng),手指抬起后得到勝利者相應(yīng),同時(shí)在 _checkUp 之后 _sentTapDown 標(biāo)識(shí)為會(huì)被重置。
這也可以分析點(diǎn)擊下的幾種場(chǎng)景:
1、區(qū)域內(nèi)只有一個(gè) TapGestureRecognizer :Down 事件時(shí)直接在競(jìng)技場(chǎng) close 時(shí)就得到競(jìng)出勝利者,調(diào)用 acceptGesture 執(zhí)行 _checkUp,到 Up 事件的時(shí)候通過(guò) handlePrimaryPointer 執(zhí)行 _checkUp,結(jié)束。
2、區(qū)域內(nèi)有多個(gè) TapGestureRecognizer :Down 事件時(shí)在競(jìng)技場(chǎng) close 不會(huì)競(jìng)出勝利者,在 Up 事件的時(shí)候,會(huì)在 route 過(guò)程通過(guò)handlePrimaryPointer 設(shè)置好 _finalPosition,之后經(jīng)過(guò)競(jìng)技場(chǎng) sweep 選取排在第一個(gè)位置的為勝利者,調(diào)用 acceptGesture,執(zhí)行 _checkDown 和 _checkUp 。
1、區(qū)域內(nèi)只有一個(gè) TapGestureRecognizer :除了 Down 事件是在 didExceedDeadline 時(shí)發(fā)出 _checkDown 外其他和上面基本沒區(qū)別。
2、區(qū)域內(nèi)有多個(gè) TapGestureRecognizer :Down 事件時(shí)在競(jìng)技場(chǎng) close 時(shí)不會(huì)競(jìng)出勝利者,但是會(huì)觸發(fā)定時(shí)器 didExceedDeadline,先發(fā)出 _checkDown,之后再經(jīng)過(guò) sweep 選取第一個(gè)座位勝利者,調(diào)用 acceptGesture,觸發(fā) _checkUp
那么問題又來(lái)了,你有沒有疑問,如果有區(qū)域兩個(gè) TapGestureRecognizer ,長(zhǎng)按的時(shí)候因?yàn)槎加|發(fā)了 didExceedDeadline 執(zhí)行 _checkDown 嗎?
答案是:會(huì)的!因?yàn)槎〞r(shí)器都觸發(fā)了 didExceedDeadline,所以 _checkDown 都會(huì)被執(zhí)行,從而都發(fā)出了 onTapDown 事件。但是后續(xù)競(jìng)爭(zhēng)后,只會(huì)執(zhí)行一個(gè) _checkUp ,所有只會(huì)有一個(gè)控件響應(yīng) onTap 。
在競(jìng)技場(chǎng)競(jìng)爭(zhēng)失敗的成員會(huì)被移出競(jìng)技場(chǎng),移除后就沒辦法參加后面事件的競(jìng)技了 ,比如 TapGestureRecognizer 在接受到 PointerMoveEvent 事件時(shí)就會(huì)直接 rejected , 并觸發(fā) rejectGesture ,之后定時(shí)器會(huì)被關(guān)閉,并且觸發(fā) onTapCancel ,然后重置標(biāo)志位.
總結(jié)下:
Down 事件時(shí)通過(guò) addPointer 加入了 GestureRecognizer 競(jìng)技場(chǎng)的區(qū)域,在沒移除的情況下,事件可以參加后續(xù)事件的競(jìng)技,在某個(gè)事件階段移除的話,之后的事件序列也會(huì)無(wú)法接受。事件的競(jìng)爭(zhēng)如果沒有勝利者,在 UP 流程中會(huì)強(qiáng)制指定第一個(gè)為勝利者。
滑動(dòng)事件也是需要在 Down 流程中 addPointer ,然后 MOVE 流程中,通過(guò)在 PointerRouter.route 之后執(zhí)行 DragGestureRecognizer.handleEvent 。
在 PointerMoveEvent 事件的 DragGestureRecognizer.handleEvent 里,會(huì)通過(guò)在 _hasSufficientPendingDragDeltaToAccept判斷是否符合條件,如:
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
如果符合條件就直接執(zhí)行 resolve(GestureDisposition.accepted); ,將流程回到競(jìng)技場(chǎng)里,然后執(zhí)行 acceptGesture ,然后觸發(fā)onStart 和 onUpdate 。
回到我們前面的上下滑動(dòng)可點(diǎn)擊列表,是不是很明確了:如果是點(diǎn)擊的話,沒有產(chǎn)生 MOVE 事件,所以 DragGestureRecognizer 沒有被接受,而Item 作為 Child 第一位,所以響應(yīng)點(diǎn)擊。如果有 MOVE 事件, DragGestureRecognizer 會(huì)被 acceptGesture,而點(diǎn)擊 GestureRecognizer 會(huì)被移除事件競(jìng)爭(zhēng),也就沒有后續(xù) UP 事件了。
那這個(gè) onUpdate 是怎么讓節(jié)目動(dòng)起來(lái)的?
我們以 ListView 為例子,通過(guò)源碼可以知道, onUpdate 最后會(huì)調(diào)用到 Scrollable 的 _handleDragUpdate ,這時(shí)候會(huì)執(zhí)行 Drag.update。
通過(guò)源碼我們知道 ListView 的 Drag 實(shí)現(xiàn)其實(shí)是 ScrollDragController, 它在 Scrollable 中是和 ScrollPositionWithSingleContext 關(guān)聯(lián)的在一起的。那么 ScrollPositionWithSingleContext 又是什么?
ScrollPositionWithSingleContext 其實(shí)就是這個(gè)滑動(dòng)的關(guān)鍵,它其實(shí)就是 ScrollPosition 的子類,而 ScrollPosition 又是 ViewportOffset 的子類,而 ViewportOffset 又是一個(gè) ChangeNotifier,出現(xiàn)如下關(guān)系:
繼承關(guān)系:ScrollPositionWithSingleContext : ScrollPosition : ViewportOffset : ChangeNotifier
所以 ViewportOffset 就是滑動(dòng)的關(guān)鍵點(diǎn)。上面我們知道響應(yīng)區(qū)域 DragGestureRecognizer 勝利之后執(zhí)行 Drag.update ,最終會(huì)調(diào)用到 ScrollPositionWithSingleContext 的 applyUserOffset,導(dǎo)致內(nèi)部確定位置的 pixels 發(fā)生改變,并執(zhí)行父類 ChangeNotifier 的方法notifyListeners 通知更新。
而在 ListView 內(nèi)部 RenderViewportBase 中,這個(gè) ViewportOffset 是通過(guò) _offset.addListener(markNeedsLayout); 綁定的,so ,觸摸滑動(dòng)導(dǎo)致 Drag.update ,最終會(huì)執(zhí)行到 RenderViewportBase 中的 markNeedsLayout 觸發(fā)頁(yè)面更新。
至于 markNeedsLayout 如何更新界面和滾動(dòng)列表,這里暫不詳細(xì)描述了,給個(gè)圖感受下:
資源推薦自此,第十三篇終于結(jié)束了!(///▽///)
Github : github.com/CarGuo
本文Demo :github.com/CarGuo/stat…
本文代碼 :github.com/CarGuo/GSYG…
GSYGithubApp Flutter
GSYGithubApp React Native
GSYGithubAppWeex
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(一、Dart語(yǔ)言和Flutter基礎(chǔ))》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(二、 快速開發(fā)實(shí)戰(zhàn)篇)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(三、 打包與填坑篇)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(四、Redux、主題、國(guó)際化)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(五、 深入探索)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(六、 深入Widget原理)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(七、 深入布局原理)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(八、 實(shí)用技巧與填坑)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(九、 深入繪制原理)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(十、 深入圖片加載流程)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(十一、全面深入理解Stream)》
《Flutter完整開發(fā)實(shí)戰(zhàn)詳解(十二、全面深入理解狀態(tài)管理設(shè)計(jì))》
《跨平臺(tái)項(xiàng)目開源項(xiàng)目推薦》
《移動(dòng)端跨平臺(tái)開發(fā)的深度解析》
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/7331.html
摘要:中的的線程是以事件循環(huán)和消息隊(duì)列的形式存在,包含兩個(gè)任務(wù)隊(duì)列,一個(gè)是內(nèi)部隊(duì)列,一個(gè)是外部隊(duì)列,而的優(yōu)先級(jí)又高于。同時(shí)還有處理按住時(shí)的事件額外處理,同時(shí)手勢(shì)處理一般在的子類進(jìn)行。谷歌大會(huì)之后,有不少人咨詢了我 Flutter 相關(guān)的問題,其中有不少是和面試相關(guān)的,如今一些招聘上也開始羅列 Flutter 相關(guān)要求,最后想了想還是寫一期總結(jié)吧,也算是 Flutter 的階段復(fù)習(xí)。 ??系統(tǒng)完...
摘要:昨天有個(gè)小學(xué)弟給我發(fā)來(lái)微信,說(shuō)他現(xiàn)在有點(diǎn)后悔選擇開發(fā)了,月月光不說(shuō),還加班特別嚴(yán)重,平時(shí)也沒有屬于自己的時(shí)間去學(xué)習(xí),問我剛畢業(yè)的時(shí)候是不是這樣。每天回到出租屋都是倒頭就睡,非常累,也沒有其他時(shí)間提升自己的技術(shù)。 昨天有個(gè)小學(xué)弟給我發(fā)來(lái)微信,說(shuō)他現(xiàn)在有點(diǎn)后悔選擇Android開發(fā)了,月月光不說(shuō)...
摘要:前言最近比較熱門,但是成體系的文章并不多,前期避免不了踩坑我這篇文章主要介紹如何使用實(shí)現(xiàn)一個(gè)比較復(fù)雜的手勢(shì)交互,順便分享一下我在使用過(guò)程中遇到的一些小坑,減少大家入坑作者鏈接先睹為快本項(xiàng)目支持運(yùn)行,效果如下對(duì)了,順便分享一下生成的小竅門,建 前言 Flutter最近比較熱門,但是Flutter成體系的文章并不多,前期避免不了踩坑;我這篇文章主要介紹如何使用Flutter實(shí)現(xiàn)一個(gè)比較復(fù)雜...
摘要:前言最近比較熱門,但是成體系的文章并不多,前期避免不了踩坑我這篇文章主要介紹如何使用實(shí)現(xiàn)一個(gè)比較復(fù)雜的手勢(shì)交互,順便分享一下我在使用過(guò)程中遇到的一些小坑,減少大家入坑作者鏈接先睹為快本項(xiàng)目支持運(yùn)行,效果如下對(duì)了,順便分享一下生成的小竅門,建 前言 Flutter最近比較熱門,但是Flutter成體系的文章并不多,前期避免不了踩坑;我這篇文章主要介紹如何使用Flutter實(shí)現(xiàn)一個(gè)比較復(fù)雜...
摘要:其實(shí),這種時(shí)候,需要做的是馬上買空氣凈化器,任何一款都好,哪怕是凈化能力差一點(diǎn)的,也能解決當(dāng)前的主要問題,更好的凈化器帶給你的凈化效果的提升,不過(guò)是多一點(diǎn)邊際收益。 前言 說(shuō)到底,是自己的選擇問題。 三百六十行,哪行容易? 但關(guān)鍵是自己的心態(tài),如果工作成了你的負(fù)擔(dān)和困擾,你得有跳出來(lái)的...
閱讀 2812·2021-10-14 09:42
閱讀 3619·2021-10-11 10:59
閱讀 2952·2019-08-30 11:25
閱讀 3088·2019-08-29 16:25
閱讀 3233·2019-08-26 17:40
閱讀 1241·2019-08-26 13:30
閱讀 1155·2019-08-26 11:46
閱讀 1337·2019-08-23 15:22