Compose 中的 touch 事件

devtools/2024/9/23 9:26:50/

在 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 处理垂直滚动。

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

  • 使用 clickablecombinedClickableselectabletoggleable 和 triStateToggleable 修饰符处理点按和按压操作。
  • 使用 horizontalScrollverticalScroll 及更通用的 scrollable 修饰符处理滚动操作。
  • 使用 draggable 和 swipeable 修饰符处理拖动操作。
  • 使用 transformable 修饰符处理多点触控手势,例如平移、缩放和旋转。

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

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

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

原始手势事件

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

pointerInput(Unit) {awaitPointerEventScope {while (true) {val event = awaitPointerEvent()// handle pointer eventLog.d(TAG, "${event.type}, ${event.changes.first().position}")}}
}
  • awaitPointerEventScope 会创建可用于等待手势事件的协程作用域。
  • awaitPointerEvent 会挂起协程,直到发生下一个手势事件。

虽然监听原始手势输入事件非常强大,但根据此原始数据编写自定义手势也很复杂。为了简化自定义手势的创建过程,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 提供了一系列方法可帮助识别手势的常见操作:

  • awaitFirstDown :挂起直到某个手势事件变为按下状态。
  • waitForUpOrCancellation :等待所有手势事件释放。
  • 使用 awaitTouchSlopOrCancellation 和 awaitDragOrCancellation 创建低层级拖动监听器。手势处理程序会先挂起,直到手势到达触摸溢出值,然后挂起,直到第一次拖动事件发生。如果只想沿单轴(水平或竖直方向)拖动,可以改用 awaitHorizontalTouchSlopOrCancellation 加 awaitHorizontalDragOrCancellation 或 awaitVerticalTouchSlopOrCancellation 加 awaitVerticalDragOrCancellation
  • awaitLongPressOrCancellation :挂起,直到长按为止。
  • 使用 drag 方法连续监听拖动事件,或使用 horizontalDrag 或 verticalDrag 监听单轴上的拖动事件。
检测完整手势

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

  • 按压、点按、点按两次和长按:detectTapGestures
  • 拖动(开始、结束、取消):detectHorizontalDragGesturesdetectVerticalDragGesturesdetectDragGestures 和 detectDragGesturesAfterLongPress
  • 转换(平移、缩放、旋转):detectTransformGestures
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 reacheddetectDragGestures { _, _ -> 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 triggereddetectDragGestures { _, _ -> log = "Dragging" }})
}
多点触控手势事件

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

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 {...// 先转换 MotionEventval 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}}

然后调用 HitPathTracker 的 addHitPath() 去记录分发路径里到名为 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}}

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

分发

首先将调用 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 = falsechildren.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 = eventreturn changed}}

cache 检查发现确有变化之后,先执行 dispatchMainEventPass(),主要任务是遍历持有目标 Node 的 Vector 进行逐个分发。

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

 internal open class NodeParent {open fun dispatchMainEventPass(...): Boolean {var dispatched = falsechildren.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 = falsechildren.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 {...// 先分发给自己,策略为 FinalmodifierNode.dispatchForKind(Nodes.PointerInput) {it.onPointerEvent(event, PointerEventPass.Final, size)}// 再分发给 childrenif (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 = onTouchEventfilter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEventfilter}

在 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.changesif (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 消费的时候标记正在 Dispatchingif (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 taptry {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 策略的事件分发。


http://www.ppmy.cn/devtools/43872.html

相关文章

强化学习算法中on-policy和off-policy

强化学习算法中on-policy和off-policy On-PolicyOff-Policy对比总结示例&#xff1a;SARSA vs Q-LearningSARSA实现Q-Learning实现 结论 在强化学习中&#xff0c;策略&#xff08;policy&#xff09;是智能体选择动作的规则。根据策略更新的方式&#xff0c;强化学习算法可以分…

【PostgreSQL】 JSON数组 提取根据索引提取对象和字段

在 PostgreSQL 中处理 JSON 数组&#xff1a;按索引提取对象和字段 在现代应用程序中&#xff0c;JSON 数据格式因其灵活性和可读性广泛应用。PostgreSQL 作为一个强大的关系型数据库管理系统&#xff0c;提供了强大的 JSON 数据类型和函数支持&#xff0c;使得在数据库中存储…

基于Keras的手写数字识别(附源码)

目录 引言 为什么要创建虚拟环境&#xff0c;好处在哪里&#xff1f; 源码 我修改的部分 调用本地数据 修改第二层卷积层 引言 本文是博主为了记录一个好的开源代码而写&#xff0c;下面是代码出处&#xff01;强烈建议收藏&#xff01;【深度学习实战—1】&#xff1a…

Python | Leetcode Python题解之第116题填充每个节点的下一个右侧节点指针

题目&#xff1a; 题解&#xff1a; class Solution:def connect(self, root: Node) -> Node:if not root:return root# 从根节点开始leftmost rootwhile leftmost.left:# 遍历这一层节点组织成的链表&#xff0c;为下一层的节点更新 next 指针head leftmostwhile head:#…

【QT】包含中文的QString转换为std::string乱码问题

问题描述 在开发过程中需要用到QStringList file_names QFileDialog::getOpenFileNames()读取文件&#xff0c;并通过QString::toStdString()方法转换为std::string类型进行后续的使用&#xff0c;但是我发现当文件名包含中文的时候&#xff0c;QString没问题&#xff0c;但是…

请叙述Vue 中使用了哪些设计模式

在Vue中&#xff0c;使用了多种设计模式来构建其框架和组件系统&#xff0c;这些设计模式使得Vue具有高效、灵活和易于维护的特性。以下是Vue中常见的设计模式及其简要描述&#xff1a; MVVM&#xff08;Model-View-ViewModel&#xff09;模式&#xff1a; Vue.js是一个MVVM框架…

TCP—三次握手和四次挥手

目录 一、三次握手和四次挥手的目的 二、TCP可靠的方面 三、什么是三次握手 四、第三次握手的目的 五、什么是四次挥手 六、超时时间的目的 七、SYN包、ACK包、FIN包 八、解决丢包和乱序 九、参考资料 一、三次握手和四次挥手的目的 TCP三次握手的目的主要是为了确保两…

汇编原理(三)编程

源程序&#xff1a; 汇编指令&#xff1a;有对应的机器码与其对应 伪指令&#xff1a;无对应的机器码&#xff0c;是由编译器来执行的指令&#xff0c;编译器根据伪指令来进行相关的编译工作。 ex1:XXX segment、XXX ends这两个是一对成对使用的伪指令&#xff0c;且必须会被用…