スクロール

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

スクロールに応答するシンプルな縦方向のリスト
ジェスチャー

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 スクロール修飾子とは異なり、scrollable ではスクロール修飾子が スクロール操作を行い、差分を取得しますが、その内容はオフセットされません。 自動的に適用されます。これは代わりにユーザーに委任されます。 ScrollableState は、この修飾子が正常に動作するために必要です。

ScrollableState を作成する際に、consumeScrollDelta を指定する必要があります。 スクロール ステップごとに呼び出される関数(ジェスチャー入力、 (スクロールやフリングなど)をピクセル単位の差分で表します。この関数は 消費されるスクロール距離の量(イベントが適切に scrollable を持つネストされた要素がある場合に伝播される 修飾子を使用します。

次のスニペットは、操作を検出してオフセットの数値を表示しますが、要素のオフセットは行いません。

@Composable
private 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())
    }
}

指の押下を検出し、数値を表示する UI 要素
指の
ロケーション

ネストされたスクロール

ネストされたスクロールは、複数のスクロール コンポーネントが含まれるシステム 1 つのスクロール操作に反応し、それに反応して相互作用することで、 スクロール差分(変更)を通知します。

ネストされたスクロール システムを使用すると、 スクロール可能で階層的にリンクされている(ほとんどの場合、同じ親を共有することによって) このシステムは、スクロール コンテナをリンクし、スクロールの操作を可能にする 差分のみが含まれます

Compose には、コンポーザブル間のネストされたスクロールを処理する方法が複数用意されています。 ネストされたスクロールの典型的な例は、別のリスト内にあるリストです。 ツールバーです。

自動ネスト スクロール

シンプルなネスト スクロールでは、アプリ側のアクションは必要ありません。スクロール アクションを開始する操作は、子から親に自動的に伝播されます。これにより、子がそれ以上スクロールできなくなると、親要素によって操作が処理されます。

自動的にネストされたスクロールがサポートされており、 Compose のコンポーネントと修飾子: verticalScroll horizontalScroll, scrollable, Lazy API とTextField。つまり、ユーザーが内側の画面をスクロールすると、 前の修飾子がスクロールを伝播させるため、 ネストされたスクロールをサポートする親に対する差分

次の例は、 verticalScroll verticalScroll を持つコンテナ内で修飾子が適用されている 適用されています。

@Composable
private fun AutomaticNestedScroll() {
    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)
                    )
                }
            }
        }
    }
}

ネストされた垂直方向にスクロールする 2 つの UI 要素。内部でのジェスチャーに応答し、
インナーの外側に
要素

nestedScroll 修飾子の使用

複数の要素間で高度な調整されたスクロールを作成する必要がある場合は、 nestedScroll 修飾子を使用すると、ネストされたスクロール階層を柔軟に定義できます。として 一部のコンポーネントには、ネスト スクロールが組み込まれています。 サポート。ただし、自動的にスクロールできないコンポーザブルの場合 Box または Column の場合、これらのコンポーネントでのスクロールの差分は、 ネスト スクロール システムであり、差分が NestedScrollConnection にも届かない 渡されます。この問題を解決するには、nestedScroll を使用して、 カスタム コンポーネントを含む他のコンポーネントへのサポート。

ネストされたスクロール サイクル

ネストされたスクロール サイクルは、上下にディスパッチされるスクロールのデルタの流れです。 ネストされたすべてのコンポーネント(またはノード)を スクロール可能なコンポーネントと修飾子を使用するなど、スクロール可能なシステム nestedScroll

ネストされたスクロール サイクルのフェーズ

スクロール可能なイベントによってトリガー イベント(操作など)が検出されたとき 実際のスクロール操作がトリガーされる前に、 デルタはネストされたスクロール システムに送信され、次の 3 つのフェーズを経由します。 事前スクロール、ノードの消費、ポストスクロールの 3 つです。

ネストされたスクロールのフェーズ
サイクル

最初の事前スクロール フェーズでは、トリガー イベントを受信したコンポーネントが デルタでは、これらのイベントが階層ツリーを通じて最上位の できます。デルタイベントはバブルダウンします。つまり、差分は 最もルートの親から下で、開始元の子に向かって ネストされたスクロールサイクルが トリガーされます

事前スクロール フェーズ - ディスパッチ
アップ

これにより、ネストされたスクロールの親(nestedScroll または スクロール可能な修飾子など)がある場合、 ノード自体で使用できます

事前スクロール フェーズ - バブリング
下げて

ノードの使用フェーズでは、ノード自体は、更新されていない 使用しないでください。この段階でスクロールの動きが実際に実行され、 表示されます。

ノードの使用量
フェーズ

この段階では、子は残りのリソースの一部を使用するか、すべてを使用するかを選択できます。 スクロールできます。残っているものは、ポスト スクロール フェーズに進むために戻されます。

最後のポストスクロール フェーズでは、ノード自体が消費しなかったものが 消費のために再び祖先に送られます

スクロール後のフェーズ - ディスパッチ
アップ

ポストスクロール フェーズは、事前スクロール フェーズと同様に機能します。 食べるかしないかを選ぶ保護者の割合。

ポスト スクロール フェーズ - バブリング
下げて

スクロールと同様に、ドラッグ操作が終了するとユーザーの意図は フリング(アニメーションを使ってスクロール)する速度に 作成します。フリングもネストされたスクロール サイクルの一部であり、 ドラッグ イベントによって生成される速度は、プリフリング、 ノード消費量、フリング後などですなお、フリング アニメーションは、 タップ操作に対応しており、a11y や a11y などのイベントではトリガーされません。 ハードウェア スクロール。

ネストされたスクロール サイクルに参加する

このサイクルに参加するということは、 デルタの消費を制御できます。Compose には、タスクを実行するための ネストされたスクロール システムの仕組みと、 たとえば、スクロールの差分処理を スクロール可能なコンポーネントが スクロールを開始する場合もあります

ネストされたスクロール サイクルがノードのチェーンで動作するシステムの場合、 nestedScroll 修飾子は、これらの変更をインターセプトして挿入する方法であり、 チェーンに伝播されるデータ(スクロール デルタ)に影響します。この 修飾子は階層のどこにでも配置でき、 ネストされたスクロール修飾子がツリーの上位に表示されるように、 このチャンネル。この修飾子の構成要素は NestedScrollConnection です。 および NestedScrollDispatcher

NestedScrollConnection ネストされたスクロール サイクルのフェーズに対応し、 ネストされたスクロールシステムですこれは 4 つのコールバック メソッドで構成され、 消費フェーズ(プリ/ポスト スクロールとプリ/フリング)のいずれかを表します。

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

各コールバックは、伝播される差分に関する情報も提供します。 その特定のフェーズで available のデルタ、消費された consumed のデルタ 理解しましたある時点で差分の伝播を停止したい場合は、 ネストされたスクロール接続を使用できます。

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

すべてのコールバックは、 NestedScrollSource あります。

NestedScrollDispatcher ネストされたスクロール サイクルを初期化します。ディスパッチャの使用とそのメソッドを呼び出す 循環をトリガーしますスクロール可能なコンテナには、 システムへのジェスチャー中にキャプチャされたデルタ。このため、ほとんどのユースケースでは ネストされたスクロールをカスタマイズするには、代わりに NestedScrollConnection を使用する 新しい差分を送信するのではなく、既存の差分に反応します。 詳しくは、 NestedScrollDispatcherSample をご覧ください。

ネストされたスクロールの相互運用

スクロール可能なコンポーザブルでスクロール可能な View 要素をネストしようとする場合、または 逆にすると、問題が発生する可能性があります。特に注目に値するのは、 子をスクロールして開始境界または終了境界に到達し、 親にスクロールを引き継ぎますただし、この想定される動作は、 期待どおりに動作しない可能性があります。

この問題は、スクロール可能なコンポーザブルに組み込まれている前提の結果です。スクロール可能なコンポーザブルには「デフォルトでネスト スクロール」というルールがあります。つまり、スクロール可能なコンテナはすべて、ネスト スクロール チェーンに、NestedScrollConnection を介して親として参加し、NestedScrollDispatcher を介して子として参加する必要があります。そうすることで、子が境界にあるときに、子が親のネスト スクロールを行います。このようなルールにより、たとえば Compose の Pager と Compose の LazyRow が適切に連携します。ただし、相互運用スクロールが ViewPager2 または RecyclerView で行われている場合、これらは NestedScrollingParent3 を実装していないため、子から親への連続スクロールはできません。

スクロール可能な View 要素とスクロール可能なコンポーザブルが両方向でネストされており、それらの間でネスト スクロールの相互運用 API を有効にする場合、以下のシナリオでネスト スクロールの相互運用 API を使用することで、このような問題を軽減できます。

ComposeView を含む、協力する親 View

連携する親 View は、すでに実装しているものです。 NestedScrollingParent3 そのため、協調するネスト構造からスクロールの差分を受け取れます。 子コンポーザブル:この場合、ComposeView が子として機能し、次のようになります。 実装する必要が NestedScrollingChild3。 協力する親の例の 1 つは、 androidx.coordinatorlayout.widget.CoordinatorLayout

スクロール可能な View である親コンテナとネストされたスクロール可能な子コンポーザブルの間にネスト スクロールの相互運用が必要な場合は、rememberNestedScrollInteropConnection() を使用できます。

rememberNestedScrollInteropConnection() により、NestedScrollingParent3 を実装する親 View と子 Compose との間でネスト スクロールの相互運用を有効にする NestedScrollConnection の許可と保存が可能になります。これは nestedScroll 修飾子と組み合わせて使用する必要があります。Compose 側ではネスト スクロールがデフォルトで有効になっているため、 この接続を使用して、View 側でネストされたスクロールの両方を有効にし、 Views とコンポーザブルの間の必要なグルーロジック。

よく使用されるユースケースでは、CoordinatorLayoutCollapsingToolbarLayout、 子コンポーザブルを返します。

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

アクティビティまたはフラグメントで、子コンポーザブルと 必須 NestedScrollConnection:

open class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalComposeUiApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

AndroidView を含む親コンポーザブル

このシナリオでは、子 AndroidView を含んでいる親コンポーザブルがある場合の Compose 側のネスト スクロールの相互運用 API の実装を扱います。AndroidView は、スクロールにおける親である Compose に対しては子になるので NestedScrollDispatcher を実装し、スクロールにおける子である View に対しては親になるので NestedScrollingParent3 を実装します。Compose の親のアクション その後、ネストされたスクロール可能な子から、ネストされたスクロールの差分を受け取ることができる View

次の例は、このスライドでネストされたスクロールの相互運用を実現する方法を示しています。 Compose の折りたたみツールバーとともに表示されます。

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

次の例は、scrollable 修飾子で、この API を使用する方法を示しています。

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

次にある最後の例では、ネスト スクロールの相互運用 API を BottomSheetDialogFragment で使用して、ドラッグして閉じる動作を実現する方法を示しています。

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

注: rememberNestedScrollInteropConnection() は、 NestedScrollConnection 必ず指定してください。NestedScrollConnection は、デルタを Compose レベルから View レベルに送信します。これにより、 要素がネストされたスクロールに参加できますが、 要素を自動的にスクロールすることもできます。スクロール不可のコンポーザブルに変更 BoxColumn など、スクロールの差分が自動的に 差分がネストされたスクロール システムに伝播され、 NestedScrollConnection 提供元: rememberNestedScrollInteropConnection()、 そのため、その差分は親の View コンポーネントに到達しません。この問題を解決するには スクロール可能な修飾子もこれらのタイプのネストされた 作成します。詳しくは、前のセクションのネストされた スクロールを使用すると、 情報です。

ComposeView を持つ非協力的な親 View

連携しない View とは、View 側で必要な NestedScrolling インターフェースを実装していない View のことです。つまり、こうした Views とのネスト スクロールの相互運用は、そのままでは機能しません。連携しない Views は、RecyclerViewViewPager2 です。