Compose 中的 touch 事件

在 Android 原生开发中对 View 的 touch 事件处理有这么几种方式:

  1. setOnClickListener:监听点击事件
  2. setOnTouchListener:监听 touch 事件
  3. 自定义View:覆写 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 等方法

方式1和2都是监听最后的结果,无需多说,方式3是通过覆写 View 中 touch 事件的分发处理流程中的关键方法从而达到对 touch 事件的处理。

dispatchTouchEvent 用于分发 touch 事件,onInterceptTouchEvent 用于是否中断(拦截)touch 事件,返回 true,表示拦截,返回 false,表示不拦截,onTouchEvent 用于处理 touch 事件,返回 true 表示消费事件。此外,还可以在 dispatchTouchEvent 方法中通过getParent().requestDisallowIntercepTouchEvent(true) 方式,禁止父控件拦截事件。

Compose 中 touch 事件处理

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

详尽的说明可以查看官方文档:
https://developer.android.google.cn/develop/ui/compose/touch-input/pointer-input/understand-gestures?hl=zh-cn

Jetpack Compose 提供了不同的抽象级别来处理手势。最顶层的是组件支持。Button等可组合项会自动支持手势。如需为自定义组件添加手势支持,可以向任意可组合项添加clickable等手势修饰符。最后,如果需要自定义手势,可以使用pointerInput修饰符。

选择正确的抽象级别是 Compose 中的常见主题。Compose 以构建可重复使用的分层组件作为理念,这意味着不应该始终以构建较低级别的构建块为目标。许多较高级别的组件不仅能够提供更多功能,而且通常还会融入最佳实践,例如支持无障碍功能等。

例如,如果想为自己的自定义组件添加手势支持,可以使用Modifier.pointerInput从头开始构建;但在此之上还有其他更高级别的组件,它们可以提供更好的起点,例如 Modifier.draggable、Modifier.scrollable 或 Modifier.swipeable。

一般来讲,最好基于能提供所需功能的最高级别的组件进行构建,以便从其包含的最佳实践中受益。

组件支持

Compose 中的许多开箱即用组件都包含某种内部手势处理。例如,Button会自动检测点按并触发点击事件、LazyColumn通过滚动其内容来响应拖动手势、SwipeToDismissBox件则包含用于关闭元素的滑动逻辑。

当这些组件中的手势处理有适合的用例时,请优先使用组件中包含的手势,因为它们包含对焦点和无障碍功能的开箱即用型支持,并且已经过充分测试。例如,Button包含用于无障碍功能的语义信息,以便无障碍服务正确地将其描述为按钮,而不是只描述任何可点击的元素clickable。

使用修饰符向任意可组合项添加特定手势

可以将手势修饰符应用于任意可组合项,以使可组合项监听手势。例如,clickable 处理点按手势,通过应用 verticalScroll 让 Column 处理垂直滚动。

有许多修饰符可用于处理不同类型的手势:

一般来说,与自定义手势处理相比,最好使用开箱即用的手势修饰符。除了手势事件处理之外,修饰符还添加了更多功能。例如,clickable 修饰符不仅添加了对按下和点按的检测,还添加了语义信息、互动的视觉指示、悬停、焦点和键盘支持。可以查看 clickable 的源代码,了解如何添加该功能。

使用 pointerInput 修饰符将自定义手势添加到任意可组合项

pointerInput 为 Compose 中处理所有手势事件的入口,可以编写自己的手势处理程序来自定义手势。

原始手势事件

pointerInput 可以监听到原始手势事件

pointerInput(Unit) {
    awaitPointerEventScope {
        while (true) {
            val event = awaitPointerEvent()
            // handle pointer event
            Log.d(TAG, "${event.type}, ${event.changes.first().position}")
        }
    }
}

虽然监听原始手势输入事件非常强大,但根据此原始数据编写自定义手势也很复杂。为了简化自定义手势的创建过程,compose提供了多种实用工具方法。

每个手势事件

根据定义,手势从按下事件开始。可以使用 awaitEachGesture 辅助方法,而不是遍历每个原始事件的 while(true) 循环。所有手势事件均被释放后,awaitEachGesture 方法会重启所在的块,表示手势已完成。

pointerInput(Unit) {
    awaitEachGesture {
        awaitFirstDown().also { it.consume() }
        val up = waitForUpOrCancellation()
        if (up != null) {
            up.consume()
            Log.d(TAG, "click one time")
        }
    }
}

在实践中,除非是在不识别手势的情况下响应手势事件,否则几乎总是需要使用 awaitEachGesture。例如 hoverable,它不响应手势按下或松开事件,它只需要知道手势何时进入或离开其边界。

特定手势事件

AwaitPointerEventScope 提供了一系列方法可帮助识别手势的常见操作:

检测完整手势

监听特定的完整手势并相应地做出响应。PointerInputScope 提供了用于完整手势的监听:

注意: 这些检测器是顶级检测器,因此无法在一个 pointerInput 修饰符中添加多个检测器。以下代码段只会检测点按操作,而不会检测拖动操作:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
                // Never reached
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}

在内部,detectTapGestures 方法会阻塞协程,并且永远不会到达第二个检测器。如果需要向可组合项添加多个手势监听器,请改用单独的 pointerInput 修饰符实例:

var log by remember { mutableStateOf("") }
Column {
    Text(log)
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .pointerInput(Unit) {
                detectTapGestures { log = "Tap!" }
            }
            .pointerInput(Unit) {
                // These drag events will correctly be triggered
                detectDragGestures { _, _ -> log = "Dragging" }
            }
    )
}
多点触控手势事件

在多点触控手势事件下,基于原始手势值所需的转换就变得很复杂。如果使用 transformable 修饰符或 detectTransformGestures 方法未能提供足够精细的控制,以下辅助方法可以监听原始事件并对其执行计算。辅助方法包括 calculateCentroidcalculateCentroidSizecalculatePancalculateRotationcalculateZoom

pointerInteropFilter

pointerInteropFilter 可以用来直接处理 ACTION DOWN、MOVE、UP 和 CANCEL 事件的函数,类似 onTouchEvent(),还可以指定是否允许父控件拦截:requestDisallowInterceptTouchEvent

pointerInteropFilter {
    when (it.action) {
        MotionEvent.ACTION_DOWN -> {}
        MotionEvent.ACTION_MOVE -> {}
        MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {}
    }
    true
}

注意: 同 onTouchEvent 中一样,如果 ACTION_DOWN 返回了 false 的话,那么之后的 ACTION_MOVE 和 ACTION_UP 就都不会过来了。

注意: pointerInteropFilter 返回 true 的话,touch 事件都将由 pointerInteropFilter 处理,pointerInput、combinedClickable、clickable等都不会被调用了。

原理分析

入口

Compose 创建的视图最终都是被添加至 AndroidComposeView 中,而 AndroidComposeView 是由 ComposeView 在 setContent 方法时创建。由 Android 原生开发 View 中 touch 事件的分发处理流程可知,入口便是 AndroidComposeView 的 dispatchTouchEvent 方法。

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

handleMotionEvent() 方法对 MotionEvent 进行处理:

 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
         } 
         ...
     }
     ...
 }

首先通过 convertToPointerInputEvent() 将 MotionEvent 转换成 PointerInputEvent。针对多点触控的 touch 信息,需要转换成 PointerInputEventData 保存到 PointerInputEvent 里的 pointers List 中。然后交由专门的 PointerInputEventProcessor 类处理PointerInputEvent

 internal class PointerInputEventProcessor(val root: LayoutNode) {
     ...
     fun process(
         pointerEvent: PointerInputEvent,
         positionCalculator: PositionCalculator,
         isInBounds: Boolean = true
     ): ProcessResult {
         ...
         try {
             isProcessing = true
             // 先转换成 InternalPointerEvent 类型
             // Gets a new PointerInputChangeEvent with the PointerInputEvent.  
             @OptIn(InternalCoreApi::class)
             val internalPointerEvent =
                 pointerInputChangeEventProducer.produce(pointerEvent, positionCalculator)
             ...
 
             // Add new hit paths to the tracker due to down events.
             for (i in 0 until internalPointerEvent.changes.size()) {
                 val pointerInputChange = internalPointerEvent.changes.valueAt(i)
                 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 实例中,并按照 PointerId 存到 InternalPointerEvent 里。

 private class PointerInputChangeEventProducer {
     fun produce(
         ...
     ): InternalPointerEvent {
         val changes: LongSparseArray<PointerInputChange> =
             LongSparseArray(pointerInputEvent.pointers.size)
         pointerInputEvent.pointers.fastForEach {
             ...
             changes.put(it.id.value, PointerInputChange( ... ))
         }
 
         return InternalPointerEvent(changes, pointerInputEvent)
     }
     ...
 }

第二步:对第一步中的信息差异changes进行遍历,逐个调用 hitTest() 将变化的 touch 信息放到 Compose 根节点 root 中进行预匹配,得到匹配了 touch 信息的 LayoutNode 的结果 HitTestResult,以确定 touch 事件分发的路径。这里最关键的是 hitInMinimumTouchTarget(),它会将匹配到的 Modifier 里设置的 touch Node 赋值进 HitTestResult 的 values 中。

 internal class HitTestResult : List<Modifier.Node> {
     fun hitInMinimumTouchTarget( ... ) {
         ...
         distanceFromEdgeAndInLayer[hitDepth] =
             DistanceAndInLayer(distanceFromEdge, isInLayer).packedValue
     }
 }

然后调用 HitPathTrackeraddHitPath() 去记录分发路径里到名为 root 的 NodeParent 实例的 Node 路径。

 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
         }
     }

第三步:有了分发路径之后,调用 HitPathTrackerdispatchChanges() 开始分发。

分发

首先将调用 buildCache() 检查 PointerEvent 是否和 cache 的信息发生了变化,如果确有变化再继续分发,反之取消。

 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {
     fun dispatchChanges(
         internalPointerEvent: InternalPointerEvent,
         isInBounds: Boolean = true
     ): Boolean {
         // 检查cache是否有变化
         val changed = root.buildCache(
             ...
         )
         if (!changed) {
             return false
         }
         // cache 确有变化,调用          
         var dispatchHit = root.dispatchMainEventPass(             
             ...         
         )
         // 最后调用 dispatchFinalEventPass          
         dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit 
         
         return dispatchHit
     }
 }

NodeParent 会调用各 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 进行逐个分发。

同样 NodeParent 也是调用各 child Node 的 dispatchMainEventPass() 进行分发。

 internal open class NodeParent {
     open fun dispatchMainEventPass(
         ...
     ): Boolean {
         var dispatched = false
         children.forEach {
             dispatched = it.dispatchMainEventPass( ... ) || dispatched
         }
         return dispatched
     }
 }
 
 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. children 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() 分发。
  3. 如果发现本 Node 仍然 attach 到了 Layout,调用 onPointerEvent() 并设置 PointerEventPass 策略为 Main,代表子节点优于父节点处理,,顺序是自下而上,便于子节点处理需要在父节点响应之前响应点击等场景。

最后调用 dispatchFinalEventPass() 进行 PointerEventPass 策略为 Final 的分发。

 internal open class NodeParent {
     open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
         var dispatched = false
         children.forEach {
             dispatched = it.dispatchFinalEventPass(internalPointerEvent) || dispatched
         }
         cleanUpHits(internalPointerEvent)
         return dispatched
     }
 }
 
  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(internalPointerEvent)
         clearCache()
         return result
     }
 }

dispatchMainEventPass() 一样,dispatchFinalEventPass() 也是先针对本 Node 执行 onPointerEvent(),再针对 child Node 逐个分发一遍。调用 onPointerEvent() 传递 PointerEventPass 策略为 Final,代表这是最终步骤的分发,顺序是自上而下,子节点可以知道父节点在 PointerInputChanges 中进行了哪些处理,比如是否已经消费了 scroll 而无需再处理点击事件了。

此外,执行完毕之后,额外需要执行以下重置工作:

  • cleanUpHits():清空 Node 中保存的 pointerId 等 touch 信息。
  • clearCache():本 touch 事件处理结束,清空 cache 事件变化信息 PointerInputChange 和 LayoutCoordinates

touch 事件处理

上面说到 onPointerEvent() 的具体逻辑取决于向 Modifier 中设置的 touch Node 类型。

pointerInput

pointerInput() 实际上会创建一个 SuspendingPointerInputModifierNodeImpl 类型的 Node 添加到 Modifier 里,pointerInput 本身的 block 会被存在 pointerInputHandler 里。

 fun Modifier.pointerInput(
     key1: Any?,
     block: suspend PointerInputScope.() -> Unit
 ): Modifier = this then SuspendPointerInputElement( 
     key1 = key1,
     pointerInputHandler = block
 )
 
 internal class SuspendPointerInputElement(
     ...
     val pointerInputHandler: suspend PointerInputScope.() -> Unit
 ) : ModifierNodeElement<SuspendingPointerInputModifierNodeImpl>() {
     ...
     override fun create(): SuspendingPointerInputModifierNodeImpl {
         return SuspendingPointerInputModifierNodeImpl(pointerInputHandler)
     }
     ...
 }

在 onPointerEvent() 分发过来的时候会调用 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent()。

 internal class SuspendPointerInputElement(
     override fun onPointerEvent(
         ...
     ) {
         ...
         // Coroutine lazily launches when first event comes in.
         if (pointerInputJob == null) {
             // 'start = CoroutineStart.UNDISPATCHED' required so handler doesn't miss first event.
             pointerInputJob = coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
                 pointerInputHandler()
             }
         }
         
         dispatchPointerEvent(pointerEvent, pass)
         ...
     }
 }

里面会执行 pointerInputHandler(),就是在 pointerInput 里设置的 block。

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

 internal class SuspendPointerInputElement(
     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 的 block 会被存在 PointerInteropFilter 里。

 fun Modifier.pointerInteropFilter(
     requestDisallowInterceptTouchEvent: (RequestDisallowInterceptTouchEvent)? = null,
     onTouchEvent: (MotionEvent) -> Boolean
 ): Modifier = composed(
     ...
 ) {
     val filter = remember { PointerInteropFilter() }
     filter.onTouchEvent = onTouchEvent
     filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent
     filter
 }

在 onPointerEvent() 分发过来的时候会调用 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 }) {
                     // We should no longer dispatch to the Android View.
                     if (state === DispatchToViewState.Dispatching) {
                         // If we were dispatching, send ACTION_CANCEL.
                         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 block:

  • ACTION_DOWN 时总是调用 onTouchEvent。
  • 其他 ACTION 依据 Consumed 情况,并赋值当前的 DispatchToViewState 状态为 Dispatching 分发中还是 NotDispatching 未分发中。
combinedClickable

combinedClickable() 实际上会创建一个 CombinedClickableNode 类型的 Node 添加到 Modifier 里。

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

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

 private class CombinedClickableNodeImpl(
     onClick: () -> Unit,
     onLongClickLabel: String?,
     private var onLongClick: (() -> Unit)?,
     onDoubleClick: (() -> Unit)?,
     ...
 ) : CombinedClickableNode,
     AbstractClickableNode(interactionSource, enabled, onClickLabel, role, onClick) {
     ...
     override val clickablePointerInputNode = delegate(
         CombinedClickablePointerInputNode(
             ...
         )
     )
 }

CombinedClickablePointerInputNode 最重要的一点是实现了 pointerInput(),调用了 detectTapGestures() 监听:

  • onTap 对应着目标的 onClick
  • onDoubleTap 对应着目标的 onDoubleClick
  • onLongPress 对应着目标的 onLongClick

也就是说 combinedClickable 实际上是调用 pointerInput 并添加了 detectTapGestures 的监听。

 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) {
             // tap was successful.
             if (onDoubleTap == null) {
                 onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
             } else {
                 // check for second tap  
                 val secondDown = awaitSecondDown(upOrCancel)
                 
                 if (secondDown == null) {
                     onTap?.invoke(upOrCancel.position) // no valid second tap started
                 } else {
                     ...
                     // Might have a long second press as the second tap
                     try {
                         withTimeout(longPressTimeout) {
                             val secondUp = waitForUpOrCancellation()
                             if (secondUp != null) {
                                 ...
                                 onDoubleTap(secondUp.position)
                             } else {
                                 launch {
                                     pressScope.cancel()
                                 }
                                 onTap?.invoke(upOrCancel.position)
                             }
                         }
                     } ...
                 }
             }
         }
     }
 }
clickable

和 combinedClickable() 类似,clickable() 实际上会创建一个 ClickableNode 类型的 Node 添加到 Modifier 里。

 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 对应着目标的 onClick

也就是说 clickable 实际上也是调用 pointerInput 并添加了 detectTapAndPress 的监听。

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

所以也是经由 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent(),抵达 detectTapAndPress。

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

总结 touch 事件分发流程

  1. 和原生开发中的 touch 事件一样,经由 InputTransport 抵达 ViewRootImpl 以及实际根 View 的 DecorView

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

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

  4. PointerInputEventProcessor处理过程中先调用 HitPathTrackeraddHitPath() 记录 touch 事件的分发路径。

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

    步骤一:首先调用 dispatchMainEventPass() 进行 InitialMain 策略的事件分发。这其中会调用各 Modifer Node 的 onPointerEvent() ,并依据 touch 逻辑回调 clickablepointerInput 等 Modifier 的 block。

    步骤二:接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发。

相关推荐
_Shirley1 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
Web阿成1 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript
雷神乐乐1 小时前
Spring学习(一)——Sping-XML
java·学习·spring
李雨非-19期-河北工职大2 小时前
思考: 与人交际
学习
哦哦~9212 小时前
深度学习驱动的油气开发技术与应用
大数据·人工智能·深度学习·学习
小木_.2 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Web阿成3 小时前
5.学习webpack配置 babel基本配置
前端·学习·webpack
hedalei3 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng3 小时前
安卓多渠道apk配置不同签名
android