Flutter触摸事件原理

2022-05-10 20:50:18 浏览数 (1)

Flutter 触摸事件的处理点在 GestureBinding中。在 GestureBinding 中存在一个 handlerPointerEvent方法,这个方法就是触摸事件在 Flutter 侧的触发点。

手势预处理

手势触发事件到事件分发给具体处理对象之前的流程如下图:

详细看下具体的源码:

代码语言:javascript复制
void handlePointerEvent(PointerEvent event) {
   if (resamplingEnabled) {
      _resampler.addOrDispatch(event);
      _resampler.sample(samplingOffset, _samplingClock);
      return;
    }
   _resampler.stop();
   _handlePointerEventImmediately(event);
}

正常情况下会执行 _handlePointerEventImmediately 方法:

这里区分不同的手势情况做了不同的处理:

  • 当手势是Down、Signal、Hover的时候,在移动端上我们一般关心 Down
代码语言:javascript复制
   hitTestResult = HitTestResult();
      hitTest(hitTestResult, event.position);
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }

这里会创建一个 HitTestResult 对象传给 hitTest 方法,并且把处理完成后的 HitTestResult放在一个 _hitTests 的 map 里面。

  • 当手势是抬起或者取消的时候

这次手势已经结束了,从 _hitTests 的map 里面移除这个result对象。

  • 不是手势触发的时候,但是仍然是down状态,这里可以理解成一个控件还是处于被按下的状态中。

因为这次完整的手势并没有结束,直接获取上一次的 HitTestResult对象。

代码语言:javascript复制
  if (hitTestResult != null ||
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position != null);
      dispatchEvent(event, hitTestResult);
    }

当 hitTestResult 不为 null 的时候,会根据这个结果进行事件的分发:那么 hitTest 是什么呢?

碰撞测试

直接按照名字看,hitTest 就是打击测试的意思。Flutter 会通过这个 test 去判断手势事件具体是交给谁处理。hitTest 会执行 RenderBinding 的 hitTest 方法,执行 renderView 的 hitTest 方法:

代码语言:javascript复制
 @override
  void hitTest(HitTestResult result, Offset position) {
    renderView.hitTest(result, position: position); 
  }

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

如果有子节点,会先执行子节点的 hitTest。会带上手势的 position。hitTest 本身的逻辑其实就是把 HitTestEntry 加入到 result 里面。

这里的 child 是 RenderBox

代码语言:javascript复制
// RenderBox
bool hitTest(BoxHitTestResult result, { required Offset position }) {
  if (_size!.contains(position)) {
    if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
      result.add(BoxHitTestEntry(this, position));
      return true;
    }
  }
  return false;
}

当这个 RenderBox的位置在这个 position 的范围的时候,会执行 hitTestChildren 方法。这个 hitTestChildren 就是各类组件的 RenderObject自己实现的了。我们来看一个例子,Stack 组件底层的 RenderBox 是一个 RenderStack对象,它的 hittestChildren 会执行 defaultHitTestChildren 方法:

代码语言:javascript复制
// RenderStack
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
  ChildType? child = lastChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    final bool isHit = result.addWithPaintOffset(
      offset: childParentData.offset,
      position: position,
      hitTest: (BoxHitTestResult result, Offset? transformed) {
        return child!.hitTest(result, position: transformed!);
      }
    );
   if (isHit)
      return true;
    child = childParentData.previousSibling;
  }
  return false;
}

这里会把每个子节点都执行一次 hitTest,如果命中了,结束这个方法。如果没有命中,就用子节点的兄弟节点去执行 hitTest,依次遍历执行。这里 Stack 就会有一个和 Android 不一样的地方:当我们在 Stack 最上面一层覆盖一个 Container 的话,那么这个 Container 会命中 hitTest 测试,这里就直接结束了整个 RenderStack 的 hitTest,即使下面一层还有组件可以响应手势,它也不会收到了。

HitTestResult 添加结果最终会把这些 HitTestEntry都添加到自己的 path 里面:

代码语言:javascript复制
 void add(HitTestEntry entry) {
    assert(entry._transform == null);
    entry._transform = _lastTransform;
    _path.add(entry);
  }

这里也就把手势命中的组件都收集到了一起。这一步完成了,下一步自然就是决定手势具体怎么分发和处理了。

事件分发

当手势的碰撞测试结束后,会继续去分发手势:

代码语言:javascript复制
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
  
  if (hitTestResult == null) {
    pointerRouter.route(event);
    return;
  }
  for (final HitTestEntry entry in hitTestResult.path) {
     entry.target.handleEvent(event.transformed(entry.transform), entry);
  }
}

这里简化了一些其他逻辑。当 hitTest 的结果不存在的时候,会执行 pointerRouter 的 route 方法。否则会把 hitTest 收集到的组件都执行一次 handleEvent 方法。

这个过程有点抽象,很难理解具体每一步的目的是什么,我们同样按照一个实际例子来看看具体的流程。这里我们使用我们最常用来处理手势的 GestureDetector

GestureDetector是一个 Widget,它的层级依次是:

代码语言:javascript复制
GestureDetector  --->  RawGestureDetector  ---->  Listener ---->  RenderPointerListener

结合上述的 hitTest 过程,path 里面已经存了多个组件。这些组件在 path 列表中,子组件在前,父组件在后。所以这里我们可以认为 path 里存的target的第一个对象是 RenderPointerListener,最后面则是 RenderViewGestureBinding

事件分发的流程如图:

RenderPointerListener 的 handleEvent 的实现如下:

代码语言:javascript复制
 // RenderPointerListener
 @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 PointerSignalEvent)
      return onPointerSignal?.call(event);
  }

处理向下手势的地方是:

代码语言:javascript复制
 // RawGestureDetectorState
 void _handlePointerDown(PointerDownEvent event) {
    for (final GestureRecognizer recognizer in _recognizers!.values)
      recognizer.addPointer(event);
  }

这里会由 GestureRecognizer 来执行 addPointer 方法,GestureRecognizer是一个抽象类。它的继承结构如下

image.png

这里调用的则是 OneSequenceGestureRecognizer 的 addPoint,也就是 GestureRecognizer的 addPoint

代码语言:javascript复制
 // GestureRecognizer
 void addPointer(PointerDownEvent event) {
    _pointerToKind[event.pointer] = event.kind;
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }

执行 addAllowPointer :

代码语言:javascript复制
// BaseTapGestureRecognizer

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

 // PrimaryPointerGestureRecognizer
 @override
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
      initialPosition = OffsetPair(local: event.localPosition, global: event.position);
      if (deadline != null)
        _timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
    }
  }


 /// OneSequenceGestureRecognizer
 void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
  }

 void startTrackingPointer(int pointer, [Matrix4? transform]) {
    GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
    _trackedPointers.add(pointer);
    assert(!_entries.containsValue(pointer));
    _entries[pointer] = _addPointerToArena(pointer);
  }

这里执行了 GestureBinding#pointerRouter 的 addRoute 方法。把事件添加到了手势路由表 和 gestureArena 里。getsureArena代表的则是一个手势竞技场,用来区分哪个手势胜出。

最后走到 GestureBidninghandleEvent:

代码语言:javascript复制
 @override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    pointerRouter.route(event);
    //....
  } 

这里会执行 PointerRouter 的 route 方法:

代码语言:javascript复制
 void route(PointerEvent event) {
    final Map<PointerRoute, Matrix4?>? routes = _routeMap[event.pointer];
    final Map<PointerRoute, Matrix4?> copiedGlobalRoutes = Map<PointerRoute, Matrix4?>.from(_globalRoutes);
    if (routes != null) {
      _dispatchEventToRoutes(
        event,
        routes,
        Map<PointerRoute, Matrix4?>.from(routes),
      );
    }
    _dispatchEventToRoutes(event, _globalRoutes, copiedGlobalRoutes);
  }

这里会执行 copiedGlobalRoutes 里面的每个方法。如果我们有这个 GestureDetector。就会调用

PrimaryPointerGestureRecognizer 的 handleEvent 方法:

代码语言:javascript复制
 /// PrimaryPointerGestureRecognizer
 @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);
  }

这里会判断手势有没有超出设定的范围,如果超出了,不算触发手势。执行 resolve 走 rejected 的流程。否则执行 handlePrimaryPointer 方法。

代码语言:javascript复制
@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!);
    }
}

这里处理了 up 和 cancel 2个手势。down 则没有处理,down 则会交给后续的 target 进行处理,也就是 GestureBinding。分头看看这几个事件:

Up: 调用到 handleTapUp 方法,执行我们传入的 onTapUp。

Cancel: 这里会调 _checkCancel 方法继而调用 handleTapCancel 方法,这里会执行我们传入的 onTapCancel 函数。

Down: 查看 GestureBinding 的处理:

代码语言:javascript复制
// GestureBinding
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  }else if (event is PointerUpEvent) {
    gestureArena.sweep(event.pointer);
  }else if (event is PointerSignalEvent) {
    pointerSignalResolver.resolve(event);
  }
}

// GestureArenaManager
void _tryToResolveArena(int pointer, _GestureArena state) {
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
  }
  //...
}

这里如果是down手势的时候分几种情况处理:

  • 竞技场只有一个成员 给这个成员处理
  • 竞技场没有内容 不处理
  • 存在竞技场的胜出者,给胜出者处理

如果是up手势,走 sweep 的逻辑:

代码语言:javascript复制
 void sweep(int pointer) {
   final _GestureArena? state = _arenas[pointer];
   if (state == null)
     return;
   if (state.members.isNotEmpty) {
     state.members.first.acceptGesture(pointer);
     for (int i = 1; i < state.members.length; i  )
       state.members[i].rejectGesture(pointer);
   }
 }

这里直接指定了第一个成员作为竞技场的胜出者。当我们写多个 GestureDetector嵌套的时候,最上层的子节点会最先进入竞技场,所以这个时候只有上面的那个才会响应我们的点击事件。

事件的 accept 和 reject 具体又做了什么呢?这里可以简单的看一下

代码语言:javascript复制
@override
void acceptGesture(int pointer) {
  super.acceptGesture(pointer);
  if (pointer == primaryPointer) {
    _checkDown();
   _wonArenaForPrimaryPointer = true;
   _checkUp();
  }
}

accept 里面执行了 down -> up

代码语言:javascript复制
@override
void rejectGesture(int pointer) {
  super.rejectGesture(pointer);
  if (pointer == primaryPointer) {
    if (_sentTapDown) {
     _checkCancel(null, 'forced');
    }
    _reset();
  }
}

如果是 down 手势,先执行 cancel。接着进行重置。这里总结一下上面的流程,事件在分发的过程中会依次遍历让命中的元素进行处理。当 GestureDetector 响应手势的时候,会把自己加入路由表和竞技场。事件从子节点往上传递的过程中会让竞技场里的元素进行竞争。竞争胜出的元素可以处理手势。当手势结束的时候竞技场会进行重新竞争,这时候竞争规则则是第一个元素胜出。

总结

最后总结一下 ,Flutter 触摸事件的处理分为两部分。

  • 利用hittets收集组件。
  • 分发事件,使用GestureArenaManager筛选事件的具体处理者。

到这里 Flutter 的触摸事件就清楚了,我们可以了解一些基本的事件处理机制,来解决开发中的一些疑惑。

0 人点赞