当前位置:网站首页>Compose原理-compose中是如何实现事件分法的
Compose原理-compose中是如何实现事件分法的
2022-08-03 18:55:00 【失落夏天】
前言:
安卓原生View的事件分发流程,我们另外一篇文章中有讲到。
android源码学习-事件分发处理机制_失落夏天的博客-CSDN博客
在compose学习中,就不禁想到,compose的事件分发应该是怎样的呢?我感觉应该和原生是有区别的,毕竟底层的渲染机制都不一样。安卓原生是View->ViewGroup->ViewGroup层层嵌套的结构,而compose中,只有
AndroidComposeView->ComposeView这两层结构而已。其余的层级结构都在AndroidComposeView中自行处理的。所以我猜测在compose中的事件分发,应该是在AndroidComposeView中有专门的转发逻辑。
所以,本文就是一个提出猜想,验证猜想的过程,我们就一步一步的来验证这个的猜测。
一.找到点击堆栈调用
为了方便,还是直接以之前文章中讲到的Demo为例了,布局结构如下:
@Composable
fun MainContent() {
Column(Modifier.padding(10.dp)) {
Button(onClick = {
jumpActivity(ComposeDataBindingActivity::class.java)
}) {
Text(text = "Compose_DataBinding")
}
Button(onClick = {
jumpActivity(ComposeListActivity::class.java);
}) {
Text(text = "Compose_List")
}
Button(onClick = {
jumpActivity(ComposeMVIActivity::class.java);
}) {
Text(text = "Compose_MVI")
}
}
}
点击其中的一个Button后,断点生效,整个堆栈流程如下图所示:
上面这张图,我们主要分为以下几块吧。
1.ViewGroup层面的转发。主要流程和原生的View的时间分发是一样的。DecorView一层层向下传递,最终传递给ComposeVIew(PS:ComposeView继承自ViewGroup)。
2.AndroidComposeView中的分发处理。
3.Node节点中的分发处理。
4.事件的执行。
第一块由于和原生是摸一模一样的,我们就不展开讲了,直接从AndroidAndroidComposeView层开始讲起。
二.AndroidComposeView中的分发处理
由于AndroidComposeView是kt写的,我们源码的阅读是不方便的,我们找到对应的类,使用反编译的方式,转换为java代码查看。
AndroidComposeView中的dispatchTouchEvent方法如下,主要流程在handleMotionEvent这一行,其余的都是一些非主流程的场景处理。
public boolean dispatchTouchEvent(@NotNull MotionEvent motionEvent) {
Intrinsics.checkNotNullParameter(motionEvent, "motionEvent");
if (this.hoverExitReceived) {
this.removeCallbacks(this.sendHoverExitEvent);
MotionEvent var10000 = this.previousMotionEvent;
Intrinsics.checkNotNull(var10000);
MotionEvent lastEvent = var10000;
if (motionEvent.getActionMasked() == 0 && !this.hasChangedDevices(motionEvent, lastEvent)) {
this.hoverExitReceived = false;
} else {
this.sendHoverExitEvent.run();
}
}
if (this.isBadMotionEvent(motionEvent)) {
return false;
} else if (motionEvent.getActionMasked() == 2 && !this.isPositionChanged(motionEvent)) {
return false;
} else {
int processResult = this.handleMotionEvent-8iAsVTc(motionEvent);
if (ProcessResult.getAnyMovementConsumed-impl(processResult)) {
this.getParent().requestDisallowInterceptTouchEvent(true);
}
return ProcessResult.getDispatchedToAPointerInputModifier-impl(processResult);
}
}
所以我们接着看一下handleMotionEvent方法,直接切换到AndroidComposeView.android.kt中看。
仍然是一些非主流程判断的处理,我们还是只看核心:
private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {
removeCallbacks(resendMotionEventRunnable)
...
sendMotionEvent(motionEvent)
...
}
sendMotionEvent的核心是交给了pointerInputEventProcessor.process进行处理。
我们看一下pointerInputEventProcessor的构造方法:
internal class PointerInputEventProcessor(val root: LayoutNode)
传入参数是root,对应的是应该是最外层的那个View,所以说明PointerInputEventProcessor是整个Compose中事件分发流程的开始。
三.PointerInputEventProcessor中的分发流程
我们仍然看最开始的那张图,接着执行到了下面这个方法。
val dispatchedToSomething =
hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds)
看一下hitPathTracker的构造方法:
private val hitPathTracker = HitPathTracker(root.coordinates)
coordinates从名字就可以推测到是坐标一类的属性对象了。所以接下来,应该会根据坐标进行进一步的分发了。
而internalPointerEvent是这个点击事件的对象封装,
isInBounds是否在范围,这个就不用多解释了。
接下来我们接着看dispatchChanges方法,一样,我们只看核心。核心就是
root.dispatchMainEventPass方法。此时的root对应的应该是NodeParent。在compose中,所有的视图结构都是以Node的方式来存储的,所以NodeParent是最外层的那个视图结构。
fun dispatchChanges(
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean = true
): Boolean {
...
var dispatchHit = root.dispatchMainEventPass(
internalPointerEvent.changes,
rootCoordinates,
internalPointerEvent,
isInBounds
)
dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit
return dispatchHit
}
接着往下看root.dispatchMainEventPass方法:
open fun dispatchMainEventPass(
changes: Map<PointerId, PointerInputChange>,
parentCoordinates: LayoutCoordinates,
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean
): Boolean {
var dispatched = false
children.forEach {
dispatched = it.dispatchMainEventPass(
changes,
parentCoordinates,
internalPointerEvent,
isInBounds
) || dispatched
}
return dispatched
}
再看一下children对象:
val children: MutableVector<Node> = mutableVectorOf()
看到这就感觉豁然开朗了,原来compose中的事件分发,也是由上层向下层一层一层传递的。
继续往下看,果然不出所料,Node的dispatchMainEventPass方法中,也存在向children传递事件的代码。
接下来,就是看Node如何处理这个事件了。若干次的dispatchMainEventPass分发后,终于到了Node处理点击的这一层,我们完整的看一下这个方法:
override fun dispatchMainEventPass(
changes: Map<PointerId, PointerInputChange>,
parentCoordinates: LayoutCoordinates,
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean
): Boolean {
...
return dispatchIfNeeded {
val event = pointerEvent!!
val size = coordinates!!.size
// Dispatch on the tunneling pass.
pointerInputFilter.onPointerEvent(event, PointerEventPass.Initial, size)
// Dispatch to children.
if (pointerInputFilter.isAttached) {
children.forEach {
it.dispatchMainEventPass(
// Pass only the already-filtered and position-translated changes down to
// children
relevantChanges,
coordinates!!,
internalPointerEvent,
isInBounds
)
}
}
if (pointerInputFilter.isAttached) {
// Dispatch on the bubbling pass.
pointerInputFilter.onPointerEvent(event, PointerEventPass.Main, size)
}
}
}
仍然是如果在惦记范围内(pointerInputFilter.isAttached进行的判断),先转发给children。
然后自身再去进行处理:
if (pointerInputFilter.isAttached) {
// Dispatch on the bubbling pass.
pointerInputFilter.onPointerEvent(event, PointerEventPass.Main, size)
}
我们注意这里的第二个参数PointerEventPass.Main,这个枚举类型有3种值类型,分别对应的是未处理,该被处理还未处理,以及处理完成三种状态。
enum class PointerEventPass {
Initial, Main, Final
}
这里传入的是Main,则代表着这个点击事件是该被当前Node处理的。继续往下看,
onPointerEvent方法如下:
override fun onPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass,
bounds: IntSize
) {
boundsSize = bounds
if (pass == PointerEventPass.Initial) {
currentEvent = pointerEvent
}
dispatchPointerEvent(pointerEvent, pass)
lastPointerEvent = pointerEvent.takeIf { event ->
!event.changes.fastAll { it.changedToUpIgnoreConsumed() }
}
}
核心是dispatchPointerEvent方法:
private fun dispatchPointerEvent(
pointerEvent: PointerEvent,
pass: PointerEventPass
) {
forEachCurrentPointerHandler(pass) {
//下面的内容当作参数传入
it.offerPointerEvent(pointerEvent, pass)
}
}
这个是kotlin的一种写法,可以理解为把{}中的内容当做参数传入。
private inline fun forEachCurrentPointerHandler(
pass: PointerEventPass,
block: (PointerEventHandlerCoroutine<*>) -> Unit
) {
// Copy handlers to avoid mutating the collection during dispatch
synchronized(pointerHandlers) {
dispatchingPointerHandlers.addAll(pointerHandlers)
}
try {
when (pass) {
PointerEventPass.Initial, PointerEventPass.Final ->
dispatchingPointerHandlers.forEach(block)
PointerEventPass.Main ->
dispatchingPointerHandlers.forEachReversed(block)
}
} finally {
dispatchingPointerHandlers.clear()
}
}
此时的pass为Initial状态,所以最终会由前向后执行刚才的那个方法体,进入到offerPointerEvent的方法流程:
fun offerPointerEvent(event: PointerEvent, pass: PointerEventPass) {
if (pass == awaitPass) {
pointerAwaiter?.run {
pointerAwaiter = null
resume(event)
}
}
}
这里的awaitPass如下:
private var awaitPass: PointerEventPass = PointerEventPass.Main
还记得上面讲的传入参数吗?此时这里的pass=Main,恰好和awaitPass相等,所以pointerAwaiter就会被执行。则会执行pointerAwaiter这个协程体。
而pointerAwaiter这个协程体对应的其实就是点击跳转的方法。
所以整个流程就串起来了。
四.总结
compose中的事件分发流程其实和原生类似,也是由上向下一层一层的传递。
在attachToWindow的时候,如果Node节点设置了点击监听,则根据监听生成续体对象,加入到一个队列当中。然后遍历这个队列,给每个Node节点都会生成SuspendingPointerInputFilter对象进行观察,其中就包含将要执行的续体PointerEventHandlerCoroutine。而这个续体对象中主要包含两个属性:
awaitPass,此时会被设置为PointerEventPass.Main。
pointerAwaiter,这个就是最终执行的点击的方法的续体。
然后点击的时候,会一层一层的由上而下遍历所有节点,如果发现awaitPass=PointerEventPass.Main,则执行其中的续体方法,也就是最终的点击事件。
五.备注
本文的分析仅仅只是基于事件分发的解读,续体如何设置到Node节点上的这块并没有进行说明。这一块作者也正在看,看完后会补充上,也欢迎讨论。
本文仅基于compose源码的分析,有可能分析结论有误,欢迎指出和讨论
边栏推荐
猜你喜欢
随机推荐
多线程和并发编程(四)
【HCIP】MPLS实验
软件测试回归案例,什么是回归测试?
红日安全内网渗透靶场-VulnStack-1
87.(cesium之家)cesium热力图(贴地形)
Flask框架——项目可安装化
JumpServer开源堡垒机完成龙芯架构兼容性认证
【ORACLE】什么时候ROWNUM等于0和ROWNUM小于0,两个条件不等价?
Web项目中简单使用线程池
架构基本概念和架构本质
有人知道flink sql 使用tableEnv.executeSql执行后,怎么获取到任务运行的
fatal error: jni.h: No such file or directory
go语言实现导出string字符串到文件中
高等数学---第十章无穷级数---常数项级数
不要小看 WebSocket!长连接、有状态、双向、全双工都是王炸技能
Mock模拟数据,并发起get,post请求(保姆级教程,一定能成功)
Big guy, who is free to help me to see what the problem is, I just read MySQL source print, and I just came into contact with flink.
快手通过国际权威信息安全和隐私保护认证,安全能力达到国际领先水平
APT级全面免杀与企业纵深防御体系的红蓝对抗
U-Net生物医学图像分割讲解(Convolutional Networks for BiomedicalImage Segmentation)