LoginSignup
26
13

【Android✖️kotlin】ZOZOTOWNのホーム画面の上の方を作ってみた

Last updated at Posted at 2021-10-28

完成形

ZOZOTOWNのホーム画面の上の方とはこの部分です。

ポイントは、

  1. スクロールした時にツールバーのみ隠れる
  2. 「すべて」「シューズ」「コスメ」のタブが切り替えられる

の大きく2点です。

1. スクロールした時にツールバーのみ隠れる

これは、CoordinatorLayoutAppBarLayoutを用いて実現することができます。

- CoodinatorLayout
    ∟ AppBarLayout
        ∟ 上部分のビュー達(ツールバー/検索フォーム/切り替えタブ)

上記のような構成になるようにxmlを記述して、スクロールした時に隠れて欲しいところにはlayout_scrollFlagsを設定します。

実際のコードはこんな感じです。

fragment_home.xml
<?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>

こんな感じの位置関係です。

参考:CoordinatorLayoutをやってみた

2. 「すべて」「シューズ」「コスメ」のタブが切り替えられる

次に切り替えタブの実装方法についてお話しします。
切り替えタブとは、今回の場合は検索フォームの下の「すべて」「シューズ」「コスメ」をタップしたりスライドしたりして切り替えられる部分です。
これは TabLayoutSwipeRefreshLayoutViewPager2を利用することで実現できます。

タブを配置したいところにTabLayoutを置き、タブを切り替えた時に表示される各Fragmentを配置したいところにSwipeRefreshLayoutViewPager2を配置します。
今回の場合、切り替えタブ自体はスクロールされた時に位置を固定させたいですが、表示される各Fragmentはスクロールに従うようにしたいので、以下のような構成にします。

<CoordinatorLayout>
    <AppBarLayout>
        // スクロールした時に表示したり隠したりするView達
        ...
        <TabLayout/>
    </AppBarLayout>
    
    <SwipeRefreshLayout>
        <ViewPager2/>
    </SwipeRefreshLayout>

</CoordinatorLayout>

上記のポイントはTabLayoutAppBarLayoutの中に配置していることです。
では1.と合わせてより具体的な実際のコードを見てみましょう。

fragment_home.xml
<?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とします。
また、「コスメ」と「シューズ」も同様に以下のように作成します。

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に代入するためのリストアイテムもクソ適当に作ります。

list_item.xml
<?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もクソ適当に作ります。
この辺は本題とは逸れるので説明はなしでコードだけ載せます。

SampleAdapter.kt
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は以下のように作成します。

HomeAllFragment.kt
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です。

HomeFragment.kt
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

完成

image.png

完成としか言いようがありません。完成です。
本当はstrings.xmlを作成したり、EditTextの角を丸めるためにstyle.xmlを作成したり細かいことも色々やって様々な学びがありましたがそこは省略します!!!!
またこのZOZOTOWNのUIをパクる企画の中で学んだことは随時記事にしていきます!頑張るぞい〜!!

追記

fragment_home.xmlで大切なことがありましてそれを記述するのを忘れておりました。

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_behaviorAppBarLayout$ScrollingViewBehaviorを設定することでAppBarLayoutの最下部の位置を取得してFragmentがAppBarLayoutに重ならないよう良い感じに調整してくれるのだと思います。

CoordinatorLayoutやBehaviorに関しては以下の記事を参考にしてみて下さい。

参考:
AndroidのCoordinatorLayoutを使いこなして、モダンなスクロールを実装しよう
AppBarLayoutを使う時にlayout_behaviorをFragmentに持たせるとviewが重ねて表示される

26
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
13