本記事の内容はすべて以下のバージョンを前提とします
- Kotlin 1.9.22
- Compose Compiler 1.5.11
- Acoompanist 0.34.0
- Compose BOM 2024.03.00
(androidx.compose.foundation:foundation-android:1.6.4)
LazyLayout
を利用した擬似的では無い実装方法も投稿しました
前置き
Composeで無限スクロールを実装しようと調べると、Accompanist + ページサイズInt.MAX_VALUE
の実装例が多く見つかります。しかしaccompanist-pagerは現時点で非推奨になっており、Compose本家のandroidx.compose.foundation.pager.HorizontalPager
を使って同様に実装してみるとANRが発生します....
Profilerで見てみると何だかヤバそうな雰囲気です。Int.MAX_VALUE
の代わりに小さめな数字を指定すると動く場合も確認しましたが、端末によってANRを起こす閾値がまちまちで安全な数字は分からずじまいでした。
完成品
Accompanistの代わりにandroidx.compose.foundation.*
を使いちゃんと動作する水平方向の無限スクロールを実装します
実装の説明
基本実装
実際のリストサイズより多くページ数を指定する点は従来と同じですが、サイズの3倍程度に抑えておきます。
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HorizontalLoopPager(
count: Int,
modifier: Modifier = Modifier,
content: @Composable PagerScope.(page: Int) -> Unit,
) {
val state = rememberPagerState(
initialPage = count,
pageCount = { count * 3 },
)
HorizontalPager(
state = state,
) {
// ダミーインデックスを正規化する
val index = it.mod(count)
content(index)
}
}
擬似的な無限スクロール実装
そのままではページ末端に到達するとそれ以上スクロールできません。そこでページ位置が両端に近づいたら、真ん中の位置にアニメーション無しでジャンプさせる方針を採用します。
ユーザー操作・アニメーション中の考慮
PagerState.currentPage
は途中でも変化するためsettledPage
を使っています。加えてスクロール中はジャンプをスキップするよう制御して自然なユーザー体験に配慮しています。
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HorizontalLoopPager(
count: Int,
modifier: Modifier = Modifier,
content: @Composable PagerScope.(page: Int) -> Unit,
) {
val state = rememberPagerState(
initialPage = count,
pageCount = { count * 3 },
)
LaunchedEffect(state) {
// 現在のページ位置を監視
snapshotFlow { state.settledPage to state.isScrollInProgress }
.filter { !it.second }
.map { it.first }
.collectLatest {
// 必要ならアニメーション無しでページ位置をジャンプ
when {
it < count -> it + count
it >= count * 2 -> it - count
else -> null
}?.let { idx ->
launch {
state.scrollToPage(idx)
}
}
}
}
// ..中略..
}
TODO
この実装は完璧ではありません。例えば、素早いスクロール操作を繰り返すとジャンプで戻る前にページ末端に到達していまいます。