Scroll 修飾子

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 に報告した差分*

ポインタが上に移動する

否定的

肯定的

ポインタが下に移動する

肯定的

否定的

ポインタが左に移動する

否定的

正(RTL の場合は負)

ポインタが右に移動する

肯定的

負(RTL の場合は正)

(*)scrollableArea デルタ記号に関する注: scrollableArea のデルタ記号は、単に反転したものではありません。次の要素がインテリジェントに考慮されます。

  1. 向き: 縦向きまたは横向き。
  2. LayoutDirection: LTR または RTL(特に水平スクロールで重要)。
  3. reverseScrolling フラグ: スクロール方向が反転しているかどうか。

scrollableArea は、スクロール デルタを反転するだけでなく、コンテンツをレイアウトの境界にクリップし、オーバースクロール効果のレンダリングを処理します。デフォルトでは、LocalOverscrollFactory が提供するエフェクトが使用されます。これをカスタマイズまたは無効にするには、OverscrollEffect パラメータを受け入れる scrollableArea オーバーロードを使用します。

scrollableArea 修飾子を使用するケース

scrollableArea 修飾子は、horizontalScroll 修飾子、verticalScroll 修飾子、遅延レイアウトでは適切に処理されないカスタム スクロール コンポーネントを構築する必要がある場合に使用します。これには、次のようなケースが含まれます。

  • カスタム レイアウト ロジック: スクロール位置に基づいてアイテムの配置が動的に変更される場合。
  • 独自の視覚効果: スクロール時に子に変換、スケーリング、その他の効果を適用します。
  • 直接制御: 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 修飾子は、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 要素。