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

 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()是个抽象方法,由子类实现。我们举个例子,假设当前Wi

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值