13 Compose 源码解析之 Touch 事件分发

2022-09-05  本文已影响0人  我爱田Hebe

先看下 Compose 跟原生 View 在 Activity 中的区别

再看下 TouchEvent 传递流程:

Compose 事件分发跟原生 View 的区别还是在 View 事件分发流程这块。虽然 setContent 的时候添加到 DecorView 中的是 ComposeView , 但参与组合的还是 AndroidComposeView。 Compose TouchEvent 分发就是从 AndroidComposeView#dispatchTouchEvent 开始的。

AndroidComposeView#dispatchTouchEvent 方法可谓一目了然,但真正的 Compose 事件分发是在 handleMotionEvent() 中实现的。

    override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
        // 检测 MotionEvent 是否合法
        if (isBadMotionEvent(motionEvent)) {
            return false // Bad MotionEvent. Don't handle it.
        }
        //处理 MotionEvent , Compose 事件分发实现
        val processResult = handleMotionEvent(motionEvent) 
        //如果事件被消耗,设置父 View 不可拦截事件传递
        if (processResult.anyMovementConsumed) {
            parent.requestDisallowInterceptTouchEvent(true)
        }
        //返回事件是否被消耗
        return processResult.dispatchedToAPointerInputModifier 
    }

1 MotionEvent 转换加工

Compose 中并不直接处理 MotionEvent ,首先将 MotionEvent 转换成 PointerInputEvent ,将每个触摸点的信息转换成 PointerInputEventData 保存到 PointerInputEvent.pointers 中。

internal actual class  (
    actual val uptime: Long,//事件发生的事件
    actual val pointers: List<PointerInputEventData>, //每个触摸点的触摸数据
    val motionEvent: MotionEvent //PointerInputEvent 的事件源
)
internal data class PointerInputEventData(
    val id: PointerId,
    val uptime: Long,
    val positionOnScreen: Offset,
    val position: Offset,
    val down: Boolean,
    val type: PointerType,
    val historical: List<HistoricalChange> = mutableListOf()
)

转换后的 PointerInputEvent 交给 PointerInputEventProcessor 继续处理。

PointerInputChangeEventProducer 中 previousPointerInputData 保存上次事件每个 PointId 对应的 PointerInputChange 信息, 遍历 pointerEvent.pointers 对比 previousPointerInputData 生成每个 pointer 对应的 PointerInputChange 并保存到 InternalPointerEvent.changes 中。

internal actual class InternalPointerEvent constructor(
    actual val changes: Map<PointerId, PointerInputChange>,
    val motionEvent: MotionEvent
) {
    actual constructor(
        changes: Map<PointerId, PointerInputChange>,
        pointerInputEvent: PointerInputEvent
    ) : this(changes, pointerInputEvent.motionEvent)
}
class PointerInputChange(
    val id: PointerId,
    val uptimeMillis: Long,
    val position: Offset,
    val pressed: Boolean,
    val previousUptimeMillis: Long,
    val previousPosition: Offset,
    val previousPressed: Boolean,
    val consumed: ConsumedData,
    val type: PointerType = PointerType.Touch
)

MotionEvent 的加工处理到此并没有结束,但现有的数据已经可以确定每个触控点(pointer)的类型(type)、是否是按下操作(pressed)以及事件需要传递到那些组件中(postion)。

2 确定事件分发路径

每个触控点按下都是这个触控点触摸事件的开始,所以要为每个按下的触控点确定事件分发路径。

        internalPointerEvent.changes.values.forEach { pointerInputChange ->
            if (isHover || pointerInputChange.changedToDownIgnoreConsumed()) {
                val isTouchEvent = pointerInputChange.type == PointerType.Touch
                //确定触摸点事件分发路径保存到 hitResult 中
                root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
                if (hitResult.isNotEmpty()) {
                    // hitPathTracker 保存本次事件所有触摸点的分发路径
                    hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
                    hitResult.clear()
                }
            }
        }
        //删除分发路径中已经 Detach compose 组件
        hitPathTracker.removeDetachedPointerInputFilters()

root 是 LayoutNode 类型,它就是我们经常说的 Compose 树的根节点。

确定分发路径的过程是从根节点开始将所有包含了事件发生坐标的子节点的 modifier.pointerFilter 添加到 HitTestResult.value 中。

有背景部分的流程只有代码中实现了 modifier.pointerInput() 才会执行到。

因为LayoutNodeWrapper.hitTest() 是抽象方法 ,在 LayoutNode modifier 属性 set 方法中可以看出只有代码中实现了 modifier.pointerInput() 的 LayoutNode 的 outerLayoutNodeWrapper 属性才会包装 PointerInputDelegationWrapper,否则就会继续向子节点传递 hitTest()

3 事件分发

现在要做的只剩下在 hitPathTracker 保存的路径中分发事件了,事件在分发之前会在 buildCache() 中转换成 PointerEvent 类型。

        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)
            }
        }

从代码可以看出分发是递归调用,事件由父到子传递,再由子到父处理,这点跟 View 体系是一样的。

事件消耗

本文的开始的第一段代码中有检测事件事件是否被消耗的代码。


/**
 * Consume the up or down change of this [PointerInputChange] if there is an up or down change to
 * consume.
 */
fun PointerInputChange.consumeDownChange() {
    if (pressed != previousPressed) {
        consumed.downChange = true
    }
}

/**
 * Consume position change if there is any
 */
fun PointerInputChange.consumePositionChange() {
    if (positionChange() != Offset.Zero) {
        consumed.positionChange = true
    }
}

事件是通过上面两个 api 消耗的,pointerInput 的 detectXXX api 的源码中都能看到这两个 api 的使用。

这也代表着只要在 pointerInput 中用了 detectXXX api 即使方法体为空也会消耗事件(不知道后面官方会不会改)。

作者:给大佬们点赞
链接:https://juejin.cn/post/7135348786364153863

上一篇 下一篇

猜你喜欢

热点阅读