手势

Compose 提供了多种 API,可帮助您检测用户互动生成的手势。API 涵盖各种用例:

  • 其中一些级别较高,旨在覆盖最常用的手势。例如,clickable 修饰符可用于轻松检测点击,此外它还提供无障碍功能,并在点按时显示视觉指示(例如涟漪)。

  • 还有一些不太常用的手势检测器,它们在较低级别提供更大的灵活性,例如 PointerInputScope.detectTapGesturesPointerInputScope.detectDragGestures,但不提供额外功能。

点按并按下

clickable 修饰符允许应用检测对已应用该修饰符的元素的点击

@Composable
fun ClickableSample() {
    val count = remember { mutableStateOf(0) }
    // content that you want to make clickable
    Text(
        text = count.value.toString(),
        modifier = Modifier.clickable { count.value += 1 }
    )
}

响应点按的界面元素示例

当需要更大灵活性时,您可以通过 pointerInput 修饰符提供点按手势检测器:

Modifier.pointerInput(Unit) {
    detectTapGestures(
        onPress = { /* Called when the gesture starts */ },
        onDoubleTap = { /* Called on Double Tap */ },
        onLongPress = { /* Called on Long Press */ },
        onTap = { /* Called on Tap */ }
    )
}

滚动

滚动修饰符

verticalScrollhorizontalScroll 修饰符提供一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScrollhorizontalScroll 修饰符,您无需转换或偏移内容。

@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,您可以更改滚动位置或获取当前状态。如需使用默认参数创建此列表,请使用 rememberScrollState()

@Composable
private fun ScrollBoxesSmooth() {

    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

可滚动的修饰符

scrollable 修饰符与滚动修饰符不同,区别在于 scrollable 可检测滚动手势,但不会偏移其内容。此修饰符需要 ScrollableController 才能正常运行。构造 ScrollableController 时,您必须提供一个 consumeScrollDelta 函数,该函数将在每个滚动步骤(通过手势输入、平滑滚动或投掷)调用,并且增量以像素为单位。为了确保正确传播事件,必须从此函数返回使用的滚动距离量。

以下代码段可检测手势并显示偏移量的数值,但不会偏移任何元素:

@Composable
fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

一种用于检测手指按下手势并显示手指位置数值的界面元素

嵌套滚动

Compose 支持嵌套滚动,可让多个元素对一个滚动手势做出回应。典型的嵌套滚动示例是在一个列表中嵌套另一个列表,而收起工具栏则是一种较为复杂的嵌套滚动情况。

自动嵌套滚动

简单的嵌套滚动无需您执行任何操作。启动滚动操作的手势会自动从子级传播到父级,这样一来,当子级无法进一步滚动时,手势就会由其父元素处理。

下面示例显示的元素应用了 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 修饰符移动元素。

swipable 修饰符中必须提供手滑式状态,且该状态可以通过 rememberSwipeableState() 创建和记住。此状态还提供了一组有用的方法,用于以程序化方式为锚点添加动画效果(请参阅 snapToanimateToperformFlingperformDrag),同时为属性添加动画效果,以观察拖动进度。

可以将滑动手势配置为具有不同的阈值类型,例如 FixedThreshold(Dp)FractionalThreshold(Float),并且对于每个锚点的起始与终止组合,它们可以是不同的。

为了获得更大的灵活性,您可以配置滑动越过边界时的 resistance,还可以配置 velocityThreshold,即使尚未达到位置 thresholds,velocityThreshold 仍将以动画方式向下一个状态滑动。

@Composable
fun SwipeableSample() {
    val width = 96.dp
    val squareSize = 48.dp

    val swipeableState = rememberSwipeableState(0)
    val sizePx = with(LocalDensity.current) { squareSize.toPx() }
    val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states

    Box(
        modifier = Modifier
            .width(width)
            .swipeable(
                state = swipeableState,
                anchors = anchors,
                thresholds = { _, _ -> FractionalThreshold(0.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 检测器。