Flutter 事件分发

4,364 阅读24分钟

flutter 事件分发流程

flutter 中的 WidgetsFlutterBinding 集成了 GestureBinding、ServicesBinding、SchedulerBinding、PaintingBinding、SemanticsBinding、RendererBinding、WidgetsBinding 等 7 种 Binding,它们都有自己在功能上的划分,其中,GestureBinding 主要负责的是事件分发、手势检测相关。

在 flutter 中,一个事件的产生、利用过程中有 native、engine、flutter 三个角色,native 是生产者(在原生体系中 native 是属于消费者,但是在 flutter 这个体系中,可以将其看作为生产者,因为在 flutter 看来它的 native 就是原生系统,但是对于原生系统--如 Android --而言,它的 native 是 linux 内核),engine 传递者,flutter 则是消费者。

native 层

native 层以 android 为例,android 中也有自己的事件分发机制,整体就是 dispatch-intercept-onTouch 这样一个流程,flutter 在 android 中是以 flutterView 为媒介的,那么同理,flutter 中所获取到的事件,实际上就是 flutterView 在 android 体系中接收到的事件,再进一步传递给 flutter。在 android 中的触摸事件一般是通过 onTouchEvent 这个方法接收到的,而其他的事件,如控制杆、鼠标、滚轮等,可以通过 onGenericMotionEvent 接收。它们的参数都是 MotionEvent,都会把这个时间交给 AndroidTouchProcessor 处理,这两种事件的处理方式大同小异,都是把 MotionEvent 存储在 ByteBuffer 之后,交给 engine 层,再进一步传递给 flutter。

比如 onTouchEvent 的处理:

public boolean onTouchEvent(@NonNull MotionEvent event) {
  int pointerCount = event.getPointerCount();
  // Prepare a data packet of the appropriate size and order.
  ByteBuffer packet =
      ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
  packet.order(ByteOrder.LITTLE_ENDIAN);
  int maskedAction = event.getActionMasked();
  int pointerChange = getPointerChangeForAction(event.getActionMasked());
  boolean updateForSinglePointer =
      maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN;
  boolean updateForMultiplePointers =
      !updateForSinglePointer
          && (maskedAction == MotionEvent.ACTION_UP
              || maskedAction == MotionEvent.ACTION_POINTER_UP);
  if (updateForSinglePointer) {
    // ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only.
    addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet);
  } else if (updateForMultiplePointers) {
    // ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers.
    // We are converting these updates to move events here in order to preserve this data.
    // We also mark these events with a flag in order to help the framework reassemble
    // the original Android event later, should it need to forward it to a PlatformView.
    for (int p = 0; p < pointerCount; p++) {
      if (p != event.getActionIndex() && event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) {
        addPointerForIndex(event, p, PointerChange.MOVE, POINTER_DATA_FLAG_BATCHED, packet);
      }
    }
    // It's important that we're sending the UP event last. This allows PlatformView
    // to correctly batch everything back into the original Android event if needed.
    addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, packet);
  } else {
    // ACTION_MOVE may not actually mean all pointers have moved
    // but it's the responsibility of a later part of the system to
    // ignore 0-deltas if desired.
    for (int p = 0; p < pointerCount; p++) {
      addPointerForIndex(event, p, pointerChange, 0, packet);
    }
  }
  // Verify that the packet is the expected size.
  if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
    throw new AssertionError("Packet position is not on field boundary");
  }
  // Send the packet to flutter.
  renderer.dispatchPointerDataPacket(packet, packet.position());
  return true;
}

如上,一个 MotionEvent 可能包括多个触摸点(多指触控),这里需要将每一个触摸点的数据拆分开,依次装载到 packet 中,通过调用 addPointerForIndex 方法,最后,调用 renderer.dispatchPointerDataPacket 开始将 pocket 向 flutter 层发送,经过几个接力之后,最终调用 flutterJNI#nativeDispatchPointerDataPacket,进入到 engine 层。

engine 层

在 engine 层中主要是负责传递事件,一方面,通过 jni 获取 android 中传递到的事件,另一方面,通过 window 调用 dart 中的函数,将事件传递给 flutter 层处理,其中还涉及到一次数据的转换。

nativeDispatchPointerDataPacket 对应 engine 中的 DispatchPointerDataPacket 函数,然后转给 PlatformView,在这里,PointerDataPacketConverter 会对 pocket 进行转换,pocket 是 android 中传过来的数据,它是一个 ByteBuffer ,多个事件都放在一起,在这里 PointerDataPacketConverter 对其进行了拆分,转换成 PointerDataPacket,PointerDataPacket 内部通过数据存放 PointerData,也就是每一个单独的事件。经过这层转换之后,PointerData 的数量可能与传进来的不一致,因为从 ByteBuffer 中取出 PointerData 之后还会再经过一次处理,ConvertPointerData 函数用户进一步处理 PointerData 数据,在这个函数中 PointerData 可能被抛弃,也有可能会衍生出新的 PointerData。比如当 native 层传来了一个事件,但是这个事件是凭空出现,并不能满足一个完整系列事件的条件时,就会被抛弃,如果两次连续事件可以看作是同一个事件(类型不变,位置不变等),也会被抛弃。还有,如果 native 层传来了同一个系列事件的连续两次事件,但是在 flutter 看来这两个事件并不能构成连续事件时,就会创建出一个合成事件,穿插在两次事件中,以保证其连续性,比如本次事件为 Up 事件,PointerDataPacketConverter 会对比上一次传来的该系列事件,如果上次时间的位置与该事件不同,那么就会在 Up 事件之前先插入一个 Move 事件,将位置移动到当前位置,再加入这个 Up 事件,如此便可以使其更加连续,在 flutter 中使用时也会更方便。总而言之,此次转换一是为了从 ByteBuffer 中拆分事件,二是会进行一些内容上的转换,保证其合理性,使 flutter 层能够更方便地处理。

完了之后 PointerDataPacket 会依次传递给 Shell、Engine、PointerDataDispatcher、RuntimeController、Window 等,其中 PointerDataDispatcher 可以对事件进行有规划的分发,比如其子类 SmoothPointerDataDispatcher 可以延迟分发事件。

最后,Window 通过 DartInvokeField 函数,调用 dart 中的 _dispatchPointerDataPacket 函数,将事件传递到 flutter 层。

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
  if (window.onPointerDataPacket != null)
    _invoke1<PointerDataPacket>(window.onPointerDataPacket, window._onPointerDataPacketZone, _unpackPointerDataPacket(packet));
}

在这里会调用 window 的 onPointerDataPacket 函数,也就是在 GestureBinding 初始化时传给给 window 的 _handlePointerDataPacket,事件分发由此进入 flutter 层。其中还涉及到数据转换,engine 层向 flutter 层传递事件数据时并不能直接传递对象,也是先转成 buffer 数据再传递,此处还需要调用 _unpackPointerDataPacket 将 buffer 数据再转回 PointerDataPacket 对象。

也就是说事件从 android 传到 flutter 中执行了 5 次转换:

  1. android 中,从 MotionEvent 中取出事件,并保存在 ByteBuffer 中
  2. engine 中,将 ByteBuffer 转成 PointerDataPacket(类对象)
  3. engine 中,为了传递给 dart,将 PointerDataPacket 转成 buffer
  4. dart 中,将 buffer 再转成 PointerDataPacket(类对象)
  5. dart 中,将 PointerData 转成 PointerEvent,供上层使用,这一步还在后面

flutter 层

从 flutter app 的启动 runApp 函数中开始,对 WidgetsflutterBinding 进行了初始化,而 WidgetsflutterBinding 的其中一个 mixin 是 GestureBinding,即实现了手势相关的能力,包括从 native 层获取到事件信息,然后从 widget 树的根结点开始一步一步往下传递事件。

在 GestureBinding 中事件传递有两种方式,一种是通过 HitTest 过程,另一种是通过 route ,前者就是常规流程,GestureBinding 获取到事件之后,在 render 树中从根结点开始向下传递,而 route 方式则是某个结点通过向 GestureBinding 中的 pointerRoute 添加路由,使得 GestureBinding 接收到事件之后直接通过路由传递给对应的结点,相较于前一种方式,更直接,也更灵活。

_handlePointerDataPacket

从上面讲述,GestureBinding 接收事件信息的函数为 _handlePointerDataPacket,它接收的参数为 PointerDataPacket,内含一系列的事件 PointerData,然后就先是通过 PointerEventConverter.expand 将其转换为 flutter 中使用的 PointerEvent 保存在 _pendingPointerEvents 中,再调用 _flushPointerEventQueue 处理事件。

在 PointerEventConverter.expand 转换事件时有使用到 sync* ,这个用法一般是用来延迟处理循环,当 Iterable 遍历时循环才会执行,但是此处在调用 PointerEventConverter.expand 之后立刻就调用了 _pendingPointerEvents.addAll,也就是说会立刻对 Iterable 进行遍历,那么这里使用 sync* 的意义就不那么明确了。

_flushPointerEventQueue

_flushPointerEventQueue 中就是一个循环,不断从 _pendingPointerEvents 中取出事件,然后交给 _handlePointerEvent 处理。

void _flushPointerEventQueue() {
  assert(!locked);
  while (_pendingPointerEvents.isNotEmpty)
    _handlePointerEvent(_pendingPointerEvents.removeFirst());
}

_handlePointerEvent

在 _handlePointerEvent 中会创建 HitTestResult,第一次看到这名字,本以为这个类就是测试用的,但是它实际上贯穿整个事件分发的过程,并起着重要的作用。首先,HitTestResult 可以表示一系列的事件,它在 PointerDownEvent 到来时被创建并加入 _hitTests,并在 PointerUpEvent/PointerCancelEvent 到来时被移出 _hitTests,在一系列事件的中间,则可以通过 _hitTests[event.pointer] 获取到对应的 HitTestResult。HitTestResult 的分发对象是由 hitTest 函数执行确定的,由 RendererBinding 的 hitTest 函数作为入口,开始调用 RenderView 的 hitTest 函数,RenderView 可以认为是 render 树的入口,它再调用 child.hitTest 使得 HitTestResult 在 render 树中传递,之后再通过 hitTest/hitTestChildren 不断递归,找到消费这个事件的 RenderObject,并保存从根结点到这个结点的路径,方面之后的系列事件分发, RenderObject 的子类可以通过重写 hitTest/hitTestChildren 判断自己是否需要消费当前事件。

如果结点(或其子结点)需要消费事件,就会调用 HitTestResult.add 将自己加入到 HitTestResult 的路径中,保存在 HitTestResult 的 _path 中,后面具体分发的时候就会根据按照这个路径进行。比如在 GestureBinding 的 hitTest 中,

void hitTest(HitTestResult result, Offset position) {
  result.add(HitTestEntry(this));
}

会将自身加入到 HitTestResult 的路径中,理论上来说 GestureBinding 应该不会处理任何事件,此处将它加入到 HitTestResult 中是为了下面的 handleEvent 回调,这个函数是在 HitTestResult 分发的过程中,各结点的回调函数:

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);
  }
}

比如通过 PointerRouter 将事件通过路由分发出去,这一点先按下不表。

hitTest 执行完下一步就是调用 dispatchEvent 执行事件的分发。

dispatchEvent

事件分发有两种,一种就是由 HitTestResult 确定的分发路径,另一种是当 HitTestResult 为 null 时(一般当使用外部设备如鼠标时,HitTestResult 就无法有效地判断分发路径,或者上层直接通过 GestureDecter 等进行手势检测),需要由路由直接导向对应的结点。

HitTestResult 方式中,dispatchEvent 会调用 HitTestResult 保存路径中每一个结点的 handleEvent 处理事件,也就是在 hitTest 阶段中确定的事件分发路径,从 GestureBinding 开始,调用他们的 handleEvent 函数。

在 route 方式中,GestureBinding 回调用 pointerRouter.route 函数执行事件分发,事件的接受者就是 _routeMap 中保存的结点,而接收者通过 addRoute 和 removeRoute 进行添加和删除,接受者分为两种,普通的 route 存储在 _routeMap 中,globalRoute 存储在 _globalRoutes 中,前者是与 pointer 绑定的,后者会响应所有的事件:

/// Calls the routes registered for this pointer event.
///
/// Routes are called in the order in which they were added to the
/// PointerRouter object.
void route(PointerEvent event) {
  final LinkedHashSet<_RouteEntry> routes = _routeMap[event.pointer];
  final List<_RouteEntry> globalRoutes = List<_RouteEntry>.from(_globalRoutes);
  if (routes != null) {
    for (_RouteEntry entry in List<_RouteEntry>.from(routes)) {
      if (routes.any(_RouteEntry.isRoutePredicate(entry.route)))
        _dispatch(event, entry);
    }
  }
  for (_RouteEntry entry in globalRoutes) {
    if (_globalRoutes.any(_RouteEntry.isRoutePredicate(entry.route)))
      _dispatch(event, entry);
  }
}

static _RouteEntryPredicate isRoutePredicate(PointerRoute route) {
  return (_RouteEntry entry) => entry.route == route;
}

但是有一点很奇怪,就是 if (routes.any(_RouteEntry.isRoutePredicate(entry.route))) 这一句,如果硬要解释的话,这句话就是要判断 entry 的 route 是否与 routes 中的某一个 entry 的 route 相同,但是 entry 本身就是遍历 routes 得到的,entry 必然在 routes 里面,这个条件应该是恒成立的才对,但是这里这么写,确实想不出其含义。

接着 route 中再通过 _dispatch 函数调用 entry 的 route 函数,从typedef PointerRoute = void Function(PointerEvent event);可知,route 就是一个接收 PointerEvent 的函数,名称可自行定义。

于是这里就还有一个问题,从上面的逻辑来看,dispatchEvent 有两种方式分发事件,route 和 HitTestResut,从这里来看二者只能取其一,但是又从 GestureBinding 的 handleEvent 函数可以看到,pointerRouter.route 也会在这里执行,所以从某种角度来看,route 分发过程是总会执行的(要么在 dispatchEvent 中,要么在 handleEvent 中),反而 HitTestResult 过程则只会在 HitTestResult 不为空时执行,所以,如果删去 handleEvent 中的 pointerRouter.route 然后在 dispatchEvent 的 route 方式中去掉 if 判断,是否也能达到同样的效果?

HitTest

具体看 HitTest 方式的分发流程,可以将其分为两部分,第一部分是 hitTest 过程,确定事件接收者路径,这个过程只在 PointerDownEvent 和 PointerSignalEvent 事件发生时执行,对于一系列事件,只会执行一次,后续的都会通过 pointer 找到首次事件时创建的 HitTestResult,如果没有就不会执行分发(这里先不考虑 route 流程);第二部分就是后面的 dispatchEvent,会调用 HitTestResult 路径中的所有结点的 handleEvent 函数,这个过程在每一个事件到来时(且有对应的 HitTestResult)会执行。而单独从 HitTestResult 角度来看,第一个过程就是给事件注册接收者,第二个过程则是将事件分发给接收者,所以它的基本流程与 route 保持一致,只不过二者在不同的维度上作用,前者依赖 Widgets 树这样一个结构,它的接收者之间有着包含关系,这是一个事件正常的传递-消费过程。route 流程相较而言更加随意,它可以直接通过 GestureBinding.instance.pointerRouter.addRoute 注册一系列事件的接收者,而不需要传递的过程,没有结点之间的限制,更适合用于手势的监听等操作。

在 HitTest 流程中,从 GestureBinding 的 hitTest 开始,首先将 GestureBinding 加入到 HitTestResult 的路径中,也就是说所有的 HitTest 流程中首先都会调用 GestureBinding 的 handleEvent 函数。然后在 RendererBinding 中通过调用了 RenderView 的 hitTest,RenderView 是 RenderObject 的子类,也是 render 树的入口,RenderObject 实现了 HitTestTarget,但是 hitTest 的实现是在 RenderBox 中,RenderBox 可以看作是 render 结点的基类,它有实现 hitTest 和 hitTestChildren 函数:

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;
}

如果事件的 position 在自己身上,就接着调用 hitTestChildren 和 hitTestSelf 判断子结点或者自身是否消费事件,决定是否将自己加入到 HitTestResult 路径,从这里也可以看出,在 HitTestResult 路径中顺序是从子结点到根结点,最后到 GestureBinding。比如 hitTestChildren 的默认实现:

bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
  // the x, y parameters have the top left of the node's box as the origin
  ChildType child = lastChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData;
    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;
}

从 lastChild 开始遍历,找到一个消费事件的结点时就返回 true,同时只能有一个子结点响应。

HitTest 最终的落地点就是 handleEvent 函数,handleEvent 在 flutter 中主要有两个地方使用,一是 GestureBinding ,上面也说过它的 handleEvent 会调用 route 流程执行,另一个就是 RenderPointerListener,这是一个 RenderObject,它依附于 _PointerListener,_PointerListener 在 Listener 中使用,其 build 函数如下:

Widget build(BuildContext context) {
  Widget result = _child;
  if (onPointerEnter != null ||
      onPointerExit != null ||
      onPointerHover != null) {
    result = MouseRegion(
      onEnter: onPointerEnter,
      onExit: onPointerExit,
      onHover: onPointerHover,
      child: result,
    );
  }
  result = _PointerListener(
    onPointerDown: onPointerDown,
    onPointerUp: onPointerUp,
    onPointerMove: onPointerMove,
    onPointerCancel: onPointerCancel,
    onPointerSignal: onPointerSignal,
    behavior: behavior,
    child: result,
  );
  return result;
}

Listener 是一个集多种监听为一身的 Widget,包括鼠标、手势两大类,不过鼠标相关的监听建议直接使用 MouseRegion,而 Listener 专注于监听手势相关的事件。我们可以直接在代码中使用 Listener 监听相关的事件,如 PointerUp、PointerDown 等,由此 HitTest 流程结束。

route

route 流程整体来说也分为两个过程,第一步是进行事件监听,通过调用 GestureBinding.instance.pointerRouter.addRoute 完成注册,此处传入参数为 pointer(一般来说,对于触摸事件,每一次触摸 pointer 都会更新,对于鼠标事件,pointer 始终为 0)、handleEvent(处理事件函数)和 transform(用作点位的转换,比如将 native 层传来的位置转换成 flutter 中的位置),在 addRoute 中它们被封装成 _RouteEntry 保存在 _routeMap 等待被分发事件。除此之外还有addGlobalRoute、removeRoute 等可用于注册全局监听、移出监听。

分发过程则是与 HitTest 同在 dispatchEvent 中,当然还有在 GestureBinding 的 handleEvent 中,就是调用 route 函数将事件继而调用每一个监听者的 PointerRoute 函数,让监听者可以自行处理。

对于 HitTest 流程来说,注册监听者的过程就是 hitTest 函数,这个函数会在新的系列事件到来时自动执行,那么下面具体看对于 route 流程,什么时候会执行注册监听者。

在进行 flutter 开发时,常会用到监听手势,比如点击、双击等,很明显这里都是对手势的监听,需要使用的 widget 为 GestureDecter ,可以看看它是怎么实现的。

从 GestureDecter 的 build 函数中可以看到,它首先将各种手势回调函数分类,整理成不同的手势之后装载到gestures 中,然后返回一个 RawGestureDetector 实例,RawGestureDetectorState build 函数:

Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if (!widget.excludeFromSemantics)
    result = _GestureSemantics(
      child: result,
      assignSemantics: _updateSemanticsForRenderObject,
    );
  return result;
}

可见,RawGestureDetector 内部还是有使用到 Listener,但是此处只是监听了 onPointerDown 事件,并不能满足 RawGestureDetector 内部多种手势的检测,所以 RawGestureDetector 的真正目的并不是通过 Listener 接收事件,而是以此引入 route 来进行手势检测。

RawGestureDetector 只通过 Listener 监听 onPointerDown 事件,交给 _handlePointerDown 处理,在 _handlePointerDown 中:

void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (GestureRecognizer recognizer in _recognizers.values)
    recognizer.addPointer(event);
}

它会将事件传递给所有的 GestureRecognizer,_recognizers 会在 initState 阶段被初始化,内部保存的就是各个 GestureRecognizer 的子类,如 TapGestureRecognizer、LongPressGestureRecognizer 等。

在 addPointer 中,会先判断 GestureRecognizer 是否需要接收此事件(比如当 TabGestureRecognizer 的所有回调函数都为空时,就不会接收事件,又或者 GestureRecognizer 有设置过 fiterKind,只接收某一类事件,如触摸、鼠标等),来决定是调用 addAllowedPointer 还是 handleNonAllowedPointer。

addAllowedPointer 在 PrimaryPointerGestureRecognizer 有实现:

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));
  }
}

startTrackingPointer 会向 GestureBinding 中添加路由,下面的是一些额外处理,比如 deadline 会用在 LongPressGestureRecognizer 中用于检测长按操作。

void startTrackingPointer(int pointer, [Matrix4 transform]) {
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  _entries[pointer] = _addPointerToArena(pointer);
}

有此处可以明确,手势相关的检测并没有使用 Listener ,而是直接走 route 流程,可能有这种方式更快的考量,回调函数为 handleEvent,其名称与 HitTest 流程的回调名一致,所以在某些时候看到代码就会不清楚这个函数到底是在哪个流程中被调用的,当然大部分时候也不需要清楚这个。

PrimaryPointerGestureRecognizer 的 handleEvent 实现如下:

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);
}

这里会判断,如果这是一个可用的点击事件,就会调用 handlePrimaryPointer,但如果触发了 move 或 up 等事件,就先发送一个 GestureDisposition.rejected 表示监听取消(取消手势检测方面的监听,对应着 GestureArenaManager 中的相关操作,这个后面会再说),然后调用 stopTrackingPointer 取消注册。

在 handlePrimaryPointer 中,以 TapGestureRecognizer 为例:

void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel('');
    }
    _reset();
  } else if (event.buttons != _initialButtons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer);
  }
}

根据当前的事件决定下一步可能是什么回调,比如这里接收到了 PointerUpEvent 事件,就会调用 _checkUp 完成调用 onTap 和 onTapUp 这两个回调,如果是 PointerCancelEvent 事件,就先发送 GestureDisposition.rejected 表示监听结束,然后判断是否需要回调 onTapCancel 函数,或者当前事件的 buttons 与初次事件的 buttons 不同时,也表示当前的监听需要结束了。

看 _checkUp 中:

void _checkUp() {
  if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
    return;
  }
  final TapUpDetails details = TapUpDetails(
    globalPosition: _finalPosition.global,
    localPosition: _finalPosition.local,
  );
  switch (_initialButtons) {
    case kPrimaryButton:
      if (onTapUp != null)
        invokeCallback<void>('onTapUp', () => onTapUp(details));
      if (onTap != null)
        invokeCallback<void>('onTap', onTap);
      break;
    case kSecondaryButton:
      if (onSecondaryTapUp != null)
        invokeCallback<void>('onSecondaryTapUp',
          () => onSecondaryTapUp(details));
      break;
    default:
  }
  _reset();
}

至此 onTap 回调结束,TapGestureRecognizer 的一次监听可以宣告结束。但是关于 TapGestureRecognizer 的工作原理,还是没有清楚,比如这里的 resolve(GestureDisposition.rejected) 具体是怎么发挥作用的?为什么这里只有 checkUp ,checkDown 是什么时候执行的?acceptGesture 和 rejectGesture 这两个函数是干什么用的?这里就需要再看一下事件分发中的 GestureArenaManager。

gestureArena

关于 GestureArenaManager,它与 route 比较类似,都有一个注册-回调的过程。再回过头去看 startTrackingPointer 函数,它除了会将 handleEvent 加入到 route 之外,还会调用 _addPointerToArena。

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null)
    return _team.add(pointer, this);
  return GestureBinding.instance.gestureArena.add(pointer, this);
}

这里就与 route 添加的过程十分类似,gestureArena.add 有两个参数,pointer 表示当前系列事件的 id,而后面的参数则是 GestureArenaMember,而它的两个函数就是 acceptGesture 和 rejectGesture。

GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
    return _GestureArena();
  });
  state.add(member);
  assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
  return GestureArenaEntry._(this, pointer, member);
}

add 函数的实现如上,_arenas 中以 pointer 为 key,保存着 _GestureArena,_GestureArena 中保存 GestureArenaMember 列表,以上就是 gestureArena 的注册过程。

然后就是 gestureArena 的分发过程,也是与 route 类似的,在 GestureBinding 的 handleEvent 中,

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);
  }
}

如上,会对 PointerDownEvent、PointerUpEvent、PointerSignalEvent 三种事件做不同的响应,PointerDown 事件到来时,GestureArenaManager 会调用 close 函数关闭向对应的 _GestureArena 中添加 member,并试图在其中找到一个 member 调用其 acceptGesture 函数,表示当前系列事件已经确定消费者,而对其余注册的 member 则会调用其 rejectGesture 函数:

void close(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  state.isOpen = false;
  assert(_debugLogDiagnostic(pointer, 'Closing', state));
  _tryToResolveArena(pointer, state);
}

void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(!state.isOpen);
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
  } else if (state.eagerWinner != null) {
    assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
    _resolveInFavorOf(pointer, state, state.eagerWinner);
  }
}

如上,根据 _GestureArena 中注册 member 数量的不同有三种方式,只有一个 member 那毫无疑问它就是 eagerWinner,没有 member 就跳过处理,多 member 时则只接收 eagerWinner 的事件监听,其余拒绝,但是如果有多个 member 并且还没有 eagerWinner 时,则需要先等着后续再处理。

如果 handleEvent 中接收到 PointerUp 事件,就意味着当前系列的事件结束(一般一个系列事件都是以 PointerDown 标志着开始,以 PointerUp/PointerCancel 标志着结束),就会调用 sweep 函数清理保存的数据,

void sweep(int pointer) {
  final _GestureArena state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  assert(!state.isOpen);
  if (state.isHeld) {
    state.hasPendingSweep = true;
    assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
    return; // This arena is being held for a long-lived member.
  }
  assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // First member wins.
    assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
    // Give all the other members the bad news.
    for (int i = 1; i < state.members.length; i++)
      state.members[i].rejectGesture(pointer);
  }
}

当并没有确定 eagerWinner 但需要清理 _GestureArena 时,就会直接将第一个 member 认定为 eagerWinner,其余的直接 reject 。而对于诸如双击这种一次事件无法完成的手势检测,当第一个单击确定是就需要将 _GestureArena hold 住,不让 GestureArenaManager 直接将其清理掉。从这里也可以看出,对于 HitTest 和 route 流程,是可以多个接收者接收同一个事件的,它们无法相互影响,HitTest 流程通过 hitTest 函数确定谁能接收到事件,这个只由当前的 render 树确定,route 流程通过 addRoute 注册监听,也只由接收者自身通过 removeRoute 移除监听。而 gestureArena 不同,从上面来看,对于一个 _GestureArena 中的所有 member ,只能有一个 eagerWinner,也就是说,同时只能监听一个手势,其余的会被拒绝,这是 gestureArena 与其他两者之间的不同之处,同时它也与手势的检测息息相关,如果说 HitTest 和 route 为手势检测提供了原始素材,那么 gestureArena 就是手势检测中的重要工具,他们之间有着本质的区别。

而如何成为 eagerWinner 则是由事件接收者自己决定的,一个 GestureRecognizer 的工作流程是这样的,首先将自己注册到 route 流程中,始终监听事件,同时将自己注册到 GestureArenaManager 中参与竞争,在接收后续事件的过程中,GestureRecognizer 可以通过 resolve 函数向 GestureArenaManager 发出请求,表明自己是否需要消费后续事件,如果是,则将其他 GestureArenaManager 移出竞争列表,同时调用它们的 rejectGesture 函数表示它们竞争失败了,在这里先调整重置一下(一般是取消在 route 中的注册,恢复默认数据等),参与下次竞争吧,然后在自己的 acceptGesture 中开始进行手势检测,知道此次事件结束。如果否,GestureArenaManager 就会将 GestureRecognizer 移出竞争列表,并试图找到一个 eagerWinner ,找不到的话就继续等,直到出现了 eagerWinner 或者此次事件结束。GestureArenaManager 始于第一个 GestureRecognizer 参与 eagerWinner 的竞争,终于找到了 eagerWinner 或事件结束。

总结:一次完整的手势检测

下面从 Flutter 中(native 层与 engine 层比较简单,只是数据转换与传递,不描述)一次完整的点击手势检测过程,来对上述内容作个总结。

首先,点击事件通过 GestureDetector 监听,在它的 build 函数中对各个回调函数分类到 GestureRecognizer 中,onTap 回调归类到 TapGestureRecognizer 中,并将各 GestureRecognizer 构造函数传递给 RawGestureDetector,RawGestureDetector 中会将各 GestureRecognizer 实例化,并通过 Listener 监听 onPointerDown 事件,Listenr 的工作基础为 HitTest 流程,根据触摸位置决定是否将事件分发到 Listener。

当 onPointerDown 事件触发时,RawGestureDetector 就会让内部的所有 GestureRecognizer 尝试去处理这个事件,通过 addPointer 这个函数,内部主要是执行了三个函数:

void addPointer(PointerDownEvent event) {
  _pointerToKind[event.pointer] = event.kind;
  if (isPointerAllowed(event)) {
    addAllowedPointer(event);
  } else {
    handleNonAllowedPointer(event);
  }
}

首先调用 isPointerAllowed 判断 GestureRecognizer 是否需要处理这个事件,不同的子类就会有不同的实现,比如 TapGestureRecognizer 只有在 onTap 系列方法不都为 null 的时候才会处理 kPrimaryButton 类事件,通过了初步验证的,就调用 addAllowedPointer 函数,准备接收该系列事件的后续事件,进一步判断是否触发了对应手势、是否需要停止检测等等。如 PrimaryPointerGestureRecognizer 的实现:

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));
  }
}

startTrackingPointer 内部就是将 PrimaryPointerGestureRecognizer 的 handleEvent 函数注册到 route 中,同时还会将自己注册到 gestureArena 中参与竞争后面事件的消费权。以上就是 HitTest 回调阶段所做的事情。

在 HitTest 阶段的最后,也就是在 GestureBinding 的 handleEvent 中,会调用 pointerRouter.route 开始 route 阶段的事件分发,而上面的 PrimaryPointerGestureRecognizer 在 HitTest 阶段已经将自己注册到 route 中了,所以此阶段就会回调它的 handleEvent 函数:

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);
}

此时,是 route 阶段。前面一部分是对 PointerMoveEvent 事件的判断,如果还在可容忍范围内的话,就继续处理,如果不满足了点击事件的需要,就会退出此次事件的手势检测(resolve(GestureDisposition.rejected) 移除在 gestureArena 中的注册,stopTrackingPointer 移除在 route 中的注册),否则就调用 handlePrimaryPointer 将其再交给子类处理,最后还是会判断一下事件是否为 PointerUpEvent/PointerCancelEvent,如果是的话,也需要移除 route 注册,注意,此处并没有移除 gestureArena 中的注册,这是因为在 GestureBinding 的 handleEvent 中,接收到 PointerUpEvent 事件会直接进行清理,也就不需要这里一个一个地移除。

handlePrimaryPointer 在 TapGestureRecognizer 中的实现如下:

void handlePrimaryPointer(PointerEvent event) {
  if (event is PointerUpEvent) {
    _finalPosition = OffsetPair(global: event.position, local: event.localPosition);
    _checkUp();
  } else if (event is PointerCancelEvent) {
    resolve(GestureDisposition.rejected);
    if (_sentTapDown) {
      _checkCancel('');
    }
    _reset();
  } else if (event.buttons != _initialButtons) {
    resolve(GestureDisposition.rejected);
    stopTrackingPointer(primaryPointer);
  }
}

分别对三种情况做处理,正常响应点击事件的话,对应 PointerUpEvent 事件,调用 _checkUp ,在里面回调 onTapUp、onTap 等。按照相应顺序的话,第一次调用到这里时应该是 PointerDownEvent,所以上面应该都不会触发。到这里,route 阶段中的第一次事件也结束了。

再下面,就是要进行 gestureArena 判断,按照正常的来,第一次的 PointerDownEvent 事件会使其调用 close 函数,内部会选择出一个 eagerWinner 接收之后的事件,对于只有单个 GestureRecognizer 来说,GestureArenaManager 会调用它的 acceptGesture,但如果有多个 GestureRecognizer 同时竞争一个事件,那么TapGestureRecognizer 就有可能竞争不过别人从而被 rejectGesture,

void rejectGesture(int pointer) {
  super.rejectGesture(pointer);
  if (pointer == primaryPointer) {
    // Another gesture won the arena.
    assert(state != GestureRecognizerState.possible);
    if (_sentTapDown)
      _checkCancel('forced ');
    _reset();
  }
}

TapGestureRecognizer 会重置一下数据,等着下一个系列事件。如果竞争成功,调用了 acceptGesture ,

void acceptGesture(int pointer) {
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown(pointer);
    _wonArenaForPrimaryPointer = true;
    _checkUp();
  }
}

就会执行 _checkDown、_checkUp 等进行手势判断,并在 handlePrimaryPointer 中持续监听事件,并对 PointerUpEvent、PointerCancelEvent 等作出响应。以上就是第一次事件 PointerDownEvent 被处理的全过程,当这个事件处理完时,可以确定的有两点:

  1. 有哪些 GestureRecognizer 正在通过 route 方式监听事件
  2. GestureArenaManager 中有哪些 GestureRecognizer 正在竞争事件

不一定能确定的有一点,就是 GestureArenaManager 中的 eagerWinner 是谁,当只有一个竞争者时 eagerWinner 就是这唯一一个竞争者,但是如果有多个竞争者,则需要有一个竞争者调用 resolve(GestureDisposition.accepted) 或者直到 PointerUpEvent 到来,GestureArenaManager 需要清理数据时,才能确定谁是最终接收者。

下面以只有 TapGestureRecognizer 一个竞争者为前提,后面到来的时候都会在 handlePrimaryPointer 中被处理,如果后面出现了 PointerMoveEvent,需要判断这个移动事件是否构成实际上的移动(即移动距离超出了可容忍距离),如果出现了 PointerCancelEvent 则取消监听,如果是 PointerUpEvent,则点击手势完成,触发 onTap 回调。到这里,我们在 GestureDetector 中传入的 onTap 函数执行,一次点击事件的手势检测结束。其余的诸如长按检测(LongPressGestureRecognizer)、双击检测(DoubleTapGestureRecognizer)等基本处理流程大致都是类似。

再做一个更简单的总结,在 flutter 中事件分发有两种,一种是常规的在 render 树中传递事件的方式,也就是 HitTest 方式,另一种是直接向 GestureBinding 中注册回调函数的方式,也就是 route 方式,它们在 flutter 系统中扮演着不同的角色,其中 HitTest 方式主要是用于监听基本的事件,例如 PointerDownEvent、PointerUpEvent 等,而 route 方式一般都是与 GestureRecognizer 一起使用,用于检测手势,如 onTap、onDoubleTap 等,另外,在手势检测的过程中,GestureArenaManager 也是重度参与用户,协助 GestureRecognizer 保证同一个事件同一时间只会触发一种手势。