通过调用栈快速探究 Compose 中 touch 事件的处理原理

本文为稀土掘金技术社区首发签约文章,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 回调的办法。

需要了留意的是:

  1. 当同时设置了 combinedClickable 的 onClick 的话,clickable 就不会被调用了
  2. 当同时设置了 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
         }
     }
     ...
 }
  1. 告知 PointerInputChangeEventProducer 调用 produce() 依据传入的 PointerInputEvent 去追踪发生变化的手指 touch 信息并返回 InternalPointerEvent 实例。具体差异的信息将逐个封装到 PointerInputChange 实例中,并按照手指 ID map 后存到 InternalPointerEvent 里

    kotlin 复制代码
     private 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)
         }
         ...
     }
  2. 遍历上面得到的 map,逐个调用 hitTest() 将变化的 touch 信息放到 Compose 根节点 root 中进行预匹配,得到匹配了 touch 信息的 LayoutNode 的结果 HitTestResult,以确定事件分发的路径。这里最关键的是 hitInMinimumTouchTarget(),它会将匹配到的 Modifier 里设置的 touch Node 赋值进 HitTestResult 的 values 中

    kotlin 复制代码
     internal class HitTestResult : List<Modifier.Node> {
         fun hitInMinimumTouchTarget( ... ) {
             ...
             distanceFromEdgeAndInLayer[hitDepth] =
                 DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
         }
     }
  3. 此后,在取得 map 下一个成员之前,调用 HitPathTrackeraddHitPath() 去记录分发路径里的 Node 路径到名为 root 的 NodeParent 实例里

    kotlin 复制代码
     internal 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
             }
         }
  4. 最后调用 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)
                 }
             }
         }
     }
 }

这个函数执行的内容比较重要:

  1. 执行本 Node 的 onPointerEvent(),传递 PointerEventPass 策略为 Initial,代表父节点优先于子节点进行处理 PointerEvent,顺序是自上而下,便于父节点处理需要在执行 scroll 时防止子 Node 里按钮响应点击等场景

    • onPointerEvent() 的具体逻辑取决于向 Modifier 中设置的 touch Node 类型,将在下个章节展开
  2. 如果本 Node attach 到 Compose Layout 了,遍历它的 child Node,继续调用 dispatchMainEventPass() 分发,后续逻辑和 1 一致,不再赘述

  3. 如果发现本 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 综合到一张图里,供大家直观地了解互相之间的关系。

  1. 和物理的 Touch 事件一样,经由 InputTransport 抵达 ViewRootImpl 以及实际根 View 的 DecorView

  2. 经由 ViewGroup 的分发抵达 Compose 最上层的 AndroidComposeViewdispatchTouchEvent()

  3. dispatchTouchEvent() 将 MotionEvent 转化为 PointerInputEvent 类型并交由 PointerInputEventProcessor 处理

  4. 首先调用 HitPathTrackeraddHitPath() 记录 Pointer 事件的分发路径

  5. 接着调用 dispatchChanges() 执行分发,并按照两个步骤抵达 Compose 的各层 Node:

    1. 首先调用 dispatchMainEventPass() 进行 InitialMain 策略的事件分发。这其中会调用各 Modifer Node 的 onPointerEvent() ,并依据 touch 逻辑回调 clickablepointerInput 等 Modifier 的 Unit
    2. 接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发

除了 pointerInput 等几个常用的 touch 处理方法以外,Compose 还支持通过 scrollableswipeabledraggabletransformable 等处理更为复杂、灵活的 touch 场景。

感兴趣的同学可以自行研究。

相关推荐
凯文的内存2 小时前
Android14 OTA升级速度过慢问题解决方案
android·ota·update engine·系统升级·virtual ab
VinRichard2 小时前
Android 常用三方库
android
Aileen_0v03 小时前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
江上清风山间明月6 小时前
Flutter DragTarget拖拽控件详解
android·flutter·ios·拖拽·dragtarget
debug_cat8 小时前
AndroidStudio Ladybug中编译完成apk之后定制名字kts复制到指定目录
android·android studio
编程洪同学13 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端
氤氲息15 小时前
Android 底部tab,使用recycleview实现
android
tmacfrank15 小时前
Coroutine 基础二 —— 结构化并发(一)
kotlin
Clockwiseee15 小时前
PHP之伪协议
android·开发语言·php
小林爱15 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio