對于移動端的開發者來說,手勢是一個非常重要的模塊,基本上做任何App都會遇到各種各樣的手勢問題,而手勢也是移動的一個不算小的模塊吧,要徹底搞得還是得費一些時間的,如果之前對Android或者IOS的手勢或者說點擊事件的原理有所了解的,那么了解其它語言的手勢原理相對來說幫助還是挺大的,
好了,切入正題,在Flutter中,對于Flutter有一定了解的人都知道,可以通過GestureDetector來給不具有點擊事件或者手勢回呼的Widget添加手勢回呼,然后為了點擊水波紋的點擊效果,大多數開發者可能會使用InkWell widget來包裝一個需要添加點擊事件的控制元件,
前戲部分: InkWell 和 GestureDetector的區別
對Flutter有一一些深入了解的人可能知道,InkWell就是對GestureDetector的一個封裝,看圖:

- InkWell是繼承于InkResponse,
- InkResponse是集成于StatelessWidget類,
- 在onBuild中回傳了_InkResponseStateWidget
由于以上這部分代碼沒有什么邏輯,為了減少篇幅我就不貼原始碼了,
_InkResponseStateWidget中的核心代碼如下:
return _ParentInkResponseProvider(
state: this,
child: Actions(
actions: _actionMap,
child: Focus(
focusNode: widget.focusNode,
canRequestFocus: _canRequestFocus,
onFocusChange: _handleFocusUpdate,
autofocus: widget.autofocus,
child: MouseRegion(
cursor: effectiveMouseCursor,
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
child: Semantics(
onTap: widget.excludeFromSemantics || widget.onTap == null ? null : _simulateTap,
onLongPress: widget.excludeFromSemantics || widget.onLongPress == null ? null : _simulateLongPress,
child: GestureDetector(//InkWell手勢的來源
onTapDown: enabled ? _handleTapDown : null,
onTap: enabled ? _handleTap : null,
onTapCancel: enabled ? _handleTapCancel : null,
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? _handleLongPress : null,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: true,
child: widget.child,
),
),
),
),
),
);
通過上述代碼可以看出,GestureDetector是Flutter中手勢的一個最基本類,我們可以直接用,也可以給予GestureDetector來做一些列的自定義封裝
切入正題
一、 GestureDetector簡介
class GestureDetector extends StatelessWidget {
// 省略代碼
}
/// A widget that detects gestures.
///
/// Attempts to recognize gestures that correspond to its non-null callbacks.
///
/// If this widget has a child, it defers to that child for its sizing behavior.
/// If it does not have a child, it grows to fit the parent instead.
///
/// By default a GestureDetector with an invisible child ignores touches;
/// this behavior can be controlled with [behavior].
這個是官方簡介,我理解的大概意思就是說GestureDetector是一個小控制元件,事件的點擊區域會以子控制元件為準,如果子控制元件為不可見或者沒有子控制元件,則會去適應父控制元件,而這個行為可以通過behavior屬性來控制,這塊內容不是今天的重點,我們先看重點吧,
二、GestureDetector功能決議
既然是Widiget,那么核心代碼肯定在onBuild中,我們直接先看看一下原始碼,
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
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;
},
);
}
if (onDoubleTap != null) {
gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(debugOwner: this),
(DoubleTapGestureRecognizer instance) {
instance
..onDoubleTapDown = onDoubleTapDown
..onDoubleTap = onDoubleTap
..onDoubleTapCancel = onDoubleTapCancel;
},
);
}
if (onLongPress != null ||
onLongPressUp != null ||
onLongPressStart != null ||
onLongPressMoveUpdate != null ||
onLongPressEnd != null ||
onSecondaryLongPress != null ||
onSecondaryLongPressUp != null ||
onSecondaryLongPressStart != null ||
onSecondaryLongPressMoveUpdate != null ||
onSecondaryLongPressEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPress = onLongPress
..onLongPressStart = onLongPressStart
..onLongPressMoveUpdate = onLongPressMoveUpdate
..onLongPressEnd = onLongPressEnd
..onLongPressUp = onLongPressUp
..onSecondaryLongPress = onSecondaryLongPress
..onSecondaryLongPressStart = onSecondaryLongPressStart
..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
..onSecondaryLongPressEnd = onSecondaryLongPressEnd
..onSecondaryLongPressUp = onSecondaryLongPressUp;
},
);
}
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;
},
);
}
if (onHorizontalDragDown != null ||
onHorizontalDragStart != null ||
onHorizontalDragUpdate != null ||
onHorizontalDragEnd != null ||
onHorizontalDragCancel != null) {
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this),
(HorizontalDragGestureRecognizer instance) {
instance
..onDown = onHorizontalDragDown
..onStart = onHorizontalDragStart
..onUpdate = onHorizontalDragUpdate
..onEnd = onHorizontalDragEnd
..onCancel = onHorizontalDragCancel
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onPanDown != null ||
onPanStart != null ||
onPanUpdate != null ||
onPanEnd != null ||
onPanCancel != null) {
gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
() => PanGestureRecognizer(debugOwner: this),
(PanGestureRecognizer instance) {
instance
..onDown = onPanDown
..onStart = onPanStart
..onUpdate = onPanUpdate
..onEnd = onPanEnd
..onCancel = onPanCancel
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onScaleStart != null || onScaleUpdate != null || onScaleEnd != null) {
gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
() => ScaleGestureRecognizer(debugOwner: this),
(ScaleGestureRecognizer instance) {
instance
..onStart = onScaleStart
..onUpdate = onScaleUpdate
..onEnd = onScaleEnd
..dragStartBehavior = dragStartBehavior;
},
);
}
if (onForcePressStart != null ||
onForcePressPeak != null ||
onForcePressUpdate != null ||
onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = onForcePressStart
..onPeak = onForcePressPeak
..onUpdate = onForcePressUpdate
..onEnd = onForcePressEnd;
},
);
}
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
代碼相當的長,看著很復雜,其實邏輯非常的簡單,就是注冊手勢的識別器,
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
將邏輯細分一下:
- 創建一個Map容器gestures
- 添加單擊識別器
- 添加雙擊識別器
- 添加縱向(y軸方向)滑動識別器
- 添加橫向(x軸方向)滑動識別器
- 添加雙向(y軸和x軸)同時滑動識別器
- 添加縮放手勢識別器
- 添加帶有力傳感器的識別器
- 最后根據這些引數創建一個RawGestureDetector控制元件,
當然,添加這些手勢識別器的前提條件就是有回呼需求,也就是if中的那些判斷,因此通過上述代碼可以總結出我們通過使用GestureDetector的功能可以理解為再有需要的情況下注冊手勢識別器的監聽,那么既然有監聽,肯定就有地方將事件發送出來,
三、手勢事件分發跟蹤
因為flutter中的很多方法都是回呼的方式,而且很多原始碼都是介面的形式去呼叫的,直接扒原始碼比較難,因此我們通過打斷點觀察方法呼叫堆疊的形式來追蹤手勢的傳遞程序,


上面兩個截圖,一個是通過InkWell來注冊一個手勢回呼,第二個截圖則是方法呼叫堆疊,從這個斷點可以看到,點擊手勢的最終來源于Honks.dart檔案中的_dispatchPointerDataPacket方法
@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
原始碼比較簡單,就是從引擎VM中獲取到點擊螢屏的一個ByteData資料包,拿到之后就丟給PlatformDispatcher中的方法去處理,
再看PlatformDispathcer._dispatchPointerDataPacket方法
// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) {
if (onPointerDataPacket != null) {
_invoke1<PointerDataPacket>(
onPointerDataPacket,
_onPointerDataPacketZone,
_unpackPointerDataPacket(packet),
);
}
}
上面這部分不是核心代碼,只是一個方法的呼叫,核心代碼_unpackPointerDataPacket方法中的邏輯,如下:
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)),
change: PointerChange.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
kind: PointerDeviceKind.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
signalKind: PointerSignalKind.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)],
device: packet.getInt64(kStride * offset++, _kFakeHostEndian),
pointerIdentifier: packet.getInt64(kStride * offset++, _kFakeHostEndian),
physicalX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
physicalY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
physicalDeltaX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
physicalDeltaY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
buttons: packet.getInt64(kStride * offset++, _kFakeHostEndian),
obscured: packet.getInt64(kStride * offset++, _kFakeHostEndian) != 0,
synthesized: packet.getInt64(kStride * offset++, _kFakeHostEndian) != 0,
pressure: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
pressureMin: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
pressureMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
distance: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
distanceMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
size: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
radiusMajor: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
radiusMinor: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
radiusMin: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
radiusMax: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
orientation: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
tilt: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
platformData: packet.getInt64(kStride * offset++, _kFakeHostEndian),
scrollDeltaX: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
scrollDeltaY: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
));
assert(offset == (i + 1) * _kPointerDataFieldCount);
}
return PointerDataPacket(data: data);
}
這塊代碼的計算邏輯比較復雜,如果不細看,只是了解大概邏輯的話,還是比較好理解的,就是從引擎獲取到的Bytedata中決議出PointerData,這個PointerData中包含了螢屏的物理觸摸位置相關的資料,
根據最開始的那張方法呼叫堆疊可以看到,接下來呼叫的是
GestureBinding._handlePointerDataPacket (binding.dart:279)
_rootRunUnary (zone.dart:1370)
_CustomZone.runUnary (zone.dart:1265)
_CustomZone.runUnaryGuarded (zone.dart:1170)
_invoke1 (hooks.dart:182)
這5個方法,由于前4個方法(從下往上)基本上沒啥業務邏輯,都是callback回呼,這就不細講了,重點關注一下GestureBinding._handlePointerDataPacket 這個方法,從方法名可以猜到,就是處理PointerData的方法,
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();
}
從這個方法的內容可以看出,這個方法主要做的事情
- PointerData資料決議成PointerEvent
- 將PointerEvent添加到_pendingPointerEvents佇列中
- 處理完之后再從佇列里取出這些資料,
看一下轉化程序的源代碼,大概了解一下邏輯就好了,
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) sync* {
for (final ui.PointerData datum in data) {
//根據解析度計算邏輯位置
final Offset position = Offset(datum.physicalX, datum.physicalY) / devicePixelRatio;
assert(position != null);
final Offset delta = Offset(datum.physicalDeltaX, datum.physicalDeltaY) / devicePixelRatio;
final double radiusMinor = _toLogicalPixels(datum.radiusMinor, devicePixelRatio);
final double radiusMajor = _toLogicalPixels(datum.radiusMajor, devicePixelRatio);
final double radiusMin = _toLogicalPixels(datum.radiusMin, devicePixelRatio);
final double radiusMax = _toLogicalPixels(datum.radiusMax, devicePixelRatio);
final Duration timeStamp = datum.timeStamp;
final PointerDeviceKind kind = datum.kind;
assert(datum.change != null);
if (datum.signalKind == null || datum.signalKind == ui.PointerSignalKind.none) {
switch (datum.change) {
case ui.PointerChange.add:
yield PointerAddedEvent(
//省略引數
);
break;
case ui.PointerChange.hover:
yield PointerHoverEvent(
//省略引數
);
break;
case ui.PointerChange.down:
yield PointerDownEvent(
//省略引數
);
break;
case ui.PointerChange.move:
yield PointerMoveEvent(
//省略引數
);
break;
case ui.PointerChange.up:
yield PointerUpEvent(
//省略引數
);
break;
case ui.PointerChange.cancel:
yield PointerCancelEvent(
//省略引數
);
break;
case ui.PointerChange.remove:
yield PointerRemovedEvent(
//省略引數
);
break;
}
} else {
switch (datum.signalKind!) {
case ui.PointerSignalKind.scroll:
final Offset scrollDelta =
Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio;
yield PointerScrollEvent(
//省略引數
);
break;
case ui.PointerSignalKind.none:
assert(false); // This branch should already have 'none' filtered out.
break;
case ui.PointerSignalKind.unknown:
// Ignore unknown signals.
break;
}
}
}
}
這段代碼比較長,但是邏輯其實也不復雜
- 將PointerData中的資料根據手機顯示幕的解析度去計算出相對應的邏輯位置
- 根據PointerChange的不同型別,把相關資料封成相對應的Event,
這里可能有兩個關鍵點不太好理解,一個是邏輯位置,一個是yield關鍵字
關于物理指標
所為的物理指標資訊就是我們手機的觸摸屏相關的資料,可以理解為從觸摸屏硬體獲取到的資料,然后這個資料其實是和手機的螢屏有關系的,
關于邏輯位置
那就是和作業系統有關系了,作業系統會根據手機的解析度生成一個包含非常多像素的一個矩陣,矩陣中的每個點通過某種映射邏輯可以對應到螢屏物理的某個位置點,
物理是客觀存在的,邏輯是主觀定義的,
這里有個關鍵字yield,不理解的可以看去查一下,和return有點類似,但是不會中斷代碼的執行,
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
這個代碼應該都可以看懂,就是佇列的出隊操作,取出資料之后將資料移除,先進先出的原則(removeFirst),
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// Because events that occur with the pointer down (like
// [PointerMoveEvent]s) should be dispatched to the same place that their
// initial PointerDownEvent was, we want to re-use the path we found when
// the pointer went down, rather than do hit detection each time we get
// such an event.
hitTestResult = _hitTests[event.pointer];
}
assert(() {
if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
debugPrint('$event');
return true;
}());
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
這個方法邏輯看著很復雜,耐心看,我們來過濾
- 創建一個hitTestResult物件
- 根據PointerEvent的不同分為三種情況
- down,signal,hover,這三種情況,因為這三種情況都是點擊事件的開始,所以他們需要進行命中測驗,
- up和cancel,如果這兩種情況,表示事件結束或者是取消了,
- 最后這種情況,其實注釋也寫的挺清楚地,就是說對于中情況,這種情況就是出去最開始和結束之間的情況,只需要重用之前已經找到的路徑,不需要再次進行命中測驗了,
- 將命中的點添加到HitTestResult
- 最后就是大家熟悉的事件分發
命中測驗這個地方,需要重點看一下這個邏輯,
bool hitTest(BoxHitTestResult result, { required Offset position }) {
// 省略assert
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
從代碼可以看出,命中測驗的時候,子控制元件是優先的,
@protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
這個是默認方法回傳false,也就是說默認不命中,
再看看本身命中測驗的方法
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
回傳值是根據這個behavior的值來確定的,
點擊事件的攔截是不是就可以通過這個方法來處理了?
好了,事件的命中測驗就大概過了一遍,接下來我們看事件的分發了,
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
// No hit test information implies that this is a [PointerHoverEvent],
// [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially
// routed here; other events will be routed through the `handleEvent` below.
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
event: event,
hitTestEntry: null,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a pointer event'),
event: event,
hitTestEntry: entry,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
}
}
這塊邏輯代碼分兩部分,兩種情況的路由是不一樣的,當沒有
- 沒有命中的時候,直接將原Event發送出去
- 有命中的時候,會將坐標轉換之后,把命中之后事件發送出去,
@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);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
不得不說這個注釋寫的非常好,源代碼里的,
前面entry.target.handleEvent這個方法把事件發送出來之后,就被這個方法接到了,梳理一下這個方法做的事情
- 把event存盤在pointerRouter這個路由里,
- 關閉競技場
- 消除競技場,把勝利者給到第一個,
- 如果是信號事件,則處理信號事件
這里有一點要特別說明一下,if中的這幾個方法,都是在我們處理完所有事件之后才會呼叫,
這里還會涉及到一個競技場的問題,由于篇幅問題,這個競技場就不詳細講了,他主要邏輯就是解決手勢搶奪問題,最終有一個widget獲得這個手勢,
接下來到了BaseTapGestureRecognizer.handlePrimaryPointer方法了,到這個地方基本上就是處理最后的事件分發的邏輯了,
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
@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!);
}
}
這里解釋一下,acceptGesture這個方法是在close關閉競技場中呼叫的,handlePrimaryPointer這個方法是在route中呼叫的,
void _checkDown() {
if (_sentTapDown) {
return;
}
handleTapDown(down: _down!);
_sentTapDown = true;
}
void _checkUp() {
if (!_wonArenaForPrimaryPointer || _up == null) {
return;
}
assert(_up!.pointer == _down!.pointer);
handleTapUp(down: _down!, up: _up!);
_reset();
}
void _checkCancel(PointerCancelEvent? event, String note) {
handleTapCancel(down: _down!, cancel: event, reason: note);
}
void _reset() {
_sentTapDown = false;
_wonArenaForPrimaryPointer = false;
_up = null;
_down = null;
}
結合這兩部分代碼可以知道,呼叫onTab之前,肯定是得先在競技場中競技成功,然后然后通過呼叫acceptGesture這個方法就可以獲取到點擊事件的down方法了,于此同時,在接收到分發的PointerUpEvent事件的時候,才會把up的回呼賦值過去、
總結下來點擊事件onTab的呼叫有2個前提條件:
- 必須競技成功
- 接收到PointerUpEvent事件,
最后執行到TapGestureRecognizer.中的handleTapDown和handleTapUp方法了
@protected
@override
void handleTapDown({required PointerDownEvent down}) {
final TapDownDetails details = TapDownDetails(
globalPosition: down.position,
localPosition: down.localPosition,
kind: getKindForPointer(down.pointer),
);
switch (down.buttons) {
case kPrimaryButton:
if (onTapDown != null)
invokeCallback<void>('onTapDown', () => onTapDown!(details));
break;
case kSecondaryButton:
if (onSecondaryTapDown != null)
invokeCallback<void>('onSecondaryTapDown', () => onSecondaryTapDown!(details));
break;
case kTertiaryButton:
if (onTertiaryTapDown != null)
invokeCallback<void>('onTertiaryTapDown', () => onTertiaryTapDown!(details));
break;
default:
}
}
@protected
@override
void handleTapUp({ required PointerDownEvent down, required PointerUpEvent up}) {
final TapUpDetails details = TapUpDetails(
kind: up.kind,
globalPosition: up.position,
localPosition: up.localPosition,
);
switch (down.buttons) {
case kPrimaryButton:
if (onTapUp != null)
invokeCallback<void>('onTapUp', () => onTapUp!(details));
if (onTap != null)
invokeCallback<void>('onTap', onTap!);
break;
case kSecondaryButton:
if (onSecondaryTapUp != null)
invokeCallback<void>('onSecondaryTapUp', () => onSecondaryTapUp!(details));
if (onSecondaryTap != null)
invokeCallback<void>('onSecondaryTap', () => onSecondaryTap!());
break;
case kTertiaryButton:
if (onTertiaryTapUp != null)
invokeCallback<void>('onTertiaryTapUp', () => onTertiaryTapUp!(details));
break;
default:
}
}
TapGestureRecognizer這個類不知道是否還記得,這個是在最開始介紹代碼的時候介紹的,在GestureDetector中注冊的手勢識別器,
看到這個方法的代碼,也就算是結束了,同樣,這里可以看到onTapUp和onTap 兩個方法的執行順序,微觀的角度上講,onTapUp方法執行的順序還是稍微靠前一點的,從宏觀的角度上來講,這2個方法幾乎是同時呼叫的,
最后總結一下flutter中手勢的分發程序,
- 當手或者觸摸筆或者是滑鼠按下的時候,引擎會手機這些手勢的資料包ByteData
- PlatformDispatcher_dispatchPointerDataPacket()方法去做資料的決議,最后得到一個PointerDataPacket資料包
- GestureBinding.handlePointerDataPacket 把PointerDataPacket資料包中的PointerData資料轉化成對應的PointerEvent手勢事件, 并且將它們加入到_pendingPointerEvents待分發的手勢事佇列中,而PointerEvent只是一個基類,它有很多實作類,如:
- PointerAddedEvent
- PointerHoverEvent
- PointerDownEvent,
- PointerMoveEvent
- PointerUpEvent
- PointerCancelEvent
- PointerRemovedEvent
- 手勢資料全部添加到待分發的事件佇列之后,從佇列中逐個取出分發下去,先進先出的原則,
- 在事件分發程序中,有兩個邏輯
- 根據命中測驗確定這個邏輯是否在點擊區域
- 根據競技結果確認把事件分發給哪個物件
- 最后只想相關回呼方法(onTapDown,onTapUp,onTap等等),
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/353445.html
標籤:其他
上一篇:Kotlin學習筆記——(六)介面、抽象類、泛型、擴展、集合運算子、與Java互操作性、單例
下一篇:從用戶輸入創建物件
