Flutter 的手势事件分发系统是一个非常灵活且复杂的系统,它将用户的触摸、点击、滑动等输入事件传递给合适的 Widget,并在需要时触发手势回调。要深入理解这个过程,必须从 PointerEvent 的捕获开始,经过命中测试(Hit Testing),再到手势识别器(Gesture Recognizers)的工作原理,以及如何最终触发手势回调。
我们从代码层面看一些整个事件处理流程:
// 触发新事件时,flutter 会调用此方法
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent ) {
hitTestResult = HitTestResult();
// 发起命中测试
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
//获取命中测试的结果,然后移除它
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) { // PointerMoveEvent
//直接获取命中测试的结果
hitTestResult = _hitTests[event.pointer];
}
// 事件分发
if (hitTestResult != null) {
dispatchEvent(event, hitTestResult);
}
}
上面代码只是核心代码,完整的代码位于GestureBinding 实现中。下面我们分别来介绍一些命中测试和事件分发过程。
1. 指针事件捕获 (PointerEvent Detection)
Flutter 中,所有与用户交互的输入事件都会首先转换为 PointerEvent,包括 PointerDownEvent、PointerMoveEvent、PointerUpEvent 等。PointerEvent 是用户与设备交互的最基础事件。这个过程是由底层的引擎负责的,PointerEvent 是对不同类型的输入设备(如触摸屏、鼠标、触控板等)的抽象。
dart
abstract class PointerEvent extends Diagnosticable {
const PointerEvent({
required this.timeStamp,
this.pointer = 0,
this.kind = PointerDeviceKind.touch,
required this.position,
// 其他参数省略...
});
final Duration timeStamp;
final int pointer;
final PointerDeviceKind kind;
final Offset position;
// 其他属性和方法省略...
}
指针事件的捕获与分发
Flutter 引擎在底层捕获到输入事件后,会将其封装为 PointerEvent 对象,并向上层传递到 RenderObject,而 RenderObject 是负责实际绘制和布局的对象。
在 Flutter 中,所有 PointerEvent 首先会被传递到根 RenderObject,即 RenderView,这是整个渲染树的根节点。RenderView 是渲染树的起点,它会捕获所有的指针事件并将这些事件交给其子节点处理。
2. 命中测试 (Hit Testing)
命中测试的作用是确定哪个 RenderObject 应该处理用户输入的事件。这个过程通过 RenderObject 的 hitTest 方法实现。
@override
void hitTest(HitTestResult result, Offset position) {
//从根节点开始进行命中测试
renderView.hitTest(result, position: position);
// 会调用 GestureBinding 中的 hitTest()方法,我们将在下一节中介绍。
super.hitTest(result, position);
}
第一步: renderView 是 RenderView 对应的 RenderObject 对象, RenderObject 对象的 hitTest 方法主要功能是:从该节点出发,按照深度优先的顺序递归遍历子树(渲染树)上的每一个节点并对它们进行命中测试。这个过程称为“渲染树命中测试”。
注意,为了表述方便,“渲染树命中测试”,也可以表述为组件树或节点树命中测试,只是我们需要知道,命中测试的逻辑都在 RenderObject 中,而并非在 Widget或 Element 中。
第二步:渲染树命中测试完毕后,会调用 GestureBinding 的 hitTest 方法,该方法主要用于处理手势,我们会在后面介绍。
我们以RenderBox为例,看看它的hitTest()实现:
bool hitTest(HitTestResult 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 中同时返回 true;如果没有则直接返回false。该方法中会递归调用子组件的 hitTest 方法。hitTestSelf()决定自身是否通过命中测试,如果节点需要确保自身一定能响应事件可以重写此函数并返回true ,相当于“强行声明”自己通过了命中测试。
需要注意,节点通过命中测试的标志是它被添加到 HitTestResult 列表中,而不是它 hitTest 的返回值,虽然大所数情况下节点通过命中测试就会返回 true,但是由于开发者在自定义组件时是可以重写 hitTest 的,所以有可能会在在通过命中测试时返回 false,或者未通过命中测试时返回 true,当然这样做并不好,我们在自定义组件时应该尽可能避免
需要注意:
- 命中测试是在 PointerDownEvent 事件触发时进行的,一个完成的事件流是 down > move > up (cancle)。
- 如果父子组件都监听了同一个事件,则子组件会比父组件先响应事件。这是因为命中测试过程是按照深度优先规则遍历的,所以子渲染对象会比父渲染对象先加入 HitTestResult 列表,又因为在事件分发时是从前到后遍历 HitTestResult 列表的,所以子组件比父组件会更先被调用 handleEvent 。
命中测试结果 (HitTestResult)
HitTestResult 是命中测试的结果,它保存了所有命中的 RenderObject 的列表。在事件分发过程中,这些 RenderObject 会按命中顺序接收事件。
dart
class HitTestResult {
final List<HitTestEntry> _path = <HitTestEntry>[];
void add(HitTestEntry entry) {
_path.add(entry);
}
// 其他方法省略...
}
每个 HitTestEntry 都封装了一个 RenderObject 和相关的事件信息。
3. 事件分发 (Event Dispatching)
完成命中测试后,Flutter 会将 PointerEvent 分发给命中测试结果中的每一个 RenderObject。这个过程是由 PipelineOwner 来管理的。
dart
class PipelineOwner {
void handlePointerEvent(PointerEvent event) {
// 遍历命中结果,调用每个 RenderObject 的 handleEvent 方法
for (final HitTestEntry entry in _hitTestResult.path) {
entry.target.handleEvent(event, entry);
}
}
}
每个 RenderObject 都会接收到事件,并根据自身逻辑处理事件。大部分情况下,RenderObject 只是将事件向上层传递,但如果是某些特定的 RenderObject(如 RenderPointerListener),则会对事件进行特定的处理。
4. 手势识别 (Gesture Detection)
Flutter 的手势系统是在 PointerEvent 之上构建的,手势识别器 (GestureRecognizer) 会将低层的指针事件(如点击、滑动等)识别为高层的手势事件(如点击、拖拽、双击等)。
手势的识别和处理都是在事件分发阶段的,GestureDetector 是一个 StatelessWidget, 包含了 RawGestureDetector.
手势识别器的工作原理
手势识别器是一个基于状态机的系统,GestureRecognizer 类是所有手势识别器的基类。它提供了一些基础的功能,如添加监听器、处理指针事件等。
dart
abstract class GestureRecognizer {
void addPointer(PointerDownEvent event) {
// 添加指针事件监听
_pointers[event.pointer] = _handleEvent;
}
// 其他方法省略...
}
GestureRecognizer 通过 addPointer 方法将自己与某个指针关联起来,当该指针有后续事件时,GestureRecognizer 会处理这些事件并判断是否形成一个手势。
具体的手势识别器(如 TapGestureRecognizer)会根据指针事件的序列,判断手势类型并触发相应的回调。
dart
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
GestureTapCallback? onTap;
@override
void handleEvent(PointerEvent event) {
if (event is PointerUpEvent) {
_checkUp();
}
}
void _checkUp() {
if (onTap != null) {
invokeCallback<void>('onTap', onTap!);
}
}
}
在这个例子中,TapGestureRecognizer 会在接收到 PointerUpEvent(表示手指抬起)时,判断是否为一个点击事件,如果是则调用 onTap 回调。
5. 事件冒泡和捕获
在 Flutter 中,事件的分发是通过一个路径列表(即 HitTestResult 中的 _path)来控制的。这个路径列表从最底层的 RenderObject(即叶子节点)开始,依次向上传递事件。这个过程类似于 Web 开发中的事件冒泡机制。
不过,Flutter 中事件是从叶子节点向父节点传播的,这一点与 Web 事件捕获(从顶层节点到目标节点)的顺序相反。
总结
Flutter 的手势事件分发系统是一个分层的、基于事件传播路径的系统。通过 PointerEvent 捕获、命中测试、事件分发、手势识别这几个步骤,Flutter 将底层的输入事件准确地传递给需要处理的组件。
- PointerEvent Detection:Flutter 捕获用户输入,生成
PointerEvent对象。 - Hit Testing:渲染树进行命中测试,确定哪个
RenderObject应该接收事件。 - Event Dispatching:将事件分发给所有命中的
RenderObject。 - Gesture Detection:手势识别器将
PointerEvent转换为高层次的手势事件,并触发对应的回调函数。