Flutter是如何处理一次点击事件

238 阅读3分钟

前言

点击事件从原生给出的回调开始处理,从PointerDownEvent开始收集命中的节点,到PointerUpEvent选出胜利者并响应点击回调结束。

在实现自研框架的扩大热区功能时,如何保证被绝对定位节点遮盖、滚出视口时不响应扩大热区成为一个难点,借机捋了一遍flutter的点击,也顺利的从整个流程中找到思路。

HitTest收集命中的节点列表

本质上是通过hitTest从根节点开始收集渲染树上所有的命中的节点

挂载原生回调:window.onPointerDataPacket = _handlePointerDataPacket; image.png

点击时调用hitTest,收集hitTestResult

hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);

image.png

RenderView 是 Flutter 渲染树的根节点(根渲染对象)
从根节点开始收集:renderView.hitTest(result, position: position); image.png

调用子节点的hitTest image.png

最终调用:RenderBox基类中的hitTest方法,判断position是否在当前的节点坐标范围内 image.png

其中RenderBox的hitTest,hitTestChildren和hitTestSelf可能会被override,比如stack重写了hitTestChildren
最终result中存储了所有坐标被position命中的节点,包括它自己,GestureBinding把自己加在了队列的末尾,用于最终的事件分发

  /// Determine which [HitTestTarget] objects are located at a given position.
  @override // from HitTestable
  void hitTest(HitTestResult result, Offset position) {
    result.add(HitTestEntry(this));
  }

hitTestResult不为空则循环调用集合中每一个对象的handleEvent方法,当然不是所有的节点都会消费此方法 image.png

例如GestureDetector最终由PrimaryPointerGestureRecognizer处理handleEvent,它的作用是调用手势竞技场的add方法,把自己作为一个参与者加入 image.png image.png 候调用sweep(打扫)

手势竞技

手势竞技决定最终由哪个节点响应点击事件

最终执行到GestureBinding的handleEvent时,会调用手势竞技场实例的相关方法决出胜利者
gestureArena是手势竞技场的实例,可以看作是本场手势竞技的法官,在down的事件时调用close,up的时候调用sweep(打扫) image.png

调用close把竞技场的状态打开状态置为false,并尝试去把这次竞技结束,并不一定能真的决出一个胜利者 image.png image.png

只有在up的时候才能真的去决出胜利者,调用sweep(打扫)竞技场
sweep中流程很简单,就是让竞争者中的第一位直接获取胜利,其他的拒绝响应。而竞争者中的第一个,就是就是Widget树中,最深的手势响应者。 image.png

响应点击

上述手势竞技中决出了胜利者,并调用了acceptGesture。其余命中组件调用rejectGesture

在GestureDetect组件中的acceptGesture源码如下,标记自己在本次竞技中获取胜利,然后调用checkDown和checkUp image.png

checkDown和checkUp分别对应响应handleTapDown和handleTapUp,其中handleTapUp会check _up是否存在,不存在则不会响应handleTapUp。
_up是在上面handlePrimaryPointer中赋值,识别为move不会赋值 image.png

最后,handleTapDown中响应onTapDown 
handleTapUp中响应onTapUp、onTap,完成一次点击事件的识别。 image.png

参考

大佬的文章介绍的很详细:深入进阶-从一次点击探寻Flutter事件分发原理