從原始碼看flutter(五):GestureDetector篇

安卓小哥發表於2020-04-23

從原始碼看flutter系列集合

開篇

flutter的觸控事件涉及到的東西比較多,本篇文章將會從 GestureDetector 作為切入點,來對觸控事件的實現做一個全面的瞭解

GestureDetector

class GestureDetector extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    ...
    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
}
複製程式碼

這裡,會將所有在 GestureDetector 設定的各種點選事件,放在各個 GestureRecognizer 中,然後通過 GestureRecognizerFactory 對其進行封裝,存入 gestures 中,這裡 GestureRecognizerFactory 的實現還是蠻有意思的,感興趣的小夥伴可以去看一下它的原始碼

這裡,我們先簡單瞭解一下 GestureRecognizer

GestureRecognizer

abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
  ...
  void addPointer(PointerDownEvent event) { 
    ...
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }
  
  @protected
  void addAllowedPointer(PointerDownEvent event) { }
  ...
}
複製程式碼

GestureRecognizer 主要提供了addPointer(downEvent) 用於接收 PointerDownEvent 物件,它會呼叫 addAllowedPointer(event) 方法,由子類去具體實現

GestureRecognizer 繼承於 GestureArenaMember,它只提供了兩個方法

abstract class GestureArenaMember {
  void acceptGesture(int pointer);

  void rejectGesture(int pointer);
}
複製程式碼

分別表示手勢競技場成員獲勝與失敗時會呼叫的方法,接下來我們來看一下 RawGestureDetector

RawGestureDetector

class RawGestureDetector extends StatefulWidget {
    ...
}

class RawGestureDetectorState extends State<RawGestureDetector> {
  ...
  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (final GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
  }
  。。。
  @override
  Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child,
    );
    if (!widget.excludeFromSemantics)
      result = _GestureSemantics(...);
    return result;
  }
  ...
}
複製程式碼

RawGestureDetector 是一個 StatefulWidget ,它預設返回的是 Listener 物件。其中還有一個 _GestureSemantics 它用於實現一些具有語義化的手勢,比如長按展示 tooltip等,這裡我們不用關注它。

這裡將之前通過 GestureDetector 傳入的 gestures 轉換成了 _recognizers,並且將他們放在了 _handlePointerDown(event) 方法裡通過 onPointerDown 傳給了 Listener 物件

這裡還需要注意, _handlePointerDown(event) 中對 GestureRecognizer 物件進行了遍歷,並呼叫了他們的 addPointer(event) 方法

接下來我們看一下 Listener

Listener

class Listener extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    Widget result = _child;
    if (onPointerEnter != null ||
        onPointerExit != null ||
        onPointerHover != null) {
      result = MouseRegion(...);
    }
    result = _PointerListener(
      onPointerDown: onPointerDown,
      onPointerUp: onPointerUp,
      onPointerMove: onPointerMove,
      onPointerCancel: onPointerCancel,
      onPointerSignal: onPointerSignal,
      behavior: behavior,
      child: result,
    );
    return result;
  }
}
複製程式碼

Listener 是一個 StatelessWidget ,當傳入的部分事件不為 null 時,比如懸停事件 onPointerHover,返回的就是 MouseRegion,它用來處理滑鼠的輸入,預設返回 _PointerListener,這裡我們只需要關注這個物件

從前面我們可以知道,我們只對 Listener 傳入了 onPointerDown ,所以這裡傳遞給 _PointerListener 的其他手勢回撥都是 null

_PointerListener

class _PointerListener extends SingleChildRenderObjectWidget {
  ...
  @override
  RenderPointerListener createRenderObject(BuildContext context) {
    return RenderPointerListener(
      onPointerDown: onPointerDown,
      ...
    );
  }
  ...
}
複製程式碼

_PointerListener 是一個 SingleChildRenderObjectWidget ,它對應的 RenderObjectRenderPointerListener ,我們來看一下這個物件

RenderPointerListener

class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
  ...
  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    ...
    if (onPointerDown != null && event is PointerDownEvent)
      return onPointerDown(event);
    ...
  }
}
複製程式碼

之前我們分析 RenderObject 時就知道, RenderObject 物件有一個 HitTestTarget 介面

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget { ... }

abstract class HitTestTarget {
  factory HitTestTarget._() => null;

  void handleEvent(PointerEvent event, HitTestEntry entry);
}
複製程式碼

這個介面提供了 handleEvent(...) 方法供 RenderObject 實現去處理各種手勢事件,最終基本上都是交給子類去實現這個方法。這裡的 RenderPointerListener 就是如此

這裡對於 GestureDetector 的整體結構有了一個初步的瞭解,並且無法再往下深入了,接下來我們將從另外一個切入點來看手勢事件。那就是手勢分發的起點

手勢分發流程

起點其實不難找,之前我們就知道過了,runApp(...) 方法作為flutter的入口,會對 WidgetsFlutterBinding 進行初始化,WidgetsFlutterBinding 混入了多個 Binding 物件,其中就有專門處理手勢的 GestureBinding,我們看一下就知道了

手勢分發起點: GestureBinding

GestureBindinginitInstances() 中可以看到如下內容

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    window.onPointerDataPacket = _handlePointerDataPacket;
  }
  ...
}
複製程式碼

這裡的 _handlePointerDataPacket 就是觸控事件的起點,它會處理裝置輸入的資訊,將其轉換為flutter中的手勢事件

_handlePointerDataPacket(...)

  final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
  
  void _handlePointerDataPacket(ui.PointerDataPacket packet) {
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
    if (!locked)
      _flushPointerEventQueue();
  }
複製程式碼

手勢事件都被存放在了一個佇列中,之後會呼叫 _flushPointerEventQueue() 來進行手勢分發

_flushPointerEventQueue(...)

  void _flushPointerEventQueue() {
    assert(!locked);
    while (_pendingPointerEvents.isNotEmpty)
      _handlePointerEvent(_pendingPointerEvents.removeFirst());
  }
複製程式碼

這裡通過遍歷佇列,呼叫 _handlePointerEvent(event) 對各個event進行處理

_handlePointerEvent(event)

  void _handlePointerEvent(PointerEvent event) {
    ...
    HitTestResult hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent) {
      ...
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
      ...
    } else if (event.down) {
      ...
    }
    ...
    if (hitTestResult != null ||
        event is PointerHoverEvent ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      dispatchEvent(event, hitTestResult);
    }
  }
複製程式碼

當你在在螢幕上點選一下,觸發的事件流程如下:

PointerHoverEvent -> PointerDownEvent -> PointerUpEvent

在螢幕上滑動時,流程如下

PointerHoverEvent -> PointerDownEvent -> ...PointerMoveEvent... -> PointerUpEvent

PointerHoverEvent 主要用於 flutter_web 中的滑鼠懸停事件,這裡我們不關注它,我們可以看一下,當觸發 PointerDownEvent 時,做了些什麼

  final Map<int, HitTestResult> _hitTests = <int, HitTestResult>{};
  ...
    if (event is PointerDownEvent || event is PointerSignalEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
      ...
    }
  ...
  @override // from HitTestable
  void hitTest(HitTestResult result, Offset position) {
    result.add(HitTestEntry(this));
  }
複製程式碼

這裡的 hitTest(...) 由介面 HitTestable 提供,值得注意的是 GestureBindingRendererBinding 都實現了這個介面,可以看一下在 RendererBinding 中的實現

  ///RendererBinding
  @override
  void hitTest(HitTestResult result, Offset position) {
    assert(renderView != null);
    renderView.hitTest(result, position: position);
    super.hitTest(result, position);
  }
複製程式碼

這裡呼叫了 RenderViewhitTest(...) 方法,我們已經知道過了,它是根 RenderObject 物件,進入它的 hitTest(...) 看一下

  ///RenderView
  bool hitTest(HitTestResult result, { Offset position }) {
    if (child != null)
      child.hitTest(BoxHitTestResult.wrap(result), position: position);
    result.add(HitTestEntry(this));
    return true;
  }
複製程式碼

這裡的 childRenderBox 物件,在 RenderBox 中預設提供了 hitTest(...) 這個方法

  ///RenderBox
  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;
  }
複製程式碼

所以看到這裡你就能知道,當觸發了 PointerDownEvent 事件時,會呼叫所有 RenderBoxhitTest(...) 方法,將符合條件的物件放入到 BoxHitTestEntry(...) 中,再存入 HitTestResult 維護的 List<HitTestEntry> _path

HitTestEntry 接受的物件是 HitTestTarget ,上面我們也提到過了,RenderObject 是實現了這個介面的。所以最後這些 RenderBox 會被存入 BoxHitTestEntry 先放入 List<HitTestEntry> 中,其次是存入了 HitTestEntryRenderView ,最後才是 GestureBinding 物件

至於為什麼要把這些物件收集起來放入 HitTestResult 呢?後面會逐步說明

HitTestResult 被建立後,會被存入GestureBinding 維護的 Map<int, HitTestResult> _hitTests 中,keyevent.pointer ,每觸發一次事件,pointer 的值都會+1,不會重複

接下來,會進入 dispatchEvent(event, hitTestResult) 方法,進行分發事件

dispatchEvent(...)

  @override // from HitTestDispatcher
  void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
    ...
    if (hitTestResult == null) {
      assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
      try {
        pointerRouter.route(event);
      } catch (exception, stack) {
        ...
      return;
    }
    for (final HitTestEntry entry in hitTestResult.path) {
      try {
        entry.target.handleEvent(event.transformed(entry.transform), entry);
      }
      ...
    }
  }
複製程式碼

我們主要關注 entry.target.handleEvent(...) 方法,這裡對之前在 hitTest(...) 中新增的各個實現了 HitTestTarget 介面的物件,呼叫其 handleEvent(...) 方法。而 hitTestResult.path 的順序我們已經說過了,大致是下面這樣:

... -> RenderPointerListener -> ... -> RenderView -> GestureBinding

這裡會依次呼叫他們實現的 handleEvent(...) 。而事件的分發,就是通過這樣實現的!

開始手勢分發

我們再次進入 RenderPointerListenerhandleEvent(...) 方法

RenderPointerListener

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    ...
    if (onPointerDown != null && event is PointerDownEvent)
      return onPointerDown(event);
    ...
  }
複製程式碼

這裡會呼叫 onPointerDown(event) ,通過前面的的瞭解,我們知道這個方法就是 RawGestureDetectorState 傳入的 _handlePointerDown(...),再來看一遍

  void _handlePointerDown(PointerDownEvent event) {
    for (final GestureRecognizer recognizer in _recognizers.values)
      recognizer.addPointer(event);
  }
複製程式碼

addPointer(event) 的內容之前也說過了,最終都是呼叫 GestureRecognizer 提供的 addAllowedPointer(...) 方法。如果我們在 GestureDetector 中設定了 onTapDown() 或者其他點選事件,這裡就會呼叫 TapGestureRecognizeraddPointer(...) 方法。我們就先以它為例,來看一下都做了些什麼

TapGestureRecognizer

我們先簡單的看一下 TapGestureRecognizer 的繼承結構

TapGestureRecognizer -> BaseTapGestureRecognizer -> PrimaryPointerGestureRecognizer -> OneSequenceGestureRecognizer -> GestureRecognizer

這些類中,只有 GestureRecognizer 實現了 addPointer(...) 方法,只有 BaseTapGestureRecognizerPrimaryPointerGestureRecognizer 實現了 addAllowedPointer(...) 方法

可以先來看一下 BaseTapGestureRecognizeraddAllowedPointer(...)

BaseTapGestureRecognizer

  PointerDownEvent _down;
  ...
  @override
  void addAllowedPointer(PointerDownEvent event) {
    assert(event != null);
    if (state == GestureRecognizerState.ready) {
      _down = event;
    }
    if (_down != null) {
      super.addAllowedPointer(event);
    }
  }
複製程式碼

這裡只是做了一個簡單的賦值,儲存傳遞的 PointerDownEvent ,它會在 _reset() 中被置空

接下來,進入 PrimaryPointerGestureRecognizeraddAllowedPointer(...)

PrimaryPointerGestureRecognizer

  @override
  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(...) 方法,它在 OneSequenceGestureRecognizer 中實現

OneSequenceGestureRecognizer

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

上面有兩個比較關鍵的地方,先看第一個 addRoute(...)

  ///PointerRouter
  final Map<int, Map<PointerRoute, Matrix4>> _routeMap = <int, Map<PointerRoute, Matrix4>>{};
  ...
  void addRoute(int pointer, PointerRoute route, [Matrix4 transform]) {
    final Map<PointerRoute, Matrix4> routes = _routeMap.putIfAbsent(
      pointer,
      () => <PointerRoute, Matrix4>{},
    );
    assert(!routes.containsKey(route));
    routes[route] = transform;
  }
複製程式碼

在這裡把 handleEvent(...) 方法作為 PointerRoute 傳入到了 PointerRouter 中,它的作用,我們後面就知道了。接下來看另一個非常關鍵的地方:_entries[pointer] = _addPointerToArena(pointer)

  GestureArenaTeam _team;
  ...
  GestureArenaEntry _addPointerToArena(int pointer) {
    ...
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }
複製程式碼

可以看到,這裡將當前的 OneSequenceGestureRecognizer 作為 GestureArenaMember 物件,傳入了 GestureBinding 中維護的 GestureArenaManager 內。也就是將需要競技的手勢成員,放入了手勢競技場內。

那麼到這裡,RenderPointerListenerhandleEvent(...) 就執行完畢了,接下來會執行 RenderViewhandleEvent(...),不過由於它並沒有重寫這個方法,所以我們會直接來到 GestureBindinghandleEvent(...)

GestureBinding

  final PointerRouter pointerRouter = PointerRouter();
  ...
  @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);
    }
  }
複製程式碼

這裡就要進入這個非常重要的 route(event) 方法了

PointerRouter

  final Map<int, Map<PointerRoute, Matrix4>> _routeMap = <int, Map<PointerRoute, Matrix4>>{};
  final Map<PointerRoute, Matrix4> _globalRoutes = <PointerRoute, Matrix4>{};


  void route(PointerEvent event) {
    final Map<PointerRoute, Matrix4> routes = _routeMap[event.pointer];
    final Map<PointerRoute, Matrix4> copiedGlobalRoutes = Map<PointerRoute, Matrix4>.from(_globalRoutes);
    if (routes != null) {
      _dispatchEventToRoutes(
        event,
        routes,
        Map<PointerRoute, Matrix4>.from(routes),
      );
    }
    _dispatchEventToRoutes(event, _globalRoutes, copiedGlobalRoutes);
  }
複製程式碼

這裡有兩個 _dispatchEventToRoutes(...) 方法,後者執行的是 _globalRoutes 中的 PointerRoute,而 _globalRoutes 中存放的是其他的全域性手勢,比如用於隱藏 ToolTips 的手勢等,目前還不太瞭解它的其他具體作用。

不過前面的 routes 就是我們之前在 OneSequenceGestureRecognizerstartTrackingPointer(...) 中新增的各個 handleEvent(...) 方法

可以看一下 _dispatchEventToRoutes(...)

  void _dispatchEventToRoutes(
    PointerEvent event,
    Map<PointerRoute, Matrix4> referenceRoutes,
    Map<PointerRoute, Matrix4> copiedRoutes,
  ) {
    copiedRoutes.forEach((PointerRoute route, Matrix4 transform) {
      if (referenceRoutes.containsKey(route)) {
        _dispatch(event, route, transform);
      }
    });
  }
  ...
  void _dispatch(PointerEvent event, PointerRoute route, Matrix4 transform) {
    try {
      event = event.transformed(transform);
      route(event);
    }
    ...
  }
複製程式碼

就是遍歷然後執行所有的 PointerRoute 方法,這裡的 PointerRoute 就是之前的各個 OneSequenceGestureRecognizer 中的 handleEvent(...)

這裡要注意不要把 OneSequenceGestureRecognizerhandleEvent(...)RenderPointerListenerhandleEvent(...) 混淆了

OneSequenceGestureRecognizer 提供了 handleEvent(...) ,交由子類去實現,我們接著之前的點選事件,實現它的點選子類是 PrimaryPointerGestureRecognizer ;如果是拖拽事件的話,實現的子類就是 DragGestureRecognizer

PrimaryPointerGestureRecognizer

  @override
  void handleEvent(PointerEvent event) {
    assert(state != GestureRecognizerState.ready);
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
      final bool isPreAcceptSlopPastTolerance = ...;
      final bool isPostAcceptSlopPastTolerance = ...;

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
        resolve(GestureDisposition.rejected);
        stopTrackingPointer(primaryPointer);
      } else {
        handlePrimaryPointer(event);
      }
    }
    stopTrackingIfPointerNoLongerDown(event);
  }
  
  @protected
  void handlePrimaryPointer(PointerEvent event);
複製程式碼

當前事件是 PointerMoveEvent 時,會做一個判斷,如果一定時間內滑動的距離超過 18px 那麼就會進入第一個判斷中,該手勢會被拒絕掉,也就是從競技場中會被移除,此時會觸發 onTapCancel

我們主要關心手勢有效時的方法 handlePrimaryPointer(event),可以看到這個方法是交由子類去實現的,實現它的子類自然就是 BaseTapGestureRecognizer

BaseTapGestureRecognizer

  @override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
      _up = event;
      _checkUp();
    } else if (event is PointerCancelEvent) {
      resolve(GestureDisposition.rejected);
      ...
      _reset();
    } else if (event.buttons != _down.buttons) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer);
    }
  }
複製程式碼

可以看到,在這裡只對 PointerUpEventPointerCancelEvent 進行了處理,並沒有處理 PointerDownEvent ,這裡很自然的就可以知道, PointerDownEvent 肯定被放在了 GestureBindinghandleEvent(...) 的後面部分進行處理

不過我們這裡還是可以先看一下 PointerUpEvent 是如何處理的,進入 _checkUp() 方法

  @protected
  void handleTapUp({ PointerDownEvent down, PointerUpEvent up });
  ...
  void _checkUp() {
    ...
    handleTapUp(down: _down, up: _up);
    _reset();
  }
複製程式碼

_checkUp() 中呼叫了 handleTapUp(...) 方法,它是一個交由子類實現的方法,而 BaseTapGestureRecognizer 的子類就是 TapGestureRecognizer

TapGestureRecognizer

  @protected
  T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
    ...
      result = callback();
    ...
    return result;
  }

  @protected
  @override
  void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
    final TapUpDetails details = TapUpDetails(...);
    switch (down.buttons) {
      case kPrimaryButton:
        if (onTapUp != null)
          invokeCallback<void>('onTapUp', () => onTapUp(details));
        if (onTap != null)
          invokeCallback<void>('onTap', onTap);
        break;
        ...
      default:
    }
  }
複製程式碼

可以看到,最終通過 invokeCallback(...) 方法執行了傳入的方法,包括 onTapUponTap ,從這裡我們就知道了,我們最最常用的點選事件,就是在 TapGestureRecognizerhandleTapUp(...) 中執行的。

TapGestureRecognizer 還有 handleTapDown(...) 用於執行 onTapDown ,它則是通過 BaseTapGestureRecognizer_checkDown() 呼叫

接下來我們可以回到 GestureBinding ,看看 PointerDownEvent 到底是如何處理的

  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent) {
      gestureArena.close(event.pointer);
    }
    ...
  }
複製程式碼

GestureArenaManager -> close(...)

  void close(int pointer) {
    final _GestureArena state = _arenas[pointer];
    ...
    state.isOpen = false;
    ...
    _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);
    }
  }
複製程式碼

close(...) 呼叫了 _tryToResolveArena(...) 方法,在這個方法中,處理了三種情況

  • 如果競技場成員只有一個
  • 如果競技場沒有任何成員
  • 如果存在競技場的獲勝者

如果成員只有一個,那麼事件理所應當交給它處理;如果沒有成員就不說了;如果存在勝利者,交給勝利者處理也是正常的。顯然,close(...) 中並沒有對競技場的成員做一個競爭的處理,它只負責沒有點選衝突的時候,也就是隻有一個點選物件。這種情況最後會通過呼叫它的 acceptGesture(...) 來觸發 onTapDown

我們繼續看 close(...) 後面的方法

  @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);
    } 
    ...
  }
複製程式碼

GestureArenaManager -> sweep(...)

  void sweep(int pointer) {
    ...
    if (state.members.isNotEmpty) {
      ...
      state.members.first.acceptGesture(pointer);
      ...
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }
複製程式碼

看看這個方法,多麼簡單粗暴,直接確定競技場成員中的第一個為獲勝者!根據前面我們說過的 hintTest(...) 的新增順序,可以知道,RenderBox數最下層的物件是最先被新增到列表中的

所以這裡的第一個成員,就是最下層的物件,在螢幕的顯示中,它就是最裡層的元素,所以如果像下面這樣,為兩個顏色塊設定點選事件的話,只有紅色的會生效

從原始碼看flutter(五):GestureDetector篇

我們可以簡單的看一下 acceptGesture(...) 做了些什麼,這裡會進入 BaseTapGestureRecognizeracceptGesture(...)

  ///BaseTapGestureRecognizer
  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown();
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
  }
複製程式碼

可以看到,這裡的 _checkDown() 中就處理了 onTapDown 事件,後面跟著了一個 _checkUp() 用於對 onTapUponTap 的處理,如果競技場成員只有一個,這裡的 _checkUp() 不會生效

到這裡,關於點選事件的整個流程我們都清楚了,可以分為下面兩種情況

  • 競技場只有一個成員時:在 GestureArenaManagerclose(...) 中完成 onTapDown 的呼叫,此時事件為 PointDownEvent; 在 PointerRouterroute(...) 方法完成對 onTapUponTap 的呼叫,此時事件為 PointUpEvent
  • 競技場有多個成員時:在 GestureArenaManagersweep(...) 方法先完成對 onTapDown 的呼叫,後完成對 onTapUponTap 的呼叫,此時事件為 PointUpEvent

接下來可能你會產生疑問了,如果上面兩個顏色塊監聽的是相同的滑動事件,在競技場中他們又是如何處理的呢?

下面就來簡單的看一下,以 PanGestureRecognizer 為例

PanGestureRecognizer

簡單的看一下它的結構

PanGestureRecognizer -> DragGestureRecognizer -> OneSequenceGestureRecognizer -> GestureRecognizer

基本上和拖拽相關的核心邏輯都在 DragGestureRecognizer 中了

從之前我們的流程就知道,會先走 addAllowedPointer(...) 方法,之後通過在 GestureBindinghandleEvent(...) 中執行 PointerRouterroute(...) 來走 GestureRecognizerhandleEvent(...) 方法

所以我們先看 DragGestureRecognizeraddAllowedPointer(...)

  @override
  void addAllowedPointer(PointerEvent event) {
    startTrackingPointer(event.pointer, event.transform);
    _velocityTrackers[event.pointer] = VelocityTracker();
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
      ...
      _checkDown();
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
    }
  }
複製程式碼

還是會先在 startTrackingPointer(...)handleEvent(...) 加入 PointerRouter ,然後把當前物件加入競技場。

接著通過 _checkDown() 執行了 onDown 方法,對於 DragGestureRecognizer 它就是 onPanDown

以上是收到 onPointDownEvent 時的事件,因為是拖拽,接下來會收到 onPointMoveEvent 事件

再看它的 handleEvent(...)

  @override
  void handleEvent(PointerEvent event) {
    ...

    if (event is PointerMoveEvent) {
      ...
      if (_state == _DragState.accepted) {
        ...
      } else {
        ...
        if (_hasSufficientGlobalDistanceToAccept)
          resolve(GestureDisposition.accepted);
      }
    }
    ...
  }
複製程式碼

其中 _hasSufficientGlobalDistanceToAccept 是交由子類去實現的方法,用於判斷滑動距離是否有效,預設大於36個畫素就有效;如果有效,就會進入 resolve(...) 方法,它最終會呼叫到 GestureArenaManager_resolveInFavorOf(...)

而這個方法,對於傳入的 GestureArenaMember 物件,直接判定其為勝出者,並將其他競技場成員清除掉。而這裡傳入的物件和我們之前再點選事件中的一樣,都是樹結構中最下層的物件,也就是螢幕上最裡層的元素

所以這裡的滑動事件在競技場中的處理就是這樣了。而我們本篇的內容也即將結束

總結

手勢的分發流程大致如下:

  • 觸發手勢: flutter接受到由底層傳來的觸控事件通知,它會觸發 GestureBinding_handlePointerDataPacket(...) 方法,flutter再這個方法中對傳來的資料進行轉換,變成flutter中適用的格式
  • HitTestTarget物件收集: 通過 hitTest(...) 方法,將 RenderObject樹中符合條件的 RenderBox 物件新增到 HitTestResult 中,新增順序是由底至上的,最上層的兩個物件分別是 GestureBindingRenderView ,這些物件都實現了 HitTestTarget 介面,也就是說他們都具備 handlerEvent(...) 方法
  • 事件分發: 在 dispatchEvent(...) 中,通過遍歷之前新增的物件,呼叫他們的 handlerEvent(...) 方法來進行事件的分發
  • GestureRecognizer物件收集: 我們的 GestureDetector 對應的 RenderPointerListener 會進行事件處理,在收到 PointDownEvent 事件時,會將所有 GestureDetector 中註冊的 GestureRecognizer 物件的 handlerEvent 方法作為 PointerRoute 傳入 PointerRouter ,並將該物件放入 GestureArenaManager 維護的競技場
  • 事件處理: 執行到最外層,也就是 GestureBindinghandleEvent 中,在這裡從競技場選出最終處理手勢的 GestureRecognizer 物件,然後進行手勢處理

ps:以上分析都是基於單點觸控事件,尚未對多點觸控事件進行分析

最後,說明一下,單點觸控事件的核心並不是所謂的手勢競技場,因為根本就沒有一個真正的競技過程。最終都是直接選擇最下層的 GestureDetector 作為手勢的處理者,單點觸控事件的核心其實是這些競技場成員被新增到競技場中的順序——是由底至上的順序

相關文章