likes
comments
collection
share

【Flutter】事件分发机制

作者站长头像
站长
· 阅读数 10

我正在参加「掘金·启航计划

缘起GestureBinding

1-onPointerDataPacket指针数据接收

flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding

  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
  }
  // 事件队列
   final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
    // 平台监听到的事件通过_pendingPointerEvents收集
  void _handlePointerDataPacket(ui.PointerDataPacket packet) {
    _pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
    if (!locked) {
      _flushPointerEventQueue();
    }
  }
  
  void _flushPointerEventQueue() {
    // 循环执行事件
    while (_pendingPointerEvents.isNotEmpty) {
      handlePointerEvent(_pendingPointerEvents.removeFirst());
    }
  }
  1. GestureBinding初始化方法initInstances中为平台分发器注册_handlePointerDataPacket
  2. 监听平台回调将事件保存在_pendingPointerEvents队列中
  3. 未锁状态下执行_flushPointerEventQueue,从队列中推出第一个事件向下分发,直到队列事件为空为止。

2-handlePointerEvent创建命中测试

flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding -> _handlePointerEventImmediately

  void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
     // 按下、开始缩放等操作时新建一个命中测试对象
      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) {
        // 已经按下或者是缩放事件时 可以在map中找到已存事件并获取
      hitTestResult = _hitTests[event.pointer];
    }
    ...
    if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      // 下一步进行分发
      dispatchEvent(event, hitTestResult);
    }
  }
  1. 事件对象进入_handlePointerEventImmediately判断事件类型做相应处理,并根据相应事件类型返回命中测试对象。
  2. 最后在将事件和生成的命中测试对象去执行dispatchEvent方法。

_handlePointerEventImmediately负责接收事件对象生成对应命中测试对象,将事件对象分三大类区分操作(大致可以分为按下、更新、抬起):按下会创建HitTestResult对象并向HitTestResult添加HitTestEntry命中对象最后保存到_hitTests表中;更新表示当前事件还存在从_hitTests表中取出即可;抬起表示当前事件结束了需要从从_hitTests表中移除。

hitTests命中测试

执行GestureBindinghitTest会先经过RenderBinding去执行hitTests方法,首先RenderView根节点判断是否存在子节点并判断子节点是否在命中范围内。

/flutter/packages/flutter/lib/src/rendering/binding.dart:RendererBinding->hitTest

  @override
  void hitTest(HitTestResult result, Offset position) {
    renderView.hitTest(result, position: position);
    super.hitTest(result, position);
  }

1-RenderView:hitTest

/flutter/packages/flutter/lib/src/rendering/view.dart:RenderView -> hitTest

  bool hitTest(HitTestResult result, { required Offset position }) {
    if (child != null) { // 子布局是否命中
      child!.hitTest(BoxHitTestResult.wrap(result), position: position);
    }
    result.add(HitTestEntry(this));
    return true;
  }

2-RenderBox:hitTest

RenderView根节点的子节点是RenderSemanticsAnnotations根本上是继承自RenderBox。默认情况下RenderBoxhitTestSelfhitTestChildren返回都是false,具体命中逻辑由继承者自行重写实现(hitTestChildren是具备有多子节点布局来实现默认情况下false表示无子节点)。

/flutter/packages/flutter/lib/src/rendering/box.dart:RenderBox -> 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;
  }
  @protected
  bool hitTestSelf(Offset position) => false;
  
  @protected
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;

若命中了自身hitTestSelf或子级hitTestChildren时添加BoxHitTestEntry(继承自HitTestEntry);hitTestChildren又是递归调用因此组件树命中测试是深度优先遍历,符合命中规则子节点会比父节点先被添加到HitTestResult队列中例如RenderDecoratedBoxhitTestSelf重写方法通过DecorationhitTest进行判断是否在命中范围内。

  @override
  bool hitTestSelf(Offset position) {
    return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
  }

X支线-defaultHitTestChildren命中逻辑

/flutter/packages/flutter/lib/src/rendering/stack.dart:RenderStack -> hitTestChildren

  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    return defaultHitTestChildren(result, position: position);
  }

Stack内部RenderStackhitTestChildren是采用RenderBoxContainerDefaultsMixin默认方法defaultHitTestChildren判断子节点是否在命中测试。

/flutter/packages/flutter/lib/src/rendering/box.dart:RenderBoxContainerDefaultsMixin -> defaultHitTestChildren

  bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
    ChildType? child = lastChild;
    while (child != null) {
      // The x, y parameters have the top left of the node's box as the origin.
      final ParentDataType childParentData = child.parentData! as ParentDataType;
      final bool isHit = result.addWithPaintOffset(
        offset: childParentData.offset,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(transformed == position - childParentData.offset);
          return child!.hitTest(result, position: transformed);
        },
      );
      if (isHit) {
        return true;
      }
      child = childParentData.previousSibling;
    }
    return false;
  }

方法中会循环遍历父节点中每个子节点对象是否命中测试,若有其中一个子节点命中则直接返回,因此其他兄弟子节点并没有机会参与命中测试而被直接忽略。由于父节点是RenderStack特殊性子节点存在重叠情况,在处理命中测试时就需要特殊处理。

  1. 类似Stack布局空间重叠的父节点其命中测试比较特殊
  2. 重叠空间中子节点遍历是倒序的,由于下层节点被上层遮盖所以先测试上层子节点。
  3. 如何打破这种规则让被遮盖的下层节点也能参与命中测试?

X支线-IgnorePointer命中忽略

IgnorePointer是支持可忽略命中组件,内部RenderIgnorePointer重写了hitTest方法,若ignoringtrue可跳过自身命中条件变为不可命中状态。

/flutter/packages/flutter/lib/src/rendering/proxy_box.dart:RenderIgnorePointer -> hitTest

  @override
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
    return !ignoring && super.hitTest(result, position: position);
  }

这就如上所述在Stack空间重叠布局中,当不可命中的IgnorePointer子节点在可点击子节点上层时不影响下层子节点参与命中测试。

dispatchEvent事件分发

flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding -> dispatchEvent

void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  
    if (hitTestResult == null) {
      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) {
        ...
      }
    }
  }
  1. pointerRouter接收事件类
  2. 遍历HitTestResultHitTestEntry数组执行handleEvent方法

【Flutter】事件分发机制 遍历顺序是从最底层子节点开始往上走,从最后的HitTestEntry可以看到分别是RenderViewGestureBinding两个类处理事件分发。

命中测试符合要求节点记录下来后通过该方法进行遍历操作并执行每个节点handleEvent方;handleEvent由每个节点重写实现具体逻辑。

1-handleEvent子分发

例如TextSpan重写是handleEvent方法,分发下来点击事件交给GestureRecognizer处理。

/flutter/packages/flutter/lib/src/painting/text_span.dart:TextSpan -> handleEvent

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    if (event is PointerDownEvent) {
      recognizer?.addPointer(event);
    }
  }

例子如下,TextSpan实现TapGestureRecognizer由其接受来自addPointer指针事件。

TextSpan(
  text"https://flutterchina.club",
  style: const TextStyle(color: Colors.blue),
  recognizer: TapGestureRecognizer()
    ..onTap = () {
      showSnackBarMsg(context, "onTap -> TextSpan");
    },
),

RawGestureDetectorSate_handlePointerDown方法会调用到addPointer会执行addAllowedPointer

/flutter/packages/flutter/lib/src/gestures/recognizer.dart:GestureRecognizer -> addPointer

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

接着会走到OneSequenceGestureRecognizeraddAllowedPointer->startTrackingPointer -> _addPointerToArena

/flutter/packages/flutter/lib/src/gestures/recognizer.dart:OneSequenceGestureRecognizer

  @override
  @protected
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
  }
  ...
  @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);
  }
  ...
  GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null) {
      return _team!.add(pointer, this);
    }
    // 重点
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }  

重点在最后调用到GestureBinding.instance.gestureArena

2-GestureArenaManager竞技场

GestureArenaManager来负责处理指针事件。这和上述提到的TextSpan实现有所不同。GestureArenaManager也是处理手势特殊管理类,它好比一个竞技场主要功能是找到真正胜利者来消费手势事件。例如上级下发一个指令只能由一个人来接手完成这个指令,如果每个人都响应指令可能会乱套,因此采用规则来找到唯一胜利者来接手指令。

/flutter/packages/flutter/lib/src/gestures/arena.dart:GestureArenaManager -> add

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

在上述提到OneSequenceGestureRecognizer事件分发最终会走到GestureArenaManageradd会将指针记录下来,等待竞技场开始比拼时做提前指针收集。

3-handleEvent最后分发

GestureBinding同样实现了HitTestTarget接口handleEvent方法,分发最后就是由GestureBinding接管最终分发结果。

/flutter/packages/flutter/lib/src/gestures/binding.dart: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);
    }
  }

事件分发由GestureArenaManager竞技场做最后决定,主要以三个方法处理:closesweepresolveGestureArenaManageradd是在各个子节点handleEvent分发时实现了。

/flutter/packages/flutter/lib/src/gestures/arena.dart:GestureArenaManager -> sweep

  void sweep(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null) {
      return; // This arena either never existed or has been resolved.
    }
    ...
    if (state.isHeld) {
      state.hasPendingSweeptrue;
      ...
      return; // This arena is being held for a long-lived member.
    }
    ...
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // 第一个成员竞争成功
      state.members.first.acceptGesture(pointer);
      // 其他成员竞争失败
      for (int i1; i < state.members.length; i++) {
        state.members[i].rejectGesture(pointer);
      }
    }
  }

关键性逻辑在GestureArenaManagersweep方法在竞技场中成员对象头一个将获胜获取到可以接收手势请求的机会;其他成员对象则没有机会。

/flutter/packages/flutter/lib/src/gestures/tap.dart:BaseTapGestureRecognizer -> acceptGesture

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

最后由BaseTapGestureRecognizer接收到手势执行方法处理手势事件。