在 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 event
Log.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 reached
detectDragGestures { _, _ -> 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 triggered
detectDragGestures { _, _ -> 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 {
...
// 先转换 MotionEvent
val 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 = 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 进行逐个分发。
同样 NodeParent 也是调用各 child Node 的 dispatchMainEventPass()
进行分发。
internal open class NodeParent {
open fun dispatchMainEventPass(
...
): Boolean {
var dispatched = false
children.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 = false
children.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 {
...
// 先分发给自己,策略为 Final
modifierNode.dispatchForKind(Nodes.PointerInput) {
it.onPointerEvent(event, PointerEventPass.Final, size)
}
// 再分发给 children
if (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 = onTouchEvent
filter.requestDisallowInterceptTouchEvent = requestDisallowInterceptTouchEvent
filter
}
在 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.changes
if (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 消费的时候标记正在 Dispatching
if (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 tap
try {
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 策略的事件分发。