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

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

## 開発環境
- macOS Mojave 10.14.5
- AndroidStudio 3.4.1

##やりたいこと
- Android でも iOS のような横スワイプをして戻れるようにしたい。

![横スワイプサンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/75eb44b0-5410-092b-34ad-4d1535768598.gif)


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


コード全文

```kotlin: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)
        }
    }

}
```

使用箇所

```kotlin: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 を作成する。

```kotlin
class SwipeRemoveViewPager : ViewPager {
    // 省略
}
```

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

```kotlin
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)` の中で前のページに戻ったかを判定している。

```kotlin
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内に記述しています。

```kotlin
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/f7002c65-6794-0681-fd10-f59ce1a4a6b8.png)

##結果

![Android横スワイプ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/a30c7613-7e26-4c10-6026-896eafe3920d.gif)
全然消えてる感がないのですが、これで前のページが消えています。😅
([ここ](https://github.com/fumiyatani/RemoveFragmentsWithSwipe)にソースコードをあげていますので、そちらも見ていただければと思います。🙇‍♂️)

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

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

### 参考URL
[FragmentStatePagerAdapter](https://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html)
[ViewPager](https://developer.android.com/reference/android/support/v4/view/ViewPager)
[ViewPager.OnPageChangeListener](https://developer.android.com/reference/android/support/v4/view/ViewPager.OnPageChangeListener)