引擎层

Flutter Engine 暴露了 FlutterEngineSendPointerEvent 接口供平台层调用。通过向该接口传递 FlutterPointerEvent 对象告知引擎当前指针状态。

FLUTTER_EXPORT
FlutterEngineResult FlutterEngineSendPointerEvent(
    FLUTTER_API_SYMBOL(FlutterEngine) engine,
    const FlutterPointerEvent* events,
    size_t events_count);

SDK 层

事件源

Flutter SDK 则通过 GestureBinding 类管理手势事件。GestureBinding 在初始化时在 window 对象中设置了 _handlePointerDataPacket 回调接收引擎层传回的指针状态。

@override
void initInstances() {
  super.initInstances();
  _instance = this;
  window.onPointerDataPacket = _handlePointerDataPacket;
}

经过指针事件队列 _pendingPointerEvents 缓冲后,handlePointerEvent 将对事件进行重采样处理。之后在 _handlePointerEventImmediately 过程中进行实际事件分发。

命中测试

_handlePointerEventImmediately 过程中事件分发前需要先对 PointerDownEvent 事件构造 HitTestResult

// @@ void _handlePointerEventImmediately(PointerEvent event) {
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;
  }());
} 

这里将会涉及 HitTestResultHitTestEntry 两个类。HitTestResult 中存储了一个点击测试顺序队列 _path,以控制测试顺序。而 HitTestEntry 则是每个点击测试的入口。

final List<HitTestEntry> _path;

GestureBinding 类中,hitTest 仅将自己作为命中入口放入 result 中。而在 HitTestResult 类的 add 方法中,都会将 HitTestEntry 存入了 _path 尾部。

@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}
void add(HitTestEntry entry) {
  assert(entry._transform == null);
  entry._transform = _lastTransform;
  _path.add(entry);
}

但这显然并不满足点击测试的需求,仅保证了根节点自己在点击测试的顺序路径中。

所以实际上 hitTest 方法在 RendererBinding 中被重载。并交由 RenderView 处理,也就是 Flutter 渲染对象树的根节点。

@override
void hitTest(HitTestResult result, Offset position) {
  assert(renderView != null);
  assert(result != null);
  assert(position != null);
  renderView.hitTest(result, position: position);
  super.hitTest(result, position);
}

RenderView 类的 hitTest 方法中,先对子节点 RenderBox 进行了判断,再将自己添加到命中测试路径中。而 RenderBox 将递归处理增加所有子节点的点击测试入口。

bool hitTest(HitTestResult result, { required Offset position }) {
  if (child != null)
    child!.hitTest(BoxHitTestResult.wrap(result), position: position);
  result.add(HitTestEntry(this));
  return true;
}

事件分发

HitTestResult 构造完成后,就是对事件进行分发。

// @@ void _handlePointerEventImmediately(PointerEvent event) {
if (hitTestResult != null ||
    event is PointerAddedEvent ||
    event is PointerRemovedEvent) {
  assert(event.position != null);
  dispatchEvent(event, hitTestResult);
}

分发实现在 dispatchEvent 方法中,并分为两种情况。

在没有点击测试结果的事件,也就是 Added、Remove 两种事件中,进行路由分发。也就是仅对在 PointerRouter 对象中注册过此事件的路由进行通知。通常在 GestureRecognizer 类中由 addPointer 方法注册。

// @@ void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult == null) {
  assert(event is PointerAddedEvent || event is PointerRemovedEvent);
  try {
    pointerRouter.route(event);
  } catch (exception, stack) {
    // ...
  }
  return;
}

对于其他有点击测试结果的事件,按照点击测试路径顺序进行事件分发。由前面 RenderView 类对点击测试入口追加的顺序来看,Flutter 采用的是由子节点到根节点的冒泡顺序。

// @@ void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
for (final HitTestEntry entry in hitTestResult.path) {
  try {
    entry.target.handleEvent(event.transformed(entry.transform), entry);
  } catch (exception, stack) {
    // ...
  }
}

根据前面的代码中,点击测试路径将按 RenderBox 树、RenderViewGestureBinding 的顺序进行调用。GestureBinding 自己也在其中。

RenderBox 树中,常见的是由 Listener Widget 产生的 RenderPointerListener 对象。该对象允许原始指针事件直接回调给 Widget 中的回调函数。

// @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
@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);
}

而在 GestureBinding 类自己的事件处理方法中,可以看到在界面渲染树点击测试处理完成后,所有事件也被送入 PointerRouter 中进行路由分发。

// @@ mixin GestureBinding
@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);
  }
}

手势探测器

Flutter SDK 推荐使用 GestureDetector 类来监测用户手势输入,而不是直接使用 Listener 来监听原始指针事件。在 GestureDetector 类中,Flutter 封装了多种常见的手势识别器,放入 RawGestureDetector 中的使用。

RawGestureDetecotr 中使用 Listener Widget 来监听原始指针事件中的指针按下事件,并通知所有手势识别器。

// @@ class RawGestureDetectorState extends State<RawGestureDetector>
Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  // ...
  return result;
}

GestureRecognizer 类是所有手势识别器的基类,但是这里,仅指针按下事件被发送给识别器。并且其中提供了 debugPrintRecognizerCallbacksTrace 全局变量以供调试手势数据。

// @@ class RawGestureDetectorState extends State<RawGestureDetector>
void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (final GestureRecognizer recognizer in _recognizers!.values)
    recognizer.addPointer(event);
}

注释中开发者给出了解释,探测器要将自己加入全局指针路由 PointerRouter,并且加入全局手势竞技场 GestureArenaManager

/// It's the GestureRecognizer's responsibility to then add itself
/// to the global pointer router (see [PointerRouter]) to receive
/// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArenaManager]) to track
/// that pointer.

手势识别器

除了基类 GestureRecognizer,flutter 还提供了几个基础抽象类,方便复用一些通用逻辑。

独占手势 OneSequenceGestureRecognizer 提供了保证同时只有一个手势生效的机制,也实现了全局路由注册管理。

单一主指针手势 PrimaryPointerGestureRecognizer 提供主指针判断机制,用于保证一些单指手势不受其它指针影响。并且提供了指针移动限制、停留时间限制来区分长按、点按、拖动等基础手势。

基于这些抽象类,flutter 提供了几种常见的手势识别器,以供使用。

手势竞技场

所有的将自己放入竞技场的手势识别器,它们将在竞技场中决出胜者。

debugPrintGestureArenaDiagnostics 全局参数提供了竞技场状态信息。

参考