PointerInputModifier 是用来做触摸反馈的 Modifier,但实际上命名为 PointerInput 比 Touch 更合适,因为 PointerInput 还代指包含鼠标等输入反馈处理,只是在 Android 基本都是触摸事件。
在讲 PointerInputModifier 之前,我们先了解下 Compose 的点击要怎么处理。
在原生的 Android 我们需要实现点击事件、长按事件、双击事件,分别会使用 onClickListener、onLongClickListener 和 GestureDetector;在 Compose 点击是 Modifier.clickable,如果要长按事件、双击事件又该怎么处理呢?
在 Compose 有一个扩展函数 Modifier.combinedClickable(),我们可以用这个扩展函数实现点击监听:
setContent {
Box(
Modifier.size(40.dp)
.background(Color.Red)
// 不指定哪种点击,默认 combinedClickable() 与 Modifier.clickable() 相同
.combinedClickable(onLongClick = {
println("long click")
}, onDoubleClick = {
println("double click")
}, onClick = {
println("click")
})
)
}
上面只是满足点击监听的需求,如果要复杂的触摸反馈定制,就需要使用另外的扩展函数 Modifier.pointerInput():
Modifier.pointerInput(Unit) {
// onTap:触碰屏幕
// onDoubleTap:双击触碰屏幕
// onLongPress:长按触碰屏幕
// onPress:触摸屏幕就会回调
detectTapGestures(onTap = {}, onDoubleTap = {}, onLongPress = {}, onPress = {})
}
或许你会有疑问:刚才的 Modifier.combinedClickable() 也是有支持单击、双击、长按,这里又来一种写法,有什么区别?
Modifier.combinedClickable() 和 detectTapGestures() 的区别在于它们的级别或者说定制深度上是不同的,detectTapGestures() 是更底层的一种实现,实际上 Modifier.combinedClickable() 底层也是使用 detectTapGestures() 实现的。
而 click 和 tap 这两个命名方式也有些讲究,tap 是指的触摸下屏幕拍下屏幕这个物理行为,click 更强调系统功能点击界面这个操作,tap 是表明发生什么事了,click 是表明要处理系统点击指令。
一般情况不做复杂定制触摸反馈的需求不会用到 Modifier.pointerInput(),最终还是要看需求场景决定使用。
如果还要做更复杂的触摸反馈要完全自己控制,Compose 还提供了 awaitPointerEventScope(),让我们监听每个触摸事件:
Modifier.pointerInput(Unit) {
// forEachGesture 循环检测监听,绝大多数都会加上这个函数
// 如果不加上 forEachGesture 循环检测,awaitPointerEventScope 只监听一次就失效了
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown()
// ...
}
}
}
绝大多数情况下我们还需要加上 forEachGesture() 循环检测每个事件,否则 awaitPointerEventScope() 监听一次就会失效。
而上面我们提到的 detectTapGestures() 也是用 awaitPointerEventScope() 实现:
TapGestureDetector.kt
suspend fun PointerInputScope.detectTapGesture(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
) = coroutineScope {
val pressScope = PressGestureScopeImpl(this@detectTapGestures)
forEachGesture {
awaitPointerEventScope {
...
}
}
}
Modifier.pointerInput() 内部使用的 detectXxxGesture() 几乎无一例外都是使用的该方案监听触摸事件。
已经简单了解了 Modifier.pointerIput() 怎么使用,接下来我们开始分析定制的触摸反馈是怎么影响到界面展示的。
还是到 LayoutNode 的源码:
LayoutNode.kt
override var modifier: Modifier = Modifier
set(value) {
...
nodes.updateFrom(value)
...
}
NodeChain.kt
private fun insertParent(node: Modifier.Node, child: Modifier.Node): Modifier.Node {
// 和 DrawModifier 处理方式一样,链表头插法将 PointerInputModifier 插入链表头
val theParent = child.parent
if (theParent != null) {
theParent.child = node
node.parent = theParent
}
child.parent = node
node.child = child
return node
}
PointerInputModifier 的处理和 DrawModifier 基本类似,在存储时会将 Modifier 包装到一个 Modifier.Node 链表,新加的 Modifier 会用头插法插入链表头部。
分析到这里就可以有两个猜测:
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<PointerInputModifierNode>,
isTouchEvent: Boolean = false,
isInLayer: Boolean = true
) {
val positionInWrapped = outerCoordinator.fromParentPosition(pointerPosition)
outerCoordinator.hitTest(
NodeCoordinator.PointerInputSource,
positionInWrapped,
hitTestResult,
isTouchEvent,
isInLayer
)
}
hitTest() 实际上是做的检测工作,主要的作用是检查触摸事件应该下发给哪个组件,检测后再把事件下发到对应组件。
NodeCoordinator.kt
fun <T : DelegatableNode> hitTest(
hitTestSource: HitTestSource<T>,
pointerPosition: Offset,
hitTestResult: HitTestResult<T>,
isTouchEvent: Boolean,
isInLayer: Boolean
) {
// 获取 PointerInputModifier 链表的头部
val head = headUnchecked(hitTestSource.entityType())
if (!withinLayoutBounds(pointerPosition)) {
...
} else if (isPointerInBounds(pointerPosition)) {
head.hit(
hitTestSource,
pointerPosition,
hitTestResult,
isTouchEvent,
isInLayer
)
}
...
}
private fun <T : DelegatableNode> T?.hit(
hitTestSource: HitTestSource<T>,
pointerPosition: Offset,
hitTestResult: HitTestResult<T>,
isTouchEvent: Boolean,
isInLayer: Boolean
) {
if (this == null) {
...
} else {
hitTestResult.hit(this, isInLayer) {
// nextUncheckedUntil 获取链表下一个节点,这里可以认为是一个递归传递事件的过程
nextUncheckedUntil(hitTestSource.entityType(), Nodes.Layout)
.hit(hitTestSource, pointerPosition, hitTestResult, isTouchEvent, isInLayer)
}
}
}
val pointerInputSource =
object: HitTestSource<PointerInputModifierNode> {
override fun entityType() = Nodes.PointerInput
...
}
HitTestResult.kt
fun hit(node: T, isInLayer: Boolean, childHitTest: () -> Unit) {
hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
}
fun hitInMinimumTouchTarget(
node: T,
distanceFromEdge: Float,
isInLayer: Boolean,
childHitTest: () -> Unit
) {
...
hitDepth++
values[hitDepth] = node // 记录每个节点
...
childHitTest() // 调用 hitTestResult.hit() 的 lambda
}
在 hitTest() 其实就是拿到 PointerInputModifier 的链表头,然后递归的传递事件给 PointerInputModifier。