【Android】画面コンポーネント / TabLayout

■ はじめに

https://dk521123.hatenablog.com/entry/2019/09/30/020307

で スワイプ処理でページ遷移できる ViewPage2 を扱ったが
TabLayoutと組み合わせて、タブ表示ができるようなので、メモ。

■ 基本的な構成

1)Activity ... 表示する画面
2)Adapter ... Adapter
3)Fragment ... ViewPage2 が表示するフラグメント

■ 使用しているクラス

* TabLayoutMediator
 ⇒ 入力値としてTabLaoutとViewPager2を渡すと、
  タグの位置とタブ表示を管理できる

https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator

■ サンプル

例1:英単語帳

* 以下、構成。

1)MainActivity.kt
2)ExamAdapter.kt
3)ExamFragment.kt

MainActivity.kt

import android.os.Bundle
import androidx.viewpager2.widget.ViewPager2
import com.dk.englishcards.BaseActivity
import com.dk.englishcards.R
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : BaseActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val exams = arrayOf(
            Exam("streamline", "(仕事を)合理化する"),
            Exam("attribute", "…に原因があると考える"),
            Exam("mandatory", "義務的な"),
            Exam("apprenticeship", "見習い期間"),
            Exam("applause", "拍手/賞賛"),
            Exam("overwhelming", "圧倒的な")
        )
        examViewPager.adapter = ExamAdapter(this, exams)
        examViewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL

        // 入力値としてTabLaoutとViewPager2を渡すと、
        // タグの位置とタブ表示を管理できる
        TabLayoutMediator(examTabLayout, examViewPager) { tab, position ->
            val number = position / 2
            tab.text = if((position % 2) == 0) "Q$number" else "A$number"
        }.attach()
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/examFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".exam.MainActivity">

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/examTabLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/examViewPager"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.viewpager2.widget.ViewPager2>

</androidx.constraintlayout.widget.ConstraintLayout>

Exam.kt

data class Exam (
    val question: String,
    val answer: String
)

ExamAdapter.kt

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter

class ExamAdapter(fragmentActivity: FragmentActivity, exams: Array<Exam>)
    : FragmentStateAdapter(fragmentActivity) {
    private val exams = exams

    override fun createFragment(position: Int): Fragment {
        val index = (position / 2)
        val exam = exams[index]
        val questionOrAnswer = if ((position % 2) == 0)
            exam.question else exam.answer
        return ExamFragment.newInstance(position, questionOrAnswer)
    }

    override fun getItemCount(): Int {
        return exams.size * 2
    }
}

ExamFragment.kt

mport android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import com.dk.englishcards.R

private const val ARG_PARAM_POSITION = "position"
private const val ARG_PARAM_LABEL = "label"

class ExamFragment : Fragment() {
    private var position: Int? = null
    private var label: String? = null

    companion object {
        fun newInstance(position: Int, label: String) =
            ExamFragment().apply {
                arguments = Bundle().apply {
                    putInt(ARG_PARAM_POSITION, position)
                    putString(ARG_PARAM_LABEL, label)
                }
            }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            position = it.getInt(ARG_PARAM_POSITION)
            label = it.getString(ARG_PARAM_LABEL)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_exam, container, false)
    }

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

        val textView = view.findViewById<TextView>(
            R.id.examTextViewFragment)
        val qOrA = if ((position!! % 2) == 0) "Q" else "A"
        val no = (position!! / 2)
        textView.text = "$qOrA-$no. $label"
    }
}

fragment_exam.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".exam.ExamFragment">

    <TextView
        android:id="@+id/examTextViewFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/hello_blank_fragment" />

</FrameLayout>

例2:ページコントロール

https://qiita.com/sokume2106/items/1af3c59d79673b90473b

の Activity版。

MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_main2.*

class MainActivity : AppCompatActivity() {
    private val indexItems = listOf(
        IndexItem.FirstItem,
        IndexItem.SecondItem,
        IndexItem.ThirdItem)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        setupViewItem()
    }

    private fun setupViewItem() {
        viewPager.adapter = object : FragmentStateAdapter(this) {
            override fun getItemCount(): Int = indexItems.size
            override fun createFragment(position: Int): Fragment {
                return indexItems[position].newInstance()
            }
        }
        viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
        TabLayoutMediator(indicator, viewPager) { _, _ -> }.attach()
    }

    private sealed class IndexItem {
        abstract fun newInstance(): Fragment

        object FirstItem : IndexItem() {
            override fun newInstance() =
                Sample1Fragment.newInstance("Hello", "Mike")
        }
        object SecondItem : IndexItem() {
            override fun newInstance() =
                Sample2Fragment.newInstance("Hi", "Tom")
        }
        object ThirdItem : IndexItem() {
            override fun newInstance() =
                Sample3Fragment.newInstance("Good evening", "Smith")
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="550dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        android:foregroundGravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/viewPager">

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/indicator"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center_horizontal"
            app:tabBackground="@drawable/indicator_selector"
            app:tabGravity="center"
            app:tabIndicatorFullWidth="true"
            app:tabIndicatorGravity="center"
            app:tabIndicatorHeight="0dp" />
    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

indicator_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape
            android:innerRadius="0dp"
            android:shape="ring"
            android:thickness="8dp"
            android:useLevel="false">
            <solid android:color="@color/design_default_color_secondary" />
        </shape>
    </item>
    <item>
        <shape
            android:innerRadius="0dp"
            android:shape="ring"
            android:thickness="8dp"
            android:useLevel="false">
            <solid android:color="@color/gray" />
        </shape>
    </item>
</selector>

Sample1Fragment.kt

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class Sample1Fragment : Fragment() {
    private var param1: String? = null
    private var param2: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_sample1, container, false)
    }

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

        val textView = view.findViewById<TextView>(
            R.id.textView1)
        textView.text = "$param1, $param2"
    }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            Sample1Fragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

fragment_sample1.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Sample1Fragment">

    <TextView
        android:id="@+id/textView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/hello_blank_fragment" />

</FrameLayout>

参考文献

https://code.luasoftware.com/tutorials/android/setup-android-viewpager2-with-tablayout-and-fragment/

関連記事

画面コンポーネント / ViewPager2
https://dk521123.hatenablog.com/entry/2019/09/30/020307
Mediatorパターン
https://dk521123.hatenablog.com/entry/2011/04/14/011448
Kotlin / Realm で英単語帳を作る
https://dk521123.hatenablog.com/entry/2020/07/20/232009
レイアウト ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2015/08/23/165632