Edited at

Android の ViewPager で無限スクロールを実現する

以前、iOS の UIScrollView で、ページつき無限スクロールを実現する という記事を書いて、Android ではどうするのかな、と思いました。少し調べてみたところ、Looping/Infinite ViewPager という記事がドンピシャでした。英語記事ということと、データの更新については触れられていない(少しユースケースが異なる)、ということでここにまとめてみます。


アイデア

ViewPager で、5ページ分のスペースを用意しておきます。

[0][1][2][3][4]


  1. 最初は1ページ目から始めます。

  2. 0ページ目にたどり着いたら、3ページ目に飛びます。

  3. 4ページ目にたどり着いたら、1ページ目に飛びます。

こうすることで、5ページ分のスペースの端っこに落ち着くことはないので、常に左右にスクロールできる状態になります。

この時、データの更新についても考えなくてはいけません。上記のステップ 2. で 0ページ目から 3ページ目に飛んだ時、現在のデータから後ろ三つを削除し、前三つを補填します。

Rewind.png

同様にステップ 3. で 4ページ目から 1ページ目に飛んだ時、現在のデータから前三つを削除し、後ろ三つを補填します。

Forward.png


コード


PagerAdapter サブクラスの実装

コールバックの実装はいたって普通です。ページ総数は 5 なので、getCount() では 5 を返しています。

    private var dataList = ... // データリスト

...

override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view == `object`
}

override fun getCount(): Int {
return 5
}

override fun instantiateItem(container: ViewGroup, position: Int): View {
val view = ... // Layout から inflate

val dataItem = dataList.get(position)

// set dataItem to the view
view.setData(data.get(position))

container.addView(view)

return view
}

override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}

次にデータの更新のために次のカスタムメソッドを用意しておきます。

    fun initializeData() {

dataList.clear()

for (i in 0 until 5) {
dataList.add(...) // 初期データの設定
}
}

fun forwardData() {
for (i in 0 until 3) {
imageList.removeAt(0)
}

for (i in 0 until 3) {
imageList.add(...) // 後データの補填
}
}

fun rewindData() {
for (i in 0 until 3) {
imageList.removeAt(5 - i - 1)
}

for (i in 0 until 3) {
imageList.add(0, ...) // 前データの補填
}
}


Activity (もしくは Fragment) 側の実装

    // adapter は PagerAdapter サブクラスのインスタンス

viewPager.adapter = adapter
// listener は ViewPager.OnPageChangeListener サブクラスのインスタンス。詳細は後述。
viewPager.addOnPageChangeListener(listener)
// 最初は 1ページ目から始める。
viewPager.setCurrentItem(1, false)

listener は ViewPager.OnPageChangeListener サブクラスのインスタンスで、この実装の肝です。ページが変わったというイベントをキャッチして、必要に応じて別のページに飛ぶという実装をここで行います。

    val listener: ViewPager.OnPageChangeListener = object: ViewPager.OnPageChangeListener {

private var jumpPosition = -1;

override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
// 何もしない
}

override fun onPageSelected(position: Int) {
if (position == 0) {
jumpPosition = 3

adapter.rewindData() // 先ほど用意した、前データの補填 method
} else if (position == 4) {
jumpPosition = 1;

adapter.forwardData() // 先ほど用意した、後データの補填 method
}
}

override fun onPageScrollStateChanged(state: Int) {
//アニメーションが終わるのを待ってから飛ぶ。
// onPageSelected(int position) でやると、スクロールアニメーションがキャンセルされてしまう。
if (state == ViewPager.SCROLL_STATE_IDLE && jumpPosition >= 0) {
view!!.viewPager.setCurrentItem(jumpPosition, false);
jumpPosition = -1;
}
}
}


結果

このように、永遠にスクロールし続ける画面を作れます。

infinitescroll.gif


まとめと考察

iOS で UIScrollView を使った時は、すべてのスクロールイベントを捉えてページをアジャストすることができたので、用意するページは 3ページで済みました。が、今回 ViewPager では、捉えられるのはページにスナップした時のイベントのみで、スクロール途中のイベントを受け取れないため、5ページが必要。iOS でも UIPageViewController なんかを使うと、まったく同じような発想で 5ページが必要になるんだと思います。

記事の内容を含んだソースコードはこちらです。