经过调用栈快速探求 Compose 中 touch 事情的处理原理

本文为稀土技术社区首发签约文章,30天内制止转载,30天后未获授权制止转载,侵权必究!

前语

Compose 视图的处理方法和 Android 传统 View 有很大不同,针对 touch 事情的处理自然也天壤之别。

如安在 Compose 中处理 touch 事情,官方已有十分翔实的阐明,能够参考:developer.android.google.cn/jetpack/com…

本文将以 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 pointerInput
java.lang.Throwable
    at 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:Waiting
java.lang.Throwable
    at 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 onClick
java.lang.Throwable
    at 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 clickable
java.lang.Throwable
    at 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 {
     ...
         // 先转换 MotionEvent
    val 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 = 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 进行逐一分发。

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 = false
    children.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 {
     ...
    // 最终调用 dispatchFinalEventPass
    dispatchHit = root.dispatchFinalEventPass(internalPointerEvent) || dispatchHit
​
    return dispatchHit
   }
   ...
  open fun dispatchFinalEventPass(internalPointerEvent: InternalPointerEvent): Boolean {
    var dispatched = false
    children.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 {
       ...
      // 先分发给自己,战略为 Final
      modifierNode.dispatchForKind(Nodes.PointerInput) {
        it.onPointerEvent(event, PointerEventPass.Final, size)
       }
​
      // 再分发给 children
      if (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 = onTouchEvent
  filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent
  filter
}

进而在 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 消费的时分标记正在 Dispatching
            if (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 事情的处理原理

  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 场景。

感兴趣的同学能够自行研讨。