对于移动端的开发者来说,手势是一个非常重要的模块,基本上做任何App都会遇到各种各样的手势问题,而手势也是移动的一个不算小的模块吧,要彻底搞得还是得费一些时间的,如果之前对Android或者IOS的手势或者说点击事件的原理有所了解的,那么了解其它语言的手势原理相对来说帮助还是挺大的。
好了,切入正题。在Flutter中,对于Flutter有一定了解的人都知道,可以通过GestureDetector来给不具有点击事件或者手势回调的Widget添加手势回调。然后为了点击水波纹的点击效果,大多数开发者可能会使用InkWell widget来包装一个需要添加点击事件的控件。
对Flutter有一一些深入了解的人可能知道,InkWell就是对GestureDetector的一个封装。看图:
由于以上这部分代码没有什么逻辑,为了减少篇幅我就不贴源码了。
_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来做一些列的自定义封装
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属性来控制。这块内容不是今天的重点,我们先看重点吧。
既然是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>{};
当然,添加这些手势识别器的前提条件就是有回调需求,也就是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();
}
从这个方法的内容可以看出,这个方法主要做的事情
看一下转化过程的源代码,大概了解一下逻辑就好了。
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;
}
}
}
}
这段代码比较长,但是逻辑其实也不复杂
这里可能有两个关键点不太好理解,一个是逻辑位置,一个是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);
}
}
这个方法逻辑看着很复杂,耐心看,我们来过滤
命中测试这个地方,需要重点看一下这个逻辑。
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);
},
));
}
}
}
这块逻辑代码分两部分,两种情况的路由是不一样的,当没有
@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这个方法把事件发送出来之后,就被这个方法接到了。梳理一下这个方法做的事情
这里有一点要特别说明一下,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个前提条件:
最后执行到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个方法几乎是同时调用的。