Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Android Composeでリストの真ん中にある要素の変更を検知する

こんにちは、Androidエンジニアの 近藤(ふなち / Hunachi) です☺︎

はじめに(紹介したいこと)

まず最近私が実装した機能の一部にこのようなリスト表示があります。

今回の記事に関係する仕様は以下の通りです。(他にも色々仕様はあり実際はもーっと複雑です☺️)

  • 画面の真ん中の要素が選択されている要素
  • このリスト自体をスクロールすることができる
  • 要素のサイズは全部同じとは限らない

この仕様を満たすには、真ん中にある要素を検知し続ける必要があります。

そこでこの記事ではComposeを使ってこのようなリストの真ん中にある要素の変更を検知する方法について紹介していきます。

今回のリストの実装(基本的な部分)

唐突に謎のプロパティが現れると分かりづらいと思うので今回の記事に関係する部分を抽出した簡易版リストの実装を紹介します。

@Composable
fun SampleList(
    selectedPosition: Int, // 選択されている要素
    changeSelectedPosition: (Int) -> Unit, // 選択されている要素の変更
) {
    val listSize = 10 // 要素数
    val initialOffset = 100f // 100は仮の値で実際は頑張ってオフセットを計算する(単位はPx)
    val listState = rememberLazyListState(
        initialFirstVisibleItemIndex = selectedPosition,
        initialFirstVisibleItemScrollOffset = initialOffset
    )
    var containerWidthPx by rememberSaveable { mutableFloatStateOf(0f) } // リスト横幅

    LazyRow(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged {
                containerWidthPx = it.width.toFloat()
            },
        state = listState
    ) {
        items(listSize) {
            ImageContent(..) // 画像を表示するComposable 
        }
    }
}

特殊な実装はしていません。

真ん中の要素を検知

よく見かける方法じゃ無理なの?

偏見かもしれませんが、スクロール検知の話題でよく見かけるのは、listState.layoutInfo.visibleItemsInfoや、listState.firstVisibleItemIndexを監視する方法です。

一番上や一番下のアイテムを知ることが目的なら、表示されている要素に変化があるときにだけ検知できれば十分なことが多いのでこれらを使うのがベストプラクティスです。また、リストの要素数が十分にあり、要素の大きさが全て同じ場合もこの方法で十分だと思います。

ですが今回の仕様のリストだとこれではうまくいきません。 例を以下に示します。

リスト自体は大きいが、要素数が少ないリスト(右端の要素が選択されている)
リスト自体は大きいが、要素数が少ないリスト(左端の要素が選択されている)

この様に、表示されている要素に変更はないが真ん中の要素(選択されるべき要素)がスクロールされることで変更される場合がある為です。

解決方法

listState.firstVisibleItemScrollOffsetLazyListState::firstVisibleItemScrollOffset)を監視する方法で解決できます🙌

これは、最初に表示される項目のスクロールオフセットの値で、監視可能なプロパティです。

※ スクロールされる度に変化するのでパフォーマンスに影響を与えてしまわないよう注意する必要があります。

コードは以下のようになります。

// SampleList内に書く
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemScrollOffset }.collect {
        val items = listState.layoutInfo.visibleItemsInfo
        if (items.isEmpty()) return@collect
        // 真ん中の要素を計算して特定する
        val horizontalCenterPositionPx = (containerWidthPx / 2).toInt()
        val centerPosition = items.find {
            horizontalCenterPositionPx in it.offset..it.offset + it.size
        }?.index ?: return@collect
        if (centerPosition != selectedPosition) {
            changePage(centerPosition)
        }
    }
}

まとめ

今回は、Composeでリストの真ん中にある要素の変更を検知する方法を紹介しました。

他にも、オフセットの計算方法、要素のサイズ変化に関連する問題等、書きたいことが沢山あるのですが、長くなるので今回は一部だけ紹介しました。

もっといい案あるよ!という方がいましたら X(私のアカウント)や勉強会で私に話しかけていただけるととっても嬉しいです⭐️

最後に

ヤプリにご興味がある方いましたら是非カジュアル面談でヤプリ社員と話しませんか?

open.talentio.com

最後まで読んでいただきありがとうございました!