滚动修饰符

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

@Composable
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

响应滚动姿势的简单垂直列表
图 1. 响应滚动手势的简单垂直列表。

借助 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))
        }
    }
}

可滚动区域修饰符

scrollableArea 修饰符是创建自定义可滚动容器的基本构建块。它提供了比 scrollable 修饰符更高级别的抽象,可处理手势增量解释、内容剪裁和滚动回弹效果等常见需求。

虽然 scrollableArea 用于自定义实现,但对于标准滚动列表,您通常应首选现成的解决方案,例如 verticalScroll、horizontalScroll 或 LazyColumn 等可组合项。这些更高级别的组件对于常见用例来说更简单,并且它们本身是使用 scrollableArea 构建的。

scrollableArea 修饰符和 scrollable 修饰符之间的区别

scrollableArea 和 scrollable 之间的主要区别在于它们如何解读用户滚动滑动手势:

  • scrollable(原始增量):增量直接反映用户在屏幕上的输入(例如指针拖动)的物理移动。
  • scrollableArea(面向内容的增量):delta 在语义上是反向的,表示滚动位置的选定变化,使内容看起来随着用户的手势移动,这通常与指针移动方向相反。

您可以这样理解:scrollable 会告诉您指针是如何移动的,而 scrollableArea 会将指针移动转换为内容在典型可滚动视图中的移动方式。这种反转就是为什么在实现标准可滚动容器时,scrollableArea 感觉更自然的原因。

下表总结了常见场景的增量符号:

用户手势

scrollable 向 dispatchRawDelta 报告的 delta

scrollableArea 向 dispatchRawDelta 报告的 delta*

指针向上移动

否定

正面

指针向下移动

正面

否定

指针向左移动

否定

正数(对于 RTL 语言,为负数)

指针向右移动

正面

负值(对于 RTL 为正值)

(*) 关于 scrollableArea 增量符号的说明:scrollableArea 的增量符号不仅仅是简单的反转。它会智能地考虑以下因素:

  1. 方向:竖屏或横屏。
  2. LayoutDirection:从左到右或从右到左(对于水平滚动尤为重要)。
  3. reverseScrolling 标志:滚动方向是否反转。

除了反转滚动增量之外,scrollableArea 还会将内容裁剪到布局的边界,并处理滚动回弹效果的渲染。默认情况下,它会使用 LocalOverscrollFactory 提供的效果。 您可以使用接受 OverscrollEffect 参数的 scrollableArea 重载来自定义或停用此功能。

何时使用 scrollableArea 修饰符

当您需要构建一个无法通过 horizontalScroll 或 verticalScroll 修饰符或延迟布局充分满足需求的自定义滚动组件时,应使用 scrollableArea 修饰符。这通常涉及以下情况:

  • 自定义布局逻辑:当项的排列方式根据滚动位置动态变化时。
  • 独特的视觉效果:在子项滚动时对其应用转换、缩放或其他效果。
  • 直接控制:需要对滚动机制进行精细控制,超出 verticalScroll 或延迟布局所提供的控制范围。

使用 scrollableArea 创建自定义滚轮式列表

以下示例演示了如何使用 scrollableArea 构建自定义垂直列表,其中列表项在远离中心时会缩小,从而营造出“轮状”视觉效果。这种依赖于滚动的转换是 scrollableArea 的完美应用场景。

图 2. 使用 scrollableArea 的自定义垂直列表。

@Composable
private fun ScrollableAreaSample() {
    // ...
    Layout(
        modifier =
            Modifier
                .size(150.dp)
                .scrollableArea(scrollState, Orientation.Vertical)
                .background(Color.LightGray),
        // ...
    ) { measurables, constraints ->
        // ...
        // Update the maximum scroll value to not scroll beyond limits and stop when scroll
        // reaches the end.
        scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)

        // Position the children within the layout.
        layout(constraints.maxWidth, viewportHeight) {
            // The current vertical scroll position, in pixels.
            val scrollY = scrollState.value
            val viewportCenterY = scrollY + viewportHeight / 2

            var placeableLayoutPositionY = 0
            placeables.forEach { placeable ->
                // This sample applies a scaling effect to items based on their distance
                // from the center, creating a wheel-like effect.
                // ...
                // Place the item horizontally centered with a layer transformation for
                // scaling to achieve wheel-like effect.
                placeable.placeRelativeWithLayer(
                    x = constraints.maxWidth / 2 - placeable.width / 2,
                    // Offset y by the scroll position to make placeable visible in the viewport.
                    y = placeableLayoutPositionY - scrollY,
                ) {
                    scaleX = scaleFactor
                    scaleY = scaleFactor
                }
                // Move to the next item's vertical position.
                placeableLayoutPositionY += placeable.height
            }
        }
    }
}
// ...

可滚动的修饰符

scrollable 修饰符与滚动修饰符的不同之处在于,scrollable 会检测滚动动作并捕获增量,但不会自动偏移其内容。而是通过 ScrollableState 委托给用户,此修饰符只有在指定了该参数的情况下,才能正常工作。

构造 ScrollableState 时,您必须提供一个 consumeScrollDelta 函数,该函数将在每个滚动步骤调用(通过手势输入、流畅滚动或快速滑动),并且增量以像素为单位。此函数必须返回已使用的滚动距离,以确保在存在具有 scrollable 修饰符的嵌套元素时,事件能够正确传播。

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

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(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())
    }
}

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