[Android] ViewPager2  無限スクロール

![](https://velog.velcdn.com/images/rmsxo5678/post/399c2b7c-d33a-4b57-8c40-7ce679bbd002/image.gif)
# *無限スクロール*

APIを通じて画像を読み込んで、読み込んだ画像とテキストを無限スクロールできるようにするために、画像とテキストビューの位置がそれぞれ異なるため、アイテムを別々に作成して設定しました。

インジケータは読み込む画像の数に応じて設定される必要があり、TabLayoutMediatorを使用すると無限に増えてしまうため、コード内で実装する必要がありました。また、ボタンを作成して、クリックすると画像が切り替わるようにしました。

コールバックを通じてメイン画像の位置を通知し、テキストとインジケータを設定します。

APIで読み込む画像は数が増える可能性があるため、サンプルでは内部画像でビューモデルやデータクラスなどは削除されており、静的ですが、アイテムの数に応じて動的に変更し、状況に応じて修正する必要があります。

</br>

## *ViewPager2*
MainFragment.kt
```kotlin
private fun setTop() {
    with(binding.viewpagerHomeBanner) {
        adapter = ImageAdapter(choonsik).apply {
            binding.viewpagerHomeBanner.orientation = ViewPager2.ORIENTATION_HORIZONTAL
        }

        val pageWidth = resources.getDimension(R.dimen.dimen_310)
        val pageMargin = resources.getDimension(R.dimen.dimen_15)
        val screenWidth = resources.displayMetrics.widthPixels
        val offset = screenWidth - pageWidth - pageMargin

        binding.viewpagerHomeBanner.offscreenPageLimit = 2

        binding.viewpagerHomeBanner.setPageTransformer { page, position ->
            page.translationX = position * -offset

        }

        // 画像の左右にスライドを表示するための画像の重ね合わせ / 切り取り設定
        val offsetBetweenPages = resources.getDimensionPixelOffset(R.dimen.dimen_30).toFloat()
        binding.viewpagerHomeBanner.setPageTransformer { page, position ->
            val myOffset = position * -(1.40f * offsetBetweenPages)
            if (position < -1) {
                page.translationX = -myOffset
            } else if (position <= 1) {

                val scaleFactor = 1.02f.coerceAtLeast(1 - abs(position))
                page.translationX = myOffset
                page.scaleY = scaleFactor
                page.alpha = scaleFactor
            } else {
                page.alpha = 0f
                page.translationX = myOffset
            }
        }

        setTopText()
        setupOnBoardingIndicators()
        setCurrentBannerIndicator(0)

        // 初期タブ位置
        currentPage = Int.MAX_VALUE / 2 - (Int.MAX_VALUE / 2) % choonsik.size
        binding.viewpagerHomeBanner.setCurrentItem(currentPage, false)

        binding.viewpagerHomeBanner.registerOnPageChangeCallback(object :
            ViewPager2.OnPageChangeCallback() {

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)

                currentPage = position
                setCurrentBannerIndicator(position)
                binding.textView.setCurrentItem(position, false)

                if (position == binding.viewpagerHomeBanner.adapter!!.itemCount - 1) {
                    binding.viewpagerHomeBanner.setCurrentItem(0, false)
                    setCurrentBannerIndicator(position)
                }
            }
        })
    }
}
  ``` 
  
* ```registerOnPageChangeCallback``` を通じてページ選択の変更を処理し、新しいページが選択されるたびに現在のページを更新し、現在の画像表示を設定し、アイテムを変更します。
* ``` Text_ViewPagerc2```  では、画像とテキストが合う必要があるため、位置を設定し、 ``` Indicator```  部分も関数で位置を調整して範囲内で繰り返すようにできます。


## Indicator 
MainFragment.kt

  ```kotlin
    private fun setupOnBoardingIndicators() {

        binding.indicators2.removeAllViews()

        // サンプルは内部で指定して行いましたが、データを受け取るときはアイテムの数を受け取って行います。
        val indicators = arrayOfNulls<ImageView>(6)

        val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
            ViewGroup.LayoutParams.WRAP_CONTENT)

        layoutParams.leftMargin = 30

        for (i in indicators.indices) {
            indicators[i] = ImageView(requireActivity().applicationContext)
            indicators[i]?.setImageDrawable(ContextCompat.getDrawable(
                requireActivity().applicationContext,
                R.drawable.arrow_indicator_inactivie
            ))

            indicators[i]?.layoutParams = layoutParams

            binding.indicators2.addView(indicators[i])
        }
    }

    private fun setCurrentBannerIndicator(index: Int) {

        val childCount = binding.indicators2.childCount
        val position = index % childCount
        for (i in 0 until childCount) {
            val imageView = binding.indicators2.getChildAt(i) as ImageView
            if (i == position) {
                imageView.setImageDrawable(activity?.let {
                    ContextCompat.getDrawable(requireActivity().applicationContext,
                        R.drawable.arrow_indicator_active)
                })
            } else {
                imageView.setImageDrawable(activity?.let {
                    ContextCompat.getDrawable(requireActivity().applicationContext,
                        R.drawable.arrow_indicator_inactivie)
                })
            }
        }
    }
  ``` 

* setupOnBoardingIndicators()  :
まず ``` binding.indicators2.removeAllViews()```  はViewModelからデータの数を受け取り、画像を読み込む際にそのビューを更新するときに数が増える現象が発生していたため、元のビューを削除する必要がありました。


* setCurrentBannerIndicator() : 
```ViewPager2```の現在の位置である```index```を引数に受け取り、
LinearLayoutの子の数を取得し、モジュール演算子を使用して位置を計算し、位置が常に子ビューの範囲内にあるようにし、forループでLinearLayoutのすべての子を繰り返し処理します。
ImageViewに対して現在の```index를(i)```が提供された位置と同じかどうかを確認し、表示設定を行います。


## Adapter
RecyclerView.Adapter

  ```kotlin
    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(items[position % items.size])
    }

    override fun getItemCount(): Int {
        return Int.MAX_VALUE
    }
  ```
* アダプターでアイテム数をサイズとして取得するのではなく、 ```MAX_VALUE```を使用して幻想を与えることができます。必ずしも```MAX_VALUE``` でなくても、適切な数値を記載しても問題ありません。

---

GitHub : https://github.com/GEUN-TAE-KIM/InfiniteViewpager2_Indicator_Sample.git

</br>

参考
https://furang-note.tistory.com/25