Flutter 事件分發

無若葉發表於2020-03-06

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 保證同一個事件同一時間只會觸發一種手勢。

相關文章