完成形
ポイントは、
- スクロールした時にツールバーのみ隠れる
- 「すべて」「シューズ」「コスメ」のタブが切り替えられる
の大きく2点です。
1. スクロールした時にツールバーのみ隠れる
これは、CoordinatorLayout
とAppBarLayout
を用いて実現することができます。
- CoodinatorLayout
∟ AppBarLayout
∟ 上部分のビュー達(ツールバー/検索フォーム/切り替えタブ)
上記のような構成になるようにxmlを記述して、スクロールした時に隠れて欲しいところにはlayout_scrollFlags
を設定します。
実際のコードはこんな感じです。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray"
android:paddingTop="10dp"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ZOZOTOWN"
android:textSize="25sp"
android:layout_gravity="center"
android:textStyle="bold"/>
<ImageButton
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="end"
android:scaleType="centerCrop"
android:background="@color/dark_gray"
android:layout_margin="8dp"
android:src="@drawable/ic_outline_shopping_cart_24"/>
<ImageButton
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="end"
android:scaleType="centerCrop"
android:background="@color/dark_gray"
android:layout_margin="8dp"
android:src="@drawable/ic_baseline_notifications_none_24"/>
</androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_rounded_corners_10dp"
android:layout_marginHorizontal="25dp"
android:layout_marginVertical="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:hint="@string/search_form_hint"
android:textStyle="bold"
android:textCursorDrawable="@drawable/shape_cursor"
android:drawableLeft="@drawable/ic_baseline_search_24"
android:padding="10dp"
android:inputType="text"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/home_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray"
app:tabTextAppearance="@style/CustomTabText"/>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
これでいけます。
実際にスクロールしてちゃんと隠したい部分が隠れてるかどうかは、適当にRecyclerViewを実装して確かめてみるのが良いです。
もしRecyclerViewを実装するならAppBarLayoutの外でCoordinatorLayoutの中の位置です。
<CoordinatorLayout>
<AppBarLayout>
<スクロールした時に表示したり隠したりするView達/>
</AppBarLayout>
<RecyclerView/>
</CoordinatorLayout>
こんな感じの位置関係です。
2. 「すべて」「シューズ」「コスメ」のタブが切り替えられる
次に切り替えタブの実装方法についてお話しします。
切り替えタブとは、今回の場合は検索フォームの下の「すべて」「シューズ」「コスメ」をタップしたりスライドしたりして切り替えられる部分です。
これは TabLayout
、SwipeRefreshLayout
、ViewPager2
を利用することで実現できます。
タブを配置したいところにTabLayout
を置き、タブを切り替えた時に表示される各Fragmentを配置したいところにSwipeRefreshLayout
とViewPager2
を配置します。
今回の場合、切り替えタブ自体はスクロールされた時に位置を固定させたいですが、表示される各Fragmentはスクロールに従うようにしたいので、以下のような構成にします。
<CoordinatorLayout>
<AppBarLayout>
// スクロールした時に表示したり隠したりするView達
...
<TabLayout/>
</AppBarLayout>
<SwipeRefreshLayout>
<ViewPager2/>
</SwipeRefreshLayout>
</CoordinatorLayout>
上記のポイントはTabLayout
をAppBarLayout
の中に配置していることです。
では1.と合わせてより具体的な実際のコードを見てみましょう。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray"
android:paddingTop="10dp"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed">
<TextView
android:id="@+id/toolbar_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ZOZOTOWN"
android:textSize="25sp"
android:layout_gravity="center"
android:textStyle="bold"/>
<ImageButton
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="end"
android:scaleType="centerCrop"
android:background="@color/dark_gray"
android:layout_margin="8dp"
android:src="@drawable/ic_outline_shopping_cart_24"/>
<ImageButton
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="end"
android:scaleType="centerCrop"
android:background="@color/dark_gray"
android:layout_margin="8dp"
android:src="@drawable/ic_baseline_notifications_none_24"/>
</androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_rounded_corners_10dp"
android:layout_marginHorizontal="25dp"
android:layout_marginVertical="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:hint="@string/search_form_hint"
android:textStyle="bold"
android:textCursorDrawable="@drawable/shape_cursor"
android:drawableLeft="@drawable/ic_baseline_search_24"
android:padding="10dp"
android:inputType="text"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/home_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dark_gray"
app:tabTextAppearance="@style/CustomTabText"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/home_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
しかし実際にはこのままでは切り替えができません。
理由は、切り替えタブがタップされた時に表示する各Fragmentをまだ作成していないことと、そのようなFragmentとViewPager2とを仲介するアダプターを作成していないからです。
次はそれらについて説明します。
切り替えタブがタップされた時に表示する各Fragmentを作成
「すべて」はクラスファイルをHomeAllFragment.kt
とし、レイアウトファイルを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">
<data>
</data>
<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を用いて適当な数字を画面にリスト状に配置します。
そうするとスクロールした時の挙動(隠れなあかんとこがちゃんと隠れてるかとか)が確認できます。
RecyclerViewに代入するためのリストアイテムもクソ適当に作ります。
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/txt_list_item"
android:layout_width="match_parent"
android:layout_height="100dp"
android:textSize="20sp"
android:padding="20dp"
android:textAlignment="center"
android:text=""
xmlns:android="http://schemas.android.com/apk/res/android" />
RecyclerViewを設置する用のAdapterもクソ適当に作ります。
この辺は本題とは逸れるので説明はなしでコードだけ載せます。
class SampleAdapter(private val sampleDataset: ArrayList<String>) :
RecyclerView.Adapter<SampleAdapter.ViewHolder>() {
class ViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SampleAdapter.ViewHolder {
val textView = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false) as TextView
return ViewHolder(textView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = sampleDataset[position]
}
override fun getItemCount() = sampleDataset.size
}
これを踏まえてHomeAllFragmentは以下のように作成します。
class HomeAllFragment : Fragment(R.layout.fragment_home_all) {
companion object {
fun newInstance() = HomeAllFragment()
}
var sampleTextList: ArrayList<String> = ArrayList()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHomeAllBinding.bind(view)
createSampleData()
binding.allRecyclerView.adapter = SampleAdapter(sampleTextList)
}
// RecyclerViewに配置する適当な数字を作成
private fun createSampleData() {
for (i in 1..10) {
sampleTextList.add(i.toString())
}
}
}
onCreateView
ないじゃん!と思った方に朗報です。
実はFragmentのコンストラクタにレイアウトファイルを入れてあげればonCreateView
を継承する必要はなくなるのです!!!
なぜこんなに得意げに話しているかというと私が知らなかったからです!!
ちなみにこの部分です。
レイアウトファイルを入れてますよね。
class HomeAllFragment : Fragment(R.layout.fragment_home_all) {
...
FragmentとViewPager2とを仲介するアダプターを作成
次にHomeAllFragmentなどのFragmentちゃん達とViewPager2とを連携させましょう。
連携にはAdapterを使用します。
とにかく以下のようなコードを書けばOKです。
class HomeFragment : Fragment(R.layout.fragment_home) {
private var _binding: FragmentHomeBinding? = null
private val binding: FragmentHomeBinding
get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
// ViewPagerのAdapterの設定
val homeViewPagerAdapter = HomeViewPagerAdapter(this)
setupViewPager(homeViewPagerAdapter)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setupViewPager(adapter: HomeViewPagerAdapter) {
binding.homeViewPager.adapter = adapter
TabLayoutMediator(binding.homeTabLayout, binding.homeViewPager) { tab, position ->
val tabTitle = getString(FragmentsOrder.values()[position].titleResId)
tab.text = tabTitle
}.attach()
}
private class HomeViewPagerAdapter(parentFragment: Fragment) : FragmentStateAdapter(parentFragment) {
val errorMsg = parentFragment.getString(R.string.error_msg_not_found_fragment)
override fun getItemCount(): Int = FragmentsOrder.values().size
override fun createFragment(position: Int): Fragment {
return when (FragmentOrder.value()[position]) {
FragmentsOrder.ALL -> HomeAllFragment.newInstance()
FragmentsOrder.SHOES -> HomeShoesFragment.newInstance()
FragmentsOrder.COSME -> HomeCosmeFragment.newInstance()
}
}
}
private enum class FragmentsOrder(val titleResId: Int) {
ALL(R.string.all),
SHOES(R.string.shoes),
COSME(R.string.cosme)
}
}
やってることはタブにタイトルをつけたり、タブと各フラグメントを関連づけさせたり、TabLayoutとViewPager2をにゃんにゃんさせたりやと思います。(あんまり分かってない)
参考:ZOZOのアプリのUIを再現しようの会 【お気に入りタブ編】その1
完成
完成としか言いようがありません。完成です。
本当はstrings.xmlを作成したり、EditTextの角を丸めるためにstyle.xmlを作成したり細かいことも色々やって様々な学びがありましたがそこは省略します!!!!
またこのZOZOTOWNのUIをパクる企画の中で学んだことは随時記事にしていきます!頑張るぞい〜!!
追記
fragment_home.xml
で大切なことがありましてそれを記述するのを忘れておりました。
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
ここの、app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior
と書かれてある部分です。
なんか...RecyclerViewが表示されているFragmentがAppBarLayoutを貫通しているといいますか...。
あまり自分もよく理解できていないのですが、おそらくScrollingViewBehavior
はスクロールした時の振る舞いに関することを意味していて、layout_behavior
でAppBarLayout$ScrollingViewBehavior
を設定することでAppBarLayoutの最下部の位置を取得してFragmentがAppBarLayoutに重ならないよう良い感じに調整してくれるのだと思います。
CoordinatorLayoutやBehaviorに関しては以下の記事を参考にしてみて下さい。
参考:
AndroidのCoordinatorLayoutを使いこなして、モダンなスクロールを実装しよう
AppBarLayoutを使う時にlayout_behaviorをFragmentに持たせるとviewが重ねて表示される