ViewPager2とTabLayoutでページコントロール&ページ切り替えを簡単に用意

- スマートフォンアプリを使っているとよく見かけるページコントロールを簡単に作ろうという話です。
    - 簡単にという事で、できればPngやJpegといった、用意するのが手間になる画像リソースも省きたいところ。

### ページコントロールってなに？
- よく見かけるこれです↓

|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/495289/0764e72f-5c23-e217-ec8c-504b563f0606.png" width=50% border="1">|
|:-:|

#### このコンポーネントって・・・
- iOSのHuman Interface Guidelines に定義されている[コンポーネント](https://developer.apple.com/design/human-interface-guidelines/ios/controls/page-controls/)なので、Androidにはありません:expressionless:
- チュートリアルなんかでよく見かけ、**iOSと同じデザインでヨロシク**と言われた人もいるんじゃないかと:frowning2:

---

## 用意していくよ

### 構成
- MainFragment (全ページを管理する親Fragment)
- Page1Fragment (1ページ目のFragment)
- Page2Fragment (2ページ目のFragment)
- Page3Fragment (3ページ目のFragment)
- Page4Fragment (4ページ目のFragment)


全ページをまとめる親ページとして`MainFragment`と、子ページとして`Page1〜4Fragment`を用意

### MainFragment

#### Layout

- 子ページを管理するための`ViewPager2`と、ページコントロール用の`TabLayout`を用意。
    - `ViewPager2` → `androidx.viewpager2.widget.ViewPager2`
    - `TabLayout ` → `com.google.android.material.tabs.TabLayout`
    - 必要に応じて[AndroidX](https://developer.android.com/jetpack/androidx/releases/viewpager2)や[Material Components](com.google.android.material.floatingactionbutton.floatingactionbutton)を取り込みましょう
- 最下部に次のページに遷移させる為の`Button`を用意。

``` fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color_white">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@+id/frameLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toTopOf="@+id/nextLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/indicator"
            android:layout_height="40dp"
            android:layout_width="wrap_content"
            android:layout_gravity="center_horizontal"
            app:tabBackground="@drawable/indicator_selector"
            app:tabGravity="center"
            app:tabIndicatorHeight="0dp"
            app:tabIndicatorFullWidth="true"
            app:tabRippleColor="@null"/>

    </FrameLayout>

    <Button
        android:id="@+id/nextLayout"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/button_color"
        android:stateListAnimator="@null"
        android:text="@string/next_button_text"
        android:textColor="@color/color_white"
        android:textSize="15sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

##### ポイント:rolling_eyes:
- `TabLayout` の `app:tabBackground="@drawable/indicator_selector"`は、選択中、非選択中を意識しておく

``` indicator_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape android:innerRadius="0dp" android:shape="ring" android:thickness="4dp" android:useLevel="false">
            <solid android:color="@color/blue" />
        </shape>
    </item>
    <item>
        <shape android:innerRadius="0dp" android:shape="ring" android:thickness="4dp" android:useLevel="false">
            <solid android:color="@color/gray" />
        </shape>
    </item>
</selector>
```

- TabLayoutの`app:tabRippleColor="@null"`は、ページコントロールの選択時のRippleEffectの設定
    - 不要であれば`@null`で表示しない設定にしておく

#### Code（必要そうな箇所を抜粋）

``` MainFragment.kt
class MainFragment : Fragment() {
    
    private sealed class IndexItem {
        abstract fun newInstance(): Fragment

        object FirstItem : IndexItem() {
            override fun newInstance() = Page1Fragment.newInstance()
        }
        object SecondItem : IndexItem() {
            override fun newInstance() = Page2Fragment.newInstance()
        }
        object ThirdItem : IndexItem() {
            override fun newInstance() = Page3Fragment.newInstance()
        }
        object FourthItem : IndexItem() {
            override fun newInstance() = Page4Fragment.newInstance()
        }
    }

    private val indexItems = listOf(
            IndexItem.FirstItem,
            IndexItem.SecondItem,
            IndexItem.ThirdItem,
            IndexItem.FourthItem)
    
    // ~~~ 省略

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        setupViewItem()
    }
    
    private fun setupViewItem() {

        viewPager.adapter = object : FragmentStateAdapter(this) {
            override fun getItemCount(): Int = indexItems.size
            override fun createFragment(position: Int): Fragment {
                return indexItems[position].newInstance()
            }
        }
        viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
        TabLayoutMediator(indicator, viewPager) { _, _ -> }.attach()
    }
}
```

##### ポイント:rolling_eyes:
- `sealed class`を利用し、子ページの処理を統一
- `ViewPager`の`adapter`は`androidx.viewpager2.adapter.FragmentStateAdapter`を利用
- [TabLayoutMediator](https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator)は`TabLayout`と`ViewPager2`の動きをリンクさせるためのクラス
    - タブのテキストを変更する為にも利用するが、今回は利用しないのでアタッチのみ実施

### Page 1~N Fragment
- 好きなページ数でつくってください
- 内容もすきなままにどうぞ:relaxed:

## おわり
- いかがでしたでしょうか。`ViewPager2`のおかげで少ないコード数で対応可能になりました。
- iOSのコンポーネントにあわせて、Androidをつくるのは:disappointed_relieved:感ありますががんばっていきましょう:grinning:

これからも楽しいアプリケーション開発を:dancers: