Flutter 触摸事件的处理点在 GestureBinding
中。在 GestureBinding
中存在一个 handlerPointerEvent
方法,这个方法就是触摸事件在 Flutter
侧的触发点。
手势预处理
手势触发事件到事件分发给具体处理对象之前的流程如下图:
详细看下具体的源码:
代码语言:javascript复制void handlePointerEvent(PointerEvent event) {
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingClock);
return;
}
_resampler.stop();
_handlePointerEventImmediately(event);
}
正常情况下会执行 _handlePointerEventImmediately 方法:
这里区分不同的手势情况做了不同的处理:
- 当手势是Down、Signal、Hover的时候,在移动端上我们一般关心 Down
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
这里会创建一个 HitTestResult
对象传给 hitTest 方法,并且把处理完成后的 HitTestResult
放在一个 _hitTests 的 map 里面。
- 当手势是抬起或者取消的时候
这次手势已经结束了,从 _hitTests 的map 里面移除这个result对象。
- 不是手势触发的时候,但是仍然是down状态,这里可以理解成一个控件还是处于被按下的状态中。
因为这次完整的手势并没有结束,直接获取上一次的 HitTestResult
对象。
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
当 hitTestResult 不为 null 的时候,会根据这个结果进行事件的分发:那么 hitTest 是什么呢?
碰撞测试
直接按照名字看,hitTest 就是打击测试的意思。Flutter 会通过这个 test 去判断手势事件具体是交给谁处理。hitTest 会执行 RenderBinding
的 hitTest 方法,执行 renderView 的 hitTest 方法:
@override
void hitTest(HitTestResult result, Offset position) {
renderView.hitTest(result, position: position);
}
// RenderView
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null)
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
如果有子节点,会先执行子节点的 hitTest。会带上手势的 position。hitTest 本身的逻辑其实就是把 HitTestEntry 加入到 result 里面。
这里的 child 是 RenderBox
:
// RenderBox
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;
}
当这个 RenderBox
的位置在这个 position 的范围的时候,会执行 hitTestChildren 方法。这个 hitTestChildren 就是各类组件的 RenderObject
自己实现的了。我们来看一个例子,Stack
组件底层的 RenderBox
是一个 RenderStack
对象,它的 hittestChildren 会执行 defaultHitTestChildren 方法:
// RenderStack
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
ChildType? child = lastChild;
while (child != null) {
final ParentDataType childParentData = child.parentData! as ParentDataType;
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset? transformed) {
return child!.hitTest(result, position: transformed!);
}
);
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
}
这里会把每个子节点都执行一次 hitTest,如果命中了,结束这个方法。如果没有命中,就用子节点的兄弟节点去执行 hitTest,依次遍历执行。这里 Stack 就会有一个和 Android 不一样的地方:当我们在 Stack 最上面一层覆盖一个 Container 的话,那么这个 Container 会命中 hitTest 测试,这里就直接结束了整个 RenderStack
的 hitTest,即使下面一层还有组件可以响应手势,它也不会收到了。
HitTestResult
添加结果最终会把这些 HitTestEntry
都添加到自己的 path 里面:
void add(HitTestEntry entry) {
assert(entry._transform == null);
entry._transform = _lastTransform;
_path.add(entry);
}
这里也就把手势命中的组件都收集到了一起。这一步完成了,下一步自然就是决定手势具体怎么分发和处理了。
事件分发
当手势的碰撞测试结束后,会继续去分发手势:
代码语言:javascript复制void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult == null) {
pointerRouter.route(event);
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
这里简化了一些其他逻辑。当 hitTest 的结果不存在的时候,会执行 pointerRouter 的 route 方法。否则会把 hitTest 收集到的组件都执行一次 handleEvent 方法。
这个过程有点抽象,很难理解具体每一步的目的是什么,我们同样按照一个实际例子来看看具体的流程。这里我们使用我们最常用来处理手势的 GestureDetector
:
GestureDetector
是一个 Widget
,它的层级依次是:
GestureDetector ---> RawGestureDetector ----> Listener ----> RenderPointerListener
结合上述的 hitTest 过程,path 里面已经存了多个组件。这些组件在 path 列表中,子组件在前,父组件在后。所以这里我们可以认为 path 里存的target的第一个对象是 RenderPointerListener
,最后面则是 RenderView
和GestureBinding
。
事件分发的流程如图:
RenderPointerListener
的 handleEvent 的实现如下:
// RenderPointerListener
@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 PointerSignalEvent)
return onPointerSignal?.call(event);
}
处理向下手势的地方是:
代码语言:javascript复制 // RawGestureDetectorState
void _handlePointerDown(PointerDownEvent event) {
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
这里会由 GestureRecognizer
来执行 addPointer 方法,GestureRecognizer
是一个抽象类。它的继承结构如下
image.png
这里调用的则是 OneSequenceGestureRecognizer
的 addPoint,也就是 GestureRecognizer
的 addPoint
// GestureRecognizer
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
执行 addAllowPointer :
代码语言:javascript复制// BaseTapGestureRecognizer
@override
void addAllowedPointer(PointerDownEvent event) {
if (state == GestureRecognizerState.ready) {
if (_down != null && _up != null) {
_reset();
}
_down = event;
}
if (_down != null) {
super.addAllowedPointer(event);
}
}
// PrimaryPointerGestureRecognizer
@override
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer, event.transform);
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));
}
}
/// OneSequenceGestureRecognizer
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer, event.transform);
}
void startTrackingPointer(int pointer, [Matrix4? transform]) {
GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
这里执行了 GestureBinding#pointerRouter
的 addRoute 方法。把事件添加到了手势路由表 和 gestureArena
里。getsureArena
代表的则是一个手势竞技场,用来区分哪个手势胜出。
最后走到 GestureBidning
的 handleEvent
:
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
//....
}
这里会执行 PointerRouter 的 route 方法:
代码语言:javascript复制 void route(PointerEvent event) {
final Map<PointerRoute, Matrix4?>? routes = _routeMap[event.pointer];
final Map<PointerRoute, Matrix4?> copiedGlobalRoutes = Map<PointerRoute, Matrix4?>.from(_globalRoutes);
if (routes != null) {
_dispatchEventToRoutes(
event,
routes,
Map<PointerRoute, Matrix4?>.from(routes),
);
}
_dispatchEventToRoutes(event, _globalRoutes, copiedGlobalRoutes);
}
这里会执行 copiedGlobalRoutes 里面的每个方法。如果我们有这个 GestureDetector
。就会调用
PrimaryPointerGestureRecognizer
的 handleEvent 方法:
/// PrimaryPointerGestureRecognizer
@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);
}
这里会判断手势有没有超出设定的范围,如果超出了,不算触发手势。执行 resolve 走 rejected 的流程。否则执行 handlePrimaryPointer 方法。
代码语言:javascript复制@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!);
}
}
这里处理了 up 和 cancel 2个手势。down 则没有处理,down 则会交给后续的 target 进行处理,也就是 GestureBinding。分头看看这几个事件:
Up: 调用到 handleTapUp 方法,执行我们传入的 onTapUp。
Cancel: 这里会调 _checkCancel 方法继而调用 handleTapCancel 方法,这里会执行我们传入的 onTapCancel 函数。
Down: 查看 GestureBinding 的处理:
代码语言:javascript复制// GestureBinding
@override
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);
}
}
// GestureArenaManager
void _tryToResolveArena(int pointer, _GestureArena state) {
if (state.members.length == 1) {
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
_arenas.remove(pointer);
}
//...
}
这里如果是down手势的时候分几种情况处理:
- 竞技场只有一个成员 给这个成员处理
- 竞技场没有内容 不处理
- 存在竞技场的胜出者,给胜出者处理
如果是up手势,走 sweep 的逻辑:
代码语言:javascript复制 void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null)
return;
if (state.members.isNotEmpty) {
state.members.first.acceptGesture(pointer);
for (int i = 1; i < state.members.length; i )
state.members[i].rejectGesture(pointer);
}
}
这里直接指定了第一个成员作为竞技场的胜出者。当我们写多个 GestureDetector
嵌套的时候,最上层的子节点会最先进入竞技场,所以这个时候只有上面的那个才会响应我们的点击事件。
事件的 accept 和 reject 具体又做了什么呢?这里可以简单的看一下
代码语言:javascript复制@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
accept 里面执行了 down -> up
代码语言:javascript复制@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
if (pointer == primaryPointer) {
if (_sentTapDown) {
_checkCancel(null, 'forced');
}
_reset();
}
}
如果是 down 手势,先执行 cancel。接着进行重置。这里总结一下上面的流程,事件在分发的过程中会依次遍历让命中的元素进行处理。当 GestureDetector
响应手势的时候,会把自己加入路由表和竞技场。事件从子节点往上传递的过程中会让竞技场里的元素进行竞争。竞争胜出的元素可以处理手势。当手势结束的时候竞技场会进行重新竞争,这时候竞争规则则是第一个元素胜出。
总结
最后总结一下 ,Flutter 触摸事件的处理分为两部分。
- 利用hittets收集组件。
- 分发事件,使用GestureArenaManager筛选事件的具体处理者。
到这里 Flutter 的触摸事件就清楚了,我们可以了解一些基本的事件处理机制,来解决开发中的一些疑惑。