Flutter的事件源
Flutter的原始事件是由window中 PointerDataPacketCallback(PointerDataPacket packet) 回調(diào)獲得的,這個回調(diào)再GestureBinding初始化中就設(shè)置了window.onPointerDataPacket = _handlePointerDataPacket,我們看一下_handlePointerDataPacket的代碼
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
//_pendingPointerEvents是一個PointerEvent的隊列,這段代碼的意思是將PointerDataPacket轉(zhuǎn)換成PointerEvent然后存在隊列中
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
//對隊列中的PointerEvent進行出隊并依次處理
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
_handlePointerEvent(_pendingPointerEvents.removeFirst());//_handlePointerEvent對每一個PointerEvent進行處理
}
_handlePointerEvent方法才是對每個PointerEvent進行處理的地方
void _handlePointerEvent(PointerEvent event) {
HitTestResult hitTestResult;
if (event is PointerDownEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);//hitTest方法,來確定命中測試的結(jié)果
_hitTests[event.pointer] = hitTestResult;//event.pointer是每次連續(xù)的PointEvent的唯一id,以id為key將hitTestResult存到_hitTests中
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);//事件結(jié)束標記,將hitTestResult從_hitTests取出并移除
} else if (event.down) {
hitTestResult = _hitTests[event.pointer];//move事件直接重用down事件的hitTestResult,避免每次都進行命中測試
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
dispatchEvent(event, hitTestResult);//分發(fā)Event
}
}
_handlePointerEvent中比較重要的兩點:
- 在PointerDownEvent事件時,通過hitTest方法來計算出HitTestResult
- 對事件序列通過dispatchEvent(event, hitTestResult)進行分發(fā)事件
上面的過程就是將原始事件轉(zhuǎn)換成我們需要的PointEvent,然后再確定命中測試結(jié)果,最后再進行分發(fā)事件。
確定HitTestResult
HitTestResult中有一個List<HitTestEntry> _path的字段,由多個HitTestEntry來組成的path(事件進行冒泡的路徑,為什么是通過冒泡后面會有解釋),HitTestEntry是每一個命中的入口,它只有一個HitTestTarget target字段,而HitTestTarget又是由RenderObject來實現(xiàn)的,所以HitTestResult其實就是一系列通過命中測試的RenderObject的集合。我們來看看是如何來確定命中測試的結(jié)果的
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);//這里是調(diào)用的GestureBinding中的hitTest方法,將WidgetsFlutterBinding加入到最后面
}
調(diào)用renderView的hitTest方法,繼續(xù)跟進
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(result, position: position);
result.add(HitTestEntry(this));
return true;
}
可以看到renderView并沒有直接實現(xiàn)HitTestable中的hitTest方法,renderView的hitTest方法中的{ Offset position }是一個可選參數(shù),并且?guī)б粋€bool類型的返回值,renderView的hitTest方法顯示對child進行命中測試,讓后再將自己添加到命中測試結(jié)果。
RenderObject中并沒有發(fā)現(xiàn)hitTest方法,但是再其子類RenderBox中發(fā)現(xiàn)了名為hitTest的方法,也沒有直接實現(xiàn)HitTestable中的hitTest方法,{ Offset position }也是一個可選參數(shù),也有一個bool類型的返回值
bool hitTest(HitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {//確定hit的位置再自己的size范圍里面
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
//先對children進行hitTest,然后再對自己進行hitTest,有一項返回true才能將自己添加到HitTestResult里面
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
我們看一個比較簡單的例子,RenderPadding中是怎樣對children進行命中測試的,RenderPadding的hitTestChildren實現(xiàn)在RenderShiftedBox中,hitTestSelf的實現(xiàn)在RenderBox中
@override
bool hitTestChildren(HitTestResult result, { Offset position }) {
if (child != null) {
final BoxParentData childParentData = child.parentData;
return child.hitTest(result, position: position - childParentData.offset);//將點擊點減去偏移應(yīng)用到child的命中測試
}
return false;
}
@protected
bool hitTestSelf(Offset position) => false;//自己進行命中測試
RenderPadding的命中測試結(jié)果就是如果child命中測試成功,則自己也會被添加的命中測試結(jié)果中,否則就不對自己進行命中測試
分發(fā)Event
接下來就是對Event的分發(fā)了,我們直接看GestureBinding中的dispatchEvent方法
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
assert(!locked);
//沒有命中測試信息意味著PointerEvent是Hover,Added,Removed其中一種
if (hitTestResult == null) {
assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
try {
//將事件分發(fā)到注冊了此次事件的路由,一般是由GestureRecognizer中void addPointer(PointerDownEvent event)方法進行注冊
pointerRouter.route(event);
} catch (exception, stack) {}
return;//進行路由分發(fā)直接返回
}
//對命中測試的結(jié)果進行遍歷,應(yīng)為是先對child進行命中測試,所以事件的序列是冒泡向上傳遞的
for (HitTestEntry entry in hitTestResult.path) {
try {
//調(diào)用target的handleEvent方法處理事件
entry.target.handleEvent(event, entry);
} catch (exception, stack) {}
}
}
當命中測試結(jié)果為空時進行路由分發(fā),當命中測試結(jié)果不為空時,就進行命中結(jié)果分發(fā),handleEvent方法的實現(xiàn)我們來看一個比較典型的,RenderPointerListener中的handleEvent,RenderPointerListener時Listener(可以監(jiān)聽原始PointEvent的Widget)對應(yīng)的RenderObject對象
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
// onPointerEnter, onPointerHover, and onPointerExit events都在MouseTracker里面處理
if (onPointerDown != null && event is PointerDownEvent)
return onPointerDown(event);
if (onPointerMove != null && event is PointerMoveEvent)
return onPointerMove(event);
if (onPointerUp != null && event is PointerUpEvent)
return onPointerUp(event);
if (onPointerCancel != null && event is PointerCancelEvent)
return onPointerCancel(event);
}
RenderPointerListener直接把事件給到對應(yīng)的回調(diào),大多數(shù)RenderObject都沒有實現(xiàn)handleEvent方法。
事件監(jiān)聽
Flutter的官方文檔推薦我們使用GestureDetector來檢測用戶手勢輸入,GestureDetector幫我們區(qū)別了各種類型的手勢,我們只需要設(shè)置需要監(jiān)聽的手勢回調(diào)就可以了,使用非常方便。
從我們上面的分析可以看到,事件的產(chǎn)生與分發(fā),我們來看一下GestureDetector是如何監(jiān)聽事件并進行區(qū)別手勢的呢?
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
...省略若干代碼
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
可以看到GestureDetector是通過RawGestureDetector來實現(xiàn)的,我們再看RawGestureDetector
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child
);
if (!widget.excludeFromSemantics)
result = _GestureSemantics(owner: this, child: result);
return result;
}
而RawGestureDetector又是通過Listener來實現(xiàn)的,上面我們知道Listener是監(jiān)聽初始事件PointerEvent的,那他是如何被區(qū)別為各種各樣的手勢的呢?
手勢識別
看一下RawGestureDetector中的_handlePointerDown方法
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (GestureRecognizer recognizer in _recognizers.values)
recognizer.addPointer(event);
}
在PointerDownEvent的時候,將所有recognizer進行addPointer(event),繼續(xù)跟進addPointer方法,在GestureRecognizer中是空實現(xiàn),我們先看一個簡單的實現(xiàn)TapGestureRecognizer
@override
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);//開始追蹤id為pointer的事件序列
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;
primaryPointer = event.pointer;
initialPosition = event.position;
if (deadline != null)
_timer = Timer(deadline, didExceedDeadline);
}
}
addPointer中最主要的方法就是startTrackingPointer方法,這個方法是OneSequenceGestureRecognizer中的,可以讓OneSequenceGestureRecognizer去追蹤這個事件序列,具體分析在下面手勢競技中再講,Recognizer追蹤了這個事件序列后,這個事件的后續(xù)事件都會被這個Recognizer處理,會觸發(fā)handleEvent方法,通過一系列判斷會走到handlePrimaryPointer方法,然后再PointerUpEvent時觸發(fā)
相關(guān)回調(diào)
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_finalPosition = event.position;
_checkUp();
} else if (event is PointerCancelEvent) {
_reset();
}
}
void _checkUp() {
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
resolve(GestureDisposition.accepted);
if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
return;
}
if (onTapUp != null)
invokeCallback<void>('onTapUp', () { onTapUp(TapUpDetails(globalPosition: _finalPosition)); });//觸發(fā)onTapUp回調(diào)
if (onTap != null)
invokeCallback<void>('onTap', onTap);//觸發(fā)onTap回調(diào)
_reset();
}
}
Recognizer會對事件進行分析,然后會去區(qū)別不同的情況去觸發(fā)不同的回調(diào)。
如果一個事件序列被多個Recognizer追蹤,比如需要監(jiān)聽用戶點擊與滑動,那么怎么去區(qū)別用戶到底是點擊還是滑動呢?
手勢競技
我們先看一下Recognizer是如何追蹤事件序列的,先看startTrackingPointer方法
@protected
void startTrackingPointer(int pointer) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
- GestureBinding(實例是WidgetsFlutterBinding)中的PointerRouter,其實就是維護了一個Map<int,LinkedHashSet<PointerRoute>> routerMap(路由表)的屬性,PointerRoute其實就是void Function(PointerEvent event)類型的回調(diào)。從上面可以看到,將pointer作為key,handleEvent方法作為值傳入。
- _trackedPointers是一個HashSet<int>,記錄此Recognizer追蹤的事件序列
- _entries是一個Map<int, GestureArenaEntry>,GestureArenaEntry中包含一個GestureArenaManager(手勢競技管理類)、_pointer(事件id)、GestureArenaMember(GestureRecognizer的基類,其實就是Recognizer本身)
繼續(xù)看一下_addPointerToArena方法
GestureArenaEntry _addPointerToArena(int pointer) {
if (_team != null)
return _team.add(pointer, this);
return GestureBinding.instance.gestureArena.add(pointer, this);
}
可以看到GestureBinding(實例是WidgetsFlutterBinding)中的gestureArena字段(它是GestureArenaManager)將這個Recognizer添加進去??纯雌鋋dd方法
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
GestureArenaEntry add(int pointer, GestureArenaMember member) {
//_GestureArena中是一個GestureArenaMember的list,其實就是一個手勢id,對應(yīng)多個GestureArenaMember
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
return _GestureArena();
});
state.add(member);//將GestureArenaMember添加到_GestureArena中
return GestureArenaEntry._(this, pointer, member);//再返回一個GestureArenaEntry對象給Recognizer中entries持有
}
從以上的分析可以總結(jié)一下startTrackingPointer主要做的事情:
- 跟據(jù)手勢的id(pointer)來添加路由,此Recognizer就可以接受處理余下的事件序列,當有余下事件序列發(fā)送過來就會調(diào)用此Recognizer中的handleEvent方法(此功能由GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent)代碼實現(xiàn))
- 將Recognizer添加到手勢競技場,處理同一個手勢id的Recognizer將被添加到同一個_GestureArena(手勢競技場)中,(此功能由_addPointerToArena方法實現(xiàn))
上面只是添加到路由以及競技場,但是我們還不知道是事件是怎樣被發(fā)送到指定的路由,以及多個手勢識別器是如何競爭處理手勢事件的
事件路由到指定Recognizer
我們知道事件的傳遞是通過冒泡來進行傳遞的,HitTestResult的最上層是WidgetsFlutterBinding,最后處理事件的應(yīng)該在GestureBinding中,我們看一下GestureBinding的handleEvent方法
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);//調(diào)用PointerRouter的route方法將事件進行路由
...省略
}
GestureBinding的dispatchEvent方法在HitTestResult為null的時候才路由,主要也就是Hover,Added,Removed這三種事件進行路由,而處理這三個的是一個global的MouseTracker。而此處路由會處理所有注冊到routerMap中的Recognizer(實際上只是其handleEvent方法)。
多個Recognizer競技
還是看GestureBinding的handleEvent方法
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
}
}
通過pointerRouter路由之后,在PointerDownEvent時候就調(diào)用GestureArenaManager的close方法(防止其他Recognizer加入到手勢競技中),所以在就是為什么addPoint注冊路由的方法需要PointerDownEvent作為參數(shù)了,一旦在down的時候不注冊,那么這個事件就與你的Recognizer無關(guān)了。看一下close方法
void close(int pointer) {
final _GestureArena state = _arenas[pointer];
if (state == null)
return;
state.isOpen = false;
_tryToResolveArena(pointer, state);
}
先將isOpen變?yōu)閒alse(在add的時候首先就會判斷isOpen),然后調(diào)用_tryToResolveArena方法,繼續(xù)跟進
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
if (state.members.length == 1) {//只有1個Recognizer,直接添加一個_resolveByDefault方法調(diào)用的task
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {//沒有Recognizer,直接移除該pointer對應(yīng)的_GestureArena
_arenas.remove(pointer);
} else if (state.eagerWinner != null) {//渴望的勝利者,在_GestureArena關(guān)閉的時候如果不為空會直接作為勝利者
_resolveInFavorOf(pointer, state, state.eagerWinner);//確定勝利者
}
}
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
assert(state == _arenas[pointer]);
assert(state != null);
assert(state.eagerWinner == null || state.eagerWinner == member);
assert(!state.isOpen);
_arenas.remove(pointer);//移除競技場,已經(jīng)得出結(jié)果不需要了
for (GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member)
rejectedMember.rejectGesture(pointer);//將不是勝利者的GestureArenaMember全部調(diào)用拒絕手勢
}
member.acceptGesture(pointer);//調(diào)用勝利這的接受手勢
}
但是我們在沒有eagerWinner的時候是怎樣來競技的呢?我們用兩個具體的手勢識別器點擊(TapGestureRecognizer)、滑動(PanGestureRecognizer)來分析
首先我們滑動一下
經(jīng)過上面的分析,在down的時候是解析不出勝利者的,后續(xù)move事件會路由給TapGestureRecognizer,PanGestureRecognizer,我們需要看一下TapGestureRecognizer的handleEvent方法
@override
void handleEvent(PointerEvent event) {
assert(state != GestureRecognizerState.ready);
if (event.pointer == primaryPointer) {
//接受手勢前滑動距離是否溢出容忍值
final bool isPreAcceptSlopPastTolerance =
state == GestureRecognizerState.possible &&
preAcceptSlopTolerance != null &&
_getDistance(event) > preAcceptSlopTolerance;
//接受手勢后滑動距離是否溢出容忍值
final bool isPostAcceptSlopPastTolerance =
state == GestureRecognizerState.accepted &&
postAcceptSlopTolerance != null &&
_getDistance(event) > postAcceptSlopTolerance;
//move事件下,超出容忍值
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);//拒絕后續(xù)事件
stopTrackingPointer(primaryPointer);//結(jié)束追蹤事件
} else {
handlePrimaryPointer(event);
}
}
stopTrackingIfPointerNoLongerDown(event);
}
所以但凡我們滑動的距離超出了容忍值(這個值是根據(jù)經(jīng)驗事件來確定的值),都會拒絕事件結(jié)束追蹤。所以事件就會落到PanGestureRecognizer身上。
我們再點一下
我們需要看一下PanGestureRecognizer的handleEvent方法
@override
void handleEvent(PointerEvent event) {
assert(_state != _DragState.ready);
if (!event.synthesized
&& (event is PointerDownEvent || event is PointerMoveEvent)) {
final VelocityTracker tracker = _velocityTrackers[event.pointer];
assert(tracker != null);
tracker.addPosition(event.timeStamp, event.position);
}
if (event is PointerMoveEvent) {
final Offset delta = event.delta;
if (_state == _DragState.accepted) {
if (onUpdate != null) {
invokeCallback<void>('onUpdate', () => onUpdate(DragUpdateDetails(
sourceTimeStamp: event.timeStamp,
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryValueFromOffset(delta),
globalPosition: event.position,
)));
}
} else {//move事件,沒有接受事件時
_pendingDragOffset += delta;
_lastPendingEventTimestamp = event.timeStamp;
//判斷是否由足夠的滑動距離來接受,也就是說滑動距離超過一定距離會主動接受
if (_hasSufficientPendingDragDeltaToAccept)
resolve(GestureDisposition.accepted);
}
}
stopTrackingIfPointerNoLongerDown(event);//up事件的時候會停止追蹤
}
由于我們是點一下,那么距離不夠是不會去主動接受的,等經(jīng)過一系列move事件結(jié)束后PanGestureRecognizer還是沒有獲得事件,最后再up的時候就停止追蹤事件了,那么事件就會落到TapGestureRecognizer身上。
通過上面兩種情況的分析,不同的Recognizer都有自己的邏輯去接受、拒絕、停止追蹤事件。
接受、拒絕、停止追蹤事件
通過resolve方法傳入一個GestureDisposition可以讓Recognizer來處置事件,我們跟進resolve方法看一下具體操作,
void resolve(GestureDisposition disposition) {
final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
_entries.clear();
for (GestureArenaEntry entry in localEntries)
entry.resolve(disposition);
}
void resolve(GestureDisposition disposition) {
_arena._resolve(_pointer, _member, disposition);
}
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
final _GestureArena state = _arenas[pointer];
if (state == null)
return;
assert(state.members.contains(member));
if (disposition == GestureDisposition.rejected) {
state.members.remove(member);//移除一個Recognizer
member.rejectGesture(pointer);//調(diào)用其rejectGesture方法
if (!state.isOpen)
_tryToResolveArena(pointer, state);//嘗試確定勝利者
} else {
if (state.isOpen) {
state.eagerWinner ??= member;
} else {
_resolveInFavorOf(pointer, state, member);//確定勝利者
}
}
}
可以看到如果是接受,就直接確認勝利者,如果是拒絕,就將其踢出并嘗試確認勝利者。再看一下stopTrackingPointer的具體操作
void stopTrackingPointer(int pointer) {
if (_trackedPointers.contains(pointer)) {
GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);//移除路由,不處理余下事件
_trackedPointers.remove(pointer);//從_trackedPointers中移除
if (_trackedPointers.isEmpty)
didStopTrackingLastPointer(pointer);
}
}
主要就是移除路由,didStopTrackingLastPointer是在沒有追蹤的PointEvent時,做一些收尾工作,具體都有不同實現(xiàn)。