【Android】ViewPager2で無限スクロールを実現させてみたら意外と簡単だった

# はじめに
皆さん、ごきげんよう！れぶです！

今回の記事では、`ViewPager2`×`FragmentStateAdapter`で無限ループさせる方法をまとめます。

`Fragment`間の無限スクロール動作を実現したい方に特に参考になればと思います。それでは、参りましょう！！

# 開発環境
* MacBook Air
* Android Studio Dolphin | 2021.3.1
* Kotlin
* compileSdkVersion 33
* targetSdkVersion 33
* minSdkVersion 21

# 動作イメージ
今回は以下のように、縦方向のスクロールで3ページ分を無限ループします。

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2456250/078d0348-92f0-0ff5-4a13-aa860ec2b58f.gif)

# 考え方

結論、**実際のページに偽の2ページ分を追加する**やり方です。あくまで**実際の最初or最後のページから移動したかを判定する**ために行います。
![Qiita_Android_ViewPager2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2456250/6a9864bb-3d59-13bb-83e6-ddd3b53054e6.png)

* 実際の最初のページ(`1`)の前に、偽の最後のページ（`FAKE3`）を配置します。これによって、偽の最後のページ（`FAKE3`）に移動したと同時に、実際の最後のページ(`3`)にアニメーションなしで見た目上バレずに移動させます。

* 実際の最後のページ(`3`)の後に、偽の最初のページ（`FAKE1`）を配置します。これによって、偽の最初のページ（`FAKE1`）に移動したと同時に、実際の最初のページ(`1`)にアニメーションなしで見た目上バレずに移動させます。

# 解説
ViewPager2と接続する`Adapter`側と、ViewPager2を表示する`View`側に実装します。

### Adapter側の実装

```ViewPagerAdapter.kt
class ViewPagerAdapter(mainActivity: MainActivity): FragmentStateAdapter(mainActivity) {

    private val realNumPage = 3

    override fun getItemCount(): Int = realNumPage + 2
    fun getRealCount() = realNumPage

    override fun createFragment(position: Int): Fragment {
        return when(position) {
            0,3 -> ThirdFragment()
            1,4 -> FirstFragment()
            2 -> SecondFragment()
            else -> ThirdFragment()
        }
    }
}
```

**ViewPager2で読み込むページ数は、`実際のページ数＋2`に設定します**。
あとは、位置がずれないように`Fragment`を配置すればOKです。

### View側の実装

```MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //ViewPager2の設定
        viewPager2 = findViewById(R.id.viewPager2)
        viewPagerAdapter = ViewPagerAdapter(this)
        viewPager2.apply {
            adapter = viewPagerAdapter
            setCurrentItem(1,false)
            offscreenPageLimit = 1
            registerOnPageChangeCallback(callBack)
        }
    }

private val callBack = object : OnPageChangeCallback() {
        private var realPosition = -1
        override fun onPageScrollStateChanged(state: Int) {
            super.onPageScrollStateChanged(state)
            //最初と最終ページの場合、無限ループになるように該当ページへ移動
            if (state == ViewPager2.SCROLL_STATE_IDLE && realPosition >= 0) {
                viewPager2.setCurrentItem(realPosition, false)
                realPosition = -1
            }
        }
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            //最初と最終ページの場合、実際の位置を変数realPositionに代入
            when (position) {
                0 -> realPosition = viewPagerAdapter.getRealCount()
                viewPagerAdapter.getRealCount() + 1 -> realPosition = 1
            }
        }
    }
```
肝は、**ViewPager2にコールバックを追加し、ページ状況を逐一管理**してあげることです。

`onPageSelected()`で、最初or最後のページが選択された場合に、それぞれ移動する位置を変数に入れます。`onPageScrollStateChanged()`で、最初or最後のページに到達した場合に、その変数の位置にアニメーションなしで見た目上バレずに移動させます。彼らはあくまで偽物で、実際の表示用ではないためです。

ViewPager2の初回読み込み時に、`setCurrentItem()`で1番目の位置に設定することを忘れずに行えば完了です。

# サンプルコード
https://github.com/seiyaleb/ViewPager2InfiniteApp

# 補足
実際の最初or最後のページで動画等の重め処理を行う際は、それらのページが一瞬固まることもあります。その場合は以下二つで回避できます。

* `offscreenPageLimit`で、ViewPager2で一度に読み込むページ数の設定を変更する
* ViewPager2に配置する`Fragment`の順番を変更する

実務で発見しました。以上余談でした。

# おわりに

今回は`ViewPager2`で無限スクロールを実現する方法を整理しました。**実際の最初or最後のページから移動したかを判定するためだけに偽の2ページを両端に追加する**という考え方が理解できれば、容易に実装できるのではないでしょうか。

ユーザのスクロール操作の負担を少しでも軽くするために、この記事が少しでも役立つと嬉しいです。以上です。ありがとうございました！

# 参考サイト
* [Android Infinite scrolling ViewPager2](https://medium.com/@ashishpandey.professional/android-infinite-scrolling-viewpager2-195e3786ec3f)
* [ViewPager2, TabLayoutを用いてタブ付きスワイプビューを作成。そのタブを無限ループさせる。](https://muneochi.hatenablog.com/entry/2021/05/21/003613)
* [[Android] ViewPagerで無限ループ出来るようにした](https://friegen.xyz/android-viewpager-infinitie-loop/)