一次点击一般会有先后两个事件:PointerDownEvent和PointerUpEvent,分别表示按下down和抬起up(如果手指发生了滑动,还会有PointerMoveEvent事件)。本文分别分析按下和抬起两个事件的处理流程。
备注:本文以GestureDetector
为示例进行点击事件处理流程的分析,基于Flutter 2.5.3。
PointerDownEvent(按下)事件的处理流程 GestureBinding GestureBinding HitTestResult RenderPointerListener RenderPointerListener TapGestureRecognizer TapGestureRecognizer handlePointerEvent (PointerDownEvent) new HitTestResult 命中测试 hitTest (RendererBinding).hitTest renderView.hitTest child!.hitTest 深度优先遍历子树 add(命中测试通过的RenderBox) add(HitTestEntry(RenderView.this)) add(HitTestEntry(GestureBinding.this)) 点击down事件分发 dispatchEvent 获取命中测试的target列表 handleEvent onPointerDown?.call(event) 对应RawGestureDetectorState. _handlePointerDown addPointer(event) addAllowedPointer(event) startTrackingPointer(pointer) pointerRouter.addRoute 加入到路由中 gestureArena.add(pointer, this) 添加到事件竞技场 handleEvent pointerRouter.route(event) handleEvent 正常按键按下的情况无操作。 在触摸点移出按键区域等特殊 情况会做清理操作。 gestureArena.close 阻止新事件竞争者加入 只有一个竞争者才会执行 scheduleMicrotask GestureArenaManager 删除pointer id对应的竞技场 acceptGesture(pointer) 唯一的竞争者此时并不会 立刻触发点击事件回调, 而是等待up事件的到来。
命中测试 hitTest 一次点击事件发生后,会首先进行命中测试。
命中测试hitTest在PointerDownEvent事件的处理流程中进行,从RendererBinding.hitTest
开始,然后调用GestureBinding.hitTest
,再调用RenderView.hitTest
,RenderView会直接调用子节点的hitTest。(其实RendererBinding和GestureBinding都是对应一个WidgetsFlutterBinding类实例,属于继承关系。)RenderView的子节点是RenderBox的子类,从此之后,参与命中测试的组件都是RenderBox或者其子类。hitTest的默认处理逻辑在RenderBox中实现:
1 2 3 4 5 6 7 8 9 10 11 abstract class RenderBox extends RenderObject { 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 ; } }
处理逻辑解读:
先判断点击的坐标是否在控件范围内,如果在,则表示命中测试,否则就返回,不会再对子节点进行命中测试;
执行hitTestChildren判断子节点是否命中测试,如果命中就加入到HitTestResult中;
最后把自己加入到HitTestResult中。
因此,hitTest采用深度优先遍历算法,先将最内部的子节点加入到HitTestResult中,最后再加入自己。命中测试会把点击坐标位置的控件都加入到命中列表中,并没有区分是否是可点击控件。
hitTestChildren RenderBox的hitTestChildren默认实现始终返回false,RenderBox子类需要自己来实现。对于RenderProxyBox,方法在RenderProxyBoxMixin
中实现:
1 2 3 4 @override bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { return child?.hitTest(result, position: position) ?? false ; }
代码解读:
hitTestChildren
会直接调用子节点的hitTest,如果没有子节点就返回false,表示节点没有命中测试。
对于具有多个子节点的RenderObject,比如Column等,处理逻辑在RenderCustomMultiChildLayoutBox.hitTestChildren
中,会使用倒序的方式调用child的hitTest。很多继承自RenderObject的子类都有自己的实现,比如RenderCustomPaint、RenderFittedBox、RenderFlex等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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) { assert (transformed == position - childParentData.offset); return child!.hitTest(result, position: transformed!); }, ); if (isHit) return true ; child = childParentData.previousSibling; } return false ; }
点击事件分发 命中测试完成后,就会执行down事件的分发,事件会分发给命中测试列表中的各个RenderBox
,执行RenderBox.handleEvent
函数。默认的handleEvent实现是空实现。GestureDetector会通过RenderPointerListener
来处理按键。具体流程可以参考上面的时序图。
经过事件分发后,只有真正的点击事件处理组件才会被加入到事件路由和事件竞技场中。
PointerUpEvent(抬起)事件处理流程 GestureBinding GestureBinding RenderPointerListener RenderPointerListener TapGestureRecognizer TapGestureRecognizer handlePointerEvent (PointerUpEvent) 删除命中测试结果 dispatchEvent handleEvent onPointerUp?.call(event) 在GestureDetector的场景, onPointerUp为null handleEvent pointerRouter.route handleEvent handlePrimaryPointer 只有一个竞争者时才会执行 handleTapUp onTap 使用者接收到onTap事件 _reset stopTrackingIfPointerNoLongerDown stopTrackingPointer pointerRouter.removeRoute gestureArena.sweep处理竞技逻辑 alt [存在多个竞争者,则执行竞技流程] [竞技胜出] acceptGesture handleTapUp onTap 使用者接收到onTap事件 _reset [竞技失败] rejectGesture _reset
PointerUpEvent
表示一次点击事件周期的结束,因此除了触发控件的点击事件回调外,还做了一些清理工作,比如删除事件路由,执行reset等。还有一项很重要的工作就是处理竞技逻辑,当点击范围存在多个可点击控件时,通过竞技逻辑来决定哪个空间来处理本次点击事件。
下面是竞技场的代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class GestureArenaManager { void sweep(int pointer) { final _GestureArena? state = _arenas[pointer]; ... _arenas.remove(pointer); if (state.members.isNotEmpty) { state.members.first.acceptGesture(pointer); for (int i = 1 ; i < state.members.length; i++) state.members[i].rejectGesture(pointer); } } }
竞技逻辑的代码实现非常简单,如果某次点击存在多个竞争者,则列表的第一个胜出,其他的都失败。所以,竞技的逻辑依赖竞争者的加入顺序,第一个加入的就会赢的胜利。这个顺序就是命中测试的顺序,可以参考hitTest一节的分析。
GestureDetector相关的类图 GestureDetector RawGestureDetector RawGestureDetectorState _handlePointerDown(event) GestureRecognizer OneSequenceGestureRecognizer PrimaryPointerGestureRecognizer BaseTapGestureRecognizer acceptGesture(int pointer) rejectGesture(int pointer) TapGestureRecognizer LongPressGestureRecognizer Listener onPointerDown SingleChildRenderObjectWidget RenderPointerListener handleEvent(event, entry) RenderProxyBoxWithHitTestBehavior bool hitTest(...) RenderProxyBoxMixin bool hitTestChildren(...) RenderProxyBox GestureBinding GestureArenaManager PointerRouter 1 n with
参考资料 《Flutter实战·第二版》 8.3 Flutter事件机制 深入理解Flutter手势系统