Android
ViewPager
android開発
infinitescroll

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ページが必要になるんだと思います。

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