捲動修飾符

在元素的內容邊界超過尺寸上限時,使用 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 回報的差異

scrollableArea*回報給 dispatchRawDelta 的差異

游標向上移動

負面

正面

游標向下移動

正面

負面

游標向左移動

負面

正值 (由右至左語言為負值)

游標向右移動

正面

負值 (由右至左語言為正值)

(*) scrollableArea 差異符號注意事項:scrollableArea 的差異符號不只是單純的反轉。這項功能會智慧考量:

  1. 方向:直向或橫向。
  2. LayoutDirection:LTR 或 RTL (對於水平捲動特別重要)。
  3. reverseScrolling 旗標:捲動方向是否反向。

除了反轉捲動差異之外,scrollableArea 也會將內容裁剪至版面配置的邊界,並處理過度捲動效果的算繪。根據預設,系統會使用 LocalOverscrollFactory 提供的效果。您可以使用接受 OverscrollEffect 參數的 scrollableArea 超載,自訂或停用這項功能。

scrollableArea 修飾符的使用時機

如需建構自訂捲動元件,但 horizontalScroll 或 verticalScroll 修飾符或 Lazy 版面配置無法充分提供服務,則應使用 scrollableArea 修飾符。這通常涉及以下情況:

  • 自訂版面配置邏輯:項目排列方式會根據捲動位置動態變更。
  • 獨特的視覺效果:在捲動時對子項套用轉換、縮放或其他效果。
  • 直接控制:需要精細控制捲動機制,超出 verticalScroll 或 Lazy 版面配置提供的控制範圍。

使用 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 修飾符和捲動修飾符不同,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())
    }
}

UI 元素會偵測按下手指並顯示手指位置的數值
圖 3. UI 元素會偵測按下手指並顯示手指位置的數值。