3
1

Accompanist無しで無限スクロールを実装する

Last updated at Posted at 2024-03-29

本記事の内容はすべて以下のバージョンを前提とします

  • 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

この実装は完璧ではありません。例えば、素早いスクロール操作を繰り返すとジャンプで戻る前にページ末端に到達していまいます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1