目录 :
- Flutter 事件分发流程梳理
- Flutter事件竞技场梳理
- Flutter 手势事件冲突解决办法
一、Flutter 事件分发流程:
先看一下手指触摸时间的触发源头是从何产生的 ,并往何处去 ,看下面这个图
如在Android中, FlutterView 的OnTouchEvent 方法接收到了Android产生的事件 , 通过C++的JNI 方法调度 ,最终传递到flutter 的dart 方法中 ,
到:GestureBinding
再到: GestureBinding
这里只要事件队列不为空,就会循环从队列里面取事件进入下面的分发流程:
flutter 业务理解的事件起源主要核心分发点就在GestureBinding 类中 ,上面我们知道了事件的传递流程 ,再来具体看一下 GestureBinding 中做了哪些事情
handlePointerEvent 方法中,开始将事件封装类PointerEvent 进行传递
当是PointerDownEvent事件的时候,会新建一个HitTestResult对象,而这个HitTestResult对象里面有一个path的属性,这个属性就是用来记录事件传递所经过的的节点。 新建HitTestResult对象后,接下来重点就是调用GestureBinding.histTest方法。 在看看hitTest方法:
由于GestureBinding 和 RendererBinding 都在flutter 的入口runApp 时初始化WidgetsFlutterBinding 时初始化,WidgetsFlutterBinding with 混合了各个binding,而 RendererBinding 是 GestureBinding 的子类,所以hitTest 会覆盖, 先会调用
RendererBinding 的hitTest 方法:
获取根view , rederView 并开始调用hitTest 方法,
再到child 子控件中,如果触摸位置在界内 ,先判断是否在子控件的范围,如果在,则把子控件加入HitTestResult , 然后将自己也加入 ,如果不在,则判断是否在自己的范围内,如果在,则将自己加入集合
接下来就开始进入递归调用的流程 ,如果判断触摸的位置在view界内的 ,会根据深度优先原则 ,既子控件会更先的顺序 加入到HitTestResult
最后会通过super.hitTest 调用回到GestureBinding
将GestureBinding 自己也添加进HitTestResult
在PointerDownEvent 中已经通过hitTest 找出了所有在触摸区域相关的控件 ,并且按深度优先的顺序放入了HitTestResult 中, 接下来就开始进入事件分发 :
通过循环调用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 时更深层次的子控件会在前面 ,所有子控件的事件一旦判断符合事件 ,会优先执行
总结:
- 我们再runApp 的时候,会启动各种跟引擎相关的binding , 如GestureBinding、RendererBinding
- 当事件从原生页面传递过来时,会执行GestureBinding 的 handlePointerEvent 方法,进行碰撞测试, 从根rederView 开始递归向子view遍历,将符合触摸坐标的控件全部添加到HitTestResult 中,最后会把GestureBinding 自己也添加进去
- 遍历HitTestResult 中记录的所有节点,执行handleEvent ,任何rederObject控件createRenderObject方法中,都创建了一个RenderPointerListener事件监听器 , 当遍历HitTestResult中的控件去执行对应的 handleEvent时 ,实际上是执行到RenderPointerListener 的 handleEvent ,这里在手指触摸上,也就是Down 事件,如果控件注册了事件处理器GestureRecognizer(竞技场选手) , 这里会将这个down 事件 初始化进GestureRecognizer 中,并生成GestureArenaEntry (拿到门票), 然后将GestureRecognizer(竞技场选手)加入到_GestureArena(竞技场)中
- 最后会执行到GestureBinding的handleEvent 方法 ,这时基本有控件的事件处理器都已经加入了竞技场, 开始关闭竞技场 ,接下来的事件开始在竞技场中竞争 ,竞争到事件的控件事件处理器相应的方法开始回 调, 一直到Up事件时,开始关闭竞技场,打扫战场 (双击事件会请求竞技场等待不要关闭 ,判断优胜者之后再关闭)。
三、事件冲突的处理 :
- GestureDetector 提供了一个behavior参数 , 传HitTestBehavior 的值,有三个值 HitTestBehavior.deferToChild(事件是否消费完全取决于它的子控件) HitTestBehavior.opaque (position在自己的范围内,子类的HitTestSlef只要不被重写,自身就会消费事件) HitTestBehavior.translucent ( position在自己的范围内,都会消费事件 )
- 自定义GestureRecognizer handleEvent判断手势是否符合自己定义,例如滑动多少距离范围;设置deadline超时时间规定手势需在多少时间内完成,或者超出多少时间才符合定义;当检测到手势符合我们定义或者不符合时,可以调起resolve决议,让其他手势识别放弃监听手势并重置状态;
- 自定义RederView 时实现自定义的RenderBox ,重写hitTest 方法,可以随意控制当前控件或者当前子控件是否接收事件处理 ,如下