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

news2025/1/21 18:56:40

Compose-base-touch.png

前言

Compose 视图的处理方式和 Android 传统 View 有很大差别,针对 touch 事件的处理自然也截然不同。

如何在 Compose 中处理 touch 事件,官方已有非常详尽的说明,可以参考:https://developer.android.google.cn/jetpack/compose/touch-input。

本文将以 Compose 中几种最典型的 touch 处理为例,分别介绍其使用场景,并打印其调用栈。最后结合栈和 touch 源码,一起综合分析 Compose 中处理 touch 的原理细节。

各种 touch 处理的写法和场景

pointerInput

Compose 中处理所有手势事件的入口,类似传统视图的 onTouch。在这里可以识别 click 手势,而且相应优先级高于 clickable。

第二个参数为 PointerInputScope 的扩展函数类型,有如下:

  • 来自 TapGestureDetector 文件中定义的 detectTapGestures:可以用来检测 onDoubleTap、onLongPress、onPress、onTap 几种手势
  • 来自 DragGestureDetector 文件中定义的 detectDragGestures:可以用来检测拖拽开始、结束、取消等手势
  • 来自 TransformGestureDetector 文件中定义的 detectTransformGestures:可以用来检测旋转、平移、缩放的手势
  • 等等
 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,

 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
     Column(
         modifier = Modifier
             .pointerInput(Unit) {
                 LogUtil.printLog(message = "GameScreen pointerInput", throwable = Throwable())
             }
     )
 }

打印其调用栈:

 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 就不会发过来了。

 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:

 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 就不会被处理了。

 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
     Column(
         modifier = Modifier
             .combinedClickable(
                 onLongClick = { },
                 onDoubleClick = { },
                 onClick = {
                     LogUtil.printLog(message = "GameScreen combinedClickable onClick", throwable = Throwable())
                 }
             )
     )
 }

同样在最基本的 onClick 里打印个 stack:

 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 就不会处理了
 fun GameScreen(
     clickable: Clickable = Clickable()
 ) {
     Column(
         modifier = Modifier
             .clickable {
                 LogUtil.printLog(message = "GameScreen clickable", throwable = Throwable())
             }
     )
 }

直接打个 stack:

 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() 开始分析。

 internal class AndroidComposeView(...) : ViewGroup(context),... {
     override fun dispatchTouchEvent(motionEvent: MotionEvent): Boolean {
         ...
         val processResult = handleMotionEvent(motionEvent)
         ...
         return processResult.dispatchedToAPointerInputModifier
     }
 }

关键的处理在 handleMotionEvent() 里。

 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()

 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 类继续。

 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 里

     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 中

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

     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 的信息发生了变化,如果确有变化再继续分发,反之取消。

 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() 进行。

 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 进行逐个分发。

 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() 的逻辑如下:

 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 的分发。

 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 而无需再处理点击事件了。

 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
 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 里。

 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()。

 internal class SuspendPointerInputElement(
     override fun onPointerEvent(
         ...
     ) {
         ...
         if (pointerInputJob == null) {
             pointerInputJob = coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                 pointerInputHandler()
             }
         }
         ...
     }
 }

接着执行 pointerInputHandler(),其就是我们在 pointerInput 里设置的 Unit。

此后,还需要调用 dispatchPointerEvent() 里会告知 forEachCurrentPointerHandler() 按照 PointerEventPass 策略决定从从上至下遍历还是从下至上遍历,并逐个添加待处理的 PointerEvent 给所有的 PointerHandler。

 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 里。

 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()。

 internal class BackwardsCompatNode(element: Modifier.Element) ... {
     override fun onPointerEvent(
         ...
     ) {
         with(element as PointerInputModifier) {
             pointerInputFilter.onPointerEvent(pointerEvent, pass, bounds)
         }
     }
     ...
 }

接着执行 PointerInteropFilter 里 onPointerEvent() 继续处理。

 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()

 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里。

 fun Modifier.combinedClickable(
     ...
 ) {
     Modifier
         ...
         .then(CombinedClickableElement(
             ...
         ))
 }private class CombinedClickableElement(
     ...
 ) : ModifierNodeElement<CombinedClickableNode>() {
     ...
 }

CombinedClickableNode 复写了 clickablePointerInputNode 属性,提供的是 CombinedClickablePointerInputNode 类型。

 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 的监听。

 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 内部的逻辑。

 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里。

 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 类型。

 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 的监听。

 private class ClickablePointerInputNode(
     onClick: () -> Unit,
     ...
 ) {
     override suspend fun PointerInputScope.pointerInput() {
         ...
         detectTapAndPress(
             ...,
             onTap = { if (enabled) onClick() }
         )
     }
 }

当 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent() 收到事件后,会抵达 detectTapAndPress 内部的逻辑。并在 onTap 处回调 ClickablePointerInputNode 传入的 onClick Unit。

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

compose_touch.drawio.png

  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 场景。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1801333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[图解]企业应用架构模式2024新译本讲解11-领域模型4

1 00:00:00,160 --> 00:00:01,870 好&#xff0c;到这里的话 2 00:00:02,620 --> 00:00:05,060 文字处理器的产品对象就生成了 3 00:00:06,880 --> 00:00:09,180 同样下面就是电子表格 4 00:00:10,490 --> 00:00:11,480 电子表格也同样的 5 00:00:11,490 -->…

【Vue】——组件的注册与引用

&#x1f4bb;博主现有专栏&#xff1a; C51单片机&#xff08;STC89C516&#xff09;&#xff0c;c语言&#xff0c;c&#xff0c;离散数学&#xff0c;算法设计与分析&#xff0c;数据结构&#xff0c;Python&#xff0c;Java基础&#xff0c;MySQL&#xff0c;linux&#xf…

【Java面试】九、微服务篇-SpringCloud(上)

文章目录 1、SpringCloud五大组件2、服务注册和发现2.1 Eurake2.2 Eurake和Nacos的区别 3、Ribbon负载均衡3.1 策略3.2 自定义负载均衡策略 4、服务雪崩与熔断降级4.1 服务雪崩4.2 服务降级4.3 服务熔断 5、服务限流5.1 Nginx限流5.2 网关限流 6、微服务监控7、面试 1、SpringC…

antd vue a-select 搜索

数据结构 list: [{ name: "序号", id: 0, show: true },{ name: "出库单编号", id: 1, show: false },{ name: "wbs元素", id: 2, show: true },{ name: "序号1", id: 3, show: true },{ name: "出库单编号1", id: 4, show…

每天五分钟深度学习pytorch:pytorch中的广播机制是什么?

本文重点 在pytorch中经常有张量和张量之间的运算,那么有一点需要注意,那就是维度要匹配,如果维度不匹配就有可能出现问题。如果维度不一致,此时也可以同时进行操作,此时就需要使用pytorch中的广播机制,本节课程就讲解pytorch中的广播机制。 广播机制示意图 如上就是py…

基于C#的计算机与安捷伦34970A通信方法

概述 安捷伦34970A采集数据&#xff0c;34970A支持RS232接口&#xff0c;但是如果直接用winform自带的seriaport类基本是没必要使用的&#xff0c;安捷伦等仪表通讯需要用到VISA的库。 库的获取 1. 是德科技的IO Library. 2. NI下载NI-VISA. 两者用法接近. 代码如下 using…

【数据分析基础】实验numpy、pandas和matplolib

文件score.xlsx 中存放了学生的各个科目的考试成绩&#xff08;如下图&#xff09;&#xff0c; 1. 编程实现&#xff1a;输入任意一个学号&#xff0c;将该学号对应的成绩&#xff0c;通过雷达图显示。 &#xff08;1&#xff09;程序代码&#xff1a; import pandas as pd…

在Java中使用SeleniumAPI,超详细

Java中 Selenium相关操作 1 定位元素 1.1 css选择器定位元素 就是定位到页面的元素&#xff0c;本质上就是一个一个的语法 下面举几个具体的例子&#xff1a; 类选择器 按照给定的 class 属性的值&#xff0c;选择所有匹配的元素。 语法&#xff1a;.classname 例子&am…

【中篇】从 YOLOv1 到 YOLOv8 的 YOLO 物体检测模型历史

YOLO 型号之所以闻名遐迩,主要有两个原因:其速度和准确性令人印象深刻,而且能够快速、可靠地检测图像中的物体。上回我解释了Yolo v1, 今天从Yolov2开始。 YOLOv2:更好、更快、更强 2017 年 7 月一个闷热的星期二下午,雷德蒙(Joseph Redmon, Yolo创始人)再次走上舞台。 …

MASA:匹配一切、分割一切、跟踪一切

摘要 在复杂场景中跨视频帧稳健地关联相同对象是许多应用的关键&#xff0c;特别是多目标跟踪&#xff08;MOT&#xff09;。当前方法主要依赖于标注的特定领域视频数据集&#xff0c;这限制了学习到的相似度嵌入的跨域泛化能力。我们提出了MASA&#xff0c;一种新颖的方法用于…

嵌入式Linux系统编程 — 3.1 Linux系统中的文件类型

目录 1 Linux 系统中的文件类型简介 2 普通文件 2.1 什么是普通文件 2.2 普通文件分类 3 目录文件 4 字符设备文件和块设备文件 4.1 什么是设备文件 4.2 查看设备文件的方法&#xff1a; 5 符号链接文件&#xff08;link&#xff09; 5.1 什么是符号链接文件 5.2 如…

生成式人工智能 - stable diffusion web-ui安装教程

一、Stable Diffusion WEB UI 屌丝劲发作了,所以本地调试了Stable Diffusion之后,就去看了一下Stable Diffusion WEB UI,网络上各种打包套件什么的好像很火。国内的也就这个层次了,老外搞创新,国内跟着屁股后面搞搞应用层,就叫大神了。 不扯闲篇了,我们这里从git源码直接…

【vscode-快捷键 一键JSON格式化】

网上有很多JSON格式化工具&#xff0c;也有很多好用的在线json格式化工具。但是其实Vscode里面的可以直接格式化JSON&#xff0c;这里分享一个我常用的小插件 Prettify JSON 未格式化的JSON数据 召唤出命令行&#xff0c;输入prettify JSON 即可! ✿✿ヽ(▽)ノ✿

C# WinForm —— 26 ImageList 介绍

1. 简介 图片集合&#xff0c;用于存储图像的资源&#xff0c;并在关联控件中显示出来 可以通过 索引、键名 访问每张图片 没有事件 2. 属性 属性解释(Name)控件ID&#xff0c;在代码里引用的时候会用到,一般以 imgList 开头ClolorDepth用于呈现图像的颜色数&#xff0c;默…

立创小tips

立创小tips 原理图中 1-修改图纸属性 保存完&#xff0c;绘制原理图的界面就出现了&#xff0c;然后我们鼠标点击原理图的边缘变成红色就可以高边表格的属性了。 2-鼠标右键可以移动整个原理图 3-查看封装 点击任意一个元器件&#xff0c;在右侧就会显示封装属性&#xff…

9.1 Go 接口的定义

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《基于日间-日内不确定集的中长期电源扩展规划》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

输入偏置电流是什么?

输入失调电流与输入补偿电流概念一样&#xff08;input offset current&#xff09;&#xff1a;同相减去反相输入端偏置电流的差值。这是由生产工艺导致同相与反相端的电流大小方向都会有所不同。 第一种情况&#xff1a;同相输入端减去反相输入端 第一种情况&#xff1a;同相…

使用cv2控制鼠标实现circle的拖拽

2.代码 import numpy as np import cv2x_center [100,200,300,400] y_center [200,200,200,200] radius 30def mouse_LButtonDown(event, x, y, flags, param):global tempif event cv2.EVENT_LBUTTONDOWN:print(f" Down Clicked at ({x}, {y})")for i in range…

利用PowerQuery控制数据行数

PowerBI报表在开发的过程中&#xff0c;经常会遇到数据量非常庞大的情况&#xff0c;在这种情况下&#xff0c;本机连接数据源如果不进行特殊处理的话&#xff0c;那么刷新数据的时候可能会发生数据刷新时间过长、数据加载内存错误、开发过程中构建DAX卡顿等情况。 那么在实际开…