当前位置:网站首页>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

【WPS-OFFICE-Word】 WPS中样式的运作原理?样式自动更新、自动改变如何处理?样式的管理方法?

谷歌浏览器安装插件教程步骤,开发用这2个插件工作效率倍增

Online monitoring of UPS power supply and operating environment in the computer room, the solution is here

【美丽天天秒】链动2+1模式开发

WEB 渗透之RCE

高等数学---第十章无穷级数---常数项级数

Bytes to beat three sides take offer: network + GC + + IO + redis + JVM red-black tree + data structure, to help you quickly into the giant!!!!!

要想成为黑客,离不开这十大基础知识
随机推荐
OneNote 教程,如何在 OneNote 中设置页面格式?
Mock模拟数据,并发起get,post请求(保姆级教程,一定能成功)
【HCIP】MPLS实验
异常与智能指针
BinaryIndexedTrees树状数组
When does MySQL use table locks and when to use row locks?You should know this
Web项目Controller统一返回实体类
借助kubekey极速安装Kubernetes
爬虫之selenium
mysql跨库关联查询(dblink)
盲僧发现了华点——教你如何使用API接口获取数据
flink-sql 客户端 可以设置并行度 吗?断开算子链
懵逼!阿里一面被虐了,幸获内推华为技术四面,成功拿到offer,年薪40w
Intelligent security contract - delegatecall (2)
5000元价位高性能轻薄本标杆 华硕无双高颜能打
Web项目中简单使用线程池
微信小程序分享功能
Blender script 删除所有幽灵对象
在线监控机房内的UPS电源及运行环境,解决方案来了
金鱼哥RHCA回忆录:CL210管理计算资源--管理计算节点+章节实验