前言
接下來我們想詳細的說一說Flutter是如何處理手勢事件的。本文将通過源碼詳細分析Flutter的事件分發與沖突處理過程,并通過示例說明不同沖突的處理方式。本文的組織架構如下:
- 手勢事件的初始化
- 命中測試
- PointerEvent的封裝
- hitTest()
- dispatchEvent()
- GestureDetector
- onTap
- onLongPress
- onDoubleTap
- onVerticalDragDown
- 手勢事件攔截
- 總結
手勢事件的初始化
還是先回到我們熟悉的runApp():
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
前面的文章介紹過WidgetsFlutterBinding混合了很多mixin:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding
手勢相關的是GestureBinding,我們來看看它的initInstances()初始化的實作:
@override
void initInstances() {
super.initInstances();
_instance = this;
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
為platformDispatcher注冊了onPointerDataPacket回調,其實作是_handlePointerDataPacket()。我們先追蹤一下onPointerDataPacket的回調時機:
PointerDataPacketCallback? get onPointerDataPacket => _onPointerDataPacket;
PointerDataPacketCallback? _onPointerDataPacket;
Zone _onPointerDataPacketZone = Zone.root;
set onPointerDataPacket(PointerDataPacketCallback? callback) {
_onPointerDataPacket = callback;
_onPointerDataPacketZone = Zone.current;
}
繼續追蹤onPointerDataPacket的調用時機:
// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) {
if (onPointerDataPacket != null) {
_invoke1<PointerDataPacket>(
onPointerDataPacket,
_onPointerDataPacketZone,
_unpackPointerDataPacket(packet),
);
}
}
void _invoke1<A>(void Function(A a)? callback, Zone zone, A arg) {
if (callback == null) {
return;
}
assert(zone != null);
if (identical(zone, Zone.current)) {
callback(arg);
} else {
zone.runUnaryGuarded<A>(callback, arg);
}
}
@pragma('vm:entry-point')
void _dispatchPointerDataPacket(ByteData packet) {
PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
從注釋上我們就可得知,_dispatchPointerDataPacket()就是接收從framework層傳遞的手勢事件,并進行處理的時機。_invoke1()其實就是調用onPointerDataPacket并将_unpackPointerDataPacket()的傳回值回調。其中packet是一組未經處理的的ByteData,通過_unpackPointerDataPacket()方法對其進行解包處理,生成手勢事件所需的實體類PointerData:
static PointerDataPacket _unpackPointerDataPacket(ByteData packet) {
const int kStride = Int64List.bytesPerElement;
const int kBytesPerPointerData = _kPointerDataFieldCount * kStride;
final int length = packet.lengthInBytes ~/ kBytesPerPointerData;
assert(length * kBytesPerPointerData == packet.lengthInBytes);
final List<PointerData> data = <PointerData>[];
for (int i = 0; i < length; ++i) {
int offset = i * _kPointerDataFieldCount;
data.add(PointerData(
embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian),
timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)),
//...
scale: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
rotation: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
));
assert(offset == (i + 1) * _kPointerDataFieldCount);
}
return PointerDataPacket(data: data);
}
好了到此為止我們可以得知,在啟動app後,由GestureBinding注冊手勢的回調事件,當engine層發送來手勢事件後,由PlatformDispatcher封裝成PointerData實體,最終回調至GestureBinding進行處理,接下來我們看接收到手勢事件後的處理流程。
命中測試
PointerEvent的封裝
在真正處理手勢之前,第一步就是将手勢事件封裝成業務可用的PointerEvent,我們回到GestureBinding的initInstances():
@override
void initInstances() {
super.initInstances();
_instance = this;
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
跟蹤_handlePointerDataPacket的實作:
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked) {
_flushPointerEventQueue();
}
}
首先就是把解包後的packet.data通過PointerEventConverter.expand()再做次轉換:
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) {
return data
.where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
.map((ui.PointerData datum) {
//...
switch (datum.signalKind ?? ui.PointerSignalKind.none) {
case ui.PointerSignalKind.none:
switch (datum.change) {
//...
case ui.PointerChange.down:
return PointerDownEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
buttons: _synthesiseDownButtons(datum.buttons, kind),
obscured: datum.obscured,
pressure: datum.pressure,
//...
);
case ui.PointerChange.move:
return PointerMoveEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
delta: delta,
buttons: _synthesiseDownButtons(datum.buttons, kind),
obscured: datum.obscured,
//...
);
//...
case ui.PointerSignalKind.unknown:
default: // ignore: no_default_cases, to allow adding a new [PointerSignalKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/34402
// This branch should already have 'unknown' filtered out, but
// we don't want to return anything or miss if someone adds a new
// enumeration to PointerSignalKind.
throw StateError('Unreachable');
}
});
}
截取了部分代碼,大緻就是将packet.data轉換成PointerEvent,并添加到_pendingPointerEvents清單中。接下來我們看_flushPointerEventQueue()的實作:
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty) {
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
}
void handlePointerEvent(PointerEvent event) {
assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingClock);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
_handlePointerEventImmediately(event);
}
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
_hitTests[event.pointer] = hitTestResult;
}
//...
} else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down || event is PointerPanZoomUpdateEvent) {
hitTestResult = _hitTests[event.pointer];
}
//...
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
這段代碼的整體思路是:
- 如果event是PointerDownEvent等四種event之一時(我們假設建立的是一個移動端app,這四種event隻考慮PointerDownEvent的情況),建立一個HitTestResult對象,并調用hitTest()方法,然後将hitTestResult指派給_hitTests[event.pointer];
- 如果是PointerUpEvent或PointerCancelEvent,那麼将此hitTestResult從_hitTests中移除并傳回給hitTestResult對象;
- 最後執行dispatchEvent(event, hitTestResult);
說完PointerEvent的封裝後,Flutter是如何處理這些手勢事件的呢?如何确定哪些Widget響應手勢事件,并确認它們的優先級呢?接下來我們講一下hitTest()的實作,即命中測試。
hitTest()
hitTest()需要确定出都哪些widget對應的RenderObject可能會響應手勢事件,并給他們設定命中響應的優先級。這個步驟是非常重要的,可以确定最終響應手勢事件的Widget。在此我們先說個結論,子優先于父響應手勢事件。我們先來看看HitTestResult的結構:
HitTestResult()
: _path = <HitTestEntry>[],
_transforms = <Matrix4>[Matrix4.identity()],
_localTransforms = <_TransformPart>[];
HitTestEntry(this.target);
其中_path是個HitTestEntry數組,HitTestResult每執行一次add()方法就會添加一個HitTestEntry對象到_path中,HitTestResult的target對象通常就是一個RenderObject對象,也就是說_path是用來記錄手勢命中的RenderObject對象數組。_transforms是記錄目标RenderObject對象相對于Global坐标系的位置。_localTransforms是記錄目标RenderObject對象相對于Parent的位置。我們繼續看hitTest(hitTestResult, event.position);的實作:
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
建立一個HitTestEntry對象并添加到HitTestResult中。由于mixin,RendererBinding是GesturesBinding的子類,而RendererBinding實作了hitTest()方法,是以我們看看RendererBinding的hitTest()的實作:
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
assert(result != null);
assert(position != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
在調用super.hitTest()之前,先調用了renderView的hitTest()方法,renderView我們很了解了,就是App的根RenderObject:
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null) {
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
result.add(HitTestEntry(this));
return true;
}
首先判斷是否有child,如果是的話需要先執行child的hitTest()方法,再将自己封裝成一個HitTestEntry添加到HitTestResult中。我們來看看child!.hitTest()方法的實作:
bool hitTest(BoxHitTestResult result, { required Offset position }) {
//...
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
如果手勢的position在目前RenderObject的_size範圍裡,判斷hitTestChildren()或hitTestSelf()是不是傳回true,如果是的話将自己封裝成BoxHitTestEntry添加到HitTestResult中。也就是說隻要子或自己命中手勢事件,就添加到HitTestResult。在這裡要注意的是,會優先判斷hitTestChildren(),這個方法是判斷子是否有命中手勢,如果子命中了就不會再走hitTestSelf()的判斷,進而子的優先級較高,會被優先加入到HitTestResult的_path隊列中。我們看看hitTestChildren()的實作:
@protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
hitTestChildren()是個抽象方法,由子類實作。我們舉個例子,假設目前Widget是個Align,它所對應的RenderObject對象其實是個RenderShiftedBox,它的hitTestChildren()實作如下:
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
if (child != null) {
final BoxParentData childParentData = child!.parentData! as BoxParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed);
},
);
}
return false;
}
bool addWithPaintOffset({
required Offset? offset,
required Offset position,
required BoxHitTest hitTest,
}) {
assert(position != null);
assert(hitTest != null);
final Offset transformedPosition = offset == null ? position : position - offset;
if (offset != null) {
pushOffset(-offset);
}
final bool isHit = hitTest(this, transformedPosition);
if (offset != null) {
popTransform();
}
return isHit;
}
其中position是PointerEvent的position,childParentData記錄了子相對于父的偏移量。執行result.addWithPaintOffset()方法,其中hitTest是個callback,執行的是child!.hitTest(result, position: transformed)。也就是說hitTestChildren()方法是周遊整個RenderObject樹,遞歸執行 child!.hitTest()方法,去判斷子是否有命中手勢事件。如果已經沒有子或者子沒有命中的話,才會判斷自己是否命中,我們回過頭來看看hitTestSelf()的實作:
@protected
bool hitTestSelf(Offset position) => false;
它也是個抽象方法,假設我們點選的是個Image,它所對應的RenderObject是RenderImage,其hitTestSelf()的實作如下:
@override
bool hitTestSelf(Offset position) => true;
隻要手勢事件的position在RenderImage的_size範圍内,就命中手勢事件。到此為止,hitTest的過程我們就梳理完成了。總結一下,hitTest實際上就是判斷目前Widget對應的RenderObject是否命中手勢事件。
- 在移動端隻有PointerDownEvent事件會進行命中測試;
- 周遊整個RenderObject樹,優先對子進行命中測試,若子命中即先添加進HitTestResult中;
- 若子沒有命中,則判斷自己是否命中;
- 隻要子或者自己命中,都會加到HitTestResult中。
我們将hitTest()的處理流程總結成流程圖如下:
在命中測試完成得到HitTestResult後,Flutter就可以進行事件分發了,分發給HitTestResult的每一個成員進行處理,具體的處理流程是怎樣的呢?怎麼處理沖突的呢?我們繼續分析dispatchEvent()的實作。
dispatchEvent()
dispatchEvent()對手勢事件做分發,我們回到之前的代碼_handlePointerEventImmediately():
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
_hitTests[event.pointer] = hitTestResult;
}
//...
} else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down || event is PointerPanZoomUpdateEvent) {
hitTestResult = _hitTests[event.pointer];
}
//...
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
當hitTestResult不為空,即有命中的情況下,則執行dispatchEvent()方法,其實作如下:
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
//...
}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
//...
}
}
}
周遊HitTestResult.path,執行entry.target.handleEvent()方法,target就是每個命中的RenderObject對象。到這裡我們可以清晰的看到,先進入HitTestResult.path隊列裡的target優先執行handleEvent(),上一章節我們提到子優先于父被加入HitTestResult.path,是以子也優先于父實作handleEvent()。
/// Override this method to receive events.
void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry);
handleEvent()是HitTestTarget這個抽象類的方法,需要其子類去實作,這裡要說的是RenderObject實作了HitTestTarget,也就是說是由RenderObject去實作handleEvent()。另外GestureBinding也是個HitTestTarget,我們看看GestureBinding的實作:
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
首先會執行 pointerRouter.route(event),這個方法的作用是将手勢事件首先分發給各個PointerRoute去處理:
void route(PointerEvent event) {
final Map<PointerRoute, Matrix4?>? routes = _routeMap[event.pointer];
final Map<PointerRoute, Matrix4?> copiedGlobalRoutes = Map<PointerRoute, Matrix4?>.of(_globalRoutes);
if (routes != null) {
_dispatchEventToRoutes(
event,
routes,
Map<PointerRoute, Matrix4?>.of(routes),
);
}
_dispatchEventToRoutes(event, _globalRoutes, copiedGlobalRoutes);
}
_routeMap是什麼呢?我們舉個例子:
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MaterialApp(
home: Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
print("onTap");
},
onLongPress: () {
print("onLongPress");
},
child: const Text("GestureDetector test"),
),
),
),
);
}
使用GestureDetector監聽onTap和onLongPress事件。代碼運作後我們都知道結果:單擊Text會列印onTap的log,長按Text會列印onLongPress的log。具體的原因之後我們再詳細分析,先說個結論:onTap和onLongPress将會注冊兩個PointerRoute存到_routeMap中。PointerRoute實際上是個callback。我們回到route()方法,看_dispatchEventToRoutes()的實作:
void _dispatchEventToRoutes(
PointerEvent event,
Map<PointerRoute, Matrix4?> referenceRoutes,
Map<PointerRoute, Matrix4?> copiedRoutes,
) {
copiedRoutes.forEach((PointerRoute route, Matrix4? transform) {
if (referenceRoutes.containsKey(route)) {
_dispatch(event, route, transform);
}
});
}
周遊copiedRoutes執行_dispatch()方法:
@pragma('vm:notify-debugger-on-exception')
void _dispatch(PointerEvent event, PointerRoute route, Matrix4? transform) {
try {
event = event.transformed(transform);
route(event);
} catch (exception, stack) {
//...
}
}
執行route(event)。具體的實作之後的章節再做說明。我們回到handleEvent()方法:
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
如果是PointerDownEvent,則執行gestureArena.close(event.pointer),如果是PointerUpEvent,則執行gestureArena.sweep(event.pointer)。這時候我們就有個疑問,gestureArena是什麼?gestureArena是個GestureArenaManager對象,是個手勢競技場管理類,它有個成員變量_arenas:
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
_GestureArena記錄了所有手勢競技成員members:
final List<GestureArenaMember> members = <GestureArenaMember>[];
之前例子中我們提到的單擊和長按其實各自都會被封裝成一個GestureArenaMember,而GestureArenaManager的作用就是判定競技場中GestureArenaMember的勝負,即最終響應的是什麼事件。我們再回到handleEvent()方法分析gestureArena.close(event.pointer)的實作:
void close(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
state.isOpen = false;
assert(_debugLogDiagnostic(pointer, 'Closing', state));
_tryToResolveArena(pointer, state);
}
在PointerDownEvent的時候,會分發手勢事件給所有的PointerRoute,待PointerRoute都注冊完成後,關閉手勢競技場,調用_tryToResolveArena(pointer, state):
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
} else if (state.eagerWinner != null) {
assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
_resolveInFavorOf(pointer, state, state.eagerWinner!);
}
}
這個方法的作用是确定競技場裡的哪個GestureArenaMember勝利。比方說我們之前的例子如果隻注冊了onTap事件而沒有注冊onLongPress,那麼state.members.length == 1為true,則調用_resolveByDefault(pointer, state):
void _resolveByDefault(int pointer, _GestureArena state) {
if (!_arenas.containsKey(pointer)) {
return; // This arena has already resolved.
}
assert(_arenas[pointer] == state);
assert(!state.isOpen);
final List<GestureArenaMember> members = state.members;
assert(members.length == 1);
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
}
_arenas裡移除目前pointer,競技場中唯一的成員調用acceptGesture(pointer)宣布勝利。acceptGesture(pointer)是個抽象方法,之後我們再舉例說明它的實作。如果注冊了多個PointerRoute且state.eagerWinner != null說明競技場裡有的member優先級高,會直接宣告勝利。我們看一下_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);
for (final GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member) {
rejectedMember.rejectGesture(pointer);
}
}
member.acceptGesture(pointer);
}
宣告其它member失敗,且宣告eagerWinner勝利。分析完gestureArena.close(event.pointer),我們再回到handleEvent(),在PointerUpEvent時會執行gestureArena.sweep(event.pointer):
void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
assert(!state.isOpen);
if (state.isHeld) {
state.hasPendingSweep = true;
assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
return; // This arena is being held for a long-lived member.
}
assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
_arenas.remove(pointer);
if (state.members.isNotEmpty) {
// First member wins.
assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
// Give all the other members the bad news.
for (int i = 1; i < state.members.length; i++) {
state.members[i].rejectGesture(pointer);
}
}
}
它的作用是清掃手勢競技場。如果state.isHeld為true,說明競技場裡有的成員生命周期比較長,需要等待,是以先不做處理。比方說上面的例子中onDoubleTap事件就會設定state.isHeld為true。否則如将會宣告競技場中第一個加入的成員勝利,其他成員失敗。到此為止,GestureBinding中的dispatchEvent()就分析完畢了,我們總結一下:
- 先周遊HitTestEntry,執行entry.target.handleEvent()方法,順序是子優先;
- 針對每一個target,執行PointerRoute.route(event),這個方法的作用是将手勢事件首先分發給PointerRouter裡_routeMap的各個成員去處理;
- 在PointerDownEvent的時候關閉手勢競技場,并根據條件判定目前就可決定的勝利成員;
- 在PointerUpEvent時清掃手勢競技場,并最終判定勝利的成員。
我們将dispatchEvent()的處理流程總結成流程圖如下:
到此為止,我們知道了手勢事件是怎麼分發和解決沖突的。接下來我們通過分析GestureDetector來看一下細節的實作。
GestureDetector
我們舉個例子:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'BuildContext Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MaterialApp(
home: Scaffold(
body: Center(
child: GestureDetector(
onTap: () {
print("onTap");
},
onLongPress: () {
print("onLongPress");
},
onDoubleTap: () {
print("onDoubleTap");
},
onVerticalDragDown: (details) {
print("onVerticalDragDown");
},
child: Container(
alignment: Alignment.center,
height: 500,
color: Colors.amber,
child: const Text("GestureDetector test"),
),
),
),
),
);
}
以上代碼的執行結果我們非常清楚:
- 單擊列印onTap
- 長按列印onLongPress
- 輕按兩下列印onDoubleTap
- 豎向拖拽列印onVerticalDragDown
以上我們可已知onTap會分别和其他三個事件沖突。假設我們先隻監聽onTap事件,看看GestureDetector的核心源碼實作:
onTap
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
if (onTapDown != null ||
onTapUp != null ||
onTap != null ||
onTapCancel != null ||
onSecondaryTap != null ||
onSecondaryTapDown != null ||
onSecondaryTapUp != null ||
onSecondaryTapCancel != null||
onTertiaryTapDown != null ||
onTertiaryTapUp != null ||
onTertiaryTapCancel != null
) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel
..onSecondaryTap = onSecondaryTap
..onSecondaryTapDown = onSecondaryTapDown
..onSecondaryTapUp = onSecondaryTapUp
..onSecondaryTapCancel = onSecondaryTapCancel
..onTertiaryTapDown = onTertiaryTapDown
..onTertiaryTapUp = onTertiaryTapUp
..onTertiaryTapCancel = onTertiaryTapCancel
..gestureSettings = gestureSettings;
},
);
}
//...
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
建立一個TapGestureRecognizer對象,并存到gestures這個Map中,TapGestureRecognizer的繼承關系如下:
class TapGestureRecognizer extends BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer extends GestureRecognizer extends GestureArenaMember
我們可以看到其實它就是一個GestureArenaMember競技場成員對象。然後傳回一個RawGestureDetector的Widget。RawGestureDetector是個StatefulWidget,我們看看它對應的RawGestureDetectorState中initState()的實作:
@override
void initState() {
super.initState();
_semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
_syncAll(widget.gestures);
}
void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
assert(_recognizers != null);
final Map<Type, GestureRecognizer> oldRecognizers = _recognizers!;
_recognizers = <Type, GestureRecognizer>{};
for (final Type type in gestures.keys) {
assert(gestures[type] != null);
assert(gestures[type]!._debugAssertTypeMatches(type));
assert(!_recognizers!.containsKey(type));
_recognizers![type] = oldRecognizers[type] ?? gestures[type]!.constructor();
assert(_recognizers![type].runtimeType == type, 'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers![type].runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
gestures[type]!.initializer(_recognizers![type]!);
}
for (final Type type in oldRecognizers.keys) {
if (!_recognizers!.containsKey(type)) {
oldRecognizers[type]!.dispose();
}
}
}
_syncAll()方法的作用是将傳入的gestures給_recognizers對象指派。再看看build()的實作:
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
onPointerPanZoomStart: _handlePointerPanZoomStart,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
if (!widget.excludeFromSemantics) {
result = _GestureSemantics(
behavior: widget.behavior ?? _defaultBehavior,
assignSemantics: _updateSemanticsForRenderObject,
child: result,
);
}
return result;
}
傳回一個Listener。這個Listener我們在實踐中也非常熟悉。監聽并識别所有最底層的基礎手勢事件,但不做處理。它是個SingleChildRenderObjectWidget,我們看看其createRenderObject的實作:
@override
RenderPointerListener createRenderObject(BuildContext context) {
return RenderPointerListener(
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
onPointerHover: onPointerHover,
onPointerCancel: onPointerCancel,
onPointerPanZoomStart: onPointerPanZoomStart,
onPointerPanZoomUpdate: onPointerPanZoomUpdate,
onPointerPanZoomEnd: onPointerPanZoomEnd,
onPointerSignal: onPointerSignal,
behavior: behavior,
);
}
在之前的章節我們提到過,在dispatchEvent()時會周遊HitTestEntry,執行entry.target.handleEvent()方法,target就是每個命中的RenderObject對象。是以我們看看RenderPointerListener裡handleEvent()的實作:
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent) {
return onPointerDown?.call(event);
}
if (event is PointerMoveEvent) {
return onPointerMove?.call(event);
}
if (event is PointerUpEvent) {
return onPointerUp?.call(event);
}
if (event is PointerHoverEvent) {
return onPointerHover?.call(event);
}
if (event is PointerCancelEvent) {
return onPointerCancel?.call(event);
}
if (event is PointerPanZoomStartEvent) {
return onPointerPanZoomStart?.call(event);
}
if (event is PointerPanZoomUpdateEvent) {
return onPointerPanZoomUpdate?.call(event);
}
if (event is PointerPanZoomEndEvent) {
return onPointerPanZoomEnd?.call(event);
}
if (event is PointerSignalEvent) {
return onPointerSignal?.call(event);
}
}
它非常的簡單,就是将基礎的手勢事件回調回去。我們回到RawGestureDetectorState的build():
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
onPointerPanZoomStart: _handlePointerPanZoomStart,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
if (!widget.excludeFromSemantics) {
result = _GestureSemantics(
behavior: widget.behavior ?? _defaultBehavior,
assignSemantics: _updateSemanticsForRenderObject,
child: result,
);
}
return result;
}
在我們的示例中其實隻監聽了onPointerDown事件,其實作_handlePointerDown代碼如下:
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values) {
recognizer.addPointer(event);
}
}
周遊_recognizers,執行recognizer.addPointer(event):
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
現在的場景中isPointerAllowed()一定為true,執行 addAllowedPointer(event):
@protected
void addAllowedPointer(PointerDownEvent event) { }
它是個抽象方法,我們看看子類BaseTapGestureRecognizer的實作:
@override
void addAllowedPointer(PointerDownEvent event) {
assert(event != null);
if (state == GestureRecognizerState.ready) {
if (_down != null && _up != null) {
assert(_down!.pointer == _up!.pointer);
_reset();
}
_down = event;
}
if (_down != null) {
super.addAllowedPointer(event);
}
}
實際上調的就是super的方法,即PrimaryPointerGestureRecognizer的addAllowedPointer(event):
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
if (state == GestureRecognizerState.ready) {
_state = GestureRecognizerState.possible;
_primaryPointer = event.pointer;
_initialPosition = OffsetPair(local: event.localPosition, global: event.position);
if (deadline != null) {
_timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
}
}
}
繼續調super,即OneSequenceGestureRecognizer的addAllowedPointer(event):
@override
@protected
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer, event.transform);
}
繼續追蹤startTrackingPointer()的實作:
@protected
void startTrackingPointer(int pointer, [Matrix4? transform]) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
首先就是調用PointerRouter的addRoute()方法,這個是不是很熟悉?
void addRoute(int pointer, PointerRoute route, [Matrix4? transform]) {
final Map<PointerRoute, Matrix4?> routes = _routeMap.putIfAbsent(
pointer,
() => <PointerRoute, Matrix4?>{},
);
assert(!routes.containsKey(route));
routes[route] = transform;
}
它的實作就是往我們之前章節提到的_routeMap添加成員,等待分發手勢事件後的回調。然後調用_addPointerToArena()。這個方法從命名中就可得知,它是往手勢競技場中添加成員:
GestureArenaEntry _addPointerToArena(int pointer) {
if (_team != null) {
return _team!.add(pointer, this);
}
return GestureBinding.instance.gestureArena.add(pointer, this);
}
GestureArenaEntry add(int pointer, GestureArenaMember member) {
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return _GestureArena();
});
state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
return GestureArenaEntry._(this, pointer, member);
}
好的現在在接收到PointerDownEvent時,onTap相應的GestureArenaMember已經進入手勢競技場了,收到任何手勢事件都會執行之前章節分析過的:針對每一個target,執行PointerRoute.route(event),這個方法的作用是将手勢事件首先分發給PointerRouter裡_routeMap的各個成員去處理。其實作在各個GestureArenaMember的handleEvent()中,我們來看PrimaryPointerGestureRecognizer對handleEvent()的實作:
@override
void handleEvent(PointerEvent event) {
assert(state != GestureRecognizerState.ready);
if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
final bool isPreAcceptSlopPastTolerance =
!_gestureAccepted &&
preAcceptSlopTolerance != null &&
_getGlobalDistance(event) > preAcceptSlopTolerance!;
final bool isPostAcceptSlopPastTolerance =
_gestureAccepted &&
postAcceptSlopTolerance != null &&
_getGlobalDistance(event) > postAcceptSlopTolerance!;
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
} else {
handlePrimaryPointer(event);
}
}
stopTrackingIfPointerNoLongerDown(event);
}
這段代碼實際上是先計算一下,如果接收到的是PointerMoveEvent事件時,移動距離是不是足夠小,如果是的話,即可以繼續處理onTap事件,會調用 handlePrimaryPointer(event):
@protected
void handlePrimaryPointer(PointerEvent event);
其實作在BaseTapGestureRecognizer:
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_up = event;
_checkUp();
} else if (event is PointerCancelEvent) {
resolve(GestureDisposition.rejected);
if (_sentTapDown) {
_checkCancel(event, '');
}
_reset();
} else if (event.buttons != _down!.buttons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
}
}
由于目前我們還在處理PointerDownEvent事件,是以不會做任何事情。到此為止,接收到PointerDownEvent事件時,最重要的事情就是将TapGestureRecognizer添加到手勢競技場等待。在隻監聽onTap事件的時候,會判斷接收到PointerMoveEvent時的移動距離,如果足夠小的話會繼續等待。接下來,我們手勢擡起,接收PointerUpEvent,我們看看會發生什麼。由于PrimaryPointerGestureRecognizer的handleEvent()對PointerUpEvent并沒有做什麼,我們回到GestureBinding的handleEvent():
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
之前章節分析過,PointerUpEvent是會走gestureArena.sweep(event.pointer)清掃手勢競技場:
void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
assert(!state.isOpen);
if (state.isHeld) {
state.hasPendingSweep = true;
assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
return; // This arena is being held for a long-lived member.
}
assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
_arenas.remove(pointer);
if (state.members.isNotEmpty) {
// First member wins.
assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
state.members.first.acceptGesture(pointer);
// Give all the other members the bad news.
for (int i = 1; i < state.members.length; i++) {
state.members[i].rejectGesture(pointer);
}
}
}
由于目前競技場中隻有TapGestureRecognizer這一個成員,會執行到 state.members.first.acceptGesture(pointer)宣告勝利。acceptGesture()的實作在BaseTapGestureRecognizer中:
//BaseTapGestureRecognizer
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
//PrimaryPointerGestureRecognizer
@override
void acceptGesture(int pointer) {
if (pointer == primaryPointer) {
_stopTimer();
_gestureAccepted = true;
}
}
先調用super的acceptGesture()方法,停止計時器。然後調用_checkDown()和_checkUp(),給業務層相應的回調去處理。同時設定 _wonArenaForPrimaryPointer為true标記勝利。是以其實在單擊事件中,down和up事件是在手勢擡起收到PointerUpEvent處理完成後才回調給使用者進行處理的。到此為止,我們使用GestureDetector監聽onTap事件的流程就分析完了。那麼現在我們增加針對onLongPress的監聽,看看手勢競技場是怎麼處理的。
onLongPress
我們回到GestureDetector的build()方法:
if (onLongPressDown != null ||
onLongPressCancel != null ||
onLongPress != null ||
onLongPressStart != null ||
onLongPressMoveUpdate != null ||
onLongPressUp != null ||
onLongPressEnd != null ||
onSecondaryLongPressDown != null ||
onSecondaryLongPressCancel != null ||
onSecondaryLongPress != null ||
onSecondaryLongPressStart != null ||
onSecondaryLongPressMoveUpdate != null ||
onSecondaryLongPressUp != null ||
onSecondaryLongPressEnd != null ||
onTertiaryLongPressDown != null ||
onTertiaryLongPressCancel != null ||
onTertiaryLongPress != null ||
onTertiaryLongPressStart != null ||
onTertiaryLongPressMoveUpdate != null ||
onTertiaryLongPressUp != null ||
onTertiaryLongPressEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressCancel = onLongPressCancel
..onLongPress = onLongPress
..onLongPressStart = onLongPressStart
..onLongPressMoveUpdate = onLongPressMoveUpdate
..onLongPressUp = onLongPressUp
..onLongPressEnd = onLongPressEnd
..onSecondaryLongPressDown = onSecondaryLongPressDown
..onSecondaryLongPressCancel = onSecondaryLongPressCancel
..onSecondaryLongPress = onSecondaryLongPress
..onSecondaryLongPressStart = onSecondaryLongPressStart
..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
..onSecondaryLongPressUp = onSecondaryLongPressUp
..onSecondaryLongPressEnd = onSecondaryLongPressEnd
..onTertiaryLongPressDown = onTertiaryLongPressDown
..onTertiaryLongPressCancel = onTertiaryLongPressCancel
..onTertiaryLongPress = onTertiaryLongPress
..onTertiaryLongPressStart = onTertiaryLongPressStart
..onTertiaryLongPressMoveUpdate = onTertiaryLongPressMoveUpdate
..onTertiaryLongPressUp = onTertiaryLongPressUp
..onTertiaryLongPressEnd = onTertiaryLongPressEnd
..gestureSettings = gestureSettings;
},
);
}
在長按時會建立一個LongPressGestureRecognizer。我們看看LongPressGestureRecognizer的構造方法:
LongPressGestureRecognizer({
Duration? duration,
// TODO(goderbauer): remove ignore when https://github.com/dart-lang/linter/issues/3349 is fixed.
// ignore: avoid_init_to_null
super.postAcceptSlopTolerance = null,
@Deprecated(
'Migrate to supportedDevices. '
'This feature was deprecated after v2.3.0-1.0.pre.',
)
super.kind,
super.supportedDevices,
super.debugOwner,
}) : super(
deadline: duration ?? kLongPressTimeout,
);
const Duration kLongPressTimeout = Duration(milliseconds: 500);
super裡傳給父類PrimaryPointerGestureRecognizer的deadline預設是500ms,這個是觸發長按事件的時長。我們回憶一下之前章節提到的,在GestureDetector走build()的時候,建立了一個Listener元件,監聽其onPointerDown方法,onPointerDown的實作是_handlePointerDown(),其代碼如下:
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values) {
recognizer.addPointer(event);
}
}
周遊_recognizers,執行recognizer.addPointer(event):
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
由于LongPressGestureRecognizer沒有實作addAllowedPointer()方法,我們看看addAllowedPointer()在PrimaryPointerGestureRecognizer的實作:
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
if (state == GestureRecognizerState.ready) {
_state = GestureRecognizerState.possible;
_primaryPointer = event.pointer;
_initialPosition = OffsetPair(local: event.localPosition, global: event.position);
if (deadline != null) {
_timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
}
}
}
這裡我們可以看到,如果deadline不為空的話,啟動一個500ms的Timer,到時間後調用 didExceedDeadlineWithEvent(event)方法:
@override
void didExceedDeadline() {
// Exceeding the deadline puts the gesture in the accepted state.
resolve(GestureDisposition.accepted);
_longPressAccepted = true;
super.acceptGesture(primaryPointer!);
_checkLongPressStart();
}
會在此時調用acceptGesture()宣告LongPressGestureRecognizer勝利,并回調onLongPress給業務層進行處理。而不會等待PointerUpEvent再去判定勝利者。給業務層的體驗就是,長按時,不需手勢擡起,就能收到onLongPress回調。好了onLongPress是怎麼處理的我們也分析完了,我們繼續分析示例中的onDoubleTap輕按兩下事件的處理。
onDoubleTap
我們還是回到GestureDetector的build()方法:
if (onDoubleTap != null) {
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap
..onDoubleTapCancel = onDoubleTapCancel
..gestureSettings = gestureSettings;
},
);
}
建立了DoubleTapGestureRecognizer。關鍵處理在它的addAllowedPointer()方法裡:
@override
void addAllowedPointer(PointerDownEvent event) {
if (_firstTap != null) {
if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
// Restart when the second tap is too close to the first (touch screens
// often detect touches intermittently), or when buttons mismatch.
_reset();
return _trackTap(event);
} else if (onDoubleTapDown != null) {
final TapDownDetails details = TapDownDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: getKindForPointer(event.pointer),
);
invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
}
}
_trackTap(event);
}
一開始_firstTap一定是空,直接執行 _trackTap(event)方法:
void _trackTap(PointerDownEvent event) {
_stopDoubleTapTimer();
final _TapTracker tracker = _TapTracker(
event: event,
entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
doubleTapMinTime: kDoubleTapMinTime,
gestureSettings: gestureSettings,
);
_trackers[event.pointer] = tracker;
tracker.startTrackingPointer(_handleEvent, event.transform);
}
建立一個_TapTracker對象,向手勢競技場注冊自己然後調用tracker.startTrackingPointer(_handleEvent, event.transform)。
void startTrackingPointer(PointerRoute route, Matrix4? transform) {
if (!_isTrackingPointer) {
_isTrackingPointer = true;
GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
}
}
将route添加到_routeMap中,等待手勢觸發回調。我們繼續按看看_handleEvent ()的實作:
void _handleEvent(PointerEvent event) {
final _TapTracker tracker = _trackers[event.pointer]!;
if (event is PointerUpEvent) {
if (_firstTap == null) {
_registerFirstTap(tracker);
} else {
_registerSecondTap(tracker);
}
} else if (event is PointerMoveEvent) {
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
_reject(tracker);
}
} else if (event is PointerCancelEvent) {
_reject(tracker);
}
}
主要關注PointerUpEvent事件的接收,因為輕按兩下事件就是判斷兩次PointerUpEvent事件是否符合輕按兩下的條件,我們先來看首次判斷時_firstTap == null,調用_registerFirstTap(tracker)的情況:
void _registerFirstTap(_TapTracker tracker) {
_startDoubleTapTimer();
GestureBinding.instance.gestureArena.hold(tracker.pointer);
// Note, order is important below in order for the clear -> reject logic to
// work properly.
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
_clearTrackers();
_firstTap = tracker;
}
先調用_startDoubleTapTimer(),啟動輕按兩下的事件Timer,預設是300ms。然後調用了GestureBinding.instance.gestureArena.hold(tracker.pointer):
void hold(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
state.isHeld = true;
assert(_debugLogDiagnostic(pointer, 'Holding', state));
}
會将state.isHeld設定為true。然後我們回到GestureBinding的handleEvent():
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
在接收到PointerUpEvent時執行gestureArena.sweep(event.pointer):
void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
assert(!state.isOpen);
if (state.isHeld) {
state.hasPendingSweep = true;
assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
return; // This arena is being held for a long-lived member.
}
//...
}
如果state.isHeld為true,說明有事件在等待判定,不可以在此時宣布誰勝利,直接return。當收到第二個點選事件時,回到addAllowedPointer():
@override
void addAllowedPointer(PointerDownEvent event) {
if (_firstTap != null) {
if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
// Ignore out-of-bounds second taps.
return;
} else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
// Restart when the second tap is too close to the first (touch screens
// often detect touches intermittently), or when buttons mismatch.
_reset();
return _trackTap(event);
} else if (onDoubleTapDown != null) {
final TapDownDetails details = TapDownDetails(
globalPosition: event.position,
localPosition: event.localPosition,
kind: getKindForPointer(event.pointer),
);
invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
}
}
_trackTap(event);
}
如果兩次點選時間小于輕按兩下門檻值的話,重新執行 _trackTap(event)。我們回到DoubleTapGestureRecognizer的_handleEvent ():
void _handleEvent(PointerEvent event) {
final _TapTracker tracker = _trackers[event.pointer]!;
if (event is PointerUpEvent) {
if (_firstTap == null) {
_registerFirstTap(tracker);
} else {
_registerSecondTap(tracker);
}
} else if (event is PointerMoveEvent) {
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
_reject(tracker);
}
} else if (event is PointerCancelEvent) {
_reject(tracker);
}
}
_firstTap不為空的情況,說明接收到第二個點選事件了,追蹤_registerSecondTap()的實作:
void _registerSecondTap(_TapTracker tracker) {
_firstTap!.entry.resolve(GestureDisposition.accepted);
tracker.entry.resolve(GestureDisposition.accepted);
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
_checkUp(tracker.initialButtons);
_reset();
}
兩個手勢事件都執行resolve(GestureDisposition.accepted)方法,這個方法其實就是宣告勝利或失敗的,我們看看它的實作:
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; // This arena has already resolved.
}
assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
assert(state.members.contains(member));
if (disposition == GestureDisposition.rejected) {
//...
} else {
assert(disposition == GestureDisposition.accepted);
if (state.isOpen) {
state.eagerWinner ??= member;
} else {
assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
_resolveInFavorOf(pointer, state, member);
}
}
}
當傳入GestureDisposition.accepted時,會執行_resolveInFavorOf()方法:
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);
for (final GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member) {
rejectedMember.rejectGesture(pointer);
}
}
member.acceptGesture(pointer);
}
在sweep()之前,直接宣告勝利。到此為止,onDoubleTap的流程也分析完成了。之前的示例中,我們還剩下一個onVerticalDragDown沒有分析。下面我們再分析一下onVerticalDragDown。
onVerticalDragDown
重新回到GestureDetector的build()方法:
if (onVerticalDragDown != null ||
onVerticalDragStart != null ||
onVerticalDragUpdate != null ||
onVerticalDragEnd != null ||
onVerticalDragCancel != null) {
gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(debugOwner: this),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = onVerticalDragDown
..onStart = onVerticalDragStart
..onUpdate = onVerticalDragUpdate
..onEnd = onVerticalDragEnd
..onCancel = onVerticalDragCancel
..dragStartBehavior= dragStartBehavior
..gestureSettings = gestureSettings;
},
);
}
建立了VerticalDragGestureRecognizer對象,之後在down事件時會被加入到手勢競技場。VerticalDragGestureRecognizer沒有實作handleEvent(),而是它的父類DragGestureRecognizer實作的:
@override
void handleEvent(PointerEvent event) {
//...
if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) {
final Offset delta = (event is PointerMoveEvent) ? event.delta : (event as PointerPanZoomUpdateEvent).panDelta;
final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
if (_state == _DragState.accepted) {
_checkUpdate(
sourceTimeStamp: event.timeStamp,
delta: _getDeltaForDetails(localDelta),
primaryDelta: _getPrimaryValueFromOffset(localDelta),
globalPosition: position,
localPosition: localPosition,
);
} else {
_pendingDragOffset += OffsetPair(local: localDelta, global: delta);
_lastPendingEventTimestamp = event.timeStamp;
_lastTransform = event.transform;
final Offset movedLocally = _getDeltaForDetails(localDelta);
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: movedLocally,
untransformedEndPosition: localPosition
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
resolve(GestureDisposition.accepted);
}
}
}
if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
_giveUpPointer(event.pointer);
}
}
我們截取了部分重要的代碼,在PointerMoveEvent階段,會一直計算_globalDistanceMoved的值,_hasSufficientGlobalDistanceToAccept()用來判斷移動的距離是否大于固定的門檻值:
@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
}
如果是的話,執行resolve(GestureDisposition.accepted),即在move階段宣告勝利。
手勢事件攔截
我們在開發過程中有時候需要對手勢事件進行攔截,這個是怎麼實作的呢?我們回想一下第三章講解的hitTest(),優先對子進行命中測試,如果命中即由子來實作手勢事件。這麼看來攔截的思路就是不要讓子進行hitTest()。Flutter中的AbsorbPointer就是來幹這件事情的。我們來看一下它的關鍵處理:
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return absorbing
? size.contains(position)
: super.hitTest(result, position: position);
}
重寫hitTest(),如果absorbing為true,那麼直接判斷手勢事件的position是否已經在AbsorbPointer的範圍内,不會再執行super.hitTest(result, position: position)對子來判定hitTest()了。
總結
先上一張圖,簡單的說明一下Flutter處理手勢事件的流程:
Flutter對于手勢事件的處理流程的思路其實非常直覺,在down事件時通過hitTest()确定可能觸發手勢事件的Widget并給它們設定優先級,再調用dispatchEvent對手勢事件進行分發并向競技場注冊成員等待判定。在handleEvent()時會根據條件解決沖突判定手勢的勝利者。本文通過源碼分析對Flutter的手勢事件分發與沖突處理進行了說明,同時通過GestureDetector的示例分析了不同沖突的具體處理方式,以及如何對手勢事件進行攔截,希望對大家了解Flutter的手勢事件有所幫助。
作者:狐友段夢瑤
來源:微信公衆号:搜狐技術産品
出處:https://mp.weixin.qq.com/s/e0kgBMwN-lAxnm9xpoS5vA