在 Android 原生开发中对 View 的 touch 事件处理有这么几种方式:
- setOnClickListener:监听点击事件
- setOnTouchListener:监听 touch 事件
- 自定义View:覆写 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 等方法
方式1和2都是监听最后的结果,无需多说,方式3是通过覆写 View 中 touch 事件的分发处理流程中的关键方法从而达到对 touch 事件的处理。
dispatchTouchEvent 用于分发 touch 事件,onInterceptTouchEvent 用于是否中断(拦截)touch 事件,返回 true,表示拦截,返回 false,表示不拦截,onTouchEvent 用于处理 touch 事件,返回 true 表示消费事件。此外,还可以在 dispatchTouchEvent 方法中通过getParent().requestDisallowIntercepTouchEvent(true) 方式,禁止父控件拦截事件。
Compose 中 touch 事件处理
Compose 视图的处理方式和 Android 传统 View 有很大差别,针对 touch 事件的处理自然也截然不同。
详尽的说明可以查看官方文档:
https://developer.android.google.cn/develop/ui/compose/touch-input/pointer-input/understand-gestures?hl=zh-cn
Jetpack Compose 提供了不同的抽象级别来处理手势。最顶层的是组件支持。Button等可组合项会自动支持手势。如需为自定义组件添加手势支持,可以向任意可组合项添加clickable等手势修饰符。最后,如果需要自定义手势,可以使用pointerInput修饰符。
选择正确的抽象级别是 Compose 中的常见主题。Compose 以构建可重复使用的分层组件作为理念,这意味着不应该始终以构建较低级别的构建块为目标。许多较高级别的组件不仅能够提供更多功能,而且通常还会融入最佳实践,例如支持无障碍功能等。
例如,如果想为自己的自定义组件添加手势支持,可以使用Modifier.pointerInput从头开始构建;但在此之上还有其他更高级别的组件,它们可以提供更好的起点,例如 Modifier.draggable、Modifier.scrollable 或 Modifier.swipeable。
一般来讲,最好基于能提供所需功能的最高级别的组件进行构建,以便从其包含的最佳实践中受益。
组件支持
Compose 中的许多开箱即用组件都包含某种内部手势处理。例如,Button会自动检测点按并触发点击事件、LazyColumn通过滚动其内容来响应拖动手势、SwipeToDismissBox件则包含用于关闭元素的滑动逻辑。
当这些组件中的手势处理有适合的用例时,请优先使用组件中包含的手势,因为它们包含对焦点和无障碍功能的开箱即用型支持,并且已经过充分测试。例如,Button包含用于无障碍功能的语义信息,以便无障碍服务正确地将其描述为按钮,而不是只描述任何可点击的元素clickable。
使用修饰符向任意可组合项添加特定手势
可以将手势修饰符应用于任意可组合项,以使可组合项监听手势。例如,clickable 处理点按手势,通过应用 verticalScroll 让 Column 处理垂直滚动。
有许多修饰符可用于处理不同类型的手势:
- 使用
clickable
、combinedClickable
、selectable
、toggleable
和triStateToggleable
修饰符处理点按和按压操作。 - 使用
horizontalScroll
、verticalScroll
及更通用的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
- 拖动(开始、结束、取消):
detectHorizontalDragGestures
、detectVerticalDragGestures
、detectDragGestures
和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
方法未能提供足够精细的控制,以下辅助方法可以监听原始事件并对其执行计算。辅助方法包括 calculateCentroid
、calculateCentroidSize
、calculatePan
、calculateRotation
和 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)}}}}}
这个函数执行的内容比较重要:
- 执行本 Node 的
onPointerEvent()
,传递 PointerEventPass 策略为Initial
,代表父节点优先于子节点进行处理 PointerEvent,顺序是自上而下,便于父节点处理需要在执行 scroll 时防止子 Node 里按钮响应点击等场景。- onPointerEvent() 的具体逻辑取决于向 Modifier 中设置的 touch Node 类型。
- 如果本 Node attach 到 Compose Layout 了,则遍历它的 child Node,继续调用 dispatchMainEventPass() 分发。
- 如果发现本 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 事件分发流程
-
和原生开发中的 touch 事件一样,经由
InputTransport
抵达ViewRootImpl
以及实际根 View 的DecorView
。 -
经由 ViewGroup 的分发抵达 Compose 最上层的
AndroidComposeView
的dispatchTouchEvent()
。 -
dispatchTouchEvent()
将MotionEvent
转化为PointerInputEvent
类型并交由PointerInputEventProcessor
处理。 -
PointerInputEventProcessor
处理过程中先调用HitPathTracker
的 addHitPath() 记录 touch 事件的分发路径。 -
接着调用 dispatchChanges() 执行分发,并按照两个步骤抵达 Compose 的各层 Node:
步骤一:首先调用 dispatchMainEventPass() 进行 Initial 和 Main 策略的事件分发。这其中会调用各
Modifer
Node 的 onPointerEvent() ,并依据 touch 逻辑回调clickable
、pointerInput
等 Modifier 的 block。步骤二:接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发。