Compose原理-compose中是如何实现事件分发的

前言:

安卓原生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后,断点生效,整个堆栈流程如下图所示:

Compose原理-compose中是如何实现事件分发的_第1张图片

上面这张图,我们主要分为以下几块吧。

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,
        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 = mutableVectorOf()

看到这就感觉豁然开朗了,原来compose中的事件分发,也是由上层向下层一层一层传递的。

继续往下看,果然不出所料,Node的dispatchMainEventPass方法中,也存在向children传递事件的代码。

接下来,就是看Node如何处理这个事件了。若干次的dispatchMainEventPass分发后,终于到了Node处理点击的这一层,我们完整的看一下这个方法:

override fun dispatchMainEventPass(
        changes: Map,
        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源码的分析,有可能分析结论有误,欢迎指出和讨论

你可能感兴趣的:(android)