Compose:PointerInputModifier

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值