在 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 策略的事件分发。这其中会调用各
ModiferNode 的 onPointerEvent() ,并依据 touch 逻辑回调clickable、pointerInput等 Modifier 的 block。步骤二:接着调用 dispatchFinalEventPass() 进行 Final 策略的事件分发。