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

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

  • 使用 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.mzph.cn/diannao/18968.shtml

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

相关文章

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

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

SD-WAN:跨国公司的组网方式

在经济全球化的时代&#xff0c;跨国企业需要与国外客户、供应商和合作伙伴进行快速的互动和沟通&#xff0c;并且会在国外建立办公指点&#xff0c;传统的WAN&#xff08;大面积网络&#xff09;架构已不能满足快速、可靠、安全的网络通信需求&#xff0c;可以采用SD-WAN的方式…

怎么将3D模型转换立面图---模大狮模型网

在建筑设计、室内设计以及产品建模等领域&#xff0c;经常需要将3D模型转换为立面图以进行展示、分析或交流。立面图能够清晰地呈现物体的外观和结构&#xff0c;是设计和施工中不可或缺的一部分。 一、导出3D模型 首先&#xff0c;需要将3D模型导出为CAD软件能够识别的格式。…

第十九节:带你梳理Vue2: 父组件向子组件传参(props传参)

1. 组件嵌套 1.1 组件的嵌套使用 之前有说过,Vue组件跟Vue实例是一样的,因此在Vue中一个组件中也可以定义并使用自己的局部组件,这就是组件的嵌套使用 例如:示例代码如下: <div id"app"><!-- 3. 使用组件 --><my-component></my-component&…

29-ESP32-S3-WIFI_Driver-00 STA模式扫描全部 AP

ESP32-S3 WIFI_Driver 引言 ESP32-S3是一款集成了Wi-Fi和蓝牙功能的芯片。关于WIFI的部分&#xff0c;其实内容比我想象的要多得多。所以通常来说&#xff0c;如果你想要编写自己的Wi-Fi应用程序&#xff0c;最快捷的方法就是先找一个类似的示例应用&#xff0c;然后将它的相…

VSCODE常用插件记录

重点提名&#xff1a; back & ForthBookmarksC/ChighlightSSH FS //SSH插件

JVM之垃圾判断的详细解析

垃圾判断 垃圾介绍 垃圾&#xff1a;如果一个或多个对象没有任何的引用指向它了&#xff0c;那么这个对象现在就是垃圾 作用&#xff1a;释放没用的对象&#xff0c;清除内存里的记录碎片&#xff0c;碎片整理将所占用的堆内存移到堆的一端&#xff0c;以便 JVM 将整理出的内…

三大主流框架

Web前端开发领域中&#xff0c;三大主流框架通常指的是&#xff1a; React&#xff1a;由Facebook开发的一个用于构建用户界面的JavaScript库。React以其组件化、声明式编程和虚拟DOM等特点而广受欢迎&#xff0c;能够高效地更新和渲染大型应用。 Vue.js&#xff1a;由尤雨溪创…

第3章 数据链路层

王道学习 考纲内容 &#xff08;一&#xff09;数据链路层的功能 &#xff08;二&#xff09;组帧 &#xff08;三&#xff09;差错控制 检错编码&#xff1b;纠错编码 &#xff08;四&#xff09;流量控制与可靠传输机制 流量控制、可靠传输与滑动窗口…

AcWing 1600:完全二叉树

【题目来源】https://www.acwing.com/problem/content/1602/【题目描述】 给定一个树&#xff0c;请你判断它是否是完全二叉树。【输入格式】 第一行包含整数 N&#xff0c;表示树的结点个数。 树的结点编号为 0∼N−1。 接下来 N 行&#xff0c;每行对应一个结点&#xff0c;并…

使用 Spring Boot 实现邮件发送功能

推荐一个AI网站&#xff0c;免费使用豆包AI模型&#xff0c;快去白嫖&#x1f449;海鲸AI SpringBoot——发送邮件 在现代应用程序中&#xff0c;发送邮件是一个常见的需求。本文将介绍如何使用 Spring Boot 发送邮件。我们将从新建一个 Spring Boot 项目开始&#xff0c;逐步…

3dmax渲染经常卡主?关掉光追即可流畅渲染

3ds Max是一款广泛应用于三维建模、动画和渲染的软件&#xff0c;它在影视、游戏、建筑可视化等领域具有重要地位。在3ds Max中&#xff0c;渲染技术的选择和应用直接影响到最终图像的质量和渲染效率。 但在实际使用过程中&#xff0c;由于3dsMax中有太多选项&#xff0c;很多…

# 解决 win11 连接共享打印机,报错 0x00000709 问题

解决 win11 连接共享打印机&#xff0c;报错 0x00000709 问题 一、问题描述&#xff1a; 当我们连接一台共享打印机&#xff0c;出现报错 0x00000709 时&#xff0c;这是由于本机注册表本配置 RPC 远程调用&#xff0c;我们需要对自己的电脑进行修改&#xff0c;而不是主机&a…

什么是 ISP 代理?

代理是路由互联网流量的中间服务器&#xff0c;通常分为三类&#xff1a;数据中心、住宅和 ISP。根据定义&#xff0c;ISP 代理隶属于互联网服务提供商&#xff0c;但实际上&#xff0c;更容易将它们视为数据中心和住宅代理的组合。 让我们仔细研究一下 ISP 代理&#xff0c;看…

国产操作系统上部署SVN版本控制系统

原文链接&#xff1a;国产操作系统上部署SVN版本控制系统 | 统信 | 麒麟 | 中科方德 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇在国产操作系统上部署SVN版本控制系统的文章。SVN&#xff08;Subversion&#xff09;是一款广泛使用的版本控制系统&#xff0c;它…

【一步一步了解Java系列】:类与对象的联系

看到这句话的时候证明&#xff1a;此刻你我都在努力加油陌生人个人主页&#xff1a;Gu Gu Study专栏&#xff1a;一步一步了解Java 喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹 喜欢的话可以点个赞谢谢了。 作者&#xff1a;小闭 对…

真实故障分享,H3C ER3208G3-X路由器-双绞线一闪一停

六类非屏蔽双绞线 网线钳 如上图所示&#xff0c;2号线接到h3c路由器出现网线一闪一停&#xff0c;用对线器测试一到8芯能一一对应&#xff0c;无法上网。2号线接到h3c交换机能正常上网&#xff0c;难道是网线对568A 568B有要求&#xff1f; 解决方式&#xff1a;通过两端568…

一文讲清楚:如何做好建设工程项目管理?

在房地产开发中&#xff0c;作为项目负责人我目前的状况成了一个大管家&#xff0c;还要管理工程质量。上至各部门领导的关系维护&#xff0c;下到工人的吃喝拉撒都要我操心&#xff0c;还要没完没了的处理四邻纠纷和拆迁户的纠纷&#xff0c;每天都搞得很疲惫&#xff0c;如何…

elementUI type=“selection“多选框选中 删除 回显 赋值问题 回显数组改变选中状态未改变

业务需求&#xff1a; 点击查询弹列表框 勾选列表选项保存 可删除可重新查询列表添加 遇到的问题&#xff1a;删除之后查询列表selection回显问题 解决&#xff1a;row-click配合:reserve-selection"true"使用 <el-tableref"refPlanTable":data"…

vue3 + ts 实现IP地址及Mac地址输入框功能

1、组件完成代码 <template><div class"ip-input"><div v-for"(item, index) in ipArr" :key"index" class"ip-input__item-wrap"><input ref"ipInput" v-model"ipArr[index]" type"t…