美文网首页
Compose 中的 touch 事件

Compose 中的 touch 事件

作者: 愿天深海 | 来源:发表于2024-05-28 18:19 被阅读0次

    在 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(Unit) {
        detectTapGestures(
            onDoubleTap = { },
            onLongPress = { },
            onPress = { },
            onTap = { }
        )
        
        detectDragGestures(
            onDragStart = { },
            onDragEnd = { },
            onDragCancel = { },
            onDrag = { change: PointerInputChange, dragAmount: Offset ->
            }
        )
        
        detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
        }
    }
    

    注意: 这些检测器是顶级检测器,因此无法在一个 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 策略的事件分发。

    相关文章

      网友评论

          本文标题:Compose 中的 touch 事件

          本文链接:https://www.haomeiwen.com/subject/crufqjtx.html