【Kotlin】TabLayout + ViewPager2とFloatingActionButtonでモダンなボトムタブを作成する

## 作りたかったもの
これが今回の完成形になります。
個人的にはかなり満足な画面になりました。
<img width="200" alt="complete.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2706893/101b1ddc-daa9-1675-5583-35d8c5fb830b.gif">

#### ソースコード
https://github.com/arutoria000/arutoria_apps/tree/feature/step1_custom_tablayout_add

# 前提条件
## 環境
・Android Studio Dolphin | 2021.3.1
・Kotlin : 1.7.20

## ライブラリ
・androidx.viewbinding.ViewBinding
・androidx.constraintlayout.widget.ConstraintLayout
・androidx.viewpager2.widget.ViewPager2
・com.google.android.material.tabs.TabLayout
・com.google.android.material.floatingactionbutton.FloatingActionButton

ViewBindingを使用するために以下を追加します。
```gradle:build.gradle
    viewBinding {
        enabled = true
    }
```
# 実装説明
## TabLayout + ViewPager2を表示する
```xml:activity_top.xml
<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">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/top_tablayout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/tablayout_height"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/top_viewpager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toTopOf="@+id/top_tablayout"
        />
</androidx.constraintlayout.widget.ConstraintLayout>
```

```Kotlin:TopViewPagerAdapter.kt
/**
 * Top画面のViewPagerAdapter.
 */
class TopViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle):
    FragmentStateAdapter(fragmentManager, lifecycle) {

    override fun getItemCount(): Int {
        // TabLayoutのタブ数
        return TabLayoutResource.values().size
    }

    override fun createFragment(position: Int): Fragment {
        // TabLayoutに表示するFragmentを返却
        return TabLayoutResource.values()[position].getFragment()
    }
}
```
```Kotlin:TopActivity.kt
/**
 * トップ画面.
 */
class TopActivity: AppCompatActivity() {

    /**
     * Viewバインディング.
     */
    private lateinit var mBinding: ActivityTopBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // View設定
        mBinding = ActivityTopBinding.inflate(layoutInflater)
        val view = mBinding.root
        setContentView(view)

        val viewPager = mBinding.topViewpager
        val tabLayout = mBinding.topTablayout

        // ViewPagerにAdapterを設定
        viewPager.adapter = TopViewPagerAdapter(supportFragmentManager, lifecycle)

        // TabLayoutとViewPagerの紐づけ
        TabLayoutMediator(tabLayout, viewPager) { tab, position ->
            // タブアイコンとテキストの設定
            tab.icon = TabLayoutResource.values()[position].getTabIcon(this)
            tab.text = TabLayoutResource.values()[position].getTabTitle(this)
        }.attach()
    }
}
```
FragmentStateAdapterを継承したTopViewPagerAdapterを作成しています。
TopViewPagerAdapterではタブ数とタブに表示するFragmentを設定しています。
TabLayoutに設定する画像や文言とFragmentはTabLayoutResourceというEnumクラスを作成することで管理するようにしました。
TabLayoutResourceクラスは[こちら](https://github.com/arutoria000/arutoria_apps/blob/feature/step1_custom_tablayout_add/WebViewBookmarker/app/src/main/java/com/example/webviewbookmarker/enum/TabLayoutResource.kt)をご参照ください。

実際のTabLayoutとViewPagerの紐づけはTabLayoutMediatorを使用しています。
紐づけを行うことで画面のスワイプでもタブの切り替えが可能となります。

## TabLayoutのカスタマイズ
```xml:activity_top.xml
    <com.google.android.material.tabs.TabLayout
        android:id="@+id/top_tablayout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/tablayout_height"
        android:background="@drawable/item_tablayout_background"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:tabTextColor="@color/item_select_tab_color"
        app:tabTextAppearance="@style/TabLayoutText"
        app:tabIndicator="@drawable/item_indicator"
        app:tabIndicatorColor="@color/tab_indicator_color"
        app:tabIndicatorGravity="stretch"
        app:tabIconTint="@color/item_select_tab_color"
        />
```

```xml:item_tablayout_background.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <color android:color="@color/tab_base_color"/>
    </item>
    <item android:bottom="6dp" android:left="6dp" android:right="6dp" android:top="6dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/tab_innar_color" />
            <corners android:radius="10dp" />
        </shape>
        <color android:color="@color/tab_base_color"/>
    </item>
</layer-list>
```

android:backgroundでタブ全体の背景色を設定します。
\<layer-list\>で2色(黒,灰色)の背景を重ねて表示しています。
灰色の背景色には外周を6dp空けて丸角表示にすることで、
インジケータがスライドできる範囲を分かりやすく表現しています。


```xml:item_select_tab_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/tab_base_color" android:state_selected="true"/>
    <item android:color="@color/tab_indicator_color" android:state_selected="false"/>
</selector>
```

app:tabTextColorでタブに表示している文字の色を設定しています。
タブ選択時は白色のインジケータが被さるため文字を黒色にし、
タブ非選択時は背景が灰色のため文字を白色にします。

app:tabIconTintも同様でタブに表示している画像の色を設定しています。
同じくタブ選択時は画像を黒色にし、タブ非選択時は画像を白色にします。

```xml:theme.xml
    <style name="TabLayoutText" parent="TextAppearance.Design.Tab">
        <item name="android:textSize">10dp</item>
        <item name="android:textColor">?attr/colorPrimaryVariant</item>
    </style>
```

app:tabTextAppearanceで文字の大きさを変更しています。
TabLayoutの文字の大きさはtextSizeが使えないためThemeから変更するようにしています。

```xml:item_indicator.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:bottom="8dp" android:left="8dp" android:right="8dpg" android:top="8dp">
        <shape android:shape="rectangle">
            <corners android:radius="10dp" />
        </shape>
    </item>
</layer-list>
```

app:tabIndicatorでインジケータを丸角にしています。
またインジケータの周囲に8dpの余白を設定することで、灰色の背景色の中でスライドするようにしています。

app:tabIndicatorColorでインジケータを白色にしています。

app:tabIndicatorGravityでインジケータを表示する位置を設定しています。
"stretch"を設定することでインジケータを塗りつぶしすることができます。

## FloatingActionButtonの実装
```xml:activity_top.xml
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/top_floating_action_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginEnd="16dp"
        android:src="@drawable/search_white"
        app:tint="@color/fab_icon_color"
        app:backgroundTint="@color/tab_base_color"
        app:fabSize="auto"
        app:layout_constraintBottom_toBottomOf="@id/top_viewpager"
        app:layout_constraintRight_toRightOf="parent"
        />
```
ここからTabLayoutと連動して切り替わるFloatingActionButtonを実装します。
まずはActivityのレイアウトにFloatingActionButtonを配置します。

```Kotlin:TopActivity.kt
        // タブ選択リスナー
        tabLayout.addOnTabSelectedListener(object: OnTabSelectedListener {
            override fun onTabSelected(tab: Tab?) {
                if (floatingActionButton.visibility == GONE) {
                    // フローティングアクションボタンに画像を設定する
                    floatingActionButton.setImageDrawable(
                        FloatingActionButtonResource.values()[tab!!.position].getIconDrawable(baseContext))
                    // フローティングアクションボタンを表示する
                    floatingActionButton.show()
                } else {
                    // 他タブ選択時にフローティングアクションボタンを非表示にする
                    floatingActionButton.hide()
                }
            }

            override fun onTabUnselected(tab: Tab?) {
            }

            override fun onTabReselected(tab: Tab?) {
            }

        })

        // フローティングアクションボタンの非表示アニメーション検知リスナー
        floatingActionButton.addOnHideAnimationListener(object: AnimatorListener{
            override fun onAnimationStart(animation: Animator) {
            }

            override fun onAnimationEnd(animation: Animator) {
                // フローティングアクションボタンのリソースを取得する
                val resource = FloatingActionButtonResource.values()[tabLayout.selectedTabPosition]
                // フローティングアクションボタンに画像を設定する
                floatingActionButton.setImageDrawable(resource.getIconDrawable(baseContext))
                // フローティングアクションボタンを表示するタブかどうか判定する
                if (resource.isShowFloatingActionButton()) {
                    floatingActionButton.show()
                }
            }

            override fun onAnimationCancel(animation: Animator) {
            }

            override fun onAnimationRepeat(animation: Animator) {
            }

        })
```

連動部分の流れとしては、タブをタップ又はスワイプをTabLayoutのOnTabSelectedListenerで検知します。
タブの切り替えを検知した場合は、FloatingActionButtonのフェードアウトアニメーション(hideメソッド)を呼び出します。
フェードアウトアニメーションが終了するとFloatingActionButtonのAnimatorListenerが反応し、
onAnimationEndでFloatingActionButtonの画像変更とフェードインアニメーション(showメソッド)の呼び出しをしています。

FloatingActionButtonの連動については、以下のマテリアルデザインに沿って作成しました。

https://m2.material.io/components/buttons-floating-action-button#behavior

## 感想
今回の実装はどのタブを選択しているのか分かりやすさを意識して作ってみました。
色を反転させるだけでも役割としては十分でしたが、せっかくのインジケータがなくなってしまうのが悲しかったので最大限活かせる形で見せることにしました。
FloatingActionButtonの連動部分はマテリアルデザインの勉強中に目にしたものが参考になりました。
次回は機能的な実装を加えていく予定です。