ViewPager2 + TabLayout + DataBinding

# はじめに
[DMMグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/dmm)3日目を担当します！@mii-chang です！

2018年新卒入社でDMMにJOINし、今は **AQUIZ** というクイズアプリのAndroidアプリを開発しています:muscle::fire:
https://aquiz.jp/

クイズに答えてお金がもらえる！:moneybag:面白いアプリなのでぜひ皆さん遊んでくださいね〜〜！:raising_hand:🥰


さて、今回はJetpackに新しく追加された`ViewPager2`について書いていきたいと思います！:rocket::rocket::rocket:


# ViewPager2 とは
従来の`ViewPager`の進化版の`ViewPager2`。
今までは、各Fragmentを`PagerAdapter`を使って表示切り替えをしていました。
`ViewPager2`では、`PagerAdapter`の代わりに`RecyclerView.Adapter`を使います。
[リファレンス](https://developer.android.com/jetpack/androidx/releases/viewpager2)


## 何がいいの？
今までは、`PagerAdapter`に切り替えたいFragmentをそれぞれセットしていましたが、
新しい`ViewPager2`では、画面切り替えをRecyclerViewベースでやってくれるので、Viewのレイアウトを作っておけばそれを再利用してくれます。
要するに、Fragmentが1つで良くなったのです:tada:
また、RecyclerViewベースなので、RecyclerViewの知識があれば扱いやすいのも魅力ですね:sunny:

# 使ってみよう
今回は、Databindingを一緒に使って、簡単なサンプルを作ってみます。

## 導入
app配下の`build.gradle`に以下を追加します

```build.gradle
implementation "androidx.viewpager2:viewpager2:1.0.0-beta04"
```
バージョン情報は[公式リファレンス](https://developer.android.com/jetpack/androidx/releases/viewpager2)を見てください！:eyes:


## 実装
サンプルとして、クリスマスアイテムの画像と名前のリストを、ViewPagar2を使って表示してみます:christmas_tree::santa:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/dc4b27fb-9471-c2c2-0a75-b7d6b5167cc5.gif" width=50%>


### レイアウト

今回は、ViewPagerと、TabLayoutを使って、インジケーター付きのレイアウトを作ります

```fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewpager"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:tabGravity="center"
            app:tabIndicatorColor="@color/colorAccent"
            app:tabIndicatorGravity="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

#### TabLayout
[TabLayoutのリファレンス](https://developer.android.com/reference/android/support/design/widget/TabLayout.html)

##### tabGravity
`tabGravity` は、インジケーター全体の横幅を決められます。

|center|fill|
|---|---|
|![center](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/b6ec1e5a-64e7-a027-01b1-565f560ad905.png)|![fill](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/e1dc062c-ca36-d63f-2343-d8aa8b8462ce.png)|
|真ん中寄せになる|横幅いっぱいにインジケーターが広がる|


##### tabIndicatorColor
`tabIndicatorColor` は、その名の通り、インジケーターの色を設定できます。

##### tabIndicatorGravity
`tabIndicatorGravity`は、TabLayout内でのインジケーターの位置を指定できます。(View全体の背景が緑、インジケーターの色がピンク)

|top|stretch|
|---|---|
|![device-2019-12-02-204335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/57f81971-57b7-6bea-5328-7c1412214591.png)|![device-2019-12-02-204356.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/9761e28f-05b7-2e30-3831-594c66918b24.png)|

|center|bottom|
|---|---|
|![device-2019-12-02-204433.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/e9127bf4-bfe8-d861-75c5-909182dbf3fb.png)|![device-2019-12-02-204634.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/5ce61302-e16b-7950-91f3-94352bf99586.png)|

---

ViewPater2にセットするアイテムのレイアウトを作ります。
今回はサンプルとして、文字と画像をDataBindingを使って表示させてみます。


```view_christmas.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="christmas"
            type="com.miichang.viewpagersample.Christmas" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{christmas.title}"
            android:textColor="#000"
            android:textSize="50sp"
            app:layout_constraintBottom_toTopOf="@id/imageView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="ケーキ" />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginTop="20dp"
            android:scaleType="fitCenter"
            android:src="@{christmas.drawable}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textView"
            tools:src="@drawable/cake" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

#### BindingAdapter
今回は、DrawableをDataBindingで受け渡す想定で、DrawableをImageViewにセットするためのBindingAdapterを作りました。

```ChristmasBindingAdapter.kt
internal object ChristmasBindingAdapter {
    @JvmStatic
    @BindingAdapter("android:src")
    fun setDrawable(
        imageView: ImageView?,
        drawable: Drawable?
    ) {
        if (imageView == null) return
        drawable?.let {
            imageView.setImageDrawable(drawable)
        }
    }
}
```


### バインドするデータクラス
今回は、`Christmas`データクラスを作って、この内容をバインドさせます

```Christmas.kt
data class Christmas(
    val title: String,
    val drawable: Drawable?
)
```

### ViewHolder

レイアウトに定義したデータタグに、Christmasデータクラスの内容を渡すために、`RecyclerView.ViewHolder`を継承したViewHolderクラスを作ります。

```ItemViewHolder.kt
internal class ItemViewHolder(
    itemView: View,
    private val binding: ViewChristmasBinding
) : RecyclerView.ViewHolder(itemView) {
    companion object {
        fun create(
            inflater: LayoutInflater,
            container: ViewGroup,
            attachToRoot: Boolean
        ): ItemViewHolder {
            val binding = ViewChristmasBinding.inflate(inflater, container, attachToRoot)
            return ItemViewHolder(binding.root, binding)
        }
    }

    fun bind(item: Christmas) {
        binding.apply {
            christmas = item
            executePendingBindings()
        }
    }
}
```

### Adapter

表示させたいデータクラスのリストをViewHolderに受け渡すために、`RecyclerView.Adapter`を継承したアダプタークラスを作ります。


```ViewPagerAdapter.kt
internal class ViewPagerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
    private var list: List<Christmas> = listOf()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ItemViewHolder.create(inflater, parent, false)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(list[position])
    }

    fun setItem(list: List<Christmas>) {
        this.list = list
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int = list.size
}
```

`onBindViewHolder`で、リストの1つ1つを受け渡します。

### Fragment
Frafmentでアダプターを初期化します
今回はサンプルなので、Fragmentでadapterに直接文字とDrawableリソースを入れたリストを突っ込みました。

```MainFragment.kt
class MainFragment : Fragment() {
    companion object {
        fun newInstance(): MainFragment = MainFragment()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val christmasItems = listOf(
            Christmas(
                title = "サンタ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.santa)
            ),
            Christmas(
                title = "ツリー",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.tree)
            ),
            Christmas(
                title = "トナカイ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.reindeer)
            ),
            Christmas(
                title = "プレゼント",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.presents)
            ),
            Christmas(
                title = "ケーキ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.cake)
            ),
            Christmas(
                title = "チキン",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.chicken)
            )
        )

        val binding = FragmentMainBinding.inflate(
            inflater,
            container,
            false
        )

        val adapter = ViewPagerAdapter()

        binding.apply {
            binding.viewpager.adapter = adapter
            adapter.setItem(christmasItems)
            TabLayoutMediator(
                tabLayout,
                viewpager,
                TabLayoutMediator.TabConfigurationStrategy { tab, position -> }
            ).attach()
        }

        return binding.root
    }
}
```

#### TabLayoutMediator
`TabLayoutMediator`にTabLaoutとViewPager2を渡してあげることで、ViewPagerのスクロールとTabLayoutのインジケーターがリンクします

```kotlin
TabLayoutMediator(
                tabLayout,
                viewpager,
                TabLayoutMediator.TabConfigurationStrategy { tab, position -> }
            ).attach()
```

[リファレンス](https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator)


これでViewPager2が使えます！RecyclerViewなので結構スムーズですね:bulb:


## ViewPager2でできること
### 縦スクロール 
ViewPager2はRecyclerViewなので、なんと、**ViewPager2自体にorientationを設定するだけ**で超かんたんに縦方向のスクロールに変更することもできます！

```xml
android:orientation="vertical"
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/484bf1aa-de54-42ef-7134-41f480404bba.gif" width=50%>


### 画面切り替えアニメーション
ViewPager2に対して、`setPageTransformer`を呼ぶと、画面切り替え時のアニメーションを設定できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93899/0fa00a97-8fb8-cb5d-954f-66ce8e8c4fb0.gif" width=50%>

今回は[リファレンス](https://developer.android.com/training/animation/screen-slide-2#zoom-out)に載っている`ZoomOutPageTransformer`アニメーションをそのまま実装してみました。

```kotlin
binding.viewpager.setPageTransformer(ZoomOutPageTransformer())
```


スワイプ時のアニメーションがこんなにリッチになるとかなりテンションが上りますね🥰

### リスナー
ViewPager2に対して`registerOnPageChangeCallback`を呼ぶと、リスナーが拾えます

```kotlin
 binding.viewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                super.onPageScrollStateChanged(state)
            }

            override fun onPageScrolled(
                position: Int,
                positionOffset: Float,
                positionOffsetPixels: Int
            ) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels)
            }

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

#### onPageScrollStateChanged
`onPageScrollStateChanged`は、スクロール状態の変更が取得できます。

|SCROLL_STATE_IDLE|SCROLL_STATE_DRAGGING|SCROLL_STATE_SETTLING|
|---|---|---|
|何もしていないとき|ドラッグ開始時|指が離れたとき|

#### onPageScrolled
`onPageScrolled` は、スクロールされているときに、画面がどれくらい動いているかを取得できます。

|position|positionOffset|positionOffsetPixels|
|---|---|---|
|ページのインデックス番号|スクロールで動いた距離|スクロールで動いた距離のピクセル数|

#### onPageSelected
`onPageSelected`は、今表示されている画面のインデックス番号を取得できます。


# おわりに
今回は`ViewPager2`について調べる機会があったので、まとめてみました！
まだQiita上にも情報が少なかったので、どなたかの役に立てれば幸いです:christmas_tree:

サンプルのリポジトリをGitHubに置いておいたので全体が見たい場合はこちらをどうぞ！
:point_down::point_down::point_down:
https://github.com/mii-chang/ViewPager2Sample
:point_up_2::point_up_2::point_up_2:

[DMMグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/dmm)明日の担当は、 @karayok さんです！:santa:



#参考
https://developer.android.com/jetpack/androidx/releases/viewpager2
https://medium.com/google-developer-experts/exploring-the-view-pager-2-86dbce06ff71
https://proandroiddev.com/look-deep-into-viewpager2-13eb8e06e419
https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator