はじめに
Jetpack Composeのscrollについて調べた。
Jetpackでスクロールする場合、2種類の修飾子が利用できる
scroll 修飾子
verticalScroll 修飾子と horizontalScroll 修飾子の2種類があり、要素の中身が最大サイズを超えた場合にスクロールする。
実装は簡単だが、拡張性はあまりない。
内部でScrollable修飾子が使われている。
Modifier.verticalScroll(rememberScrollState())
scrollable 修飾子
scrollableはscroll修飾子と違い、スクロールしても自動で要素のオフセットを動かしてくれない。
スクロールのデルタ値を取得し、その値を元に自ら処理を書く必要がある。
Modifier
.scrollable(
orientation = Orientation.Vertical,
state = rememberScrollableState { delta ->
// ここでスクロール処理
delta
}
)
Scrollableは内部的にDraggable修飾子を使っている。
同じOrientationのDraggableがネストすると子にイベントを吸われる。
その為、スクロールをネストさせたい場合はNestedScrollを用いる必要がある。
nestedScroll修飾子
スクロールをネストさせるためのもの。
スクロールの中にスクロールを入れる感じ
基本的には子が先にスクロールを消費し、消費しきれなかった量を親に伝え、親は伝わってきた量だけスクロールする。
上記のverticalScroll、horizontalScroll、scrollableに関しては内部でnestedScrollが実装されており、単に入れ子にするだけでスクロールのネストが可能。
NestedScrollを構成する要素
NestedScrollの呼び出し方
Modifier.nestedScroll(nestedScrollConnection, nestedScrollDispatcher)
NestedScrollDispatcher
NestedScrollを動かす大元になるもの
scrollableではネストされている一番下の子がNestedScrollDispatcherを発火させ、NestedScrollDispatcherが親のNestedScrollConnectionを発火させる実装になっている。
class NestedScrollDispatcher {
// ---中略---
fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
return parent?.onPreScroll(available, source) ?: Offset.Zero
}
fun dispatchPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
}
suspend fun dispatchPreFling(available: Velocity): Velocity {
return parent?.onPreFling(available) ?: Velocity.Zero
}
suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
return parent?.onPostFling(consumed, available) ?: Velocity.Zero
}
}
NestedScrollConnection
NestedScrollの親と子をつなぐもの
scrollableにおいては子のNestedScrollDispatcherまたはNestedScrollConnectionから発火され、親のNestedScrollConnectionを発火する。
実際には親の発火処理などはnestedScrollの内部で行われているので、nestedScrollに渡すnestedScrollConnectionでは自身のflingとscrollの処理だけを書けばよい。
内部で行われている処理
internal class NestedScrollModifierLocal(
val dispatcher: NestedScrollDispatcher,
val connection: NestedScrollConnection
) : ModifierLocalConsumer, ModifierLocalProvider<NestedScrollModifierLocal?>,
NestedScrollConnection {
// ---中略---
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
val parentPreConsumed = parent?.onPreScroll(available, source) ?: Offset.Zero
val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
return parentPreConsumed + selfPreConsumed
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
val selfConsumed = connection.onPostScroll(consumed, available, source)
val parentConsumed =
parent?.onPostScroll(consumed + selfConsumed, available - selfConsumed, source)
?: Offset.Zero
return selfConsumed + parentConsumed
}
override suspend fun onPreFling(available: Velocity): Velocity {
val parentPreConsumed = parent?.onPreFling(available) ?: Velocity.Zero
val selfPreConsumed = connection.onPreFling(available - parentPreConsumed)
return parentPreConsumed + selfPreConsumed
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
val selfConsumed = connection.onPostFling(consumed, available)
val parentConsumed =
parent?.onPostFling(consumed + selfConsumed, available - selfConsumed) ?: Velocity.Zero
return selfConsumed + parentConsumed
}
}
実装してnestedScrollに渡すNestedScrollConnection
val connection = object : NestedScrollConnection {
override suspend fun onPreFling(available: Velocity): Velocity {
// Fling処理
// 子要素よりも先に呼ばれる
return Velocity.Zero // 消費量
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
// Fling処理
// 子要素よりも後に呼ばれる
return Velocity.Zero // 消費量
}
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Scroll処理
// 子要素よりも先に呼ばれる
return Offset.Zero // 消費量
}
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
// Scroll処理
// 子要素よりも後に呼ばれる
return Offset.Zero // 消費量
}
}
下記のような3つのComposable A-B-C(Aが一番上の親) が存在し、それぞれにscrollable修飾子をつけた場合、下記のような順でスクロールが発火される
発火順
scrollableがスクロールを検知すると、NestedScrollDipatcherを発火し、それが親に伝播していく。
scrollableC(スクロール検出)
↓
NestedScrollDipatcherC(onPreScroll)
↓
NestedScrollConnectionB(onPreScroll)
↓
NestedScrollConnectionA(onPreScroll)
↓
scrollableC(スクロール処理)
↓
NestedScrollDipatcherC(onPostScroll)
↓
NestedScrollConnectionB(onPostScroll)
↓
NestedScrollConnectionA(onPostScroll)
イベントの処理順
内部的には下記のように親のonPreScrollを先に処理したりしているので実際の処理順は変わって来る。
override fun onPreScroll(
available: Offset,
source: NestedScrollSource
): Offset {
val parentPreConsumed = parent?.onPreScroll(available, source) ?: Offset.Zero
val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
return parentPreConsumed + selfPreConsumed
}
PreScrollA
↓
PreScrollB
↓
scrollableC(スクロール処理)
↓
PostScrollB
↓
PostScrollA
Orientation
scrollableは方向を持っており、縦横両方のスクロール量を取得できない。
縦横自由にスクロールさせる場合は、scrollableを使わずにdraggableで縦横両方のスクロール量を取得し反映する必要がある。