一文搞懂Flutter的手势事件——事件分发与冲突处理详解

news2024/12/23 23:46:40

 633f48ce3b8128f456cd93e045a25d31.gif

本文字数:43617

预计阅读时间:110分钟

前言

之前有两篇文章都围绕着runApp()进行展开,讲解了布局绘制的详细过程。

  • https://www.jianshu.com/p/2ef749ff4d40/

  • https://www.jianshu.com/p/f37f8da235ec

那么接下来我们想详细的说一说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,我们回到GestureBindinginitInstances()

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

这段代码的整体思路是:

  • 如果eventPointerDownEvent等四种event之一时(我们假设创建的是一个移动端app,这四种event只考虑PointerDownEvent的情况),创建一个HitTestResult对象,并调用hitTest()方法,然后将hitTestResult赋值给_hitTests[event.pointer];

  • 如果是PointerUpEventPointerCancelEvent,那么将此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中,HitTestResulttarget对象通常就是一个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中。由于mixinRendererBindingGesturesBinding的子类,而RendererBinding实现了hitTest()方法,所以我们看看RendererBindinghitTest()的实现:

@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()之前,先调用了renderViewhitTest()方法,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,如果是的话需要先执行childhitTest()方法,再将自己封装成一个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;
  }

其中positionPointerEventpositionchildParentData记录了子相对于父的偏移量。执行result.addWithPaintOffset()方法,其中hitTest是个callback,执行的是child!.hitTest(result, position: transformed)。也就是说hitTestChildren()方法是遍历整个RenderObject树,递归执行 child!.hitTest()方法,去判断子是否有命中手势事件。如果已经没有子或者子没有命中的话,才会判断自己是否命中,我们回过头来看看hitTestSelf()的实现:

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

它也是个抽象方法,假设我们点击的是个Image,它所对应的RenderObjectRenderImage,其hitTestSelf()的实现如下:

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

只要手势事件的positionRenderImage_size范围内,就命中手势事件。到此为止,hitTest的过程我们就梳理完成了。总结一下,hitTest实际上就是判断当前Widget对应的RenderObject是否命中手势事件。

  • 在移动端只有PointerDownEvent事件会进行命中测试;

  • 遍历整个RenderObject树,优先对子进行命中测试,若子命中即先添加进HitTestResult中;

  • 若子没有命中,则判断自己是否命中;

  • 只要子或者自己命中,都会加到HitTestResult中。

我们将hitTest()的处理流程总结成流程图如下:

5c42a4808a981fc52ed636fe839effb0.jpeg

在命中测试完成得到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监听onTaponLongPress事件。代码运行后我们都知道结果:单击Text会打印onTaplog,长按Text会打印onLongPresslog。具体的原因之后我们再详细分析,先说个结论:onTaponLongPress将会注册两个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 == 1true,则调用_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)是个抽象方法,之后我们再举例说明它的实现。如果注册了多个PointerRoutestate.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.isHeldtrue,说明竞技场里有的成员生命周期比较长,需要等待,所以先不做处理。比方说上面的例子中onDoubleTap事件就会设置state.isHeldtrue。否则如将会宣告竞技场中第一个加入的成员胜利,其他成员失败。到此为止,GestureBinding中的dispatchEvent()就分析完毕了,我们总结一下:

  • 先遍历HitTestEntry,执行entry.target.handleEvent()方法,顺序是子优先;

  • 针对每一个target,执行PointerRoute.route(event),这个方法的作用是将手势事件首先分发给PointerRouter_routeMap的各个成员去处理;

  • PointerDownEvent的时候关闭手势竞技场,并根据条件判定当前就可决定的胜利成员;

  • PointerUpEvent时清扫手势竞技场,并最终判定胜利的成员。

我们将dispatchEvent()的处理流程总结成流程图如下:

78b03c579ddd99ac2f57b99124abc6a6.jpeg

到此为止,我们知道了手势事件是怎么分发和解决冲突的。接下来我们通过分析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竞技场成员对象。然后返回一个RawGestureDetectorWidgetRawGestureDetector是个StatefulWidget,我们看看它对应的RawGestureDetectorStateinitState()的实现:

@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对象。所以我们看看RenderPointerListenerhandleEvent()的实现:

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

它非常的简单,就是将基础的手势事件回调回去。我们回到RawGestureDetectorStatebuild()

@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的方法,即PrimaryPointerGestureRecognizeraddAllowedPointer(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,即OneSequenceGestureRecognizeraddAllowedPointer(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);
  }

首先就是调用PointerRouteraddRoute()方法,这个是不是很熟悉?

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的各个成员去处理。其实现在各个GestureArenaMemberhandleEvent()中,我们来看PrimaryPointerGestureRecognizerhandleEvent()的实现:

@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,我们看看会发生什么。由于PrimaryPointerGestureRecognizerhandleEvent()PointerUpEvent并没有做什么,我们回到GestureBindinghandleEvent()

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

先调用superacceptGesture()方法,停止计时器。然后调用_checkDown()_checkUp(),给业务层相应的回调去处理。同时设置 _wonArenaForPrimaryPointertrue标记胜利。所以其实在单击事件中,downup事件是在手势抬起收到PointerUpEvent处理完成后才回调给用户进行处理的。到此为止,我们使用GestureDetector监听onTap事件的流程就分析完了。那么现在我们增加针对onLongPress的监听,看看手势竞技场是怎么处理的。

onLongPress 

我们回到GestureDetectorbuild()方法:

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里传给父类PrimaryPointerGestureRecognizerdeadline默认是500ms,这个是触发长按事件的时长。我们回忆一下之前章节提到的,在GestureDetectorbuild()的时候,创建了一个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 

我们还是回到GestureDetectorbuild()方法:

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。然后我们回到GestureBindinghandleEvent()

@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.isHeldtrue,说明有事件在等待判定,不可以在此时宣布谁胜利,直接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 

重新回到GestureDetectorbuild()方法:

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(),如果absorbingtrue,那么直接判断手势事件的position是否已经在AbsorbPointer的范围内,不会再执行super.hitTest(result, position: position)对子来判定hitTest()了。

总结

先上一张图,简单的说明一下Flutter处理手势事件的流程:

dad0629dc06e7f5ba1ae193159fd2668.jpeg

Flutter对于手势事件的处理流程的思路其实非常直观,在down事件时通过hitTest()确定可能触发手势事件的Widget并给它们设置优先级,再调用dispatchEvent对手势事件进行分发并向竞技场注册成员等待判定。在handleEvent()时会根据条件解决冲突判定手势的胜利者。本文通过源码分析对Flutter的手势事件分发与冲突处理进行了说明,同时通过GestureDetector的示例分析了不同冲突的具体处理方式,以及如何对手势事件进行拦截,希望对大家理解Flutter的手势事件有所帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/600373.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

哔哩哔哩视频云画质与窄带高清AI落地实践

视频赛道卷到下半场&#xff0c;一定会面临体验与成本的对抗&#xff0c;尤其是在行业大环境“过冬”的背景下&#xff0c;想要在有限带宽下获得最佳的画质观感变得异常具备挑战性。从视频云业务场景的视角来看&#xff0c;如何有效解决cross-domain问题、如何突破低业务延迟下…

留学生用ChatGPT写论文?真的会被开除!!!

转眼进入6月&#xff0c;大家的论文完成的怎么样了&#xff1f;很多留学生都有这样的疑问&#xff0c;用ChatGPT写论文靠谱吗&#xff1f; 其实在最近&#xff0c;&#xff0c;ChatGPT被学生用来辅助写论文&#xff0c;已经不是什么新鲜事了。它作为新型人工智能&#xff0c;搜…

二十三种设计模式:状态模式

状态模式&#xff0c;就是把所有的状态抽象成一个个具体的类&#xff0c;然后继承一个抽象状态类&#xff0c;在每一个状态类内封装对应状态的行为&#xff0c;符合开放封闭原则&#xff0c;当增加新的状态或减少状态时&#xff0c;只需修改关联的类即可。很适合多分支行为方法…

SpringBoot使用flywaydb实现数据库版本管理【附源码】

一、项目背景 本文主要是配合SpringBoot使用用户输入的自定义数据源启动一文附带产出。前文主要介绍了SpringBoot无数据源启动&#xff0c;然后通过用户录入自定义数据库配置后&#xff0c;连接数据库的操作。但是当整个项目交给用户使用时&#xff0c;谁使用都不知道情况下&a…

JetBrains的.NET和ASP.NET集成开发环境Rider 2023版本在Linux系统的下载与安装配置教程

目录 前言一、Rider 安装二、使用配置总结 前言 Rider是一款专为.NET和ASP.NET开发人员设计的集成开发环境&#xff08;IDE&#xff09;。它提供了丰富的功能和工具&#xff0c;可以帮助开发人员更高效地编写、调试和部署.NET和ASP.NET应用程序。注&#xff1a;已在CentOS7.9和…

Java课程设计之购物车管理系统

一、项目准备 1、开发工具&#xff1a;JDK、Eclipse 2、需求分析&#xff1a; 包括商品管理和购物车管理。 1&#xff09;商品管理主要功能 商品信息导入 显示所有商品信息 2&#xff09;购物车主要功能 添加商品到购物车 修改购物车中的商品数量 显示购物车中的所有商…

运维小白必学篇之基础篇第六集:权限实验

权限实验 实验作业&#xff1a; 1、创建1.txt文件&#xff0c;修改1.txt文件权限为属主最大&#xff0c;属组读写&#xff0c;其他人无权限 2、单独为1.txt文件的属组赋予执行权限 3、修改1.txt的属组为a1 4、修改用户a2的登录shell为/bin/bash 5、创建a1用户&#xff0c;设置…

chatgpt赋能python:Python内置函数求和

Python内置函数求和 Python是一种易学易用的编程语言&#xff0c;是许多开发人员和数据分析师的首选语言。Python提供了多种内置函数来处理不同的任务&#xff0c;其中包括求和函数。本文将介绍Python中的内置求和函数以及如何使用它们。 Python内置求和函数 在Python中&…

【51单片机】AT24C20数据帧(I2C总线)

&#x1f38a;专栏【51单片机】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【Love Story】 &#x1f970;大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 小吉先向大家道个歉&#xff0c;因为最近在期末…

“微商城”项目(4首页)

1.显示轮播图 首页和商品详情页都有图片轮播图展示&#xff0c;考虑到Vue组件代码的复用性&#xff0c;把轮播图相关代码单独放置在src\components\swiper.vue文件中。 在src\pages\Home.vue文件中&#xff0c;编写HTML结构代码&#xff0c;示例代码如下。 <template>…

大数据AI课程更新——6月AI绘画入门小课

在这个课程中&#xff0c;我们将探索人工智能在绘画领域的应用&#xff0c;学习如何利用AI技术创造出令人惊叹的艺术作品。无论你是对绘画有兴趣的初学者&#xff0c;还是已经有一定绘画基础的学生&#xff0c;本训练营都将为你提供一个展示创造力和实践技巧的平台。 2022年是A…

永远年轻,永远在路上的AI TIME

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; 在2019年的智源人工智能大会上&#xff0c;我第一次接触AI TIME&#xff0c;时至今日&#xff0c;加入AI TIME已经四年了。在过去的四年里&#xff0c;AI技术、产业发生了翻天覆地的变化&#xff0c;我自己的思…

80后程序员涛哥的童年

博主&#xff1a;爱码叔 个人博客站点&#xff1a; icodebook 公众号&#xff1a;漫话软件设计 微博&#xff1a;程序员涛哥 专注于软件设计与架构、技术管理。擅长用通俗易懂的语言讲解技术。对技术管理工作有自己的一定见解。文章会第一时间首发在个站上&#xff0c;欢迎大家…

态路小课堂丨交换机堆叠—简化组网结构,增强网络可靠性

TARLUZ态路 01、什么是交换机堆叠 交换机堆叠是指将多台支持堆叠特性的交换机通过堆叠线缆连接起来&#xff0c;从逻辑上虚拟成一台交换设备&#xff0c;该交换机中的所有交换机共享相同的配置信息和路由信息。当向逻辑交换机增加和减少单体交换机时不会影响其性能。 02、有什么…

新鲜出炉

最近发现了一爆款APP重新来袭&#xff0c;心中不由感叹“经典永不过时”&#xff01;它可是能节省真金白银的&#xff0c;尤其是几年前&#xff0c;流量真是不便宜&#xff0c;出门到了哪里都是赶紧寻找wifi和密码&#xff0c;直到它的出现&#xff0c;可以完全自动化&#xff…

除了降价,阿里云还有王炸

1. 写本文的前因 最近业内多家云厂商都找到了简单的宣传方法&#xff0c;那就是跟风降列表价。我并不反对这种常规宣传&#xff0c;我那篇科普谈降价的文章&#xff0c;据说也让某些云参透了敲锣不要钱&#xff0c;宣传没损失。 在阵阵锣声中&#xff0c;我随口和朋友们发牢骚&…

盘点一个Python自动化办公需求,实现数据自动填充(上篇)

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 临别殷勤重寄词&#xff0c;词中有誓两心知。 大家好&#xff0c;我是皮皮。 一、前言 前几天遇到了一个小需求&#xff0c;粉丝自己在实际工作中的需求…

牛叉,一行代码不写,就可以开发系统

如今AI和低代码越来越火&#xff0c;可以瞬间完成一个系统的开发。 不用一行代码&#xff0c;轻松实现业务数字化&#xff0c;是怎么做到的&#xff1f; 前面小孟开发了大量的系统&#xff0c;很多时候不是我写代码多么快&#xff0c;也不是我技术多么的厉害&#xff0c;而是…

GPU与CPU

1 什么是GPU&#xff1f; GPU的英文全称Graphics Processing Unit&#xff0c;图形处理单元。通俗来说&#xff0c;GPU是一款专门的图形处理芯片&#xff0c;做图形渲染、数值分析、金融分析、密码破解&#xff0c;以及其他数学计算与几何运算的。GPU可以在PC、工作站、游戏主…

chatgpt赋能python:Python如何关闭对话框?

Python如何关闭对话框&#xff1f; 如果你是一名Python编程经验丰富的工程师&#xff0c;你一定已经遇到过需要在Python中关闭对话框的场景。在这篇文章中&#xff0c;我将详细介绍如何处理这个问题&#xff0c;并提供一些有用的技巧和建议。 什么是对话框&#xff1f; 在编…