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