完成形
「性別とか切り替えできるタブ」とはこの部分です。
(なんかGIFに変換すると遅いな...)
動作の特徴としては以下が挙げられます。
- 男性・女性・子供のアイコンをクリックすると色が変化する
- 男性・女性・子供のアイコンをクリックするとサイズが大きくなる
ですので、今回のポイントは
- 何のビューを利用して切り替えタブを実装するのか
- クリック時のアニメーションのプログラムをどのように実装するのか
の大きく二つが挙げられるかと思います。
注:実は本物のZOZOTOWNの場合だとアイコンを二回連続タップすると全てのアイコンが選択状態になるというクソムズムーブ付きなのですが...今回は華麗にスルーしていきます(^^)
1. 何のビューを利用して切り替えタブを実装するのか
TabLayoutとか色々考えましたが、最終的にはImageViewを横に三つポンポンポンっと並べるという方法を取りました。また、前提としてこのタブはRecyclerViewの一行目に入れます。ZOZOTOWNのアプリ見てもらったらわかるのですが切り替えタブの下にも色々な色々が配置されてるんですよね。そして切り替えタブも含めてスクロールが可能になっています。なので切り替えタブはRecyclerViewの一行目に入れることになります。それを踏まえて、レイアウトファイルを見ていきましょう。
これはホーム画面の中でも特に「全て」が選択されている画面のxmlです。
注:ホーム画面には「すべて」「シューズ」「コスメ」の画面があります。
詳細:【Android✖️kotlin】ZOZOTOWNのホーム画面の上の方を作ってみた
<?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です。
<?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の拡張関数を作成してサイズの変化を行う
上記の図がかなりごちゃごちゃしてるのとコードを省略しているので、以下から実際のコードを見ながら説明していきます。
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の上部でこのようにインポート??というかデータバインディングを導入するためのコードを書かれています。
<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
クラスの全貌は以下の通りです。
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)
}
}
順番に説明していきます。
まず上半分。
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
を継承させます。
viewBinding.listener = listener
viewBinding.selectedGender = selectedGender
viewBinding.executePendingBindings()
この部分は単純にxmlで使うものを渡してあげてます。
例えばlistenerとかselectedGenderとかを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
を更新してあげてます。
次に、下半分をみていきます。
この部分です。
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
を継承しております。
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
の全貌はこちらの通りです。
@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やからって感じです。
override fun onGenderClick(gender: Gender) {
renderData.value = renderData.value?.copy(selectedGender = gender)
}
この部分でonGenderClick
の具体的な内容を実装しています。
具体的なと言ってもこの一行なのですが。
やっていることとしてはselectedGender
の値を変更して、renderData
を更新しております。
selectedGender
を変更することで切り替えタブがクリックされた時にperson_switch_tab.xml
のImageViewの中のこの部分
<ImageView
...
app:isSelected="@{selectedGender==Gender.MAN}"
... />
のselectedGender
が変更されます。
これが変更されることによって@{selectedGender==Gender.MAN}
部分がtrue
になり、isSelected
が実行されます。
isSelected
って何やねんとなったところで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
になれば
ScaleAnimation(1f, 1.5f, 1f, 1.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,0.5f)
が実行されて選択されたタブのアイコンが拡大されます。
また、false
の場合は以下のプログラムが実行されて
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
の全貌は以下の通りです。
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
}
}
先ほどの「監視できる」という部分に該当するのは以下のソースコード部分です。
viewModel.renderData.observe(viewLifecycleOwner) {
adapter.update(it, viewModel)
}
はい、いかにも監視している感じです。萌えます。はぁ????????
終わり
終わりです。
Androidってむずいですね〜〜〜
何でこんなに難しいことを楽しいと思ってるんですかね私は。
AndroidエンジニアってみんなドMですよね。
そういえば私の知り合いのAndroidエンジニアにすごいドMの方がいました。
あなたはAndroidが好きですか?
あなたは...ドMですか??
〜完〜