详细了解Flutter事件分发的工作原理

515 阅读7分钟

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,包括 PointerDownEventPointerMoveEventPointerUpEvent 等。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 应该处理用户输入的事件。这个过程通过 RenderObjecthitTest 方法实现。

@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 将底层的输入事件准确地传递给需要处理的组件。

  1. PointerEvent Detection:Flutter 捕获用户输入,生成 PointerEvent 对象。
  2. Hit Testing:渲染树进行命中测试,确定哪个 RenderObject 应该接收事件。
  3. Event Dispatching:将事件分发给所有命中的 RenderObject
  4. Gesture Detection:手势识别器将 PointerEvent 转换为高层次的手势事件,并触发对应的回调函数。