詳解! ComposeでNestedScroll その1 基本の仕組み編

この記事をシェア

NestedScrollは、スクロール可能な親コンポーネントの上に、同じ方向にスクロールする子コンポーネントが配置され、スクロールが入れ子になっている実装のことです。今回から数回に分けて、NestedScrollについて説明します。ComposeのNestedScrollの仕組みを説明し、サンプルを交えて紹介しながら、ちゃんと理解して実装できる状態になることを目指します。

第1回の今回は、ComposeにおけるNestedScrollの基本的な仕組みを説明します。

NestedScrollの例

まずはNestedScrollの動きを確認しておきます。次の例は、LazyColumnのスクロールに連動してTopAppBarの高さが変化するサンプルです。

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LazyColumnOnLazyColumn() {
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            LargeTopAppBar(
                title = { Text("TopAppBar") },
                colors = TopAppBarDefaults.largeTopAppBarColors(
                    containerColor = Color.LightGray
                ),
                scrollBehavior = scrollBehavior,
            )
        }
    ) { innerPadding ->
        LazyColumn(
            contentPadding = innerPadding,
            modifier = Modifier.fillMaxSize()
        ) {
            items(50) {
                Text(
                    text = "Item $it",
                    modifier = Modifier.fillMaxWidth().height(40.dp)
                )
            }
        }
    }
}

Scaffold(親コンポーネント)の中にLazyColumn(子コンポーネント)が配置されています。スクロールイベントを検出するのはLazyColumnですが、Scaffold(が保持しているTopAppBar)も連動します。このように、子コンポーネントで検出したスクロールイベントを、親子のコンポーネントで共有して連動する仕組みが、NestedScrollです。

TopAppBarの高さが変わるタイミングにも注目してください。上にスクロールすると、LazyColumnがスクロールする前にTopAppBarの高さが変わりますが、下にスクロールすると、LazyColumnがスクロールした後にTopAppBarの高さが変わります。親コンポーネントがスクロールするタイミングは、子コンポーネントの前と後の2回あることになります。

また、指を離したあともしばらくスクロールが継続することをフリング(fling)と呼びますが、フリング動作中にもTopAppBarの高さが変わっていることが分かります。

NestedScrollDispatcherとNestedScrollConnection

NestedScrollは、子コンポーネントで発生したスクロールイベントを、子コンポーネントから親コンポーネントへ伝えて、それぞれが適切に処理することで実現します。これを実現するために、ComposeではNestedScrollDispatcherクラスとNestedScrollConnectionインターフェースを使います。

NestedScrollDispatcher

スクロールイベントを、親コンポーネントに向けて発行するクラスです。子コンポーネントにセットすることで、NestedScrollの起点となります。スクロールとフリングのイベントを発行する下記の関数が用意されています。PreScrollは子コンポーネントの前に親コンポーネントがスクロールする場合、PostScrollは子コンポーネントの後に親コンポーネントがスクロールする場合です。

  • dispatchPreScroll
  • dispatchPostScroll
  • dispatchPreFling
  • dispatchPostFling

NestedScrollConnection

子コンポーネントのNestedScrollDispatcherが発行したスクロールイベントを受け取ってスクロールを処理します。インターフェースに下記の4つの関数が定義されており、NestedScrollDispatcherの4つの関数にそれぞれ対応しています。これらを実装して親コンポーネントに設定します。

  • onPreScroll
  • onPostScroll
  • onPreFling
  • onPostFling

PreScrollとPostScroll

子コンポーネントがスクロールするに親コンポーネントがスクロールすることをPreScrollと呼び、子コンポーネントがスクロールしたに親コンポーネントがスクロールすることをPostScrollと呼びます。

子コンポーネントを実装する際は、スクロールイベントを検出したら最初にNestedScrollDispatcher.dispatchPreScrollを呼び出します。このとき、親コンポーネントが消費可能なスクロール量をOffsetで指定します。すると、親コンポーネントのNestedScrollConnection.onPreScrollが呼び出されるので、子コンポーネントより先に実行したい親コンポーネントのスクロール処理を実行し、実際に消費したスクロール量を返します。

PreScrollが完了したら、子コンポーネントが自分自身のスクロールを処理します。スクロールに利用できるのは、PreScrollで親コンポーネントが消費して残った分です。

最後に、NestedScrollDispatcher.dispatchPostScrollを呼び出します。ここで親コンポーネントに渡すのは、PreScrollで親コンポーネントが消費した分と、子コンポーネントが消費した分を引いた残りのスクロール量です。親コンポーネント側ではNestedScrollConnection.onPostScrollが呼び出されるので、子コンポーネントのスクロールの後に実行したい親コンポーネントのスクロール処理を実行します。

Fling

NestedScrollをなめらかな操作感で実現するためには、フリングにも対応する必要があります。

フリングもスクロールと同様に、PreとPostの2つのタイミングで子コンポーネントから親コンポーネントの処理を呼び出します。子コンポーネントで呼び出すのはNestedScrollDispatcher.dispatchPreFlingNestedScrollDispatcher.dispatchPostFlingです。親コンポーネントで呼び出されるのはNestedScrollConnection.onPreFlingNestedScrollConnection.onPostFlingです。

処理の流れはスクロールの場合と同じです。違うのは、親子間で受け渡すのがスクロール量(Offset)ではなくスクロール速度(Velocity)という点です。スクロールの指が離れた時点の速度を起点として、親子間で速度を順に消費していきます。

ただし、NestedScrollのフリングを実現する方法は2種類あり、必ずしもdispatchXXXFlingとonXXXFlingを実装する必要はありません。詳しくは今後の記事でコードとともに説明します。

Modifier.nestedScroll

NestedScrollDispatcherNestedScrollConnectionをコンポーザブルエレメントにセットするには、Modifier.nestedScrollを使います。引数は、NestedScrollConnectionが必須、NestedScrollDispatcherがオプションとなっています。

fun Modifier.nestedScroll(
    connection: NestedScrollConnection,
    dispatcher: NestedScrollDispatcher? = null
): Modifier

役割の異なる2つのオブジェクトをまとめて引数で渡すので、作用を予想しづらい関数だと思います。親コンポーネントは子コンポーネントからイベントを受け取る必要があるので、connectionをセットします。子コンポーネントは、親コンポーネントにイベントを発行するため、dispatcherをセットします。connectionは省略できないので、何も実装していない空のNestedScrollConnectionを渡します(*)。

(*)ただし、子コンポーネント自体がさらに別の孫コンポーネントで発生したスクロールイベントを受け取る必要がある場合は、onXxxScrollやonXxxFlingを実装したNestedScrollConnectionをセットする必要があります。

NestedScrollConnectionチェーン

ここまでの説明では、NestedScrollに関わるコンポーザブルエレメントは、親と子の2つでしたが、NestedScrollは3つ以上のエレメント間で連携して動作させることもできます。NestedScrollConnectionは、下位のコンポーネントから上位のコンポーネントへ順にチェーンを形成し、NestedScrollDispatcherが発行したイベントを伝播させます。

親・子・孫の3つのコンポーネントがあり、孫コンポーネントにNestedScrollDispatcherを実装し、それを子コンポーネントと親コンポーネントが受け取るケースを考えます。

PreScrollは、上位のコンポーネントから順に消費されます。孫コンポーネントがdispatchPreScrollを発行すると、最初に親コンポーネントでonPreScrollが呼び出され、次に子コンポーネントでonPreScrollが呼び出されて、孫コンポーネントに処理が戻ってきます。

PostScrollは、下位のコンポーネントから順に消費されます。孫コンポーネントがdispatchPostScrollを発行すると、最初に子コンポーネントでonPostScrollが呼び出され、次に親コンポーネントでonPostScrollが呼び出されて、一連のスクロール処理が完了します。


第1回は以上です。第2回は、NestedScrollConnectionの実装を説明します

この記事をシェア