概述
Jetpack Compose 为我们提供了许多手势处理 Modifier,对于常见业务需求来说已足够我们使用了,然而如果说我们对手势有定制需求,就需要具备自定义手势处理的能力了。通过使用官方所提供的基础 API 来完成各类手势交互需求,触摸反馈基础 API 类似传统 View 系统的 onTouchEvent()
。 当然 Compose 中也支持类似传统 ViewGroup 通过 onInterceptTouchEvent()
定制手势事件分发流程。通过对自定义手势处理的学习将帮助大家掌握处理绝大多数场景下手势需求的能力。
使用 PointerInput Modifier
对于所有手势操作的处理都需要封装在这个 Modifier 中,我们知道 Modifier 是用来修饰 UI 组件的,所以将手势操作的处理封装在 Modifier 符合开发者设计直觉,这同时也做到了手势处理逻辑与 UI 视图的解耦,从而提高复用性。
通过翻阅 Swipeable Modifier
、Draggable Modifier
以及 Transformer Modifier
,我们都能看到 PointerInput Modifier
的身影。因为这类上层的手势处理 Modifier 其实都是基于这个基础 Modifier 实现的。所以既然要自定义手势处理流程,自定义逻辑也必然要在这个 Modifier 中进行实现。
通过 PointerInput Modifier
实现我们可以看出,我们所定义的自定义手势处理流程均发生在 PointerInputScope
中,suspend 关键字也告知我们自定义手势处理流程是发生在协程中。这其实是无可厚非的,在探索重组工作原理的过程中我们也经常能够看到协程的身影。伴随着越来越多的主流开发技术拥抱协程,这也就意味着协程成了 Android 开发者未来必须掌握的技能。推广协程同时其实也是在推广 Kotlin,即使官方一直强调不会放弃 Java,然而谁又会在 Java 中使用 Kotlin 协程呢?
fun Modifier.pointerInput(
vararg keys: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
...
) {
...
remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply {
LaunchedEffect(this, *keys) {
block()
}
}
}
复制代码
接下来我们就看看 PointerInputScope
作用域中,为我们可以使用哪些 API 来处理手势交互。本文将会根据手势能力分类进行解释说明。
拖动类型基础 API
API 介绍
API名称 | 作用 |
---|---|
detectDragGestures | 监听拖动手势 |
detectDragGesturesAfterLongPress | 监听长按后的拖动手势 |
detectHorizontalDragGestures | 监听水平拖动手势 |
detectVerticalDragGestures | 监听垂直拖动手势 |
谈及拖动,许多人第一个反应就是 Draggable Modifier
,因为 Draggable Modifier
为我们提供了监听 UI 组件拖动能力。然而 Draggable Modifier
在提供了监听 UI 组件拖动能力的同时也拓展增加其他功能,我们通过 Draggable Modifier
参数列表即可看出。例如通过使用 DraggableState
允许开发者根据需求使 UI 组件自动被拖动。
fun Modifier.draggable(
state: DraggableState,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
reverseDirection: Boolean = false
)
复制代码
我们上面所罗列的这些拖动 API 只提供了监听 UI 组件拖动的能力,我们可以根据需求为其拓展功能,这也是这些API所存在的意义。我们从字面上就可以看出每个 API 所对应的含义,由于这些API的功能与参数相近,这里我们仅以 detectDragGestures
作为举例说明。
举例说明
接下来我们将完成一个绿色方块的手势拖动。在 Draggabel Modifier
中我们还只能监听垂直或水平中某一个方向的手势拖动,而使用 detectDragGestures
所有手势信息都是可以拿到的。如果我们还是只希望拿到某一个方向的手势拖动,使用 detectHorizontalDragGestures
或 detectVerticalDragGestures
即可,当然我们也可以使用 detectDragGestures
并且忽略掉某个方向的手势信息。如果我们希望在长按后才能拿到手势信息可以使用 detectDragGesturesAfterLongPress
。
detectDragGestures
提供了四个参数。
onDragStart (可选):拖动开始时回调
onDragEnd (可选):拖动结束时回调
onDragCancel (可选):拖动取消时回调
onDrag (必须):拖动时回调
decectDragGestures
的源码分析在 awaitTouchSlopOrCancellation 小节会有讲解。
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)
复制代码
💡 Tips
有些同学可能困惑
onDragCancel
触发时机。在一些场景中,当组件拖动时会根据事件分发顺序进行事件分发,当前面先处理事件的组件满足了设置的消费条件,导致手势事件被消费,导致本组件拿到的是被消费的手势事件,从而会执行onDragCancel
回调。如何定制事件分发顺序并消费事件后续会进行详细的描述。
示例如下所示
@Preview
@Composable
fun DragGestureDemo() {
var boxSize = 100.dp
var offset by remember { mutableStateOf(Offset.Zero) }
Box(contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Box(Modifier
.size(boxSize)
.offset {
IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
}
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
// 拖动开始
},
onDragEnd = {
// 拖动结束
},
onDragCancel = {
// 拖动取消
},
onDrag = { change: PointerInputChange, dragAmount: Offset ->
// 拖动中
offset += dragAmount
}
)
}
)
}
}
复制代码
点击类型基础 API
API 介绍
API名称 | 作用 |
---|---|
detectTapGestures | 监听点击手势 |
与 Clickable Modifier
不同的是,detectTapGestures
可以监听更多的点击事件。作为手机监听的基础 API,必