Flutter 事件分发原理及事件冲突处理

713 阅读7分钟

目录 : 

  1. Flutter 事件分发流程梳理
  2. Flutter事件竞技场梳理
  3. Flutter 手势事件冲突解决办法

一、Flutter 事件分发流程:

先看一下手指触摸时间的触发源头是从何产生的 ,并往何处去 ,看下面这个图

flutter_touch_event_1.png

如在Android中, FlutterView 的OnTouchEvent 方法接收到了Android产生的事件 , 通过C++的JNI 方法调度 ,最终传递到flutter 的dart 方法中 ,

flutter_touch_event_2.png

到:GestureBinding

flutter_touch_event_3.png

再到: GestureBinding

flutter_touch_event_4.png

这里只要事件队列不为空,就会循环从队列里面取事件进入下面的分发流程:

flutter 业务理解的事件起源主要核心分发点就在GestureBinding 类中 ,上面我们知道了事件的传递流程 ,再来具体看一下 GestureBinding 中做了哪些事情

flutter_touch_event_5.png

handlePointerEvent 方法中,开始将事件封装类PointerEvent 进行传递

flutter_touch_event_6.png

当是PointerDownEvent事件的时候,会新建一个HitTestResult对象,而这个HitTestResult对象里面有一个path的属性,这个属性就是用来记录事件传递所经过的的节点。 新建HitTestResult对象后,接下来重点就是调用GestureBinding.histTest方法。 在看看hitTest方法:

由于GestureBinding  和  RendererBinding  都在flutter 的入口runApp 时初始化WidgetsFlutterBinding 时初始化,WidgetsFlutterBinding with  混合了各个binding,而 RendererBinding 是  GestureBinding  的子类,所以hitTest 会覆盖, 先会调用

RendererBinding 的hitTest 方法:

flutter_touch_event_7.png

获取根view , rederView 并开始调用hitTest 方法,

flutter_touch_event_8.png

再到child 子控件中,如果触摸位置在界内 ,先判断是否在子控件的范围,如果在,则把子控件加入HitTestResult ,  然后将自己也加入 ,如果不在,则判断是否在自己的范围内,如果在,则将自己加入集合

flutter_touch_event_9.png

接下来就开始进入递归调用的流程 ,如果判断触摸的位置在view界内的 ,会根据深度优先原则 ,既子控件会更先的顺序  加入到HitTestResult

最后会通过super.hitTest 调用回到GestureBinding  

flutter_touch_event_10.png

将GestureBinding 自己也添加进HitTestResult  

在PointerDownEvent  中已经通过hitTest  找出了所有在触摸区域相关的控件 ,并且按深度优先的顺序放入了HitTestResult 中, 接下来就开始进入事件分发 :

flutter_touch_event_11.png

通过循环调用HitTestResult  中记录的具体RederBox 中的  handleEvent  中  , 接下来的MOVE 、up 事件也会按这个冒泡的顺序进行事件分发

handleEvent 中 会去 逐步判断子View 中是否注册  GestureRecognizer 事件接收回调 ,  在Down 事件时, 将注册的事件加进 GestureArenaManager 中的_GestureArena 中,等到最后执行到GestureBinding 时 ,

开始根据_GestureArena 中的事件 ,找到最终需要接收的回调 ,去执行事件

二、事件竞技场:

这里标明几个类的作用:

我们这里将事件最终判断为什么事件 、比如双击、滑动、长按、或者点击事件 , 最后又由谁去执行 ,形象比喻成一个事件竞技场来决策:

各种Recognizer : 竞技场选手  

_GestureArena : 竞技场场地

GestureArenaManager: 竞技场地管理员

GestureArenaEntry : 竞技场门票,竞技场选手需要拿到门票才能加入对应的场地

当我们的选手拿着对应的入场券进场后,现在各个场地都聚集了一批选手,叮的一声(PointerDown事件),各个场地入口关闭,过了一会激烈的竞技,又叮的一声(PointerUp事件)竞技结束,我们就要打扫竞技场看一下哪一位选手胜利了。

那么怎么判断哪个手势是最后赢得胜利留下来的呢,不像现实竞技场那么残酷,这里是很斯文优雅的,对手自己会判断是否要退出竞争,判断条件当然是PointerDown,PointerMove,PointerUp事件传递的信息是否符合当前手势的定义,如果不符合就自动退出,如果符合就向竞技场(_GestureArena)申请我符合条件,请判我获取胜利,其他手势只能判断为失败了。 但是这里也会有一些情况需要特别处理:

  • 如果参与者只有一个,或者其他参与者退出后只剩一个,就会让唯一剩下的参与为胜利
  • 如果没有手势请求获取胜利,竞技场也没被其他手势hold住,怎么办,那么竞技场调用sweep方法会让默认第一个手势会判断为胜利,其他判断为失败
  • 如果手势之间有冲突,例如一个DoubleTap和一个Tap,DoubleTap手势可以请求竞技场Hold住(等一下不要那么快打扫,判断优胜者),但是请求竞技场hold住的手势,必须之后主动请求竞技场release(好了,你可以打扫了),等DoubleTap手势决定是否是优胜还是自动退出,就可以知道Tap手势是否最终生效,这样看Tap手势好像不会乱搞事情,就静静的等待所有对手退出,自己最终符合第一或者第二个条件,而判断为胜利。

所以整个竞技场的核心,只是仅仅让当前手势知道已经没有别的手势竞争,可以自己判断是否符合当前手势的定义而触发相应的事件,所以竞技场胜利的一方并不是百分百触发手势的,获得竞技场胜利只是触发手势的必要非充分条件。

竞技场也是深度优先原理 ,因为遍历HitTestResult 时更深层次的子控件会在前面 ,所有子控件的事件一旦判断符合事件 ,会优先执行 

总结: 

  1. 我们再runApp 的时候,会启动各种跟引擎相关的binding , 如GestureBinding、RendererBinding 
  2. 当事件从原生页面传递过来时,会执行GestureBinding 的 handlePointerEvent  方法,进行碰撞测试, 从根rederView 开始递归向子view遍历,将符合触摸坐标的控件全部添加到HitTestResult 中,最后会把GestureBinding 自己也添加进去
  3. 遍历HitTestResult 中记录的所有节点,执行handleEvent ,任何rederObject控件createRenderObject方法中,都创建了一个RenderPointerListener事件监听器 , 当遍历HitTestResult中的控件去执行对应的       handleEvent时 ,实际上是执行到RenderPointerListener 的 handleEvent ,这里在手指触摸上,也就是Down 事件,如果控件注册了事件处理器GestureRecognizer(竞技场选手) ,  这里会将这个down 事件       初始化进GestureRecognizer 中,并生成GestureArenaEntry (拿到门票), 然后将GestureRecognizer(竞技场选手)加入到_GestureArena(竞技场)中
  4. 最后会执行到GestureBinding的handleEvent 方法 ,这时基本有控件的事件处理器都已经加入了竞技场, 开始关闭竞技场 ,接下来的事件开始在竞技场中竞争 ,竞争到事件的控件事件处理器相应的方法开始回            调, 一直到Up事件时,开始关闭竞技场,打扫战场 (双击事件会请求竞技场等待不要关闭 ,判断优胜者之后再关闭)。

              

三、事件冲突的处理 : 

  • GestureDetector 提供了一个behavior参数 , 传HitTestBehavior 的值,有三个值   HitTestBehavior.deferToChild(事件是否消费完全取决于它的子控件) HitTestBehavior.opaque (position在自己的范围内,子类的HitTestSlef只要不被重写,自身就会消费事件)  HitTestBehavior.translucent ( position在自己的范围内,都会消费事件 )
  • 自定义GestureRecognizer handleEvent判断手势是否符合自己定义,例如滑动多少距离范围;设置deadline超时时间规定手势需在多少时间内完成,或者超出多少时间才符合定义;当检测到手势符合我们定义或者不符合时,可以调起resolve决议,让其他手势识别放弃监听手势并重置状态;
  • 自定义RederView 时实现自定义的RenderBox ,重写hitTest 方法,可以随意控制当前控件或者当前子控件是否接收事件处理 ,如下

flutter_touch_event_12.png

flutter_touch_event_13.png