LoginSignup
1
1

More than 3 years have passed since last update.

iOS の横スワイプ操作での「戻る」を Android でも実装してみた。

Posted at

はじめに

今回自分が実装したものはとりあえず実現したくらいのレベルなので、こうやったんだ位の参考程度で見ていただければと思います。🙇‍♂️

開発環境

  • macOS Mojave 10.14.5
  • AndroidStudio 3.4.1

やりたいこと

  • Android でも iOS のような横スワイプをして戻れるようにしたい。

横スワイプサンプル.gif

実装手順

大まかに以下のようになっています。
1. ViewPager を継承した、カスタム ViewPager を作成する。
2. カスタム ViewPager 内に OnPageChangeListener を実装したインナークラスを作成する。
3. onPageStateChanged(state: Int) で現在のページから一つ前のページに戻ったのかを判定し、一つ前のページに戻っていた場合は今までいたページを削除する。

コード全文

SwipeRemoveViewPager.kt
class SwipeRemoveViewPager(
    context: Context,
    attrs: AttributeSet?) 
    : ViewPager(context, attrs) {

    companion object {
        fun createSwipeRemoveViewPagerAdapter(fm: FragmentManager, viewPager: ViewPager): SwipeRemoveViewPagerAdapter {
            return SwipeRemoveViewPagerAdapter(fm, viewPager ,ArrayList(0))
        }
    }

    init {
        overScrollMode = View.OVER_SCROLL_NEVER
        addOnPageChangeListener(SwipePageChangeListener())
    }

    inner class SwipePageChangeListener: OnPageChangeListener {

        private var currentPage = 0
        private var newPage = 0

        override fun onPageScrollStateChanged(state: Int) {
            when(state) {
                SCROLL_STATE_IDLE -> {
                    val pagePositionDeffer = newPage - currentPage
                    if (pagePositionDeffer < 0) {
                        if (adapter is SwipeRemoveViewPagerAdapter) {
                            (adapter as SwipeRemoveViewPagerAdapter).removeFragment(currentPage)
                        }
                    }
                    currentPage = newPage
                }
            }
        }
        override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}

        override fun onPageSelected(position: Int) {
            newPage = position
        }
    }

    class SwipeRemoveViewPagerAdapter(
        fragmentManager: FragmentManager,
        private val viewPager: ViewPager,
        private var mFragments: ArrayList<Fragment>)
        : FragmentStatePagerAdapter(fragmentManager) {

        override fun getItem(position: Int): Fragment {
            return mFragments[position]
        }

        override fun getCount(): Int {
            return mFragments.size
        }

        override fun getItemPosition(`object`: Any): Int {
            val target = `object` as Fragment
            return if (target in mFragments) {
                PagerAdapter.POSITION_UNCHANGED
            } else {
                PagerAdapter.POSITION_NONE
            }
        }

        fun addFragment(fragment: Fragment) {
            mFragments.add(fragment)
            notifyDataSetChanged()
            if (viewPager.currentItem < count - 1) {
                // 表示中のページから、次のページを遷移させる
                viewPager.setCurrentItem(viewPager.currentItem + 1, true)
            }
        }

        private var removePosition = 0

        private var runnable = Runnable {
            mFragments.removeAt(removePosition)
            notifyDataSetChanged()
        }

        fun removeFragment(position: Int) {
            removePosition = position
            // 以下のメソッドだけにした際に食い気味でスワイプ操作をすると Fragment already add というエラーで死んでしまうので、postDelayedを行うことによって回避しています。
            // ここで微妙にタイミングをずらしてあげることによって、Fragment already add のエラーを回避している
            Handler().postDelayed(runnable, 100)
        }
    }

}

使用箇所

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var adapter: SwipeRemoveViewPager.SwipeRemoveViewPagerAdapter

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

        // Adapterを生成
        adapter = SwipeRemoveViewPager.createSwipeRemoveViewPagerAdapter(supportFragmentManager, swipeDeleteViewPager)

        swipeDeleteViewPager.adapter = adapter

        // 表示したいFragmentを追加
        adapter.addFragment(Fragment1())

        fab.setOnClickListener {
            addFragment()
        }
    }

    private fun addFragment() {
        if (adapter.count % 2 != 0) {
            adapter.addFragment(Fragment2())
        } else {
            adapter.addFragment(Fragment1())
        }
    }
}

1. ViewPager を継承した、カスタム ViewPager を作成する。

class SwipeRemoveViewPager : ViewPager {
    // 省略
}

2. カスタム ViewPager 内に OnPageChangeListener を実装したインナークラスを作成する。

inner class SwipePageChangeListener: OnPageChangeListener {

    override fun onPageScrollStateChanged(state: Int) {
        // ここで前のページに戻ったかを判定する
    }
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {

    }

    override fun onPageSelected(position: Int) {
        // ここで新しいページを取得する
    }

}

3. 現在のページから一つ前のページに戻ったのかを判定し、一つ前のページに戻っていた場合は今までいたページを削除する。

2.で作成した SwipePageChangeListener 内の onPageScrollStateChanged(state: Int) の中で前のページに戻ったかを判定している。

inner class SwipePageChangeListener: OnPageChangeListener {

    private var currentPage = 0
    private var newPage = 0

    // 省略
    override fun onPageScrollStateChanged(state: Int) {
        when(state) {
            SCROLL_STATE_IDLE -> {
                val pagePositionDeffer = newPage - currentPage
                if (pagePositionDeffer < 0) {
                    if (adapter is SwipeRemoveViewPagerAdapter) {
                        (adapter as SwipeRemoveViewPagerAdapter).removeFragment(currentPage)
                    }
                }
                currentPage = newPage
            }
        }
    }

    override fun onPageSelected(position: Int) {
        newPage = position
    }
}

onPageSelected(position: Int) が呼ばれた際にどのページに遷移したかを格納して、 onPageScrollStateChanged(state) の SCROLL_STATE_IDLE が呼ばれたタイミング(=完全に前のページに遷移したタイミング)で今まで表示されていたページを削除している。

実際の削除する処理はAdapter内に記述しています。

class SwipeRemoveViewPagerAdapter(
    fragmentManager: FragmentManager,
    private val viewPager: ViewPager,
    private var mFragments: ArrayList<Fragment>)
    : FragmentStatePagerAdapter(fragmentManager) {

    // 省略

    override fun getItemPosition(`object`: Any): Int {
        val target = `object` as Fragment
        return if (target in mFragments) {
            PagerAdapter.POSITION_UNCHANGED
        } else {
            PagerAdapter.POSITION_NONE
        }
    }

    private var removePosition = 0

    private var runnable = Runnable {
        mFragments.removeAt(removePosition)
        notifyDataSetChanged()
    }

    fun removeFragment(position: Int) {
        removePosition = position
        // 以下のメソッドだけにした際に食い気味でスワイプ操作をすると Fragment already add というエラーで死んでしまうので、postDelayedを行うことによって回避しています。
        // ここで微妙にタイミングをずらしてあげることによって、Fragment already add のエラーを回避している
        Handler().postDelayed(runnable, 100)
    }
}

removeFragment() が呼ばれると SwipeRemoveViewPagerAdapter 内の リストの中身から、該当の Fragment を取り除きます。
その際に notifyDataSetChanged() を呼び、 getItemPosition(object: Any) で変更のあった Fragment に対して、 PagerAdapter.POSITION_NONE を返してあげることによって ViewPager 内の Fragment を更新します。

イメージとしては以下の画像です。
RemovePage.png

結果

Android横スワイプ.gif
全然消えてる感がないのですが、これで前のページが消えています。😅
(ここにソースコードをあげていますので、そちらも見ていただければと思います。🙇‍♂️)

終わりに

まだまだ ViewPager の内部的な処理を追っかけきれていないなぁと思いました。。
ただ、前から作りたかったものが作れたのでよかったです😇

見ていただきありがとうございます!

参考URL

FragmentStatePagerAdapter
ViewPager
ViewPager.OnPageChangeListener

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1