深度解析 Compose 的 Modifier 原理 — PointerInputModifier

在这里插入图片描述


" Jetpack Compose - - Modifier 系列文章 "

    ?? 《 深入解析 Compose 的 Modifier 原理 - - Modifier、CombinedModifier 》

    ?? 《 深度解析 Compose 的 Modifier 原理 - - Modifier.composed()、ComposedModifier 》

    ?? 《 深入解析 Compose 的 Modifier 原理 - - Modifier.layout()、LayoutModifier 》

    ?? 《 深度解析 Compose 的 Modifier 原理 - - DrawModifier 》

    ?? 《 深度解析 Compose 的 Modifier 原理 - - PointerInputModifier 》

    ?? 《 深度解析 Compose 的 Modifier 原理 - - ParentDataModifier 》


其实原理性分析的文章,真的很难讲的通俗易懂,讲的简单了就没必要写了,讲的繁琐难懂往往大家也不乐意看,所以只能尽量想办法,找个好的角度(比如从 Demo 代码示例出发)慢慢带着大家去钻源码,如果确实能帮助到大家完全理解了文章所讲述到的源码理论,那就值了。

在正式开始分析 DrawModifier 之前,建议你先看看 【LayoutModifier 和 Modifier.layout 用法及原理】这篇文章,毕竟它是作为 Modifier 原理解析的第一篇文章,对你了解整个 Modifier 架构还是很有帮助的,或者说它是最基础的一篇文章,如果不熟悉,后面的系列 Modifier 你可能会看的比较费劲… …


在 Compose 中处理点击事件,最简单的方式就是:Modifier.clickable

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .clickable { 
                        // 单击处理,添加逻辑
                    }) {
                }
            }
        }
    }
}

但 Modifier.clickable() 只能处理单击事件,如果你需要处理长按、双击等事件,则需要用到另外一个函数:Modifier.combinedClickable()

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .combinedClickable {

                    }) {
                }
            }
        }
    }
}

combinedClickable() 是 Modifier 的一个扩展函数:

@ExperimentalFoundationApi
fun Modifier.combinedClickable(
    enabled: Boolean = true,
    onClickLabel: String? = null,
    role: Role? = null,
    onLongClickLabel: String? = null,
    onLongClick: (() -> Unit)? = null,    // 长按
    onDoubleClick: (() -> Unit)? = null,  // 双击
    onClick: () -> Unit                   // 单击
)

从函数的字面意思就可以知道它是一个组合类型的 clickable,可以通过参数指定单击类型,如果不填写任何参数,那它跟 clickable 没有任何区别。

Modifier.clickable {  }
// 无参数情况下,等同
Modifier.combinedClickable {  }

现在我们来测试下 combinedClickable 的用法:

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .combinedClickable(
                        onLongClick = { println("@@@ 长按了 Box") },
                        onDoubleClick = { println("@@@ 双击了 Box") }
                    ) {
                        // onClick()
                        println("@@@ 单击了 Box")
                    }) {

                }
            }
        }
    }
}

在这里插入图片描述

上面只是满足点击监听的需求,如果需要复杂的触摸反馈定制(类似于 View 的 onTouchEvent),我们可以使用另外一个扩展函数:Modifier.pointerInput()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .pointerInput(Unit) {
                        detectTapGestures()
                    }
                )
            }
        }
    }
}

我们来看看 detectTapGestures() 函数:

suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) -> Unit)? = null,  // 双击
    onLongPress: ((Offset) -> Unit)? = null,  // 长按
    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,  // 触摸到即触发
    onTap: ((Offset) -> Unit)? = null         // 单击
)

它一样可以监听双击、长按、单击事件,唯独多了一个 onPress,那跟 combinedClickable 有什么区别?

Modifier.combinedClickable() 和 detectTapGestures() 的区别在于它们的级别或者说定制深度上是不同的,detectTapGestures() 是更底层的一种实现,实际上 Modifier.combinedClickable() 底层也是使用 detectTapGestures() 实现的。

@ExperimentalFoundationApi
fun Modifier.combinedClickable(...) = composed(...) {
    Modifier.combinedClickable(...)
}

@ExperimentalFoundationApi
fun Modifier.combinedClickable(...) = composed(
    factory = {
		... ...
        val gesture =
            Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) {
                centreOffset.value = size.center.toOffset()
                detectTapGestures(
                    onDoubleTap = ...,
                    onLongPress = ...,
                    onPress = ...,  // onPress 并没有暴露出来
                    onTap = ...
                )
            }
	... ...
)

如果还要做更复杂的触摸反馈且完全由我们自己控制,Compose 还提供了 awaitPointerEventScope(),让我们可以监听每个触摸事件:

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .combinedClickable {  }
                    .pointerInput(Unit) {
                        awaitPointerEventScope { 
							// 这里面就要完全自定义触摸事件处理逻辑了
							val down = awaitFirstDown() // 获取一个按压事件
						}
                    }
                )
            }
        }
    }
}

这样就可以在 awaitPointerEventScope 内部进行触摸事件处理了,但往往我们还会给 awaitPointerEventScope 套一层 forEachGesture

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalFoundationApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                Box(Modifier.size(40.dp)
                    .background(Color.Green)
                    .combinedClickable {  }
                    .pointerInput(Unit) {
                        forEachGesture {
                            awaitPointerEventScope {
                                val down = awaitFirstDown()
                            }
                        }
                    }
                ) {

                }
            }
        }
    }
}

forEachGesture() :循环检测每个事件,否则 awaitPointerEventScope() 监听一次点击之后就会失效。

其实 detectTapGestures 内部也是用 awaitPointerEventScope() 实现的:

suspend fun PointerInputScope.detectTapGestures(...) = coroutineScope {
    val pressScope = PressGestureScopeImpl(this@detectTapGestures)

    forEachGesture {
        awaitPointerEventScope {
            val down = awaitFirstDown()
            down.consume()
            ... ...
        }
    }
}

Modifier.pointerInput() 内部使用的 detectXxxGesture() 几乎无一例外都是使用的该方案监听触摸事件。

在这里插入图片描述

至此,我们已经简单了解了 Modifier.pointerIput() 怎么使用,接下来开始分析定制的触摸反馈是怎么影响到界面展示的。

如果你已经看过 【 DrawModifier 原理解析】 的文章,那么对 PointerInputModifier 的处理位置应该会不陌生了。

我们直接看源码:

override var modifier: Modifier = Modifier
	set(value) {
	    ... ...
	    
	    val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
	        if (mod is RemeasurementModifier) {
	            mod.onRemeasurementAvailable(this)
	        }
	
	        toWrap.entities.addBeforeLayoutModifier(toWrap, mod)  // here
	
	        if (mod is OnGloballyPositionedModifier) {
	            getOrCreateOnPositionedCallbacks() += toWrap to mod
	        }
	
	        val wrapper = if (mod is LayoutModifier) {
	            // Re-use the layoutNodeWrapper if possible.
	            (reuseLayoutNodeWrapper(toWrap, mod)
	                ?: ModifiedLayoutNode(toWrap, mod)).apply {
	                onInitialize()
	                updateLookaheadScope(mLookaheadScope)
	            }
	        } else {
	            toWrap
	        }
	        wrapper.entities.addAfterLayoutModifier(wrapper, mod)
	        wrapper
	    }
	
	    ... ...
	}

对 PointerInputModifier 的处理和 DrawModifier 一样:

toWrap.entities.addBeforeLayoutModifier(toWrap, mod)  // here

我们跟踪进去:

fun addBeforeLayoutModifier(layoutNodeWrapper: LayoutNodeWrapper, modifier: Modifier) {
    if (modifier is DrawModifier) {
        add(DrawEntity(layoutNodeWrapper, modifier), DrawEntityType.index)
    }
    if (modifier is PointerInputModifier) {
        add(PointerInputEntity(layoutNodeWrapper, modifier), PointerInputEntityType.index)
    }
	... ...
}

现在看就很明显了,PointerInputModifier 跟 DrawModifier 的存储方式一摸一样。在存储时也会将 PointerInputModifier 包装到一个链表中,后续新加的 PointerInputModifier 会用头插法插入链表头部。

那么分析到这里就可以有两个猜测:

1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?

// PointerInputModifier 对右边的 LayoutModifier 生效
// 想要对哪个 LayoutModifier 生效,就把 PointerInputModifier 写在哪个的左边
Modifier.pointerInput().padding()

2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?

// 两个 PointerInputModifier 影响着 LayoutModifier
// 两个 PointerInputModifier 是父子关系,最左边的 PointerInputModifier 管理右边的  PointerInputModifier 
Modifier.pointerInput().pointerInput().size()

现在我们从源码角度来看看这两个猜测是否正确。

// LayoutNode.kt

internal fun hitTest(
    pointerPosition: Offset,
    hitTestResult: HitTestResult<PointerInputFilter>,
    isTouchEvent: Boolean = false,
    isInLayer: Boolean = true
) {
    val positionInWrapped = outerLayoutNodeWrapper.fromParentPosition(pointerPosition)
    outerLayoutNodeWrapper.hitTest(
        LayoutNodeWrapper.PointerInputSource,
        positionInWrapped,
        hitTestResult,
        isTouchEvent,
        isInLayer
    )
}

hitTest() 实际上是做的检测工作,主要的作用是检查触摸事件应该下发给哪个组件,检测后再把事件分发到对应组件。

// LayoutNodeWrapper.kt

fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> hitTest(
    hitTestSource: HitTestSource<T, C, M>,
    pointerPosition: Offset,
    hitTestResult: HitTestResult<C>,
    isTouchEvent: Boolean,
    isInLayer: Boolean
) {
    val head = entities.head(hitTestSource.entityType()) // 获取 PointerInputModifier 链表的头部
    if (!withinLayerBounds(pointerPosition)) {
        ... ...
    } else if (isPointerInBounds(pointerPosition)) {
        // A real hit
        head.hit(
            hitTestSource,
            pointerPosition,
            hitTestResult,
            isTouchEvent,
            isInLayer
        )
    } else {
        ... ...
    }
}

现在我们再来看 head.hit()

// LayoutNodeWrapper.kt

private fun <T : LayoutNodeEntity<T, M>, C, M : Modifier> T?.hit(
    hitTestSource: HitTestSource<T, C, M>,
    pointerPosition: Offset,
    hitTestResult: HitTestResult<C>,
    isTouchEvent: Boolean,
    isInLayer: Boolean
) {
    if (this == null) {
        hitTestChild(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
    } else {
        // 核心代码
        hitTestResult.hit(hitTestSource.contentFrom(this), isInLayer) {
            next.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
        }
    }
}

首先需要了解一下:hitTestSource.contentFrom(this) 做了什么?-- 返回了 PointerInputModifier 链表的头节点内部包含的 PointerInputModifier 自身。

现在我们再往下跟踪:

// HitTestResult.kt

fun hit(node: T, isInLayer: Boolean, childHitTest: () -> Unit) {
    hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
}

又调用了 hitInMinimumTouchTarget():

// HitTestResult.kt

fun hitInMinimumTouchTarget(
    node: T,                    // 1. 这里的 node 就是传进来的 PointInputModifier
    distanceFromEdge: Float,
    isInLayer: Boolean,
    childHitTest: () -> Unit
) {
    val startDepth = hitDepth
    hitDepth++
    ensureContainerSize()
    values[hitDepth] = node     // 2. 将 PointInputModifier 放进一个数组里,记录每个节点
    distanceFromEdgeAndInLayer[hitDepth] =
        DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
    resizeToHitDepth()
    childHitTest()              // 3. 又调用了 childHitTest()
    hitDepth = startDepth
}

childHitTest 是传进来的,往回找就会发现其实 childHitTest 就是:

在这里插入图片描述

看到了 next ?进行下一个节点的 hit 函数处理,典型的递归调用了。

所以看到这里,我们再看回刚才的两个猜想:

1、既然 DrawModifier 也是对最接近的右边的 LayoutModifier 生效,PointerInputModifier 是不是也是一样的?

2、连续的 PointerInputModifier,最左边的 PointerInputModifier 会包含右边的 PointerInputModifier?

这两条猜想都是正确的!