【Android✖️kotlin】ZOZOTOWNのホーム画面の上の方にある性別とかを切り替えできるタブを作ってみた

# 完成形
「性別とか切り替えできるタブ」とはこの部分です。
(なんかGIFに変換すると遅いな...)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/68282267-9eb7-70db-e696-4049c5f9f48b.gif" width=40%>

動作の特徴としては以下が挙げられます。

- 男性・女性・子供のアイコンをクリックすると色が変化する
- 男性・女性・子供のアイコンをクリックするとサイズが大きくなる

ですので、今回のポイントは

1. 何のビューを利用して切り替えタブを実装するのか
2. クリック時のアニメーションのプログラムをどのように実装するのか

の大きく二つが挙げられるかと思います。

注：実は本物のZOZOTOWNの場合だとアイコンを二回連続タップすると全てのアイコンが選択状態になるというクソムズムーブ付きなのですが...今回は華麗にスルーしていきます(^^)

# 1. 何のビューを利用して切り替えタブを実装するのか
TabLayoutとか色々考えましたが、最終的にはImageViewを横に三つポンポンポンっと並べるという方法を取りました。また、前提としてこのタブはRecyclerViewの一行目に入れます。ZOZOTOWNのアプリ見てもらったらわかるのですが切り替えタブの下にも色々な色々が配置されてるんですよね。そして切り替えタブも含めてスクロールが可能になっています。なので切り替えタブはRecyclerViewの一行目に入れることになります。それを踏まえて、レイアウトファイルを見ていきましょう。

これはホーム画面の中でも特に「全て」が選択されている画面のxmlです。
注：ホーム画面には「すべて」「シューズ」「コスメ」の画面があります。
詳細：[【Android✖️kotlin】ZOZOTOWNのホーム画面の上の方を作ってみた](https://qiita.com/mi_iroha/items/94039ff498fc2e0ae16a)

```fragment_home_all.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">

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

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/all_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

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

```
内容としてはただRecyclerViewをはっつけてるだけです。

次に、こちらは切り替えタブの方のレイアウトxmlです。

```person_switch_tab.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">

    <data>
        <variable
            name="listener"
            type="com.nemo.androiduitraining.view.fragment.home.SwitchTabItem.OnClickListener" />
        <import type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
        <variable
            name="selectedGender"
            type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/mens_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.MAN)}"
            app:tintColor="@{selectedGender==Gender.MAN ? @color/person_image_blue : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.MAN}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/ic_mens_before"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/ladies_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.WOMAN)}"
            app:tintColor="@{selectedGender==Gender.WOMAN ? @color/person_image_pink : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.WOMAN}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/ic_ladies_before"
            android:layout_weight="1" />

        <ImageView
            android:id="@+id/kids_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.KIDS)}"
            app:tintColor="@{selectedGender==Gender.KIDS ? @color/person_image_yellow : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.KIDS}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_kids_before"
            android:layout_gravity="center"
            android:layout_weight="1" />

    </LinearLayout>
</layout>

```
内容としては、シンプルに横にImageViewを三つ並べてるだけですが、DataBindingを導入しています。
DataBinding部分は、後ほど流れにそって説明していきますので一旦無視させてください。


# 2. クリック時のアニメーションのプログラムをどのように実装するのか

## 登場人物紹介
ここからタブの実装を行うにあたって様々なクラスが登場するので、あらかじめ登場人物を紹介します。

登場人物

- `HomeAllFragment`...ホーム画面
- `HomeAdapter`...RecyclerViewにアイテムを表示させるのに必要
- `HomeAllViewModel`...どのタブが選択されたかをLiveDataに渡す
- `SwitchTabItem`...RecyclerViewに表示したいアイテムを作成する。
- `ImageViewExt.kt`...ImageViewの拡張関数を作成して色の変化を行う
- `ViewExt.kt`...Viewの拡張関数を作成してサイズの変化を行う

関係性を図に示すと以下の通りになります
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/906559/5fea8b78-4770-bcbb-0a31-0b2873540b80.jpeg">


上記の図がかなりごちゃごちゃしてるのとコードを省略しているので、以下から実際のコードを見ながら説明していきます。

## person_switch_tab.xml
すでにコードを載せてますが上記の図に沿ってもう一度コードを載せます。
図にある部分以外のコードは一旦割愛します。

```person_switch_tab.xml
<ImageView
            android:id="@+id/kids_tab"
            android:paddingVertical="8dp"
            app:onClick="@{()->listener.onGenderClick(Gender.KIDS)}"
            app:tintColor="@{selectedGender==Gender.KIDS ? @color/person_image_yellow : @color/text_gray}"
            app:isSelected="@{selectedGender==Gender.KIDS}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_kids_before"
            android:layout_gravity="center"
            android:layout_weight="1" />
```
このImageViewは切り替えタブのうち子供アイコンを表示させているImageViewです。
`app:onClick="@{()->listener.onGenderClick(Gender.KIDS)}"`とある通り、この`ImageView`がクリックされると`onClick`に設定してある`onGenderClick()`メソッドが発火します。

`databinding`を利用しているので、この`onGenderClick`や`selectedGender`などのメソッドや変数はこのレイアウトxmlの上部でこのようにインポート？？というかデータバインディングを導入するためのコードを書かれています。

```person_switch_tab.xml
<data>
        <variable
            name="listener"
            type="com.nemo.androiduitraining.view.fragment.home.SwitchTabItem.OnClickListener" />
        <import type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
        <variable
            name="selectedGender"
            type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
</data>
```

`listener`の型は`SwitchTabItem.OnClickListener`なので、`ImageView`がクリックされると`SwitchTabItem`クラスの`OnClickListener`で宣言されているインタフェースの中にある`onGenderClick()`に処理が飛びます。

`onGenderClick`の処理を見てみましょう。

## SwitchTabItem.kt
`SwitchTabItem`クラスの全貌は以下の通りです。

```SwitchTabItem.kt
class SwitchTabItem(private val selectedGender: Gender, private val listener: OnClickListener) : BindableItem<PersonSwitchTabBinding>() {
    override fun bind(viewBinding: PersonSwitchTabBinding, position: Int) {
        viewBinding.listener = listener
        viewBinding.selectedGender = selectedGender
        viewBinding.executePendingBindings()

    }

    override fun getLayout(): Int = R.layout.person_switch_tab

    override fun initializeViewBinding(view: View): PersonSwitchTabBinding = PersonSwitchTabBinding.bind(view)
    override fun isSameAs(other: Item<*>): Boolean = other is SwitchTabItem
    override fun hasSameContentAs(other: Item<*>): Boolean = (other as? SwitchTabItem)?.selectedGender == selectedGender

    interface OnClickListener {
        fun onGenderClick(gender: Gender)
    }
}
```

順番に説明していきます。
まず上半分。

```SwitchTabItem.kt
class SwitchTabItem(private val selectedGender: Gender, private val listener: OnClickListener) : BindableItem<PersonSwitchTabBinding>() {
    override fun bind(viewBinding: PersonSwitchTabBinding, position: Int) {
        viewBinding.listener = listener
        viewBinding.selectedGender = selectedGender
        viewBinding.executePendingBindings()

    }
    ...
```
このクラスはRecyclerViewに表示するアイテムのクラスで、今回のプロジェクトではGroupieを使用しているので、`BindableItem`を継承しています。
Groupieを使う場合、RecyclerViewに表示させるアイテムのクラスには`BindableItem`を継承させ、Adapterには`GroupAdapter`を継承させます。

```kotlin
viewBinding.listener = listener
viewBinding.selectedGender = selectedGender
viewBinding.executePendingBindings()
```
この部分は単純にxmlで使うものを渡してあげてます。
例えばlistenerとかselectedGenderとかをxml側で使ってましたよね？
この部分です。↓

```person_switch_tab.xml
<data>
        <variable
            name="listener"
            type="com.nemo.androiduitraining.view.fragment.home.SwitchTabItem.OnClickListener" />
        <import type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
        <variable
            name="selectedGender"
            type="com.nemo.androiduitraining.view.fragment.home.Gender"/>
</data>
```

xml側で`listener`や`selectedGender`をxml側で上記のように使用するために渡してあげてる感じです。
`viewBinding.executePendingBindings()`では`DataBinding`を更新してあげてます。

次に、下半分をみていきます。
この部分です。

```kotlin
override fun getLayout(): Int = R.layout.person_switch_tab

    override fun initializeViewBinding(view: View): PersonSwitchTabBinding = PersonSwitchTabBinding.bind(view)
    override fun isSameAs(other: Item<*>): Boolean = other is SwitchTabItem
    override fun hasSameContentAs(other: Item<*>): Boolean = (other as? SwitchTabItem)?.selectedGender == selectedGender

    interface OnClickListener {
        fun onGenderClick(gender: Gender)
    }
```

overrideしているやつは。。もう。。。おまじないです(白目)
そして`SwitchTabItem.OnClickListener`の型を作るために`interface`を作成し、そこに`onGenderClick`メソッドを宣言しておきます。
切り替えタブがクリックされた時に実行される関数です。


また、Groupieの話が出たので、念のためAdapterのソースコードも載せておきます。
`update()`の引数のlistenerの型が`SwitchTabItem.OnClickListener`になっています。
Groupieを使っているので`GroupAdapter`を継承しております。

```HomeAdapter.kt
class HomeAdapter : GroupAdapter<GroupieViewHolder>() {
    fun update(renderData: HomeAllViewModel.RenderData, listener: SwitchTabItem.OnClickListener) {
        val group = mutableListOf<BindableItem<out ViewBinding>>()
        group.add(SwitchTabItem(renderData.selectedGender, listener))

        updateAsync(group)
    }
}
```

さて、DataBindingのonClickに設置されているメソッドの処理はViewModelに書くのが一般的です。
今回の場合だと、`onGenderClick`です。
インタフェースは`SwitchTabItem`クラスに書きましたが、実際の処理はViewModelに書いていきます。


## HomeAllViewModel.kt
`HomeAllViewModel`の全貌はこちらの通りです。

```HomeAllViewModel.kt
@HiltViewModel
class HomeAllViewModel @Inject constructor() : ViewModel(), SwitchTabItem.OnClickListener {
    val renderData = MutableLiveData<RenderData>(RenderData(Gender.KIDS))
    data class RenderData(val selectedGender: Gender)

    override fun onGenderClick(gender: Gender) {
        renderData.value = renderData.value?.copy(selectedGender = gender)
    }
}


enum class Gender {
    MAN,
    WOMAN,
    KIDS
}

```
Hilt使っているので`@HiltViewModel`してます。
`@Inject constractor()`の部分もHiltやからって感じです。

```kotlin
override fun onGenderClick(gender: Gender) {
     renderData.value = renderData.value?.copy(selectedGender = gender)
}
```
この部分で`onGenderClick`の具体的な内容を実装しています。
具体的なと言ってもこの一行なのですが。
やっていることとしては`selectedGender`の値を変更して、`renderData`を更新しております。

`selectedGender`を変更することで切り替えタブがクリックされた時に`person_switch_tab.xml`のImageViewの中のこの部分

```person_switch_tab.xml
<ImageView
   ...
   app:isSelected="@{selectedGender==Gender.MAN}"
   ... />
```
の`selectedGender`が変更されます。
これが変更されることによって`@{selectedGender==Gender.MAN}`部分が`true`になり、`isSelected`が実行されます。
`isSelected`って何やねんとなったところで`ViewExt.kt`を見ていきましょう。

## ViewExt.kt
`ViewExt.kt`の全貌は以下の通りです。

```ViewExt.kt
@BindingAdapter("isSelected")
fun View.setScale(isSelected: Boolean?) {
    isSelected ?: return

    val animation =  if (isSelected) {
        ScaleAnimation(1f, 1.5f, 1f, 1.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f)
    } else {
        ScaleAnimation(1.5f, 1f, 1.5f, 1f, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f)
    }.apply { fillAfter = true }
    startAnimation(animation)
}
```

ここでは、`isSelected`の中身？である`@{selectedGender==Gender.MAN}`が`true`になれば

```kotlin
ScaleAnimation(1f, 1.5f, 1f, 1.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f)
```
が実行されて選択されたタブのアイコンが拡大されます。

また、`false`の場合は以下のプログラムが実行されて

```kotlin
ScaleAnimation(1.5f, 1f, 1.5f, 1f, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f)
```
拡大状態のアイコンを元に戻します。


こんな感じで、切り替えタブ選択時のアニメーションは実装できました。

ちなみに、`ViewModel`の`LiveData`(今回の場合だと`renderData`という名前の変数)の値をFragmentで監視することもできます。最後に`HomeAllFragment`を見ていきましょう！


## HomeAllFragment.kt
`HomeAllFragment`の全貌は以下の通りです。

```HomeAllFragment.kt
class HomeAllFragment : Fragment(R.layout.fragment_home_all) {
    companion object {
        fun newInstance() = HomeAllFragment()
    }

    private var _binding: FragmentHomeAllBinding? = null
    private val binding
        get() = _binding!!

    private val adapter = HomeAdapter()
    private val viewModel: HomeAllViewModel by viewModels()

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

        _binding = FragmentHomeAllBinding.bind(view)
        binding.allRecyclerView.adapter = adapter
        viewModel.renderData.observe(viewLifecycleOwner) {
            adapter.update(it, viewModel)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
```

先ほどの「監視できる」という部分に該当するのは以下のソースコード部分です。

```kotlin
viewModel.renderData.observe(viewLifecycleOwner) {
     adapter.update(it, viewModel)
}
```

はい、いかにも監視している感じです。萌えます。はぁ？？？？？？？？


# 終わり
終わりです。
Androidってむずいですね〜〜〜
何でこんなに難しいことを楽しいと思ってるんですかね私は。
AndroidエンジニアってみんなドMですよね。
そういえば私の知り合いのAndroidエンジニアにすごいドMの方がいました。
あなたはAndroidが好きですか？
あなたは...ドMですか？？
〜完〜