手势
Compose 提供了多种 API,可帮助您检测用户互动生成的手势。API 涵盖各种用例:
- 其中一些级别较高,旨在覆盖最常用的手势。例如,
clickable
修饰符可用于轻松检测点击,此外它还提供无障碍功能,并在点按时显示视觉指示(例如涟漪) - 还有一些不太常用的手势检测器,它们在较低级别提供更大的灵活性,例如
PointerInputScope.detectTapGestures
或PointerInputScope.detectDragGestures
,但不提供额外功能
点击
clickable
修饰符允许应用检测对已应用该修饰符的元素的点击
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// 你想要实现点击效果的组件
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
当需要更大灵活性时,您可以通过 pointerInput
修饰符提供点按手势检测器:
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* 按下事件,所有手势事件的开始 */ },
onDoubleTap = { /* 双击事件 */ },
onLongPress = { /* 长按事件 */ },
onTap = { /* 点击事件 */ }
)
}
在使用
pointerInput
必须传递至少一个Key
,当Key
发生变化时会重新调用pointerInput
,若不想其发生变化传递一个Unit
即可fun Modifier.pointerInput( key1: Any?, block: suspend PointerInputScope.() -> Unit ) { ... }
可以看到
block
为挂起函数,因为等待手势必然是异步事件,需要使用协程等待手势事件的发生
PointerInputScope
内我们可以在awaitPointerEventScope
协程作用域内检测手势事件发生,详情请看官网介绍
当然级别高的手势检测器内部都是通过低级别的手势检测器完成,比如
clickable
内部是通过封装pointerInput
完成点击事件的检测fun Modifier.clickable( interactionSource: MutableInteractionSource, indication: Indication?, enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onClick: () -> Unit ) = composed( factory = { val onClickState = rememberUpdatedState(onClick) ... val gesture = Modifier.pointerInput(interactionSource, enabled) { detectTapAndPress( onPress = { offset -> if (enabled) { handlePressInteraction( offset, interactionSource, pressedInteraction, delayPressInteraction ) } }, onTap = { if (enabled) onClickState.value.invoke() } ) } Modifier .then( ... ).genericClickableWithoutGesture( gestureModifiers = gesture, interactionSource = interactionSource, indication = indication, enabled = enabled, onClickLabel = onClickLabel, role = role, onLongClickLabel = null, onLongClick = null, onClick = onClick ) }, inspectorInfo = ... )
滚动
注意:如果您想要显示项列表,请考虑使用
LazyColumn
和LazyRow
而不是使用这些 API。LazyColumn
和LazyRow
具有滚动功能,它们的效率远高于滚动修饰符,因为它们仅在需要时组合各个项
滚动修饰符
verticalScroll
和 horizontalScroll
修饰符提供一种最简单的方法,可让元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScroll
和 horizontalScroll
修饰符,无需转换或偏移内容
@Composable
fun ScrollBoxes() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.verticalScroll(rememberScrollState())
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
借助
ScrollState
,您可以更改滚动位置或获取当前状态
scrollable修饰符
scrollable
修饰符与滚动修饰符不同,区别在于 scrollable
可检测滚动手势,但不会偏移其内容。必须有 ScrollableState
参数,此修饰符才能正常工作
构造 ScrollableState
时,必须提供一个 consumeScrollDelta
函数,该函数将在每个滚动步骤调用(通过手势输入、流畅滚动或快速滑动),并且增量以像素为单位。该函数会返回所消耗的滚动距离,以确保在存在具有 scrollable
修饰符的嵌套元素时,可以正确传播相应事件
以下代码段可检测手势并显示偏移量的数值,但不会偏移任何元素:
@Composable
fun ScrollableSample() {
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
//不是 rememberScrollState,而且rememberScrollState也可以作为这里的参数
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
// 可以通过offset实现滚动效果
// .offset(0.dp, LocalDensity.current.run {
// offset.toDp()
// })
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
scrollable
修饰符不会影响它所应用到的元素的布局。这意味着,对元素布局或其子级进行的任何更改都必须通过由ScrollableState
提供的增量进行处理。另外请务必注意,scrollable
不会考虑子级的布局,这意味着它无需测量子级,即可传播滚动增量
嵌套滚动
Compose 支持嵌套滚动,可让多个元素对一个滚动手势做出回应。典型的嵌套滚动示例是在一个列表中嵌套另一个列表
自动嵌套滚动
简单的嵌套滚动无需您执行任何操作。启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理
部分 Compose 组件和修饰符原生支持自动嵌套滚动,包括:verticalScroll
、horizontalScroll
、scrollable
、Lazy API
和TextField
。这意味着,当用户滚动嵌套组件的内部子级时,之前的修饰符会将滚动增量传播到支持嵌套滚动的父级
以下示例显示的元素应用了 verticalScroll
修饰符,而其所在的容器同样应用了 verticalScroll
修饰符
//渐变色
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(32.dp)
) {
Column {
repeat(6) {
Box(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"Scroll here",
modifier = Modifier
.border(12.dp, Color.DarkGray)
.background(brush = gradient)
.padding(24.dp)
.height(150.dp)
)
}
}
}
}
协调滚动
如果需要在多个元素之间创建高级协调滚动,可以使用 nestedScroll
修饰符定义嵌套滚动层次结构来提高灵活性。 请注意,一些组件内置对嵌套滚动的支持。 我们可以使用 nestedScroll
向其他组件(包括自定义组件)提供此类支持
拖动
draggable
修饰符是向单一方向拖动手势的高级入口点,并且会报告拖动距离(以像素为单位)
请务必注意,此修饰符与 scrollable
类似,仅检测手势。若需要保存状态并在屏幕上表示,例如通过 offset
修饰符移动元素:
var offsetX by remember { mutableStateOf(0f) }
Text(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
),
text = "Drag me!"
)
如果您需要控制整个拖动手势,请考虑改为通过 pointerInput
修饰符使用拖动手势检测器
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
滑动
我们可以使用 swipeable
修饰符拖动元素,释放后,这些元素通常朝一个方向定义的两个或多个锚点呈现动画效果。其常见用途有滑动关闭、图块滑动验证等
请务必注意,此修饰符不会移动元素,而只检测手势。您需要保存状态并在屏幕上表示,例如通过 offset
修饰符移动元素
在 swipeable
修饰符中必须提供可滑动状态,且该状态可以通过 rememberSwipeableState()
创建和记住。此状态还提供了一组有用的方法,用于以程序化方式为锚点添加动画效果,同时为属性添加动画效果,以观察拖动进度
可以将滑动手势配置为具有不同的阈值类型,例如 FixedThreshold(Dp)
和 FractionalThreshold(Float)
,并且对于每个锚点的起始与终止组合,它们可以是不同的
阈值:当滑动元素超过阈值指定的值,此时松开手指,滑动元素会自动滑动至同方向上的下一个anchor(锚点)
为了获得更大的灵活性,您可以配置滑动越过边界时的 resistance
,还可以配置 velocityThreshold
,即使尚未达到位置 thresholds
,velocityThreshold
仍将以动画方式向下一个状态滑动
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SwipeableSample() {
val width = 96.dp
val squareSize = 48.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
// Maps anchor points (in px) to states
// 锚点元素不能空,而且value也必须唯一
val anchors = mapOf(0f to 0, sizePx to 1)
Box(
modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
)
}
}
多点触控:平移、缩放、旋转
如需检测用于平移、缩放和旋转的多点触控手势,您可以使用 transformable
修饰符。此修饰符本身不会转换元素,只会检测手势
@Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
如果您需要将缩放、平移和旋转与其他手势结合使用,可以使用 PointerInputScope.detectTransformGestures
检测器
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)
centroid:质点,旋转中心
pan:位移量
zoom:放大系数
rotation:旋转角度