本篇將帶你深入了解 Flutter 中的手勢事件傳遞屯换、事件分發(fā)编丘、事件沖突競爭与学,滑動流暢等等的原理,幫你構(gòu)建一個完整的 Flutter 閉環(huán)手勢知識體系嘉抓,這也許是目前最全面的手勢事件和滑動源碼的深入文章了索守。
文章匯總地址:
Flutter 中默認情況下,以 Android 為例抑片,所有的事件都是起原生源于 io.flutter.view.FlutterView
這個 SurfaceView
的子類卵佛,整個觸摸手勢事件實質(zhì)上經(jīng)歷了 JAVA => C++ => Dart 的一個流程,整個流程如下圖所示敞斋,無論是 Android 還是 IOS 截汪,原生層都只是將所有事件打包下發(fā),比如在 Android 中植捎,手勢信息被打包成 ByteBuffer
進行傳遞衙解,最后在 Dart 層的 _dispatchPointerDataPacket
方法中,通過 _unpackPointerDataPacket
方法解析成可用的 PointerDataPacket
對象使用焰枢。
那么具體在 Flutter 中是如何分發(fā)使用手勢事件的呢蚓峦?
1、事件流程
在前面的流程圖中我們知道济锄,在 Dart 層中手勢事件都是從 _dispatchPointerDataPacket
開始的暑椰,之后會通過 Zone
判斷環(huán)境回調(diào),會執(zhí)行 GestureBinding
這個膠水類中的 _handlePointerEvent
方法荐绝。(如果對 Zone
或者 GestureBinding
有疑問可以翻閱前面的篇章)
如下代碼所示干茉, GestureBinding
的 _handlePointerEvent
方法中主要是 hitTest
和 dispatchEvent
: 通過 hitTest
碰撞,得到一個包含控件的待處理成員列表 HitTestResult
很泊,然后通過 dispatchEvent
分發(fā)事件并產(chǎn)生競爭角虫,得到勝利者相應(yīng)。
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) {
///復(fù)用機制,抬起和取消昏兆,不用hitTest枫虏,移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
///復(fù)用機制,手指處于滑動中爬虱,不用hitTest
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
///開始分發(fā)事件
dispatchEvent(event, hitTestResult);
}
}
了解了結(jié)果后隶债,接下來深入分析這兩個關(guān)鍵方法:
1.1 、hitTest
hitTest
方法主要為了得到一個 HitTestResult
跑筝,這個 HitTestResult
內(nèi)有一個 List<HitTestEntry>
是用于分發(fā)和競爭事件的死讹,而每個 HitTestEntry.target
都會存儲每個控件的 RenderObject
。
因為 RenderObject
默認都實現(xiàn)了 HitTestTarget
接口曲梗,所以可以理解為: HitTestTarget
大部分時候都是 RenderObject
赞警,而 HitTestResult
就是一個帶著碰撞測試后的控件列表妓忍。
事實上 hitTest
是 HitTestable
抽象類的方法,而 Flutter 中所有實現(xiàn) HitTestable
的類有 GestureBinding
和 RendererBinding
愧旦,它們都是 mixins
在 WidgetsFlutterBinding
這個入口類上世剖,并且因為它們的 mixins
順序的關(guān)系,所以 RendererBinding
的 hitTest
會先被調(diào)用笤虫,之后才調(diào)用 GestureBinding
的 hitTest
旁瘫。
那么這兩個 hitTest 又分別干了什么事呢?
1.2琼蚯、RendererBinding.hitTest
在 RendererBinding.hitTest
中會執(zhí)行 renderView.hitTest(result, position: position);
酬凳,如下代碼所示,renderView.hitTest
方法內(nèi)會執(zhí)行 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
判斷自己是否屬于響應(yīng)區(qū)域赐俗,確認響應(yīng)后執(zhí)行 hitTestChildren
和 hitTestSelf
拉队,嘗試添加下級的 child 和自己添加進去,這樣的遞歸就讓我們自下而上的得到了一個 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;
}
1.3、GestureBinding.hitTest
最后 GestureBinding.hitTest
方法不過最后把 GestureBinding
自己也添加到 HitTestResult
里叔扼,最后因為后面我們的流程還會需要回到 GestureBinding
中去處理事哭。
1.4、dispatchEvent
dispatchEvent
中主要是對事件進行分發(fā)瓜富,并且通過上述添加進去的 target.handleEvent
處理事件鳍咱,如下代碼所示,在存在碰撞結(jié)果的時候与柑,是會通過循環(huán)對每個控件內(nèi)部的handleEvent
進行執(zhí)行谤辜。
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
///如果沒有碰撞結(jié)果,那么通過 `pointerRouter.route` 將事件分發(fā)到全局處理价捧。
if (hitTestResult == null) {
try {
pointerRouter.route(event);
} catch (exception, stack) {
return;
}
///上面我們知道 HitTestEntry 中的 target 是一系自下而上的控件
///還有 renderView 和 GestureBinding
///循環(huán)執(zhí)行每一個的 handleEvent 方法
for (HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event, entry);
} catch (exception, stack) {
}
}
}
事實上并不是所有的控件的 RenderObject
子類都會處理 handleEvent
丑念,大部分時候,只有帶有 RenderPointerListener
(RenderObject) / Listener
(Widget) 的才會處理 handleEvent
事件结蟋,并且從上述源碼可以看出脯倚,handleEvent 的執(zhí)行是不會被攔截打斷的。
那么問題來了椎眯,如果同一個區(qū)域內(nèi)有多個控件都實現(xiàn)了 handleEvent
時挠将,那最后事件應(yīng)該交給誰消耗呢胳岂?
更具體為一個場景問題就是:比如一個列表頁面內(nèi)编整,存在上下滑動和 Item 點擊時舔稀,F(xiàn)lutter 要怎么分配手勢事件? 這就涉及到事件的競爭了掌测。
核心要來了内贮,高能預(yù)警!9夜郁!
2、事件競爭
Flutter 在設(shè)計事件競爭的時候粘勒,定義了一個很有趣的概念:通過一個競技場竞端,各個控件參與競爭,直接勝利的或者活到最后的第一位庙睡,你就獲勝得到了勝利事富。 那么為了分析接下來的“戰(zhàn)爭”,我們需要先看幾個概念:
GestureRecognizer
:手勢識別器基類乘陪,基本上RenderPointerListener
中需要處理的手勢事件统台,都會分發(fā)到它對應(yīng)的GestureRecognizer
,并經(jīng)過它處理和競技后再分發(fā)出去啡邑,常見有 :OneSequenceGestureRecognizer
贱勃、MultiTapGestureRecognizer
、VerticalDragGestureRecognizer
谤逼、TapGestureRecognizer
等等贵扰。GestureArenaManagerr
:手勢競技管理,它管理了整個“戰(zhàn)爭”的過程流部,原則上競技勝出的條件是 :第一個競技獲勝的成員或最后一個不被拒絕的成員戚绕。GestureArenaEntry
:提供手勢事件競技信息的實體,內(nèi)封裝參與事件競技的成員贵涵。GestureArenaMember
:參與競技的成員抽象對象列肢,內(nèi)部有acceptGesture
和rejectGesture
方法,它代表手勢競技的成員宾茂,默認GestureRecognizer
都實現(xiàn)了它瓷马,所有競技的成員可以理解為就是GestureRecognizer
之間的競爭。_GestureArena
:GestureArenaManager
內(nèi)的競技場跨晴,內(nèi)部持參與競技的members
列表欧聘,官方對這個競技場的解釋是: 如果一個手勢試圖在競技場開放時(isOpen=true)獲勝,它將成為一個帶有“渴望獲勝”的屬性的對象端盆。當(dāng)競技場關(guān)閉(isOpen=false)時怀骤,競技場將尋找一個“渴望獲勝”的對象成為新的參與者费封,如果這時候剛好只有一個,那這一個參與者將成為這次競技場勝利的青睞存在蒋伦。
好了弓摘,知道這些概念之后我們開始分析流程,我們知道 GestureBinding
在 dispatchEvent
時會先判斷是否有 HitTestResult
是否有結(jié)果痕届,一般情況下是存在的韧献,所以直接執(zhí)行循環(huán) entry.target.handleEvent
。
2.1研叫、PointerDownEvent
循環(huán)執(zhí)行過程中锤窑,我們知道 entry.target.handleEvent
會觸發(fā)RenderPointerListener
的 handleEvent
,而事件流程中第一個事件一般都會是 PointerDownEvent
嚷炉。
PointerDownEvent
的流程在事件競技流程中相當(dāng)關(guān)鍵渊啰,因為它會觸發(fā)GestureRecognizer.addPointer
。
GestureRecognizer
只有通過 addPointer
方法將 PointerDownEvent
事件和自己綁定申屹,并添加到 GestureBinding
的 PointerRouter
事件路由和 GestureArenaManager
事件競技中绘证,后續(xù)的事件這個控件的 GestureRecognizer
才能響應(yīng)和參與競爭。
事實上 Down 事件在 Flutter 中一般都是用來做添加判斷的独柑,如果存在競爭時迈窟,大部分時候是不會直接出結(jié)果的,而 Move 事件在不同
GestureRecognizer
中會表現(xiàn)不同忌栅,而 UP 事件之后车酣,一般會強制得到一個結(jié)果。
所以我們知道了事件在 GestureBinding
開始分發(fā)的時候索绪,在 PointerDownEvent
時需要響應(yīng)事件的 GestureRecognizer
們湖员,會調(diào)用 addPointer
將自己添加到競爭中。之后流程中如果沒有特殊情況瑞驱,一般會執(zhí)行到參與競爭成員列表的 last娘摔,也就是 GestureBinding
自己這個 handleEvent 。
如下代碼所示唤反,走到 GestureBinding
的 handleEvent
凳寺,在 Down 事件的流程中,一般 pointerRouter.route
不會怎么處理邏輯彤侍,然后就是 gestureArena.close
關(guān)閉競技場了肠缨,嘗試得到勝利者。
@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)閉這個 Down 事件的競技晒奕,嘗試得到勝利
/// 如果沒有的話就留到 MOVE 或者 UP。
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
///已經(jīng)到 UP 了,強行得到結(jié)果脑慧。
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
讓我們看 GestureArenaManager
的 close
方法魄眉,下面代碼我們可以看到,如果前面 Down 事件中沒有通過 addPointer
添加成員到 _arenas
中闷袒,那會連參加的機會都沒有坑律,而進入 _tryToResolveArena
之后,如果 state.members.length == 1
霜运,說明只有一個成員了脾歇,那就不競爭了蒋腮,直接它就是勝利者淘捡,直接響應(yīng)后續(xù)所有事件。 那么如果是多個的話池摧,就需要后續(xù)的競爭了焦除。
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);
}
}
2.2 開始競爭
那競爭呢?接下來我們以 TapGestureRecognizer
為例子作彤,如果控件區(qū)域內(nèi)存在兩個 TapGestureRecognizer
膘魄,那么在 PointerDownEvent
流程是不會產(chǎn)生勝利者的,這時候如果沒有 MOVE 打斷的話竭讳,到了 UP 事件時创葡,就會執(zhí)行 gestureArena.sweep(event.pointer);
強行選取一個。
而選擇的方式也是很簡單绢慢,就是 state.members.first
灿渴,從我們之前 hitTest
的結(jié)果上理解的話,就是控件樹的最里面 Child 了胰舆。 這樣勝利的 member 會通過 members.first.acceptGesture(pointer)
回調(diào)到 TapGestureRecognizer.acceptGesture
中骚露,設(shè)置 _wonArenaForPrimaryPointer
為 ture 標(biāo)志為勝利區(qū)域,然后執(zhí)行
_checkDown
和 _checkUp
發(fā)出事件響應(yīng)觸發(fā)給這個控件缚窿。
而這里有個有意思的就是 棘幸,Down 流程的 acceptGesture
中的 _checkUp
因為沒有 _finalPosition
此時是不會被執(zhí)行的,_finalPosition
會在 handlePrimaryPointer
方法中倦零,獲得_finalPosition
并判斷 _wonArenaForPrimaryPointer
標(biāo)志為误续,再次執(zhí)行 _checkUp
才會成功。
handlePrimaryPointer
是在 UP 流程中pointerRouter.route
觸發(fā)TapGestureRecognizer
的handleEvent
觸發(fā)的扫茅。
那么問題來了蹋嵌,_checkDown
和 _checkUp
時在 UP 事件一次性被執(zhí)行,那么如果我長按住的話诞帐,_checkDown
不是沒辦法正確回調(diào)了欣尼?
當(dāng)然不會,在 TapGestureRecognizer
中有一個 didExceedDeadline
的機制,在前面 Down 流程中愕鼓,在 addPointer
時 TapGestureRecognizer
會創(chuàng)建一個定時器钙态,這個定時器的時間時 kPressTimeout = 100毫秒
,如果我們長按住的話菇晃,就會等待到觸發(fā) didExceedDeadline
去執(zhí)行 _checkDown
發(fā)出 onTabDown
事件了册倒。
_checkDown
執(zhí)行發(fā)送過程中,會有一個標(biāo)志為_sentTapDown
判斷是否已經(jīng)發(fā)送過磺送,如果發(fā)送過了也不會在重發(fā)驻子,之后回到原本流程去競爭,手指抬起后得到勝利者相應(yīng)估灿,同時在_checkUp
之后_sentTapDown
標(biāo)識為會被重置崇呵。
這也可以分析點擊下的幾種場景:
普通按下:
1、區(qū)域內(nèi)只有一個
TapGestureRecognizer
:Down 事件時直接在競技場close
時就得到競出勝利者馅袁,調(diào)用acceptGesture
執(zhí)行_checkUp
域慷,到 Up 事件的時候通過handlePrimaryPointer
執(zhí)行_checkUp
,結(jié)束汗销。2犹褒、區(qū)域內(nèi)有多個
TapGestureRecognizer
:Down 事件時在競技場close
不會競出勝利者,在 Up 事件的時候弛针,會在route
過程通過handlePrimaryPointer
設(shè)置好_finalPosition
叠骑,之后經(jīng)過競技場sweep
選取排在第一個位置的為勝利者,調(diào)用acceptGesture
削茁,執(zhí)行_checkDown
和_checkUp
宙枷。
長按之后抬起:
1、區(qū)域內(nèi)只有一個 TapGestureRecognizer
:除了 Down 事件是在 didExceedDeadline
時發(fā)出 _checkDown
外其他和上面基本沒區(qū)別付材。
- 2朦拖、區(qū)域內(nèi)有多個
TapGestureRecognizer
:Down 事件時在競技場close
時不會競出勝利者,但是會觸發(fā)定時器didExceedDeadline
厌衔,先發(fā)出_checkDown
璧帝,之后再經(jīng)過sweep
選取第一個座位勝利者,調(diào)用acceptGesture
富寿,觸發(fā)_checkUp
那么問題又來了睬隶,你有沒有疑問,如果有區(qū)域兩個 TapGestureRecognizer
页徐,長按的時候因為都觸發(fā)了 didExceedDeadline
執(zhí)行 _checkDown
嗎苏潜?
答案是:會的!因為定時器都觸發(fā)了 didExceedDeadline
变勇,所以 _checkDown
都會被執(zhí)行恤左,從而都發(fā)出了 onTapDown
事件贴唇。但是后續(xù)競爭后,只會執(zhí)行一個 _checkUp
飞袋,所有只會有一個控件響應(yīng) onTap
戳气。
競技失敗:
在競技場競爭失敗的成員會被移出競技場巧鸭,移除后就沒辦法參加后面事件的競技了 瓶您,比如 TapGestureRecognizer
在接受到 PointerMoveEvent
事件時就會直接 rejected
, 并觸發(fā) rejectGesture
,之后定時器會被關(guān)閉纲仍,并且觸發(fā) onTapCancel
呀袱,然后重置標(biāo)志位.
總結(jié)下:
Down 事件時通過 addPointer
加入了 GestureRecognizer
競技場的區(qū)域,在沒移除的情況下郑叠,事件可以參加后續(xù)事件的競技夜赵,在某個事件階段移除的話,之后的事件序列也會無法接受锻拘。事件的競爭如果沒有勝利者油吭,在 UP 流程中會強制指定第一個為勝利者。
2.3 滑動事件
滑動事件也是需要在 Down 流程中 addPointer
署拟,然后 MOVE 流程中,通過在 PointerRouter.route
之后執(zhí)行 DragGestureRecognizer.handleEvent
歌豺。
在 PointerMoveEvent
事件的 DragGestureRecognizer.handleEvent
里推穷,會通過在 _hasSufficientPendingDragDeltaToAccept
判斷是否符合條件,如:
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
如果符合條件就直接執(zhí)行 resolve(GestureDisposition.accepted);
类咧,將流程回到競技場里馒铃,然后執(zhí)行 acceptGesture
,然后觸發(fā)onStart
和 onUpdate
痕惋。
回到我們前面的上下滑動可點擊列表区宇,是不是很明確了:如果是點擊的話,沒有產(chǎn)生 MOVE 事件值戳,所以 DragGestureRecognizer
沒有被接受议谷,而Item 作為 Child 第一位,所以響應(yīng)點擊堕虹。如果有 MOVE 事件卧晓, DragGestureRecognizer
會被 acceptGesture
,而點擊 GestureRecognizer
會被移除事件競爭赴捞,也就沒有后續(xù) UP 事件了逼裆。
那這個 onUpdate
是怎么讓節(jié)目動起來的?
我們以 ListView
為例子赦政,通過源碼可以知道胜宇, onUpdate
最后會調(diào)用到 Scrollable
的 _handleDragUpdate
,這時候會執(zhí)行 Drag.update
。
通過源碼我們知道 ListView
的 Drag
實現(xiàn)其實是 ScrollDragController
, 它在 Scrollable
中是和 ScrollPositionWithSingleContext
關(guān)聯(lián)的在一起的桐愉。那么 ScrollPositionWithSingleContext
又是什么封寞?
ScrollPositionWithSingleContext
其實就是這個滑動的關(guān)鍵,它其實就是 ScrollPosition
的子類仅财,而 ScrollPosition
又是 ViewportOffset
的子類狈究,而 ViewportOffset
又是一個 ChangeNotifier
,出現(xiàn)如下關(guān)系:
繼承關(guān)系:ScrollPositionWithSingleContext : ScrollPosition : ViewportOffset : ChangeNotifier
所以 ViewportOffset 就是滑動的關(guān)鍵點盏求。上面我們知道響應(yīng)區(qū)域 DragGestureRecognizer
勝利之后執(zhí)行 Drag.update
抖锥,最終會調(diào)用到 ScrollPositionWithSingleContext
的 applyUserOffset
,導(dǎo)致內(nèi)部確定位置的 pixels
發(fā)生改變碎罚,并執(zhí)行父類 ChangeNotifier
的方法notifyListeners
通知更新磅废。
而在 ListView
內(nèi)部 RenderViewportBase
中,這個 ViewportOffset
是通過 _offset.addListener(markNeedsLayout);
綁定的荆烈,so 拯勉,觸摸滑動導(dǎo)致 Drag.update
,最終會執(zhí)行到 RenderViewportBase
中的 markNeedsLayout
觸發(fā)頁面更新憔购。
至于 markNeedsLayout
如何更新界面和滾動列表宫峦,這里暫不詳細描述了,給個圖感受下:
自此玫鸟,第十三篇終于結(jié)束了导绷!(///▽///)
資源推薦
- 本文Demo :https://github.com/CarGuo/state_manager_demo
- Github : https://github.com/CarGuo/
- 開源 Flutter 完整項目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學(xué)習(xí)型項目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實戰(zhàn)電子書項目:https://github.com/CarGuo/GSYFlutterBook