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

Compose-base-touch.png

前言

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

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

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

各种 touch 处理的写法和场景

pointerInput

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

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

  • 来自 TapGestureDetector 文件中定义的 detectTapGestures:可以用来检测 onDoubleTap、onLongPress、onPress、onTap 几种手势
  • 来自 DragGestureDetector 文件中定义的 detectDragGestures:可以用来检测拖拽开始、结束、取消等手势
  • 来自 TransformGestureDetector 文件中定义的 detectTransformGestures:可以用来检测旋转、平移、缩放的手势
  • 等等
 fun GameScreen(clickable: Clickable = Clickable()) {Column(modifier = Modifier....pointerInput(Unit) {detectTapGestures(onDoubleTap = { },onLongPress = { },onPress = { },onTap = { })detectDragGestures(onDragStart = { },onDragEnd = { },onDragCancel = { },onDrag = { change: PointerInputChange, dragAmount: Offset -> // Todo})​detectTransformGestures { centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->// Todo}}) {...}}

我们在 pointerInput 里一进来加上 log,

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

打印其调用栈:

 GameScreen pointerInputjava.lang.Throwableat com.ellison.flappybird.view.GameScreenKt$GameScreen$3.invokeSuspend(GameScreen.kt:51)...androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invokeSuspend(SuspendingPointerInputFilter.kt:562)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:8)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invoke(Unknown Source:4)...at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:561)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)...at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)...

pointerInteropFilter

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

需要留意的是如果 DOWN return 了 false 的话,那么 ACTION_UP 就不会发过来了。

 fun GameScreen(clickable: Clickable = Clickable()) {Column(modifier = Modifier.pointerInteropFilter {when (it.action) {ACTION_DOWN -> {LogUtil.printLog(message = "GameScreen pointerInteropFilter ACTION_DOWN status:${viewState.gameStatus}", throwable = Throwable())}​MotionEvent.ACTION_MOVE -> {// Todo}​MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {// Todo}}true})}

我们在 ACTION_DOWN 里加个 log 看下 stack:

 GameScreen pointerInteropFilter ACTION_DOWN status:Waitingjava.lang.Throwableat com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:58)at com.ellison.flappybird.view.GameScreenKt$GameScreen$4$1.invoke(GameScreen.kt:53)at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:301)at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1$dispatchToView$3.invoke(PointerInteropFilter.android.kt:294)at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-ubNVwUQ(PointerInteropUtils.android.kt:81)at androidx.compose.ui.input.pointer.PointerInteropUtils_androidKt.toMotionEventScope-d-4ec7I(PointerInteropUtils.android.kt:35)at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.dispatchToView(PointerInteropFilter.android.kt:294)at androidx.compose.ui.input.pointer.PointerInteropFilter$pointerInputFilter$1.onPointerEvent-H0pRuoY(PointerInteropFilter.android.kt:229)at androidx.compose.ui.node.BackwardsCompatNode.onPointerEvent-H0pRuoY(BackwardsCompatNode.kt:365)​at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:297)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)...at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)...

combinedClickable

综合单击、双击、长按三种点击事件的处理函数,但至少需要指定处理单击 onClick 的 lambda。

如果同时设置了 pointerInteropFilter 并返回 true 的话,那么 combinedClickable Unit 就不会被处理了。

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

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

 GameScreen combinedClickable onClickjava.lang.Throwableat com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:56)at com.ellison.flappybird.view.GameScreenKt$GameScreen$4.invoke(GameScreen.kt:45)at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke-k-4lQ0M(Clickable.kt:939)at androidx.compose.foundation.CombinedClickablePointerInputNode$pointerInput$5.invoke(Clickable.kt:927)at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapGestures$2$1.invokeSuspend(TapGestureDetector.kt:144)...at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine$withTimeout$job$1.invokeSuspend(SuspendingPointerInputFilter.kt:724)at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)...

clickable

clickable 算是最简单的设置 click 回调的办法。

需要了留意的是:

  1. 当同时设置了 combinedClickable 的 onClick 的话,clickable 就不会被调用了
  2. 当同时设置了 pointerInteropFilter 并返回 true 的话,和 combinedClickable 一样,clickable 就不会处理了
 fun GameScreen(clickable: Clickable = Clickable()) {Column(modifier = Modifier.clickable {LogUtil.printLog(message = "GameScreen clickable", throwable = Throwable())})}

直接打个 stack:

 GameScreen clickablejava.lang.Throwableat com.ellison.flappybird.view.GameScreenKt$GameScreen$1.invoke(GameScreen.kt:43)at com.ellison.flappybird.view.GameScreenKt$GameScreen$1.invoke(GameScreen.kt:41)at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:895)at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:889)at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)...at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:328)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:665)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:544)at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:566)at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:855)at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:703)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:317)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:183)at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:102)at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:96)at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1446)at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1398)at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1338)...

各种 touch 的原理分析

一般来说,看原理可以通过直接看代码或调试的方式来了解,但有的时候因为代码的复杂度、线程切换等因素导致阅读和调试比较困难,还容易导致忽略重要的步骤,不得已跟错流程。

这次我们事先打印了 stack,便可以直观地看到某个 touch 回调的主线处理,非常方便。后面看到源码中发现某些细节不清的时候,可以回到 stack 里找到准确的答案。

预处理

通过观察上述几个栈,你会发现基本上调用入口均是 AndroidComposeView 的 dispatchTouchEvent()。原因显而易见,它是 Compose 上连接 Android 传统 View 树的 View 对象。

那么我们便从 AndroidComposeView 的 dispatchTouchEvent() 开始分析。

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

关键的处理在 handleMotionEvent() 里。

 internal class AndroidComposeView(...) : ViewGroup(context),... {private fun handleMotionEvent(motionEvent: MotionEvent): ProcessResult {removeCallbacks(resendMotionEventRunnable)try {...val result = trace("AndroidOwner:onTouch") {...sendMotionEvent(motionEvent)}return result} finally {forceUseMatrixCache = false}}...}

跳过针对 HOVER 类型的事件有些特殊处理,我们直接看重要的 sendMotionEvent()

 internal class AndroidComposeView(...) : ViewGroup(context),... {private fun sendMotionEvent(motionEvent: MotionEvent): ProcessResult {...// 先转换 MotionEventval pointerInputEvent =motionEventAdapter.convertToPointerInputEvent(motionEvent, this)return if (pointerInputEvent != null) {...// 再交由 Processor 处理val result = pointerInputEventProcessor.process(pointerInputEvent,this,isInBounds(motionEvent))...result} ...}...}

sendMotionEvent() 并不直接处理 MotionEvent,而是通过 convertToPointerInputEvent() 将 MotionEvent 转换成 PointerInputEvent。针对多点触控的手指信息,需要转换成 PointerInputEventData 保存到 PointerInputEvent 里的 List 中。

然后接下来的处理交由专门的 PointerInputEventProcessor 类继续。

 internal class PointerInputEventProcessor(val root: LayoutNode) {...fun process(pointerEvent: PointerInputEvent,positionCalculator: PositionCalculator,isInBounds: Boolean = true): ProcessResult {...try {isProcessing = true// 先转换成 InternalPointerEvent 类型val internalPointerEvent =pointerInputChangeEventProducer.produce(pointerEvent, positionCalculator)...​internalPointerEvent.changes.values.forEach { pointerInputChange ->if (isHover || pointerInputChange.changedToDownIgnoreConsumed()) {val isTouchEvent = pointerInputChange.type == PointerType.Touch// path 匹配root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)if (hitResult.isNotEmpty()) {// path 记录hitPathTracker.addHitPath(pointerInputChange.id, hitResult)hitResult.clear()}}}...// 开始分发val dispatchedToSomething =hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds)...} finally {isProcessing = false}}...}
  1. 告知 PointerInputChangeEventProducer 调用 produce() 依据传入的 PointerInputEvent 去追踪发生变化的手指 touch 信息并返回 InternalPointerEvent 实例。具体差异的信息将逐个封装到 PointerInputChange 实例中,并按照手指 ID map 后存到 InternalPointerEvent 里

     private class PointerInputChangeEventProducer {fun produce(...): InternalPointerEvent {val changes: MutableMap<PointerId, PointerInputChange> =LinkedHashMap(pointerInputEvent.pointers.size)pointerInputEvent.pointers.fastForEach {...changes[it.id] = PointerInputChange( ... )}return InternalPointerEvent(changes, pointerInputEvent)}...}
    
  2. 遍历上面得到的 map,逐个调用 hitTest() 将变化的 touch 信息放到 Compose 根节点 root 中进行预匹配,得到匹配了 touch 信息的 LayoutNode 的结果 HitTestResult,以确定事件分发的路径。这里最关键的是 hitInMinimumTouchTarget(),它会将匹配到的 Modifier 里设置的 touch Node 赋值进 HitTestResult 的 values 中

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

     internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {...fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {...eachPin@ for (i in pointerInputNodes.indices) {...val node = Node(pointerInputNode).apply {pointerIds.add(pointerId)}parent.children.add(node)parent = node}}
    
  4. 最后调用 dispatchChanges() 开始分发

分发

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

 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {fun dispatchChanges(internalPointerEvent: InternalPointerEvent,isInBounds: Boolean = true): Boolean {val changed = root.buildCache(...)if (!changed) {return false}...}}

当然该方法实际会调用 root 中各 child Node 的 buildCache() 进行。

 internal open class NodeParent {open fun buildCache( ... ): Boolean {var changed = 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 进行逐个分发。

 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {fun dispatchChanges(internalPointerEvent: InternalPointerEvent,isInBounds: Boolean = true): Boolean {val changed = root.buildCache( ...)// cache 确有变化,调用 var dispatchHit = root.dispatchMainEventPass(...)...}...open fun dispatchMainEventPass(...): Boolean {var dispatched = falsechildren.forEach {dispatched = it.dispatchMainEventPass( ... ) || dispatched}return dispatched}}

那么,Node 中的 dispatchMainEventPass() 的逻辑如下:

 internal class Node(val modifierNode: Modifier.Node) : NodeParent() {override fun dispatchMainEventPass(...): Boolean {return dispatchIfNeeded {...// 1. 本 Node 优先处理modifierNode.dispatchForKind(Nodes.PointerInput) {it.onPointerEvent(event, PointerEventPass.Initial, size)}// 2. 子 Node 处理if (modifierNode.isAttached) {children.forEach {it.dispatchMainEventPass( ... )}}if (modifierNode.isAttached) {// 3. 子 Node 优先处理modifierNode.dispatchForKind(Nodes.PointerInput) {it.onPointerEvent(event, PointerEventPass.Main, size)}}}}}

这个函数执行的内容比较重要:

  1. 执行本 Node 的 onPointerEvent(),传递 PointerEventPass 策略为 Initial,代表父节点优先于子节点进行处理 PointerEvent,顺序是自上而下,便于父节点处理需要在执行 scroll 时防止子 Node 里按钮响应点击等场景

    • onPointerEvent() 的具体逻辑取决于向 Modifier 中设置的 touch Node 类型,将在下个章节展开
  2. 如果本 Node attach 到 Compose Layout 了,遍历它的 child Node,继续调用 dispatchMainEventPass() 分发,后续逻辑和 1 一致,不再赘述

  3. 如果发现本 Node 仍然 attach 到了 Layout,调用 onPointerEvent() 并设置 PointerEventPass 策略为 Main,代表子节点优于父节点处理,,顺序是自下而上,便于子节点处理需要在父节点响应之前响应点击等场景

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

 internal class HitPathTracker(private val rootCoordinates: LayoutCoordinates) {fun dispatchChanges(internalPointerEvent: InternalPointerEvent,isInBounds: Boolean = true): Boolean {...// 最后调用 dispatchFinalEventPassdispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit​return dispatchHit}...open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {var dispatched = falsechildren.forEach {dispatched = it.dispatchFinalEventPass(internalPointerEvent) || dispatched}cleanUpHits(internalPointerEvent)return dispatched}}

dispatchMainEventPass() 一样,dispatchFinalEventPass() 需要先针对本 Node 执行 onPointerEvent(),再针对 child Node 逐个分发一遍。

区别的是此处传递的 PointerEventPass 策略为 Final,意味着这是最终步骤的分发,,顺序是自上而下,子节点可以知道父节点在 PointerInputChanges 中进行了哪些处理,比如是否已经消费了 scroll 而无需再处理点击事件了。

 internal class Node(val modifierNode: Modifier.Node) : NodeParent() {...override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {val result = dispatchIfNeeded {...// 先分发给自己,策略为 FinalmodifierNode.dispatchForKind(Nodes.PointerInput) {it.onPointerEvent(event, PointerEventPass.Final, size)}// 再分发给 childrenif (modifierNode.isAttached) {children.forEach { it.dispatchFinalEventPass(internalPointerEvent) }}}...}...}

另一个有个区别的地方是,执行完毕之后,额外需要执行如下重置工作:

  • cleanUpHits():清空 Node 中保存的 touch id 等 Event 信息
  • clearCache():本 touch 事件处理结束,清空 cache 事件变化信息 PointerInputChange 的 map 和 LayoutCoordinates
 internal class Node(val modifierNode: Modifier.Node) : NodeParent() {...override fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {...// 重置数据cleanUpHits(internalPointerEvent)clearCache()return result}override fun cleanUpHits(internalPointerEvent: InternalPointerEvent) {...event.changes.fastForEach { change ->val remove = !change.pressed &&(!internalPointerEvent.issuesEnterExitEvent(change.id) || !isIn)if (remove) {pointerIds.remove(change.id)}}...}private fun clearCache() {relevantChanges.clear()coordinates = null}...}

具体 touch 处理

书接上面的 onPointerEvent(),具体看看如何抵达的 Modifier 的各个 touch 处理。

pointerInput

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

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

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

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

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

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

 internal class SuspendPointerInputElement(override fun onPointerEvent( ... ) {...dispatchPointerEvent(pointerEvent, pass)}private fun dispatchPointerEvent( ... ) {forEachCurrentPointerHandler(pass) {it.offerPointerEvent(pointerEvent, pass)}}private inline fun forEachCurrentPointerHandler( ... ) {...try {when (pass) {PointerEventPass.Initial, PointerEventPass.Final ->dispatchingPointerHandlers.forEach(block)​PointerEventPass.Main ->dispatchingPointerHandlers.forEachReversed(block)}} finally {dispatchingPointerHandlers.clear()}}}
pointerInteropFilter

pointerInteropFilter() 实际上会创建一个 PointerInteropFilter 实例,由系统添加到 BackwardsCompatNode 类型的 Node里,onTouchEvent 的 Unit 会被存在 PointerInteropFilter 里。

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

进而在 PointerEvent 分发过来的时候会调用 BackwardsCompatNode 的 onPointerEvent()。

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

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

 internal class PointerInteropFilter : PointerInputModifier {override val pointerInputFilter =object : PointerInputFilter() {override fun onPointerEvent(...) {...if (state !== DispatchToViewState.NotDispatching) {if (pass == PointerEventPass.Initial && dispatchDuringInitialTunnel) {dispatchToView(pointerEvent)}if (pass == PointerEventPass.Final && !dispatchDuringInitialTunnel) {dispatchToView(pointerEvent)}}...}}

onPointerEvent() 将依据 DispatchToViewState 的当前状态,决定是否调用 dispatchToView()

 internal class PointerInteropFilter : PointerInputModifier {...override val pointerInputFilter =object : PointerInputFilter() {...private fun dispatchToView(pointerEvent: PointerEvent) {val changes = pointerEvent.changes​if (changes.fastAny { it.isConsumed }) {if (state === DispatchToViewState.Dispatching) {pointerEvent.toCancelMotionEventScope(this.layoutCoordinates?.localToRoot(Offset.Zero)?: error("layoutCoordinates not set")) { motionEvent ->// 如果之前消费了并且在 Dispatching,继续调用 onTouchEvent()onTouchEvent(motionEvent)}}state = DispatchToViewState.NotDispatching} else {pointerEvent.toMotionEventScope(this.layoutCoordinates?.localToRoot(Offset.Zero)?: error("layoutCoordinates not set")) { motionEvent ->// ACTION_DOWN 的时候总是发送给 onTouchEvent()// 并在返回 true 消费的时候标记正在 Dispatchingif (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {state = if (onTouchEvent(motionEvent)) {DispatchToViewState.Dispatching} else {DispatchToViewState.NotDispatching}} else {onTouchEvent(motionEvent)}}...}}}}

dispatchToView() 会依据 MotionEvent 的 ACTION 类型和是否已经消费的 Consumed 值决定是否调用 onTouchEvent Unit:

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

combinedClickable() 实际上会创建一个 CombinedClickableElement 实例,该实例包裹的 CombinedClickableNode 会被添加到 Modifier Node里。

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

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

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

CombinedClickablePointerInputNode 重要的一点是实现了 pointerInput(),调用 detectTapGestures() 设置了 onTap 之类的几个 Unit,并有一一对应关系:

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

换句话说,combinedClickable 事实上是调用 pointerInput 添加了 onTap 等 Gesture 的监听。

 private class CombinedClickablePointerInputNode(...) {override suspend fun PointerInputScope.pointerInput() {interactionData.centreOffset = size.center.toOffset()detectTapGestures(onDoubleTap = if (enabled && onDoubleClick != null) {{ onDoubleClick?.invoke() }} else null,onLongPress = if (enabled && onLongClick != null) {{ onLongClick?.invoke() }} else null,...,onTap = { if (enabled) onClick() })}}

既然采用了 pointerInput,那么还是会和前面的一样经由 SuspendingPointerInputModifierNodeImpl 的 onPointerEvent(),抵达 detectTapGestures 内部的逻辑。

 suspend fun PointerInputScope.detectTapGestures(...) = coroutineScope {val pressScope = PressGestureScopeImpl(this@detectTapGestures)​awaitEachGesture {...if (upOrCancel != null) {if (onDoubleTap == null) {onTap?.invoke(upOrCancel.position) // no need to check for double-tap.} else {...if (secondDown == null) {onTap?.invoke(upOrCancel.position) // no valid second tap started} else {...try {withTimeout(longPressTimeout) {val secondUp = waitForUpOrCancellation()if (secondUp != null) {...onDoubleTap(secondUp.position)} else {launch {pressScope.cancel()}onTap?.invoke(upOrCancel.position)}}} ...}}}}}

并在 onTap 处,回调经由 CombinedClickablePointerInputNode 传入的 onClick Unit。

clickable

和 combinedClickable() 类似,实际上会创建一个 ClickableElement 实例,该实例包裹的 ClickableNode 会被添加到 Modifier Node里。

 fun Modifier.clickable(...onClick: () -> Unit) = inspectable(...) {Modifier....then(ClickableElement(interactionSource, enabled, onClickLabel, role, onClick))}private class ClickableElement(...private val onClick: () -> Unit) : ModifierNodeElement<ClickableNode>() {...}

ClickableNode 复写了 clickablePointerInputNode 属性,提供的是 ClickablePointerInputNode 类型。

 private class ClickableNode(...onClick: () -> Unit) : AbstractClickableNode(interactionSource, enabled, onClickLabel, role, onClick) {...override val clickablePointerInputNode = delegate(ClickablePointerInputNode(...,onClick = onClick,interactionData = interactionData))}

ClickablePointerInputNode 的重点也是实现了 pointerInput(),它调用的 detectTapAndPress() 设置了 onTap Unit,并对应着目标的 onClick,即事实上也是调用 pointerInput 添加了 onTap Gesture 的监听。

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

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

 internal suspend fun PointerInputScope.detectTapAndPress(...) {val pressScope = PressGestureScopeImpl(this)coroutineScope {awaitEachGesture {...if (up == null) {launch {pressScope.cancel() // tap-up was canceled}} else {up.consume()launch {pressScope.release()}onTap?.invoke(up.position)}}}}

结语

最后,我们将 Compose 中几种典型的 touch 处理的 process 综合到一张图里,供大家直观地了解互相之间的关系。

compose_touch.drawio.png

  1. 和物理的 Touch 事件一样,经由 InputTransport 抵达 ViewRootImpl 以及实际根 View 的 DecorView

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

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

  4. 首先调用 HitPathTrackeraddHitPath() 记录 Pointer 事件的分发路径

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

    1. 首先调用 dispatchMainEventPass() 进行 InitialMain 策略的事件分发。这其中会调用各 Modifer Node 的 onPointerEvent() ,并依据 touch 逻辑回调 clickablepointerInput 等 Modifier 的 Unit
    2. 接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发

除了 pointerInput 等几个常用的 touch 处理方法以外,Compose 还支持通过 scrollableswipeabledraggabletransformable 等处理更为复杂、灵活的 touch 场景。

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

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

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

相关文章

产品NPDP+项目PMP助你成长

前言 从管理的角度来讲,产品经理和项目经理的区别,我们应该吧项目经理和产品的区别分为一纵一横,那一纵就是我们的项目经理,项目经理在整个新产品研发过程中他扮演的是管理监督项目参与者的角色,其中包括研发部门、技术部门、市场部门或是销售部门等等。他所要做的事情就…

第二十讲:文件操作

第二十讲&#xff1a;文件操作 1.什么是文件1.1什么是文件1.1.1程序文件1.1.2数据文件 1.2二进制文件和文本文件数据的存储方式 2.流和标准流2.1流2.2标准流 3.文件指针4.文件的打开与关闭5.文件顺序读写5.1顺序读写函数介绍5.2打开和关闭函数5.2.1fopen函数5.2.2fclose函数 5.…

matlab 计算三维空间点到直线的距离

目录 一、算法原理二、代码实现三、结果展示四、参考链接本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫与GPT。 一、算法原理 直线的点向式方程为: x − x 0 m = y

DT-MIL:用于组织病理学图像的MIL方法

学习信息表示对于组织病理学图像的分类和预测任务至关重要。由于图像大小巨大&#xff0c;通常使用多实例学习&#xff08;MIL&#xff09;方案来处理整张组织病理学图像&#xff08;whole-slide histopathological image&#xff09;。然而&#xff0c;MIL的弱监督性质导致了学…

能在电脑和手机上使用的便签app 好用的便签软件

随着科技的日新月异&#xff0c;越来越多的软件被开发出来&#xff0c;极大地便利了我们的工作和生活。其中&#xff0c;便签软件凭借其便捷的记录功能&#xff0c;受到了广大用户的喜爱。特别是那些能在电脑和手机上同步使用的便签app&#xff0c;更是为我们的工作和生活带来了…

【阿里YYDS】通义千问正式开源 Qwen2

Qwen2–72B正式开源&#xff0c;性能全面超越开源模型Llama3-70B&#xff0c;也超过文心4.0、豆包pro、混元pro等众多中国闭源大模型。 在过去一段时间里&#xff0c;Qwen系列模型从Qwen1.5升级到Qwen2&#xff0c;Qwen2分5个尺寸&#xff0c;包括Qwen2-0.5B、Qwen2-1.5B、Qwen…

Nice Mind 手机版本的思维导图,最新V8.9.0版本,无内购!

这是一款非常好用的手机思维导图APP&#xff0c;为了防止和谐&#xff0c;名字就不说了&#xff0c;软件可以帮助用户快速制作思维导图&#xff0c;不管什么类型的都可以直接使用。如果需要电脑版本的思维导图&#xff0c;可以看置顶文章的另一款软件。 软件获取方式&#xff…

PostgreSQL基础(十):PostgreSQL的并发问题

文章目录 PostgreSQL的并发问题 一、事务的隔离级别 二、MVCC PostgreSQL的并发问题 一、事务的隔离级别 在不考虑隔离性的前提下&#xff0c;事务的并发可能会出现的问题&#xff1a; 脏读&#xff1a;读到了其他事务未提交的数据。&#xff08;必须避免这种情况&#xf…

TCP攻击是怎么实现的,如何防御?

TCP&#xff08;Transmission Control Protocol&#xff09;是互联网协议族中的重要组成部分&#xff0c;用于在不可靠的网络上提供可靠的数据传输服务。然而&#xff0c;TCP协议的一些特性也使其成为攻击者的目标&#xff0c;尤其是DDoS&#xff08;Distributed Denial of Ser…

安装windows11系统跳过微软账号登录,使用本地账号登录方法

在安装win11系统&#xff0c;进行到如图下所示界面的时候&#xff0c;暂停下 我们可以按下键盘的ShiftF10按键&#xff08;部分电脑是FnShiftF10&#xff09;&#xff0c;这时屏幕会出现命令行窗口&#xff0c;如图下所示 我们需要在命令行内输入代码oobe\bypassnro.cmd然后回车…

[ 网络通信基础 ]——网络的传输介质(双绞线,光纤,标准,线序)

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;网络通信基础TCP/IP专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年6月8日14点23分 &#x1f004;️文章质量&#xff1a;94分 前言—— 在现代通信网络中&#xff0c;传输介质是数据传…

09 platfrom 设备驱动

platform 设备驱动,也叫做平台设备驱动。请各位重点学习! 1、驱动的分离与分层 1)驱动的分隔与分离 Linux 操作系统,代码的重用性非常重要。驱动程序占用了 Linux 内核代码量的大头,如果不对驱动程序加以管理,用不了多久 Linux 内核的文件数量就庞大到无法接受的地步。…

手猫助手Agent技术探索总结

随着LLM的发展&#xff0c;ChatGPT能力不断增强&#xff0c;AI不断有新的概念提出&#xff0c;一种衍生类型的应用AI Agent也借着这股春风开启了一波话题热度&#xff0c;各种初创公司&#xff0c;包括Open AI内部也都在密切关注着AI Agent领域的变化。阿里集团内的AI团队也有很…

three.js指南

threejs 相关资料 threejs 官网threejs 案例 安装&#xff08;Installation&#xff09; 使用 NPM 和构建工具进行安装 对于大多数用户而已&#xff0c;从 npm 包注册表中心 安装并使用 构建工具 会是一个更推荐的方案。因为项目需要的依赖越多&#xff0c;就越有可能遇到静…

限时限量!6.18云服务器大促盘点,错过一次,再等一年!

随着云计算技术的飞速发展&#xff0c;云服务器已成为企业和个人构建和扩展在线业务的首选平台。特别是在大型促销活动如618年中大促期间&#xff0c;云服务提供商纷纷推出极具吸引力的优惠&#xff0c;以降低用户上云的门槛。以下是对当前市场上几个主流云服务提供商的优惠活动…

C++第二十五弹---从零开始模拟STL中的list(下)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1、函数补充 2、迭代器完善 3、const迭代器 总结 1、函数补充 拷贝构造 思路&#xff1a; 先构造一个头结点&#xff0c;然后将 lt 类中的元…

柴油十六烷值检测 液压油硫含量检测 变压器油检测

检测的油品包括&#xff1a;柴油、润滑油、液压油、机油、汽油、以及一些工业用油等。 柴油检测项目GB19147-2013&#xff1a;氧化安定性、硫含量、酸度、10%蒸余物残炭、灰分、铜片腐蚀、水分、机械杂质、润滑性、多环芳烃、运动粘度、凝点、冷滤点、闪点、十六烷值、馏程、密…

什么是Docker ?

在软件开发的星辰大海中&#xff0c;有一个神奇的技术&#xff0c;它能够将应用程序及其依赖环境封装在一个轻量级的、可移植的容器中。这项技术就是Docker。它不仅简化了应用的部署流程&#xff0c;还让开发和运维之间的界限变得模糊&#xff0c;使得跨平台部署变得前所未有的…

如何搭建跨境电商独立站||搭建跨境电商独立站必须具备的功能板块设计

在搭建跨境电商独立站时&#xff0c;需要确保网站具备一系列关键的功能板块&#xff0c;以提供用户友好的购物体验并确保业务的顺利进行。以下是这些功能板块的详细归纳&#xff1a; 注册登录与身份验证&#xff1a; 用户注册与登录&#xff1a;允许用户创建账户&#xff0c;通…

动态规划学习(混合背包,有依赖的背包,以及背包思想)

混合背包的定义&#xff1a; 混合背包问题就是混合01背包、完全背包和多重背包&#xff0c;可供选择的物体i可能有一个、或者无数个、或者有限个。 所以&#xff0c;就不要考虑这么多了&#xff0c;直接分这三种情况考虑就行&#xff01;&#xff01; 样例&#xff1a; for(…