本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
前言
Compose 视图的处理方式和 Android 传统 View 有很大差别,针对 touch 事件的处理自然也截然不同。
如何在 Compose 中处理 touch 事件,官方已有非常详尽的说明,可以参考:developer.android.google.cn/jetpack/com...。
本文将以 Compose 中几种最典型的 touch 处理为例,分别介绍其使用场景,并打印其调用栈。最后结合栈和 touch 源码,一起综合分析 Compose 中处理 touch 的原理细节。
各种 touch 处理的写法和场景
pointerInput
Compose 中处理所有手势事件的入口,类似传统视图的 onTouch。在这里可以识别 click 手势,而且相应优先级高于 clickable。
第二个参数为 PointerInputScope
的扩展函数类型,有如下:
- 来自
TapGestureDetector
文件中定义的 detectTapGestures:可以用来检测 onDoubleTap、onLongPress、onPress、onTap 几种手势 - 来自
DragGestureDetector
文件中定义的 detectDragGestures:可以用来检测拖拽开始、结束、取消等手势 - 来自
TransformGestureDetector
文件中定义的 detectTransformGestures:可以用来检测旋转、平移、缩放的手势 - 等等
kotlin
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
...
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = { },
onLongPress = { },
onPress = { },
onTap = { }
)
detectDragGestures(
onDragStart = { },
onDragEnd = { },
onDragCancel = { },
onDrag = { change: PointerInputChange, dragAmount: Offset ->
// Todo
}
)
detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
// Todo
}
}
) {
...
}
}
我们在 pointerInput 里一进来加上 log,
kotlin
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
.pointerInput(Unit) {
LogUtil.printLog(message = "GameScreen pointerInput", throwable = Throwable())
}
)
}
打印其调用栈:
bash
GameScreen pointerInput
java.lang.Throwable
at com.ellison.flappybird.view.GameScreenKt$GameScreen$3.invokeSuspend(GameScreen.kt:51)
...androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invokeSuspend(SuspendingPointerInputFilter.kt:562)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:8)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:4)
...
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:561)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
...
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
...
pointerInteropFilter
pointerInteropFilter 可以用来直接处理 ACTION DOWN、MOVE、UP 和 CANCEL 事件的函数,类似 onTouchEvent(),还可以指定是否允许父亲拦截:requestDisallowInterceptTouchEvent
。
需要留意的是如果 DOWN return 了 false 的话,那么 ACTION_UP 就不会发过来了。
kotlin
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
.pointerInteropFilter {
when (it.action) {
ACTION_DOWN -> {
LogUtil.printLog(message = "GameScreen pointerInteropFilter ACTION_DOWN status:${viewState.gameStatus}", throwable = Throwable())
}
MotionEvent.ACTION_MOVE -> {
// Todo
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
// Todo
}
}
true
}
)
}
我们在 ACTION_DOWN 里加个 log 看下 stack:
bash
GameScreen pointerInteropFilter ACTION_DOWN status:Waiting
java.lang.Throwable
at com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:58)
at com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:53)
at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:301)
at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:294)
at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-ubNVwUQ(PointerInteropUtils.android.kt:81)
at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-d-4ec7I(PointerInteropUtils.android.kt:35)
at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.dispatchToView(PointerInteropFilter.android.kt:294)
at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.onPointerEvent-H0pRuoY(PointerInteropFilter.android.kt:229)
at androidx.compose.ui.node.BackwardsCompatNode.onPointerEvent-H0pRuoY(BackwardsCompatNode.kt:365)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
...
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
...
combinedClickable
综合单击、双击、长按三种点击事件的处理函数,但至少需要指定处理单击 onClick 的 lambda。
如果同时设置了 pointerInteropFilter 并返回 true 的话,那么 combinedClickable Unit 就不会被处理了。
kotlin
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
.combinedClickable(
onLongClick = { },
onDoubleClick = { },
onClick = {
LogUtil.printLog(message = "GameScreen combinedClickable onClick", throwable = Throwable())
}
)
)
}
同样在最基本的 onClick 里打印个 stack:
bash
GameScreen combinedClickable onClick
java.lang.Throwable
at com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:56)
at com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:45)
at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke-k-4lQ0M(Clickable.kt:939)
at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke(Clickable.kt:927)
at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapGestures$2$1.invokeSuspend(TapGestureDetector.kt:144)
...
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine$withTimeout$job$1.invokeSuspend(SuspendingPointerInputFilter.kt:724)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
...
clickable
clickable 算是最简单的设置 click 回调的办法。
需要了留意的是:
- 当同时设置了 combinedClickable 的 onClick 的话,clickable 就不会被调用了
- 当同时设置了 pointerInteropFilter 并返回 true 的话,和 combinedClickable 一样,clickable 就不会处理了
kotlin
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
.clickable {
LogUtil.printLog(message = "GameScreen clickable", throwable = Throwable())
}
)
}
直接打个 stack:
bash
GameScreen clickable
java.lang.Throwable
at com.ellison.flappybird.view.GameScreenKt$GameScreen$1.invoke(GameScreen.kt:43)
at com.ellison.flappybird.view.GameScreenKt$GameScreen$1.invoke(GameScreen.kt:41)
at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:895)
at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:889)
at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)
...
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:665)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:544)
at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:566)
at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:855)
at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:703)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:317)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)
at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)
at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)
at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)
at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)
at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)
...
各种 touch 的原理分析
一般来说,看原理可以通过直接看代码或调试的方式来了解,但有的时候因为代码的复杂度、线程切换等因素导致阅读和调试比较困难,还容易导致忽略重要的步骤,不得已跟错流程。
这次我们事先打印了 stack,便可以直观地看到某个 touch 回调的主线处理,非常方便。后面看到源码中发现某些细节不清的时候,可以回到 stack 里找到准确的答案。
预处理
通过观察上述几个栈,你会发现基本上调用入口均是 AndroidComposeView 的 dispatchTouchEvent()
。原因显而易见,它是 Compose 上连接 Android 传统 View 树的 View 对象。
那么我们便从 AndroidComposeView 的 dispatchTouchEvent()
开始分析。
kotlin
internal class AndroidComposeView(...) : ViewGroup(context),... {
override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
...
val processResult = handleMotionEvent(motionEvent)
...
return processResult.dispatchedToAPointerInputModifier
}
}
关键的处理在 handleMotionEvent()
里。
kotlin
internal class AndroidComposeView(...) : ViewGroup(context),... {
private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {
removeCallbacks(resendMotionEventRunnable)
try {
...
val result = trace("AndroidOwner:onTouch") {
...
sendMotionEvent(motionEvent)
}
return result
} finally {
forceUseMatrixCache = false
}
}
...
}
跳过针对 HOVER 类型的事件有些特殊处理,我们直接看重要的 sendMotionEvent()
。
kotlin
internal class AndroidComposeView(...) : ViewGroup(context),... {
private fun sendMotionEvent(motionEvent: MotionEvent): ProcessResult {
...
// 先转换 MotionEvent
val pointerInputEvent =
motionEventAdapter.convertToPointerInputEvent(motionEvent, this)
return if (pointerInputEvent != null) {
...
// 再交由 Processor 处理
val result = pointerInputEventProcessor.process(
pointerInputEvent,
this,
isInBounds(motionEvent)
)
...
result
}
...
}
...
}
sendMotionEvent() 并不直接处理 MotionEvent,而是通过 convertToPointerInputEvent() 将 MotionEvent 转换成 PointerInputEvent
。针对多点触控的手指信息,需要转换成 PointerInputEventData
保存到 PointerInputEvent 里的 List 中。
然后接下来的处理交由专门的 PointerInputEventProcessor
类继续。
kotlin
internal class PointerInputEventProcessor(val root: LayoutNode) {
...
fun process(
pointerEvent: PointerInputEvent,
positionCalculator: PositionCalculator,
isInBounds: Boolean = true
): ProcessResult {
...
try {
isProcessing = true
// 先转换成 InternalPointerEvent 类型
val internalPointerEvent =
pointerInputChangeEventProducer.produce(pointerEvent, positionCalculator)
...
internalPointerEvent.changes.values.forEach { pointerInputChange ->
if (isHover || pointerInputChange.changedToDownIgnoreConsumed()) {
val isTouchEvent = pointerInputChange.type == PointerType.Touch
// path 匹配
root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
if (hitResult.isNotEmpty()) {
// path 记录
hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
hitResult.clear()
}
}
}
...
// 开始分发
val dispatchedToSomething =
hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds)
...
} finally {
isProcessing = false
}
}
...
}
-
告知
PointerInputChangeEventProducer
调用 produce() 依据传入的 PointerInputEvent 去追踪发生变化的手指 touch 信息并返回InternalPointerEvent
实例。具体差异的信息将逐个封装到PointerInputChange
实例中,并按照手指 ID map 后存到 InternalPointerEvent 里kotlinprivate class PointerInputChangeEventProducer { fun produce( ... ): InternalPointerEvent { val changes: MutableMap<PointerId, PointerInputChange> = LinkedHashMap(pointerInputEvent.pointers.size) pointerInputEvent.pointers.fastForEach { ... changes[it.id] = PointerInputChange( ... ) } return InternalPointerEvent(changes, pointerInputEvent) } ... }
-
遍历上面得到的 map,逐个调用
hitTest()
将变化的 touch 信息放到 Compose 根节点 root 中进行预匹配,得到匹配了 touch 信息的 LayoutNode 的结果HitTestResult
,以确定事件分发的路径。这里最关键的是hitInMinimumTouchTarget()
,它会将匹配到的 Modifier 里设置的 touch Node 赋值进 HitTestResult 的 values 中kotlininternal class HitTestResult : List<Modifier.Node> { fun hitInMinimumTouchTarget( ... ) { ... distanceFromEdgeAndInLayer[hitDepth] = DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue } }
-
此后,在取得 map 下一个成员之前,调用
HitPathTracker
的addHitPath()
去记录分发路径里的 Node 路径到名为 root 的NodeParent
实例里kotlininternal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) { ... fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) { ... eachPin@ for (i in pointerInputNodes.indices) { ... val node = Node(pointerInputNode).apply { pointerIds.add(pointerId) } parent.children.add(node) parent = node } }
-
最后调用
dispatchChanges()
开始分发
分发
dispatchChanges() 首先将调用 buildCache()
检查 PointerEvent 是否和 cache 的信息发生了变化,如果确有变化再继续分发,反之取消。
kotlin
internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {
fun dispatchChanges(
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean = true
): Boolean {
val changed = root.buildCache(
...
)
if (!changed) {
return false
}
...
}
}
当然该方法实际会调用 root 中各 child Node 的 buildCache()
进行。
kotlin
internal open class NodeParent {
open fun buildCache( ... ): Boolean {
var changed = false
children.forEach {
changed = it.buildCache( ... ) || changed
}
return changed
}
...
}
internal class Node(val modifierNode: Modifier.Node) : NodeParent() {
override fun buildCache(
...
): Boolean {
...
for (i in pointerIds.lastIndex downTo 0) {
val pointerId = pointerIds[i]
if (!changes.containsKey(pointerId)) {
pointerIds.removeAt(i)
}
}
...
val changed = childChanged || event.type != PointerEventType.Move ||
hasPositionChanged(pointerEvent, event)
pointerEvent = event
return changed
}
}
cache 检查发现确有变化之后,先执行 dispatchMainEventPass()
,主要任务是遍历持有目标 Node 的 Vector 进行逐个分发。
kotlin
internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {
fun dispatchChanges(
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean = true
): Boolean {
val changed = root.buildCache( ...)
// cache 确有变化,调用
var dispatchHit = root.dispatchMainEventPass(
...
)
...
}
...
open fun dispatchMainEventPass(
...
): Boolean {
var dispatched = false
children.forEach {
dispatched = it.dispatchMainEventPass( ... ) || dispatched
}
return dispatched
}
}
那么,Node 中的 dispatchMainEventPass() 的逻辑如下:
kotlin
internal class Node(val modifierNode: Modifier.Node) : NodeParent() {
override fun dispatchMainEventPass(
...
): Boolean {
return dispatchIfNeeded {
...
// 1. 本 Node 优先处理
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Initial, size)
}
// 2. 子 Node 处理
if (modifierNode.isAttached) {
children.forEach {
it.dispatchMainEventPass( ... )
}
}
if (modifierNode.isAttached) {
// 3. 子 Node 优先处理
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Main, size)
}
}
}
}
}
这个函数执行的内容比较重要:
-
执行本 Node 的
onPointerEvent()
,传递 PointerEventPass 策略为 Initial,代表父节点优先于子节点进行处理 PointerEvent,顺序是自上而下,便于父节点处理需要在执行 scroll 时防止子 Node 里按钮响应点击等场景- onPointerEvent() 的具体逻辑取决于向 Modifier 中设置的 touch Node 类型,将在下个章节展开
-
如果本 Node attach 到 Compose Layout 了,遍历它的 child Node,继续调用 dispatchMainEventPass() 分发,后续逻辑和 1 一致,不再赘述
-
如果发现本 Node 仍然 attach 到了 Layout,调用 onPointerEvent() 并设置 PointerEventPass 策略为 Main,代表子节点优于父节点处理,,顺序是自下而上,便于子节点处理需要在父节点响应之前响应点击等场景
最后调用 dispatchFinalEventPass()
进行 PointerEventPass 策略为 Final 的分发。
kotlin
internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {
fun dispatchChanges(
internalPointerEvent: InternalPointerEvent,
isInBounds: Boolean = true
): Boolean {
...
// 最后调用 dispatchFinalEventPass
dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit
return dispatchHit
}
...
open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
var dispatched = false
children.forEach {
dispatched = it.dispatchFinalEventPass(internalPointerEvent) || dispatched
}
cleanUpHits(internalPointerEvent)
return dispatched
}
}
和 dispatchMainEventPass()
一样,dispatchFinalEventPass() 需要先针对本 Node 执行 onPointerEvent(),再针对 child Node 逐个分发一遍。
区别的是此处传递的 PointerEventPass 策略为 Final,意味着这是最终步骤的分发,,顺序是自上而下,子节点可以知道父节点在 PointerInputChanges 中进行了哪些处理,比如是否已经消费了 scroll 而无需再处理点击事件了。
kotlin
internal class Node(val modifierNode: Modifier.Node) : NodeParent() {
...
override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
val result = dispatchIfNeeded {
...
// 先分发给自己,策略为 Final
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Final, size)
}
// 再分发给 children
if (modifierNode.isAttached) {
children.forEach { it.dispatchFinalEventPass(internalPointerEvent) }
}
}
...
}
...
}
另一个有个区别的地方是,执行完毕之后,额外需要执行如下重置工作:
cleanUpHits()
:清空 Node 中保存的 touch id 等 Event 信息clearCache()
:本 touch 事件处理结束,清空 cache 事件变化信息 PointerInputChange 的 map 和LayoutCoordinates
kotlin
internal class Node(val modifierNode: Modifier.Node) : NodeParent() {
...
override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
...
// 重置数据
cleanUpHits(internalPointerEvent)
clearCache()
return result
}
override fun cleanUpHits(internalPointerEvent: InternalPointerEvent) {
...
event.changes.fastForEach { change ->
val remove = !change.pressed &&
(!internalPointerEvent.issuesEnterExitEvent(change.id) || !isIn)
if (remove) {
pointerIds.remove(change.id)
}
}
...
}
private fun clearCache() {
relevantChanges.clear()
coordinates = null
}
...
}
具体 touch 处理
书接上面的 onPointerEvent(),具体看看如何抵达的 Modifier 的各个 touch 处理。
pointerInput
pointerInput() 实际上会创建一个 SuspendingPointerInputModifierNodeImpl
类型的 Node 添加到 Modifier 里,pointerInput 本身的 Unit 会被存在 pointerInputHandler 里。
kotlin
fun Modifier.pointerInput(
key1: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier = this then SuspendPointerInputElement( ... )
internal class SuspendPointerInputElement(
...
val pointerInputHandler: suspend PointerInputScope.() -> Unit
) : ModifierNodeElement<SuspendingPointerInputModifierNodeImpl>() {
...
override fun create(): SuspendingPointerInputModifierNodeImpl {
return SuspendingPointerInputModifierNodeImpl(pointerInputHandler)
}
...
}
进而在 PointerEvent 分发过来的时候会调用 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent()。
kotlin
internal class SuspendPointerInputElement(
override fun onPointerEvent(
...
) {
...
if (pointerInputJob == null) {
pointerInputJob = coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
pointerInputHandler()
}
}
...
}
}
接着执行 pointerInputHandler(),其就是我们在 pointerInput 里设置的 Unit。
此后,还需要调用 dispatchPointerEvent() 里会告知 forEachCurrentPointerHandler() 按照 PointerEventPass 策略决定从从上至下遍历还是从下至上遍历,并逐个添加待处理的 PointerEvent 给所有的 PointerHandler。
kotlin
internal class SuspendPointerInputElement(
override fun onPointerEvent( ... ) {
...
dispatchPointerEvent(pointerEvent, pass)
}
private fun dispatchPointerEvent( ... ) {
forEachCurrentPointerHandler(pass) {
it.offerPointerEvent(pointerEvent, pass)
}
}
private inline fun forEachCurrentPointerHandler( ... ) {
...
try {
when (pass) {
PointerEventPass.Initial, PointerEventPass.Final ->
dispatchingPointerHandlers.forEach(block)
PointerEventPass.Main ->
dispatchingPointerHandlers.forEachReversed(block)
}
} finally {
dispatchingPointerHandlers.clear()
}
}
}
pointerInteropFilter
pointerInteropFilter() 实际上会创建一个 PointerInteropFilter
实例,由系统添加到 BackwardsCompatNode
类型的 Node里,onTouchEvent 的 Unit 会被存在 PointerInteropFilter 里。
kotlin
fun Modifier.pointerInteropFilter(
requestDisallowInterceptTouchEvent: (RequestDisallowInterceptTouchEvent)? = null,
onTouchEvent: (MotionEvent) -> Boolean
): Modifier = composed(
...
) {
val filter = remember { PointerInteropFilter() }
filter.onTouchEvent = onTouchEvent
filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent
filter
}
进而在 PointerEvent 分发过来的时候会调用 BackwardsCompatNode 的 onPointerEvent()。
kotlin
internal class BackwardsCompatNode(element: Modifier.Element) ... {
override fun onPointerEvent(
...
) {
with(element as PointerInputModifier) {
pointerInputFilter.onPointerEvent(pointerEvent, pass, bounds)
}
}
...
}
接着执行 PointerInteropFilter 里 onPointerEvent() 继续处理。
kotlin
internal class PointerInteropFilter : PointerInputModifier {
override val pointerInputFilter =
object : PointerInputFilter() {
override fun onPointerEvent(
...
) {
...
if (state !== DispatchToViewState.NotDispatching) {
if (pass == PointerEventPass.Initial && dispatchDuringInitialTunnel) {
dispatchToView(pointerEvent)
}
if (pass == PointerEventPass.Final && !dispatchDuringInitialTunnel) {
dispatchToView(pointerEvent)
}
}
...
}
}
onPointerEvent() 将依据 DispatchToViewState
的当前状态,决定是否调用 dispatchToView()
。
kotlin
internal class PointerInteropFilter : PointerInputModifier {
...
override val pointerInputFilter =
object : PointerInputFilter() {
...
private fun dispatchToView(pointerEvent: PointerEvent) {
val changes = pointerEvent.changes
if (changes.fastAny { it.isConsumed }) {
if (state === DispatchToViewState.Dispatching) {
pointerEvent.toCancelMotionEventScope(
this.layoutCoordinates?.localToRoot(Offset.Zero)
?: error("layoutCoordinates not set")
) { motionEvent ->
// 如果之前消费了并且在 Dispatching,继续调用 onTouchEvent()
onTouchEvent(motionEvent)
}
}
state = DispatchToViewState.NotDispatching
} else {
pointerEvent.toMotionEventScope(
this.layoutCoordinates?.localToRoot(Offset.Zero)
?: error("layoutCoordinates not set")
) { motionEvent ->
// ACTION_DOWN 的时候总是发送给 onTouchEvent()
// 并在返回 true 消费的时候标记正在 Dispatching
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
state = if (onTouchEvent(motionEvent)) {
DispatchToViewState.Dispatching
} else {
DispatchToViewState.NotDispatching
}
} else {
onTouchEvent(motionEvent)
}
}
...
}
}
}
}
dispatchToView() 会依据 MotionEvent 的 ACTION 类型和是否已经消费的 Consumed
值决定是否调用 onTouchEvent Unit:
- ACTION_DOWN 时总是调用 onTouchEvent
- 其他 ACTION 依据 Consumed 情况
- 并赋值当前的 DispatchToViewState 状态为 Dispatching 分发中还是 NotDispatching 未分发中
combinedClickable
combinedClickable() 实际上会创建一个 CombinedClickableElement
实例,该实例包裹的 CombinedClickableNode
会被添加到 Modifier Node里。
kotlin
fun Modifier.combinedClickable(
...
) {
Modifier
...
.then(CombinedClickableElement(
...
))
}
private class CombinedClickableElement(
...
) : ModifierNodeElement<CombinedClickableNode>() {
...
}
CombinedClickableNode 复写了 clickablePointerInputNode 属性,提供的是 CombinedClickablePointerInputNode
类型。
kotlin
private class CombinedClickableNode(
...
onClick: () -> Unit,
onLongClickLabel: String?,
private var onLongClick: (() -> Unit)?,
onDoubleClick: (() -> Unit)?
) : AbstractClickableNode(interactionSource, enabled, onClickLabel, role, onClick) {
...
override val clickablePointerInputNode = delegate(
CombinedClickablePointerInputNode(
...
)
)
}
CombinedClickablePointerInputNode 重要的一点是实现了 pointerInput(),调用 detectTapGestures() 设置了 onTap 之类的几个 Unit,并有一一对应关系:
- onTap 对应着目标的 onClick
- onDoubleTap 对应着目标的 onDoubleClick
- onLongPress 对应着目标的 onLongClick
换句话说,combinedClickable 事实上是调用 pointerInput 添加了 onTap 等 Gesture 的监听。
kotlin
private class CombinedClickablePointerInputNode(
...
) {
override suspend fun PointerInputScope.pointerInput() {
interactionData.centreOffset = size.center.toOffset()
detectTapGestures(
onDoubleTap = if (enabled && onDoubleClick != null) {
{ onDoubleClick?.invoke() }
} else null,
onLongPress = if (enabled && onLongClick != null) {
{ onLongClick?.invoke() }
} else null,
...,
onTap = { if (enabled) onClick() }
)
}
}
既然采用了 pointerInput,那么还是会和前面的一样经由 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent(),抵达 detectTapGestures 内部的逻辑。
kotlin
suspend fun PointerInputScope.detectTapGestures(
...
) = coroutineScope {
val pressScope = PressGestureScopeImpl(this@detectTapGestures)
awaitEachGesture {
...
if (upOrCancel != null) {
if (onDoubleTap == null) {
onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
} else {
...
if (secondDown == null) {
onTap?.invoke(upOrCancel.position) // no valid second tap started
} else {
...
try {
withTimeout(longPressTimeout) {
val secondUp = waitForUpOrCancellation()
if (secondUp != null) {
...
onDoubleTap(secondUp.position)
} else {
launch {
pressScope.cancel()
}
onTap?.invoke(upOrCancel.position)
}
}
} ...
}
}
}
}
}
并在 onTap 处,回调经由 CombinedClickablePointerInputNode 传入的 onClick Unit。
clickable
和 combinedClickable() 类似,实际上会创建一个 ClickableElement
实例,该实例包裹的 ClickableNode
会被添加到 Modifier Node里。
kotlin
fun Modifier.clickable(
...
onClick: () -> Unit
) = inspectable(
...
) {
Modifier
...
.then(ClickableElement(interactionSource, enabled, onClickLabel, role, onClick))
}
private class ClickableElement(
...
private val onClick: () -> Unit
) : ModifierNodeElement<ClickableNode>() {
...
}
ClickableNode 复写了 clickablePointerInputNode 属性,提供的是 ClickablePointerInputNode
类型。
kotlin
private class ClickableNode(
...
onClick: () -> Unit
) : AbstractClickableNode(interactionSource, enabled, onClickLabel, role, onClick) {
...
override val clickablePointerInputNode = delegate(
ClickablePointerInputNode(
...,
onClick = onClick,
interactionData = interactionData
)
)
}
ClickablePointerInputNode 的重点也是实现了 pointerInput(),它调用的 detectTapAndPress()
设置了 onTap Unit,并对应着目标的 onClick,即事实上也是调用 pointerInput 添加了 onTap Gesture 的监听。
kotlin
private class ClickablePointerInputNode(
onClick: () -> Unit,
...
) {
override suspend fun PointerInputScope.pointerInput() {
...
detectTapAndPress(
...,
onTap = { if (enabled) onClick() }
)
}
}
当 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent() 收到事件后,会抵达 detectTapAndPress 内部的逻辑。并在 onTap 处回调 ClickablePointerInputNode 传入的 onClick Unit。
kotlin
internal suspend fun PointerInputScope.detectTapAndPress(
...
) {
val pressScope = PressGestureScopeImpl(this)
coroutineScope {
awaitEachGesture {
...
if (up == null) {
launch {
pressScope.cancel() // tap-up was canceled
}
} else {
up.consume()
launch {
pressScope.release()
}
onTap?.invoke(up.position)
}
}
}
}
结语
最后,我们将 Compose 中几种典型的 touch 处理的 process 综合到一张图里,供大家直观地了解互相之间的关系。
-
和物理的 Touch 事件一样,经由
InputTransport
抵达ViewRootImpl
以及实际根 View 的DecorView
-
经由 ViewGroup 的分发抵达 Compose 最上层的
AndroidComposeView
的 dispatchTouchEvent() -
dispatchTouchEvent() 将
MotionEvent
转化为PointerInputEvent
类型并交由PointerInputEventProcessor
处理 -
首先调用
HitPathTracker
的 addHitPath() 记录 Pointer 事件的分发路径 -
接着调用 dispatchChanges() 执行分发,并按照两个步骤抵达 Compose 的各层 Node:
- 首先调用 dispatchMainEventPass() 进行 Initial 和 Main 策略的事件分发。这其中会调用各
Modifer
Node 的 onPointerEvent() ,并依据 touch 逻辑回调clickable
、pointerInput
等 Modifier 的 Unit - 接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发
- 首先调用 dispatchMainEventPass() 进行 Initial 和 Main 策略的事件分发。这其中会调用各
除了 pointerInput 等几个常用的 touch 处理方法以外,Compose 还支持通过 scrollable
、swipeable
、draggable
、transformable
等处理更为复杂、灵活的 touch 场景。
感兴趣的同学可以自行研究。