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.dispatchPreFling
とNestedScrollDispatcher.dispatchPostFling
です。親コンポーネントで呼び出されるのはNestedScrollConnection.onPreFling
とNestedScrollConnection.onPostFling
です。
処理の流れはスクロールの場合と同じです。違うのは、親子間で受け渡すのがスクロール量(Offset
)ではなくスクロール速度(Velocity
)という点です。スクロールの指が離れた時点の速度を起点として、親子間で速度を順に消費していきます。
ただし、NestedScrollのフリングを実現する方法は2種類あり、必ずしもdispatchXXXFlingとonXXXFlingを実装する必要はありません。詳しくは今後の記事でコードとともに説明します。
Modifier.nestedScroll
NestedScrollDispatcher
やNestedScrollConnection
をコンポーザブルエレメントにセットするには、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
の実装を説明します。