スクロール

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 はカスタム実装に使用されますが、標準のスクロール リストには、一般的に verticalScrollhorizontalScroll などの既製のソリューションや、LazyColumn などのコンポーザブルを使用することをおすすめします。これらの上位コンポーネントは、一般的なユースケースではよりシンプルであり、scrollableArea を使用して構築されています。

scrollableArea 修飾子と scrollable 修飾子の違い

scrollableAreascrollable の主な違いは、ユーザーのスクロール ジェスチャーの解釈方法にあります。

  • scrollable(未加工のデルタ): デルタは、画面上のユーザー入力(ポインタのドラッグなど)の物理的な動きを直接反映します。
  • scrollableArea(コンテンツ指向のデルタ): delta は、選択したスクロール位置の変化を表すように意味的に反転され、コンテンツがユーザーのジェスチャーに合わせて移動するように見えます。これは通常、ポインタの動きとは逆になります。

scrollable はポインタの移動方法を示し、scrollableArea はそのポインタの移動を、一般的なスクロール可能なビュー内でコンテンツがどのように移動すべきかに変換します。この反転により、標準のスクロール可能なコンテナを実装する際に scrollableArea がより自然に感じられます。

次の表に、一般的なシナリオのデルタ符号の概要を示します。

ユーザー ジェスチャー

scrollabledispatchRawDelta に報告したデルタ

scrollableAreadispatchRawDelta に報告したデルタ*

ポインタがに移動する

否定的

肯定的

ポインタが DOWN に移動

肯定的

否定的

ポインタがに移動する

否定的

Positive(RTL の場合は Negative

ポインタが RIGHT に移動

肯定的

負(RTL の場合は正

(*)scrollableArea デルタ記号に関する注: scrollableArea からのデルタの記号は、単純な反転ではありません。次の点を考慮して、インテリジェントに処理します。

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

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

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

horizontalScroll 修飾子、verticalScroll 修飾子、または遅延レイアウトでは十分に対応できないカスタム スクロール コンポーネントを構築する必要がある場合は、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 修飾子が scroll 修飾子と異なる点は、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 要素。