手势 (Gestures)
Compose 提供了多种处理用户触控输入的方式,从简单的点击到复杂的多点触控和嵌套滚动。
简单的点击与交互
最常用的方式是使用 Modifier.clickable 等高级修饰符。
kotlin
Text(
text = "Click Me",
modifier = Modifier
.clickable { /* 处理点击 */ }
.padding(16.dp)
)其他类似的修饰符:
combinedClickable: 支持双击和长按。draggable: 监听单向(水平或垂直)拖拽。swipeable: 用于实现滑动开关或底部抽屉等吸附效果。scrollable: 使无滚动逻辑的组件支持滚动手势(通常不需要手动使用,LazyColumn已内置)。
指针输入 (Pointer Input)
对于更复杂的自定义手势,可以使用 Modifier.pointerInput。这是一个构建块 API,通常与 detect*Gestures 系列函数配合使用。
1. 点击手势 (Tap)
kotlin
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onTap = { offset -> /* 单击 */ },
onDoubleTap = { offset -> /* 双击 */ },
onLongPress = { offset -> /* 长按 */ },
onPress = { offset ->
// 按下时触发
// 必须调用 tryAwaitRelease() 等待释放
tryAwaitRelease()
// 释放时触发
}
)
}
)2. 拖拽手势 (Drag)
kotlin
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume() // 消费事件,防止向上传递
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)3. 变换手势 (Transform)
支持多点触控的平移、缩放和旋转。
kotlin
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, rotate ->
scale *= zoom
rotation += rotate
offset += pan
}
}
)事件分发与消费
在 Compose 中,指针事件通过 UI 树分发:
- 初始阶段 (Initial pass): 从根节点向下传递到目标节点。
- 主阶段 (Main pass): 从目标节点向上传递回根节点。
在 PointerInputChange 对象上调用 consume() 可以阻止其他组件处理该事件。
冲突处理
如果在同一个组件或父子组件上使用了相互冲突的手势(例如父容器是 clickable,子容器检测 tap),Compose 会尝试合理处理,但有时需要手动调整。
例如,如果你在 Scrollable 容器(如 LazyColumn)中使用 detectDragGestures,可能需要确保只有特定方向的拖拽被捕获,以免干扰列表滚动。
嵌套滚动 (Nested Scrolling)
Compose 提供了 NestedScrollConnection 来处理父子组件之间的滚动协调(例如,列表滚动时隐藏顶部的 Toolbar)。
kotlin
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// 在子组件滚动之前,父组件有机会先消费滚动距离
return Offset.Zero
}
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
// 子组件滚动之后,父组件可以处理剩余的滚动距离
return Offset.Zero
}
}
}
Box(
modifier = Modifier.nestedScroll(nestedScrollConnection)
) {
LazyColumn { ... }
}