隔离的这14天,慢慢的研究了Flutter的指针事件,在这个过程中,又重新梳理了一下Element和Render Tree的形成过程。这篇文章,主要对指针事件在Fluter中如何下发到各个组件的过程进行梳理。(指针是指针,手势是手势,手势是指针事件的某种行为,且只有一个胜者,这点要区分清楚。)
好像要一只dash啊。🤤🤤🤤
好的,进入正题。当你点击了屏幕,Flutter做了什么呢?
过程解析
通过调用栈,我们逐步分析过程。
1.从平台处获得指针事件数据并分发
指针数据被包装成Dart的ByteData 类型,调用PlatformDispatcher 的_dispatchPointerDataPacket 方法,至于指针数据怎么被平台包装,内容是什么,这就涉及到各个平台处理指针的方式,掘金由许多相关文章。(作者目前不懂任何原生知识)
2.在GestureBinding和中处理得到的指针数据
经过了几层看不懂的调用,指针数据走到了这个地方,并且以队列 的形式被处理。(这里又有window,又有lock,暂时不管)。
注意到,这里的packet是ui.PointerDataPacket 类型,该类内部仅有一个final List<PointerData> 的data成员。
既然是队列 ,那么就是循环出列,逐个处理事件。这个过程由_flushPointerEventQueue 完成。
handlePointerEvent 方法,如其名,这里传过来的数据已经成为了PointerEvent 类型。(resanlingEnabled 默认为false,官方文档的解释是通过这只这个选项,对于设备的指针采样率 和屏幕刷新率 有某种关系的,可以让指针更丝滑)
3.在GestureBinding和RendererBinding中进行hitTest
_handlePointerEventImmediately 方法被handlePointerEvent (上面那个家伙)调用。 这个方法是个重量级的家伙。
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
//省略...
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position); //看这里!!!!!!!!!!!
//省略...
}
dispatchEvent(event, hitTestResult);
}
}
复制代码
到这里,先缓一缓,也许上面我写的不好看不懂也没关系,你只需要记住一件事,我们已经拿到了类型为PointerEvent 的指针事件的数据event 。
(PointerEvent 有多个子类,PointerDownEvent ,PointerMoveEvent ,PointerUpEvent 等等,对应点击 ,移动 ,抬起 )
核心内容开始
这里以PointerDownEvent 举例,这个事件为用户点击屏幕后产生的。
为什么用户要点击?我猜他在某种APP内发现了一张涩图 想点进去看看。图片肯定是个RenderObject (不然你能看到个**),那写代码的怎么知道用户点的是哪张图 呢?
Flutter带Hit 开头的接口帮我们做这件事,和RenderObject 相关的有三个接口。
1. HitTestable 的hitTest 方法,让这个RenderObject 能点,什么是能点?稍后的HitTestResult 就会告诉你。
2. HitTestDispatcher 的dispatchEvent 方法,嗯,能发事件。
3. HitTestTarget 的handleEvent 方法,RenderObject 能被点了,那事件你处理不处理,怎么处理,就是这个方法的内容了。
高能来了
从RenderView 的hitTest 进行递归,跟据点击指针事件event 的position ,调用child 的hitTest 。RenderView 是RenderTree的根,怎么来的可以看看Binding 的相关内容。
这里插一则,GestureBinding 有hitTest ,RendererBinding 也有,从runApp 方法可以看到调用内容,super.hitTest 对应GestureBinding 的hitTest ,截图对应的是RendererBinding 。(不影响阅读后面的内容)
递归调用内容解析
RenderTree从RenderView 开始hitTest 。大多数情况下,我们创建的都是RenderBox ,这个盒模型,有长和宽等大小信息,使用笛卡尔坐标系。有的有单个child ,或者双链表式的children 。面对这多种情形,不同的组件有不同的点击测试 内容。
这里通过Stack 和Container 组件,让大家理解下这个递归过程。
Stack(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: (){print("blue");},
child: Container(width: 300,height: 300,color:Colors.blue)),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: (){print("red");},
child: Container(width: 150,height: 150,color:Colors.red))
],
),
复制代码
某些乱七八糟的BLOG说设置GestureDetector 的behavior 就能实现点击穿透 ,然而点击红色方块控制台只输出red。
这是为什么呢?答案是和hitTest 有关。
Stack 对应的是RenderStack ,是一个双链表 的child 模型(由ContainerRenderObjectMixin 实现)。其hitTest 是RenderBox 的方法,原汁原味。
如果这个Box包含点击点,通过hitTestChildren 先对children 逐个进行hitTest ,前者不中则再通过hitTestSelf 自己进行hitTest 。只要中了,就把自己添加进result 。
result 储存的是所有通过的测试的RenderBox ,这些都会接收到指针事件。
RenderStack 的hitTestChildren
hitTest 过程可以解释为从lastChild 开始,向前进行点击测试,直到有一个child 通过,addWithPaintOffset 的内部会添加进result ,然后返回true 。
为什么从lastChild 开始?因为是栈顶 对应的RenderBox ,这样就保证了上面的盖住了下方的,使得一般情况下的点击无法穿透。
hitTestSelf 默认返回false ,子类可以根据需要重写。(GestureDetector ,RenderPointerListener ,Listener ,RenderBoxWithHitTestBehavior 等有详细的内容,之后的点击穿透会讲)
所以结论是,在HitTestChildren 中,红色方块的RenderBox 被添加进了HitTestResult 中,此时就跳出循环,递归回调,所以蓝色方框得不到指针信息。
hitTest 总结
这是一个自上而下 ,递归 的过程,内部主要由hitTestChildren 和hitTestSelf 实现。点击处的坐标在RenderBox 的内部是能够进行hitTest 的前提,但是通不通过hitTest 取决于组件内部自己是如何实现hitTest ,能否接收到指针事件取决于是否把RenderBox 添加到HitTestResult 中。
好奇result 的内容,在_handPointEventImmediately 中打印result 即可。
dispatchEvent分发通知
把获得的HitTestResult 通过内部path 遍历,调用各个RenderObject 的handleEvent 。其实内部还有pointRoute 等内容,暂时没研究。
在插播一条无关消息,GestureBinding 这个方法下面的handleEvent 就是手势竞技场的内容。(手势是手势的事情,指针不管手势的事)
总结
所以平台的指针事件下发需经历如下3个过程。
-
包装成Flutter能看得懂的指针数据 -
挑选出需要响应事件的RenderObject -
分发事件执行RenderObject 的handleEvent
更多Android知识,扫码即可了解
?
|