前言
Compose 视图的处理方式和 Android 传统 View 有很大差别,针对 touch 事件的处理自然也截然不同。
如何在 Compose 中处理 touch 事件,官方已有非常详尽的说明,可以参考:https://developer.android.google.cn/jetpack/compose/touch-input。
本文将以 Compose 中几种最典型的 touch 处理为例,分别介绍其使用场景,并打印其调用栈。最后结合栈和 touch 源码,一起综合分析 Compose 中处理 touch 的原理细节。
各种 touch 处理的写法和场景
pointerInput
Compose 中处理所有手势事件的入口,类似传统视图的 onTouch。在这里可以识别 click 手势,而且相应优先级高于 clickable。
第二个参数为 PointerInputScope
的扩展函数类型,有如下:
- 来自
TapGestureDetector
文件中定义的 detectTapGestures:可以用来检测 onDoubleTap、onLongPress、onPress、onTap 几种手势 - 来自
DragGestureDetector
文件中定义的 detectDragGestures:可以用来检测拖拽开始、结束、取消等手势 - 来自
TransformGestureDetector
文件中定义的 detectTransformGestures:可以用来检测旋转、平移、缩放的手势 - 等等
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
...
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
},
onLongPress = {
},
onPress = {
},
onTap = {
}
)
detectDragGestures(
onDragStart = {
},
onDragEnd = {
},
onDragCancel = {
},
onDrag = {
change: PointerInputChange, dragAmount: Offset ->
// Todo
}
)
detectTransformGestures {
centroid: Offset, pan: Offset, zoom: Float, rotation: Float ->
// Todo
}
}
) {
...
}
}
我们在 pointerInput 里一进来加上 log,
fun GameScreen(
clickable: Clickable = Clickable()
) {
Column(
modifier = Modifier
.pointerInput(Unit) {
LogUtil.printLog(message = "GameScreen pointerInput", throwable = Throwable())
}
)
}
打印其调用栈:
GameScreen 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 回调的办法。
需要了留意的是:
- 当同时设置了 combinedClickable 的 onClick 的话,clickable 就不会被调用了
- 当同时设置了 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