前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter触摸事件原理

Flutter触摸事件原理

作者头像
烧麦程
发布2022-05-10 20:50:18
1.4K0
发布2022-05-10 20:50:18
举报
文章被收录于专栏:半行代码

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 的触摸事件就清楚了,我们可以了解一些基本的事件处理机制,来解决开发中的一些疑惑。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 手势预处理
  • 碰撞测试
  • 事件分发
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档