天天看點

一文搞懂Flutter的手勢事件——事件分發與沖突處理詳解

作者:閃念基因

前言

接下來我們想詳細的說一說Flutter是如何處理手勢事件的。本文将通過源碼詳細分析Flutter的事件分發與沖突處理過程,并通過示例說明不同沖突的處理方式。本文的組織架構如下:

  • 手勢事件的初始化
  • 命中測試
    • PointerEvent的封裝
    • hitTest()
  • dispatchEvent()
  • GestureDetector
    • onTap
    • onLongPress
    • onDoubleTap
    • onVerticalDragDown
  • 手勢事件攔截
  • 總結

手勢事件的初始化

還是先回到我們熟悉的runApp():

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}
           

前面的文章介紹過WidgetsFlutterBinding混合了很多mixin:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding
           

手勢相關的是GestureBinding,我們來看看它的initInstances()初始化的實作:

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

為platformDispatcher注冊了onPointerDataPacket回調,其實作是_handlePointerDataPacket()。我們先追蹤一下onPointerDataPacket的回調時機:

PointerDataPacketCallback? get onPointerDataPacket => _onPointerDataPacket;
  PointerDataPacketCallback? _onPointerDataPacket;
  Zone _onPointerDataPacketZone = Zone.root;
  set onPointerDataPacket(PointerDataPacketCallback? callback) {
    _onPointerDataPacket = callback;
    _onPointerDataPacketZone = Zone.current;
  }
           

繼續追蹤onPointerDataPacket的調用時機:

// Called from the engine, via hooks.dart
  void _dispatchPointerDataPacket(ByteData packet) {
    if (onPointerDataPacket != null) {
      _invoke1<PointerDataPacket>(
        onPointerDataPacket,
        _onPointerDataPacketZone,
        _unpackPointerDataPacket(packet),
      );
    }
  }

void _invoke1<A>(void Function(A a)? callback, Zone zone, A arg) {
  if (callback == null) {
    return;
  }

  assert(zone != null);

  if (identical(zone, Zone.current)) {
    callback(arg);
  } else {
    zone.runUnaryGuarded<A>(callback, arg);
  }
}

@pragma('vm:entry-point')
void _dispatchPointerDataPacket(ByteData packet) {
  PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
           

從注釋上我們就可得知,_dispatchPointerDataPacket()就是接收從framework層傳遞的手勢事件,并進行處理的時機。_invoke1()其實就是調用onPointerDataPacket并将_unpackPointerDataPacket()的傳回值回調。其中packet是一組未經處理的的ByteData,通過_unpackPointerDataPacket()方法對其進行解包處理,生成手勢事件所需的實體類PointerData:

static PointerDataPacket _unpackPointerDataPacket(ByteData packet) {
    const int kStride = Int64List.bytesPerElement;
    const int kBytesPerPointerData = _kPointerDataFieldCount * kStride;
    final int length = packet.lengthInBytes ~/ kBytesPerPointerData;
    assert(length * kBytesPerPointerData == packet.lengthInBytes);
    final List<PointerData> data = <PointerData>[];
    for (int i = 0; i < length; ++i) {
      int offset = i * _kPointerDataFieldCount;
      data.add(PointerData(
        embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian),
        timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)),
//...
        scale: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
        rotation: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
      ));
      assert(offset == (i + 1) * _kPointerDataFieldCount);
    }
    return PointerDataPacket(data: data);
  }
           

好了到此為止我們可以得知,在啟動app後,由GestureBinding注冊手勢的回調事件,當engine層發送來手勢事件後,由PlatformDispatcher封裝成PointerData實體,最終回調至GestureBinding進行處理,接下來我們看接收到手勢事件後的處理流程。

命中測試

PointerEvent的封裝

在真正處理手勢之前,第一步就是将手勢事件封裝成業務可用的PointerEvent,我們回到GestureBinding的initInstances():

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

跟蹤_handlePointerDataPacket的實作:

void _handlePointerDataPacket(ui.PointerDataPacket packet) {
    // We convert pointer data to logical pixels so that e.g. the touch slop can be
    // defined in a device-independent manner.
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
    if (!locked) {
      _flushPointerEventQueue();
    }
  }
           

首先就是把解包後的packet.data通過PointerEventConverter.expand()再做次轉換:

static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) {
    return data
        .where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
        .map((ui.PointerData datum) {
//...
          switch (datum.signalKind ?? ui.PointerSignalKind.none) {
            case ui.PointerSignalKind.none:
              switch (datum.change) {
         //...
                case ui.PointerChange.down:
                  return PointerDownEvent(
                    timeStamp: timeStamp,
                    pointer: datum.pointerIdentifier,
                    kind: kind,
                    device: datum.device,
                    position: position,
                    buttons: _synthesiseDownButtons(datum.buttons, kind),
                    obscured: datum.obscured,
                    pressure: datum.pressure,
//...
                  );
                case ui.PointerChange.move:
                  return PointerMoveEvent(
                    timeStamp: timeStamp,
                    pointer: datum.pointerIdentifier,
                    kind: kind,
                    device: datum.device,
                    position: position,
                    delta: delta,
                    buttons: _synthesiseDownButtons(datum.buttons, kind),
                    obscured: datum.obscured,
//...
                  );
//...
            case ui.PointerSignalKind.unknown:
            default: // ignore: no_default_cases, to allow adding a new [PointerSignalKind]
                     // TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/34402
              // This branch should already have 'unknown' filtered out, but
              // we don't want to return anything or miss if someone adds a new
              // enumeration to PointerSignalKind.
              throw StateError('Unreachable');
          }
        });
  }

           

截取了部分代碼,大緻就是将packet.data轉換成PointerEvent,并添加到_pendingPointerEvents清單中。接下來我們看_flushPointerEventQueue()的實作:

void _flushPointerEventQueue() {
    assert(!locked);

    while (_pendingPointerEvents.isNotEmpty) {
      handlePointerEvent(_pendingPointerEvents.removeFirst());
    }
  }
           
void handlePointerEvent(PointerEvent event) {
    assert(!locked);

    if (resamplingEnabled) {
      _resampler.addOrDispatch(event);
      _resampler.sample(samplingOffset, _samplingClock);
      return;
    }

    // Stop resampler if resampling is not enabled. This is a no-op if
    // resampling was never enabled.
    _resampler.stop();
    _handlePointerEventImmediately(event);
  }
           
void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
//...
    } else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down || event is PointerPanZoomUpdateEvent) {
      hitTestResult = _hitTests[event.pointer];
    }
//...
    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      dispatchEvent(event, hitTestResult);
    }
  }
           

這段代碼的整體思路是:

  • 如果event是PointerDownEvent等四種event之一時(我們假設建立的是一個移動端app,這四種event隻考慮PointerDownEvent的情況),建立一個HitTestResult對象,并調用hitTest()方法,然後将hitTestResult指派給_hitTests[event.pointer];
  • 如果是PointerUpEvent或PointerCancelEvent,那麼将此hitTestResult從_hitTests中移除并傳回給hitTestResult對象;
  • 最後執行dispatchEvent(event, hitTestResult);

說完PointerEvent的封裝後,Flutter是如何處理這些手勢事件的呢?如何确定哪些Widget響應手勢事件,并确認它們的優先級呢?接下來我們講一下hitTest()的實作,即命中測試。

hitTest()

hitTest()需要确定出都哪些widget對應的RenderObject可能會響應手勢事件,并給他們設定命中響應的優先級。這個步驟是非常重要的,可以确定最終響應手勢事件的Widget。在此我們先說個結論,子優先于父響應手勢事件。我們先來看看HitTestResult的結構:

HitTestResult()
     : _path = <HitTestEntry>[],
       _transforms = <Matrix4>[Matrix4.identity()],
       _localTransforms = <_TransformPart>[];
           
HitTestEntry(this.target);
           

其中_path是個HitTestEntry數組,HitTestResult每執行一次add()方法就會添加一個HitTestEntry對象到_path中,HitTestResult的target對象通常就是一個RenderObject對象,也就是說_path是用來記錄手勢命中的RenderObject對象數組。_transforms是記錄目标RenderObject對象相對于Global坐标系的位置。_localTransforms是記錄目标RenderObject對象相對于Parent的位置。我們繼續看hitTest(hitTestResult, event.position);的實作:

@override // from HitTestable
  void hitTest(HitTestResult result, Offset position) {
    result.add(HitTestEntry(this));
  }
           

建立一個HitTestEntry對象并添加到HitTestResult中。由于mixin,RendererBinding是GesturesBinding的子類,而RendererBinding實作了hitTest()方法,是以我們看看RendererBinding的hitTest()的實作:

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

在調用super.hitTest()之前,先調用了renderView的hitTest()方法,renderView我們很了解了,就是App的根RenderObject:

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

首先判斷是否有child,如果是的話需要先執行child的hitTest()方法,再将自己封裝成一個HitTestEntry添加到HitTestResult中。我們來看看child!.hitTest()方法的實作:

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在目前RenderObject的_size範圍裡,判斷hitTestChildren()或hitTestSelf()是不是傳回true,如果是的話将自己封裝成BoxHitTestEntry添加到HitTestResult中。也就是說隻要子或自己命中手勢事件,就添加到HitTestResult。在這裡要注意的是,會優先判斷hitTestChildren(),這個方法是判斷子是否有命中手勢,如果子命中了就不會再走hitTestSelf()的判斷,進而子的優先級較高,會被優先加入到HitTestResult的_path隊列中。我們看看hitTestChildren()的實作:

@protected
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
           

hitTestChildren()是個抽象方法,由子類實作。我們舉個例子,假設目前Widget是個Align,它所對應的RenderObject對象其實是個RenderShiftedBox,它的hitTestChildren()實作如下:

@override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    if (child != null) {
      final BoxParentData childParentData = child!.parentData! as BoxParentData;
      return result.addWithPaintOffset(
        offset: childParentData.offset,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(transformed == position - childParentData.offset);
          return child!.hitTest(result, position: transformed);
        },
      );
    }
    return false;
  }
           
bool addWithPaintOffset({
    required Offset? offset,
    required Offset position,
    required BoxHitTest hitTest,
  }) {
    assert(position != null);
    assert(hitTest != null);
    final Offset transformedPosition = offset == null ? position : position - offset;
    if (offset != null) {
      pushOffset(-offset);
    }
    final bool isHit = hitTest(this, transformedPosition);
    if (offset != null) {
      popTransform();
    }
    return isHit;
  }

           

其中position是PointerEvent的position,childParentData記錄了子相對于父的偏移量。執行result.addWithPaintOffset()方法,其中hitTest是個callback,執行的是child!.hitTest(result, position: transformed)。也就是說hitTestChildren()方法是周遊整個RenderObject樹,遞歸執行 child!.hitTest()方法,去判斷子是否有命中手勢事件。如果已經沒有子或者子沒有命中的話,才會判斷自己是否命中,我們回過頭來看看hitTestSelf()的實作:

@protected
  bool hitTestSelf(Offset position) => false;
           

它也是個抽象方法,假設我們點選的是個Image,它所對應的RenderObject是RenderImage,其hitTestSelf()的實作如下:

@override
  bool hitTestSelf(Offset position) => true;
           

隻要手勢事件的position在RenderImage的_size範圍内,就命中手勢事件。到此為止,hitTest的過程我們就梳理完成了。總結一下,hitTest實際上就是判斷目前Widget對應的RenderObject是否命中手勢事件。

  • 在移動端隻有PointerDownEvent事件會進行命中測試;
  • 周遊整個RenderObject樹,優先對子進行命中測試,若子命中即先添加進HitTestResult中;
  • 若子沒有命中,則判斷自己是否命中;
  • 隻要子或者自己命中,都會加到HitTestResult中。

我們将hitTest()的處理流程總結成流程圖如下:

一文搞懂Flutter的手勢事件——事件分發與沖突處理詳解

在命中測試完成得到HitTestResult後,Flutter就可以進行事件分發了,分發給HitTestResult的每一個成員進行處理,具體的處理流程是怎樣的呢?怎麼處理沖突的呢?我們繼續分析dispatchEvent()的實作。

dispatchEvent()

dispatchEvent()對手勢事件做分發,我們回到之前的代碼_handlePointerEventImmediately():

void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
      assert(!_hitTests.containsKey(event.pointer));
      hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
//...
    } else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
      hitTestResult = _hitTests.remove(event.pointer);
    } else if (event.down || event is PointerPanZoomUpdateEvent) {
      hitTestResult = _hitTests[event.pointer];
    }
//...
    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      dispatchEvent(event, hitTestResult);
    }
  }
           

當hitTestResult不為空,即有命中的情況下,則執行dispatchEvent()方法,其實作如下:

void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
    assert(!locked);
    if (hitTestResult == null) {
      assert(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);
      } catch (exception, stack) {
//...
      }
    }
  }
           

周遊HitTestResult.path,執行entry.target.handleEvent()方法,target就是每個命中的RenderObject對象。到這裡我們可以清晰的看到,先進入HitTestResult.path隊列裡的target優先執行handleEvent(),上一章節我們提到子優先于父被加入HitTestResult.path,是以子也優先于父實作handleEvent()。

/// Override this method to receive events.
  void handleEvent(PointerEvent event, HitTestEntry<HitTestTarget> entry);
           

handleEvent()是HitTestTarget這個抽象類的方法,需要其子類去實作,這裡要說的是RenderObject實作了HitTestTarget,也就是說是由RenderObject去實作handleEvent()。另外GestureBinding也是個HitTestTarget,我們看看GestureBinding的實作:

@override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
      gestureArena.sweep(event.pointer);
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
    }
  }
           

首先會執行 pointerRouter.route(event),這個方法的作用是将手勢事件首先分發給各個PointerRoute去處理:

void route(PointerEvent event) {
    final Map<PointerRoute, Matrix4?>? routes = _routeMap[event.pointer];
    final Map<PointerRoute, Matrix4?> copiedGlobalRoutes = Map<PointerRoute, Matrix4?>.of(_globalRoutes);
    if (routes != null) {
      _dispatchEventToRoutes(
        event,
        routes,
        Map<PointerRoute, Matrix4?>.of(routes),
      );
    }
    _dispatchEventToRoutes(event, _globalRoutes, copiedGlobalRoutes);
  }
           

_routeMap是什麼呢?我們舉個例子:

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MaterialApp(
        home: Scaffold(
            body: Center(
              child: GestureDetector(
                onTap: () {
                  print("onTap");
                },
                onLongPress: () {
                  print("onLongPress");
                },
                child: const Text("GestureDetector test"),
              ),
            ),

      ),
    );
  }
           

使用GestureDetector監聽onTap和onLongPress事件。代碼運作後我們都知道結果:單擊Text會列印onTap的log,長按Text會列印onLongPress的log。具體的原因之後我們再詳細分析,先說個結論:onTap和onLongPress将會注冊兩個PointerRoute存到_routeMap中。PointerRoute實際上是個callback。我們回到route()方法,看_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);
      }
    });
  }
           

周遊copiedRoutes執行_dispatch()方法:

@pragma('vm:notify-debugger-on-exception')
  void _dispatch(PointerEvent event, PointerRoute route, Matrix4? transform) {
    try {
      event = event.transformed(transform);
      route(event);
    } catch (exception, stack) {
//...
    }
  }
           

執行route(event)。具體的實作之後的章節再做說明。我們回到handleEvent()方法:

@override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
      gestureArena.sweep(event.pointer);
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
    }
  }
           

如果是PointerDownEvent,則執行gestureArena.close(event.pointer),如果是PointerUpEvent,則執行gestureArena.sweep(event.pointer)。這時候我們就有個疑問,gestureArena是什麼?gestureArena是個GestureArenaManager對象,是個手勢競技場管理類,它有個成員變量_arenas:

final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
           

_GestureArena記錄了所有手勢競技成員members:

final List<GestureArenaMember> members = <GestureArenaMember>[];
           

之前例子中我們提到的單擊和長按其實各自都會被封裝成一個GestureArenaMember,而GestureArenaManager的作用就是判定競技場中GestureArenaMember的勝負,即最終響應的是什麼事件。我們再回到handleEvent()方法分析gestureArena.close(event.pointer)的實作:

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

在PointerDownEvent的時候,會分發手勢事件給所有的PointerRoute,待PointerRoute都注冊完成後,關閉手勢競技場,調用_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!);
    }
  }
           

這個方法的作用是确定競技場裡的哪個GestureArenaMember勝利。比方說我們之前的例子如果隻注冊了onTap事件而沒有注冊onLongPress,那麼state.members.length == 1為true,則調用_resolveByDefault(pointer, state):

void _resolveByDefault(int pointer, _GestureArena state) {
    if (!_arenas.containsKey(pointer)) {
      return; // This arena has already resolved.
    }
    assert(_arenas[pointer] == state);
    assert(!state.isOpen);
    final List<GestureArenaMember> members = state.members;
    assert(members.length == 1);
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
  }
           

_arenas裡移除目前pointer,競技場中唯一的成員調用acceptGesture(pointer)宣布勝利。acceptGesture(pointer)是個抽象方法,之後我們再舉例說明它的實作。如果注冊了多個PointerRoute且state.eagerWinner != null說明競技場裡有的member優先級高,會直接宣告勝利。我們看一下_resolveInFavorOf(pointer, state, state.eagerWinner!)的實作:

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
    _arenas.remove(pointer);
    for (final GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member) {
        rejectedMember.rejectGesture(pointer);
      }
    }
    member.acceptGesture(pointer);
  }
           

宣告其它member失敗,且宣告eagerWinner勝利。分析完gestureArena.close(event.pointer),我們再回到handleEvent(),在PointerUpEvent時會執行gestureArena.sweep(event.pointer):

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

它的作用是清掃手勢競技場。如果state.isHeld為true,說明競技場裡有的成員生命周期比較長,需要等待,是以先不做處理。比方說上面的例子中onDoubleTap事件就會設定state.isHeld為true。否則如将會宣告競技場中第一個加入的成員勝利,其他成員失敗。到此為止,GestureBinding中的dispatchEvent()就分析完畢了,我們總結一下:

  • 先周遊HitTestEntry,執行entry.target.handleEvent()方法,順序是子優先;
  • 針對每一個target,執行PointerRoute.route(event),這個方法的作用是将手勢事件首先分發給PointerRouter裡_routeMap的各個成員去處理;
  • 在PointerDownEvent的時候關閉手勢競技場,并根據條件判定目前就可決定的勝利成員;
  • 在PointerUpEvent時清掃手勢競技場,并最終判定勝利的成員。

我們将dispatchEvent()的處理流程總結成流程圖如下:

一文搞懂Flutter的手勢事件——事件分發與沖突處理詳解

到此為止,我們知道了手勢事件是怎麼分發和解決沖突的。接下來我們通過分析GestureDetector來看一下細節的實作。

GestureDetector

我們舉個例子:

@override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'BuildContext Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MaterialApp(
        home: Scaffold(
            body: Center(
              child: GestureDetector(
                onTap: () {
                  print("onTap");
                },
                onLongPress: () {
                  print("onLongPress");
                },
                onDoubleTap: () {
                  print("onDoubleTap");
                },
                onVerticalDragDown: (details) {
                  print("onVerticalDragDown");
                },
                child: Container(
                  alignment: Alignment.center,
                  height: 500,
                  color: Colors.amber,
                  child: const Text("GestureDetector test"),
                ),
              ),
            ),
      ),
    );
  }
           

以上代碼的執行結果我們非常清楚:

  • 單擊列印onTap
  • 長按列印onLongPress
  • 輕按兩下列印onDoubleTap
  • 豎向拖拽列印onVerticalDragDown

以上我們可已知onTap會分别和其他三個事件沖突。假設我們先隻監聽onTap事件,看看GestureDetector的核心源碼實作:

onTap

@override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
    final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;

    if (onTapDown != null ||
        onTapUp != null ||
        onTap != null ||
        onTapCancel != null ||
        onSecondaryTap != null ||
        onSecondaryTapDown != null ||
        onSecondaryTapUp != null ||
        onSecondaryTapCancel != null||
        onTertiaryTapDown != null ||
        onTertiaryTapUp != null ||
        onTertiaryTapCancel != null
    ) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel
            ..onSecondaryTap = onSecondaryTap
            ..onSecondaryTapDown = onSecondaryTapDown
            ..onSecondaryTapUp = onSecondaryTapUp
            ..onSecondaryTapCancel = onSecondaryTapCancel
            ..onTertiaryTapDown = onTertiaryTapDown
            ..onTertiaryTapUp = onTertiaryTapUp
            ..onTertiaryTapCancel = onTertiaryTapCancel
            ..gestureSettings = gestureSettings;
        },
      );
    }

//...

    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
           

建立一個TapGestureRecognizer對象,并存到gestures這個Map中,TapGestureRecognizer的繼承關系如下:

class TapGestureRecognizer extends BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer extends GestureRecognizer extends GestureArenaMember
           

我們可以看到其實它就是一個GestureArenaMember競技場成員對象。然後傳回一個RawGestureDetector的Widget。RawGestureDetector是個StatefulWidget,我們看看它對應的RawGestureDetectorState中initState()的實作:

@override
  void initState() {
    super.initState();
    _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this);
    _syncAll(widget.gestures);
  }
           
void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    assert(_recognizers != null);
    final Map<Type, GestureRecognizer> oldRecognizers = _recognizers!;
    _recognizers = <Type, GestureRecognizer>{};
    for (final Type type in gestures.keys) {
      assert(gestures[type] != null);
      assert(gestures[type]!._debugAssertTypeMatches(type));
      assert(!_recognizers!.containsKey(type));
      _recognizers![type] = oldRecognizers[type] ?? gestures[type]!.constructor();
      assert(_recognizers![type].runtimeType == type, 'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers![type].runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
      gestures[type]!.initializer(_recognizers![type]!);
    }
    for (final Type type in oldRecognizers.keys) {
      if (!_recognizers!.containsKey(type)) {
        oldRecognizers[type]!.dispose();
      }
    }
  }
           

_syncAll()方法的作用是将傳入的gestures給_recognizers對象指派。再看看build()的實作:

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

傳回一個Listener。這個Listener我們在實踐中也非常熟悉。監聽并識别所有最底層的基礎手勢事件,但不做處理。它是個SingleChildRenderObjectWidget,我們看看其createRenderObject的實作:

@override
  RenderPointerListener createRenderObject(BuildContext context) {
    return RenderPointerListener(
      onPointerDown: onPointerDown,
      onPointerMove: onPointerMove,
      onPointerUp: onPointerUp,
      onPointerHover: onPointerHover,
      onPointerCancel: onPointerCancel,
      onPointerPanZoomStart: onPointerPanZoomStart,
      onPointerPanZoomUpdate: onPointerPanZoomUpdate,
      onPointerPanZoomEnd: onPointerPanZoomEnd,
      onPointerSignal: onPointerSignal,
      behavior: behavior,
    );
  }
           

在之前的章節我們提到過,在dispatchEvent()時會周遊HitTestEntry,執行entry.target.handleEvent()方法,target就是每個命中的RenderObject對象。是以我們看看RenderPointerListener裡handleEvent()的實作:

@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 PointerPanZoomStartEvent) {
      return onPointerPanZoomStart?.call(event);
    }
    if (event is PointerPanZoomUpdateEvent) {
      return onPointerPanZoomUpdate?.call(event);
    }
    if (event is PointerPanZoomEndEvent) {
      return onPointerPanZoomEnd?.call(event);
    }
    if (event is PointerSignalEvent) {
      return onPointerSignal?.call(event);
    }
  }
           

它非常的簡單,就是将基礎的手勢事件回調回去。我們回到RawGestureDetectorState的build():

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

在我們的示例中其實隻監聽了onPointerDown事件,其實作_handlePointerDown代碼如下:

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

周遊_recognizers,執行recognizer.addPointer(event):

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

現在的場景中isPointerAllowed()一定為true,執行 addAllowedPointer(event):

@protected
  void addAllowedPointer(PointerDownEvent event) { }
           

它是個抽象方法,我們看看子類BaseTapGestureRecognizer的實作:

@override
  void addAllowedPointer(PointerDownEvent event) {
    assert(event != null);
    if (state == GestureRecognizerState.ready) {
      if (_down != null && _up != null) {
        assert(_down!.pointer == _up!.pointer);
        _reset();
      }
      _down = event;
    }
    if (_down != null) {
      super.addAllowedPointer(event);
    }
  }
           

實際上調的就是super的方法,即PrimaryPointerGestureRecognizer的addAllowedPointer(event):

@override
  void addAllowedPointer(PointerDownEvent event) {
    super.addAllowedPointer(event);
    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));
      }
    }
  }
           

繼續調super,即OneSequenceGestureRecognizer的addAllowedPointer(event):

@override
  @protected
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
  }
           

繼續追蹤startTrackingPointer()的實作:

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

首先就是調用PointerRouter的addRoute()方法,這個是不是很熟悉?

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

它的實作就是往我們之前章節提到的_routeMap添加成員,等待分發手勢事件後的回調。然後調用_addPointerToArena()。這個方法從命名中就可得知,它是往手勢競技場中添加成員:

GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null) {
      return _team!.add(pointer, this);
    }
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }
           
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);
  }
           

好的現在在接收到PointerDownEvent時,onTap相應的GestureArenaMember已經進入手勢競技場了,收到任何手勢事件都會執行之前章節分析過的:針對每一個target,執行PointerRoute.route(event),這個方法的作用是将手勢事件首先分發給PointerRouter裡_routeMap的各個成員去處理。其實作在各個GestureArenaMember的handleEvent()中,我們來看PrimaryPointerGestureRecognizer對handleEvent()的實作:

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

這段代碼實際上是先計算一下,如果接收到的是PointerMoveEvent事件時,移動距離是不是足夠小,如果是的話,即可以繼續處理onTap事件,會調用 handlePrimaryPointer(event):

@protected
  void handlePrimaryPointer(PointerEvent event);
           

其實作在BaseTapGestureRecognizer:

@override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
      _up = event;
      _checkUp();
    } else if (event is PointerCancelEvent) {
      resolve(GestureDisposition.rejected);
      if (_sentTapDown) {
        _checkCancel(event, '');
      }
      _reset();
    } else if (event.buttons != _down!.buttons) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer!);
    }
  }
           

由于目前我們還在處理PointerDownEvent事件,是以不會做任何事情。到此為止,接收到PointerDownEvent事件時,最重要的事情就是将TapGestureRecognizer添加到手勢競技場等待。在隻監聽onTap事件的時候,會判斷接收到PointerMoveEvent時的移動距離,如果足夠小的話會繼續等待。接下來,我們手勢擡起,接收PointerUpEvent,我們看看會發生什麼。由于PrimaryPointerGestureRecognizer的handleEvent()對PointerUpEvent并沒有做什麼,我們回到GestureBinding的handleEvent():

@override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
      gestureArena.sweep(event.pointer);
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
    }
  }
           

之前章節分析過,PointerUpEvent是會走gestureArena.sweep(event.pointer)清掃手勢競技場:

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

由于目前競技場中隻有TapGestureRecognizer這一個成員,會執行到 state.members.first.acceptGesture(pointer)宣告勝利。acceptGesture()的實作在BaseTapGestureRecognizer中:

//BaseTapGestureRecognizer
  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (pointer == primaryPointer) {
      _checkDown();
      _wonArenaForPrimaryPointer = true;
      _checkUp();
    }
  }
           
//PrimaryPointerGestureRecognizer
  @override
  void acceptGesture(int pointer) {
    if (pointer == primaryPointer) {
      _stopTimer();
      _gestureAccepted = true;
    }
  }
           

先調用super的acceptGesture()方法,停止計時器。然後調用_checkDown()和_checkUp(),給業務層相應的回調去處理。同時設定 _wonArenaForPrimaryPointer為true标記勝利。是以其實在單擊事件中,down和up事件是在手勢擡起收到PointerUpEvent處理完成後才回調給使用者進行處理的。到此為止,我們使用GestureDetector監聽onTap事件的流程就分析完了。那麼現在我們增加針對onLongPress的監聽,看看手勢競技場是怎麼處理的。

onLongPress

我們回到GestureDetector的build()方法:

if (onLongPressDown != null ||
        onLongPressCancel != null ||
        onLongPress != null ||
        onLongPressStart != null ||
        onLongPressMoveUpdate != null ||
        onLongPressUp != null ||
        onLongPressEnd != null ||
        onSecondaryLongPressDown != null ||
        onSecondaryLongPressCancel != null ||
        onSecondaryLongPress != null ||
        onSecondaryLongPressStart != null ||
        onSecondaryLongPressMoveUpdate != null ||
        onSecondaryLongPressUp != null ||
        onSecondaryLongPressEnd != null ||
        onTertiaryLongPressDown != null ||
        onTertiaryLongPressCancel != null ||
        onTertiaryLongPress != null ||
        onTertiaryLongPressStart != null ||
        onTertiaryLongPressMoveUpdate != null ||
        onTertiaryLongPressUp != null ||
        onTertiaryLongPressEnd != null) {
      gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
        () => LongPressGestureRecognizer(debugOwner: this),
        (LongPressGestureRecognizer instance) {
          instance
            ..onLongPressDown = onLongPressDown
            ..onLongPressCancel = onLongPressCancel
            ..onLongPress = onLongPress
            ..onLongPressStart = onLongPressStart
            ..onLongPressMoveUpdate = onLongPressMoveUpdate
            ..onLongPressUp = onLongPressUp
            ..onLongPressEnd = onLongPressEnd
            ..onSecondaryLongPressDown = onSecondaryLongPressDown
            ..onSecondaryLongPressCancel = onSecondaryLongPressCancel
            ..onSecondaryLongPress = onSecondaryLongPress
            ..onSecondaryLongPressStart = onSecondaryLongPressStart
            ..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
            ..onSecondaryLongPressUp = onSecondaryLongPressUp
            ..onSecondaryLongPressEnd = onSecondaryLongPressEnd
            ..onTertiaryLongPressDown = onTertiaryLongPressDown
            ..onTertiaryLongPressCancel = onTertiaryLongPressCancel
            ..onTertiaryLongPress = onTertiaryLongPress
            ..onTertiaryLongPressStart = onTertiaryLongPressStart
            ..onTertiaryLongPressMoveUpdate = onTertiaryLongPressMoveUpdate
            ..onTertiaryLongPressUp = onTertiaryLongPressUp
            ..onTertiaryLongPressEnd = onTertiaryLongPressEnd
            ..gestureSettings = gestureSettings;
        },
      );
    }
           

在長按時會建立一個LongPressGestureRecognizer。我們看看LongPressGestureRecognizer的構造方法:

LongPressGestureRecognizer({
    Duration? duration,
    // TODO(goderbauer): remove ignore when https://github.com/dart-lang/linter/issues/3349 is fixed.
    // ignore: avoid_init_to_null
    super.postAcceptSlopTolerance = null,
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
    super.kind,
    super.supportedDevices,
    super.debugOwner,
  }) : super(
         deadline: duration ?? kLongPressTimeout,
       );
           
const Duration kLongPressTimeout = Duration(milliseconds: 500);
           

super裡傳給父類PrimaryPointerGestureRecognizer的deadline預設是500ms,這個是觸發長按事件的時長。我們回憶一下之前章節提到的,在GestureDetector走build()的時候,建立了一個Listener元件,監聽其onPointerDown方法,onPointerDown的實作是_handlePointerDown(),其代碼如下:

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

周遊_recognizers,執行recognizer.addPointer(event):

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

由于LongPressGestureRecognizer沒有實作addAllowedPointer()方法,我們看看addAllowedPointer()在PrimaryPointerGestureRecognizer的實作:

@override
  void addAllowedPointer(PointerDownEvent event) {
    super.addAllowedPointer(event);
    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));
      }
    }
  }
           

這裡我們可以看到,如果deadline不為空的話,啟動一個500ms的Timer,到時間後調用 didExceedDeadlineWithEvent(event)方法:

@override
  void didExceedDeadline() {
    // Exceeding the deadline puts the gesture in the accepted state.
    resolve(GestureDisposition.accepted);
    _longPressAccepted = true;
    super.acceptGesture(primaryPointer!);
    _checkLongPressStart();
  }
           

會在此時調用acceptGesture()宣告LongPressGestureRecognizer勝利,并回調onLongPress給業務層進行處理。而不會等待PointerUpEvent再去判定勝利者。給業務層的體驗就是,長按時,不需手勢擡起,就能收到onLongPress回調。好了onLongPress是怎麼處理的我們也分析完了,我們繼續分析示例中的onDoubleTap輕按兩下事件的處理。

onDoubleTap

我們還是回到GestureDetector的build()方法:

if (onDoubleTap != null) {
      gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
        () => DoubleTapGestureRecognizer(debugOwner: this),
        (DoubleTapGestureRecognizer instance) {
          instance
            ..onDoubleTapDown = onDoubleTapDown
            ..onDoubleTap = onDoubleTap
            ..onDoubleTapCancel = onDoubleTapCancel
            ..gestureSettings = gestureSettings;
        },
      );
    }
           

建立了DoubleTapGestureRecognizer。關鍵處理在它的addAllowedPointer()方法裡:

@override
  void addAllowedPointer(PointerDownEvent event) {
    if (_firstTap != null) {
      if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
        // Ignore out-of-bounds second taps.
        return;
      } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
        // Restart when the second tap is too close to the first (touch screens
        // often detect touches intermittently), or when buttons mismatch.
        _reset();
        return _trackTap(event);
      } else if (onDoubleTapDown != null) {
        final TapDownDetails details = TapDownDetails(
          globalPosition: event.position,
          localPosition: event.localPosition,
          kind: getKindForPointer(event.pointer),
        );
        invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
      }
    }
    _trackTap(event);
  }
           

一開始_firstTap一定是空,直接執行 _trackTap(event)方法:

void _trackTap(PointerDownEvent event) {
    _stopDoubleTapTimer();
    final _TapTracker tracker = _TapTracker(
      event: event,
      entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
      doubleTapMinTime: kDoubleTapMinTime,
      gestureSettings: gestureSettings,
    );
    _trackers[event.pointer] = tracker;
    tracker.startTrackingPointer(_handleEvent, event.transform);
  }
           

建立一個_TapTracker對象,向手勢競技場注冊自己然後調用tracker.startTrackingPointer(_handleEvent, event.transform)。

void startTrackingPointer(PointerRoute route, Matrix4? transform) {
    if (!_isTrackingPointer) {
      _isTrackingPointer = true;
      GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
    }
  }
           

将route添加到_routeMap中,等待手勢觸發回調。我們繼續按看看_handleEvent ()的實作:

void _handleEvent(PointerEvent event) {
    final _TapTracker tracker = _trackers[event.pointer]!;
    if (event is PointerUpEvent) {
      if (_firstTap == null) {
        _registerFirstTap(tracker);
      } else {
        _registerSecondTap(tracker);
      }
    } else if (event is PointerMoveEvent) {
      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
        _reject(tracker);
      }
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
    }
  }
           

主要關注PointerUpEvent事件的接收,因為輕按兩下事件就是判斷兩次PointerUpEvent事件是否符合輕按兩下的條件,我們先來看首次判斷時_firstTap == null,調用_registerFirstTap(tracker)的情況:

void _registerFirstTap(_TapTracker tracker) {
    _startDoubleTapTimer();
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
    // Note, order is important below in order for the clear -> reject logic to
    // work properly.
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _clearTrackers();
    _firstTap = tracker;
  }
           

先調用_startDoubleTapTimer(),啟動輕按兩下的事件Timer,預設是300ms。然後調用了GestureBinding.instance.gestureArena.hold(tracker.pointer):

void hold(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null) {
      return; // This arena either never existed or has been resolved.
    }
    state.isHeld = true;
    assert(_debugLogDiagnostic(pointer, 'Holding', state));
  }
           

會将state.isHeld設定為true。然後我們回到GestureBinding的handleEvent():

@override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
      gestureArena.close(event.pointer);
    } else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
      gestureArena.sweep(event.pointer);
    } else if (event is PointerSignalEvent) {
      pointerSignalResolver.resolve(event);
    }
  }
           

在接收到PointerUpEvent時執行gestureArena.sweep(event.pointer):

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.
    }
//...
  }
           

如果state.isHeld為true,說明有事件在等待判定,不可以在此時宣布誰勝利,直接return。當收到第二個點選事件時,回到addAllowedPointer():

@override
  void addAllowedPointer(PointerDownEvent event) {
    if (_firstTap != null) {
      if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
        // Ignore out-of-bounds second taps.
        return;
      } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
        // Restart when the second tap is too close to the first (touch screens
        // often detect touches intermittently), or when buttons mismatch.
        _reset();
        return _trackTap(event);
      } else if (onDoubleTapDown != null) {
        final TapDownDetails details = TapDownDetails(
          globalPosition: event.position,
          localPosition: event.localPosition,
          kind: getKindForPointer(event.pointer),
        );
        invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
      }
    }
    _trackTap(event);
  }
           

如果兩次點選時間小于輕按兩下門檻值的話,重新執行 _trackTap(event)。我們回到DoubleTapGestureRecognizer的_handleEvent ():

void _handleEvent(PointerEvent event) {
    final _TapTracker tracker = _trackers[event.pointer]!;
    if (event is PointerUpEvent) {
      if (_firstTap == null) {
        _registerFirstTap(tracker);
      } else {
        _registerSecondTap(tracker);
      }
    } else if (event is PointerMoveEvent) {
      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
        _reject(tracker);
      }
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
    }
  }
           

_firstTap不為空的情況,說明接收到第二個點選事件了,追蹤_registerSecondTap()的實作:

void _registerSecondTap(_TapTracker tracker) {
    _firstTap!.entry.resolve(GestureDisposition.accepted);
    tracker.entry.resolve(GestureDisposition.accepted);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _checkUp(tracker.initialButtons);
    _reset();
  }
           

兩個手勢事件都執行resolve(GestureDisposition.accepted)方法,這個方法其實就是宣告勝利或失敗的,我們看看它的實作:

void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
           
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null) {
      return; // This arena has already resolved.
    }
    assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
    assert(state.members.contains(member));
    if (disposition == GestureDisposition.rejected) {
//...
    } else {
      assert(disposition == GestureDisposition.accepted);
      if (state.isOpen) {
        state.eagerWinner ??= member;
      } else {
        assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
        _resolveInFavorOf(pointer, state, member);
      }
    }
  }
           

當傳入GestureDisposition.accepted時,會執行_resolveInFavorOf()方法:

void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    assert(state == _arenas[pointer]);
    assert(state != null);
    assert(state.eagerWinner == null || state.eagerWinner == member);
    assert(!state.isOpen);
    _arenas.remove(pointer);
    for (final GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member) {
        rejectedMember.rejectGesture(pointer);
      }
    }
    member.acceptGesture(pointer);
  }
           

在sweep()之前,直接宣告勝利。到此為止,onDoubleTap的流程也分析完成了。之前的示例中,我們還剩下一個onVerticalDragDown沒有分析。下面我們再分析一下onVerticalDragDown。

onVerticalDragDown

重新回到GestureDetector的build()方法:

if (onVerticalDragDown != null ||
        onVerticalDragStart != null ||
        onVerticalDragUpdate != null ||
        onVerticalDragEnd != null ||
        onVerticalDragCancel != null) {
      gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
        () => VerticalDragGestureRecognizer(debugOwner: this),
        (VerticalDragGestureRecognizer instance) {
          instance
            ..onDown = onVerticalDragDown
            ..onStart = onVerticalDragStart
            ..onUpdate = onVerticalDragUpdate
            ..onEnd = onVerticalDragEnd
            ..onCancel = onVerticalDragCancel
            ..dragStartBehavior= dragStartBehavior
            ..gestureSettings = gestureSettings;
        },
      );
    }
           

建立了VerticalDragGestureRecognizer對象,之後在down事件時會被加入到手勢競技場。VerticalDragGestureRecognizer沒有實作handleEvent(),而是它的父類DragGestureRecognizer實作的:

@override
  void handleEvent(PointerEvent event) {
//...
    if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) {
      final Offset delta = (event is PointerMoveEvent) ? event.delta : (event as PointerPanZoomUpdateEvent).panDelta;
      final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
      final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
      final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
      if (_state == _DragState.accepted) {
        _checkUpdate(
          sourceTimeStamp: event.timeStamp,
          delta: _getDeltaForDetails(localDelta),
          primaryDelta: _getPrimaryValueFromOffset(localDelta),
          globalPosition: position,
          localPosition: localPosition,
        );
      } else {
        _pendingDragOffset += OffsetPair(local: localDelta, global: delta);
        _lastPendingEventTimestamp = event.timeStamp;
        _lastTransform = event.transform;
        final Offset movedLocally = _getDeltaForDetails(localDelta);
        final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
        _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
          transform: localToGlobalTransform,
          untransformedDelta: movedLocally,
          untransformedEndPosition: localPosition
        ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
        if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
          resolve(GestureDisposition.accepted);
        }
      }
    }
    if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
      _giveUpPointer(event.pointer);
    }
  }
           

我們截取了部分重要的代碼,在PointerMoveEvent階段,會一直計算_globalDistanceMoved的值,_hasSufficientGlobalDistanceToAccept()用來判斷移動的距離是否大于固定的門檻值:

@override
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
  }
           

如果是的話,執行resolve(GestureDisposition.accepted),即在move階段宣告勝利。

手勢事件攔截

我們在開發過程中有時候需要對手勢事件進行攔截,這個是怎麼實作的呢?我們回想一下第三章講解的hitTest(),優先對子進行命中測試,如果命中即由子來實作手勢事件。這麼看來攔截的思路就是不要讓子進行hitTest()。Flutter中的AbsorbPointer就是來幹這件事情的。我們來看一下它的關鍵處理:

@override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    return absorbing
        ? size.contains(position)
        : super.hitTest(result, position: position);
  }
           

重寫hitTest(),如果absorbing為true,那麼直接判斷手勢事件的position是否已經在AbsorbPointer的範圍内,不會再執行super.hitTest(result, position: position)對子來判定hitTest()了。

總結

先上一張圖,簡單的說明一下Flutter處理手勢事件的流程:

一文搞懂Flutter的手勢事件——事件分發與沖突處理詳解

Flutter對于手勢事件的處理流程的思路其實非常直覺,在down事件時通過hitTest()确定可能觸發手勢事件的Widget并給它們設定優先級,再調用dispatchEvent對手勢事件進行分發并向競技場注冊成員等待判定。在handleEvent()時會根據條件解決沖突判定手勢的勝利者。本文通過源碼分析對Flutter的手勢事件分發與沖突處理進行了說明,同時通過GestureDetector的示例分析了不同沖突的具體處理方式,以及如何對手勢事件進行攔截,希望對大家了解Flutter的手勢事件有所幫助。

作者:狐友段夢瑤

來源:微信公衆号:搜狐技術産品

出處:https://mp.weixin.qq.com/s/e0kgBMwN-lAxnm9xpoS5vA

繼續閱讀