LoginSignup
35
30

More than 3 years have passed since last update.

ViewPager2 + TabLayout + DataBinding

Last updated at Posted at 2019-12-03

はじめに

DMMグループ Advent Calendar 20193日目を担当します!@mii-chang です!

2018年新卒入社でDMMにJOINし、今は AQUIZ というクイズアプリのAndroidアプリを開発しています:muscle::fire:
https://aquiz.jp/

クイズに答えてお金がもらえる!:moneybag:面白いアプリなのでぜひ皆さん遊んでくださいね〜〜!:raising_hand:🥰

さて、今回はJetpackに新しく追加されたViewPager2について書いていきたいと思います!:rocket::rocket::rocket:

ViewPager2 とは

従来のViewPagerの進化版のViewPager2
今までは、各FragmentをPagerAdapterを使って表示切り替えをしていました。
ViewPager2では、PagerAdapterの代わりにRecyclerView.Adapterを使います。
リファレンス

何がいいの?

今までは、PagerAdapterに切り替えたいFragmentをそれぞれセットしていましたが、
新しいViewPager2では、画面切り替えをRecyclerViewベースでやってくれるので、Viewのレイアウトを作っておけばそれを再利用してくれます。
要するに、Fragmentが1つで良くなったのです:tada:
また、RecyclerViewベースなので、RecyclerViewの知識があれば扱いやすいのも魅力ですね:sunny:

使ってみよう

今回は、Databindingを一緒に使って、簡単なサンプルを作ってみます。

導入

app配下のbuild.gradleに以下を追加します

build.gradle
implementation "androidx.viewpager2:viewpager2:1.0.0-beta04"

バージョン情報は公式リファレンスを見てください!:eyes:

実装

サンプルとして、クリスマスアイテムの画像と名前のリストを、ViewPagar2を使って表示してみます:christmas_tree::santa:

レイアウト

今回は、ViewPagerと、TabLayoutを使って、インジケーター付きのレイアウトを作ります

fragment_main.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"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context=".MainFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewpager"
            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" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tab_layout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:tabGravity="center"
            app:tabIndicatorColor="@color/colorAccent"
            app:tabIndicatorGravity="center"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

TabLayout

TabLayoutのリファレンス

tabGravity

tabGravity は、インジケーター全体の横幅を決められます。

center fill
center fill
真ん中寄せになる 横幅いっぱいにインジケーターが広がる
tabIndicatorColor

tabIndicatorColor は、その名の通り、インジケーターの色を設定できます。

tabIndicatorGravity

tabIndicatorGravityは、TabLayout内でのインジケーターの位置を指定できます。(View全体の背景が緑、インジケーターの色がピンク)

top stretch
device-2019-12-02-204335.png device-2019-12-02-204356.png
center bottom
device-2019-12-02-204433.png device-2019-12-02-204634.png

ViewPater2にセットするアイテムのレイアウトを作ります。
今回はサンプルとして、文字と画像をDataBindingを使って表示させてみます。

view_christmas.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"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="christmas"
            type="com.miichang.viewpagersample.Christmas" />
    </data>

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

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{christmas.title}"
            android:textColor="#000"
            android:textSize="50sp"
            app:layout_constraintBottom_toTopOf="@id/imageView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="ケーキ" />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginTop="20dp"
            android:scaleType="fitCenter"
            android:src="@{christmas.drawable}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/textView"
            tools:src="@drawable/cake" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

BindingAdapter

今回は、DrawableをDataBindingで受け渡す想定で、DrawableをImageViewにセットするためのBindingAdapterを作りました。

ChristmasBindingAdapter.kt
internal object ChristmasBindingAdapter {
    @JvmStatic
    @BindingAdapter("android:src")
    fun setDrawable(
        imageView: ImageView?,
        drawable: Drawable?
    ) {
        if (imageView == null) return
        drawable?.let {
            imageView.setImageDrawable(drawable)
        }
    }
}

バインドするデータクラス

今回は、Christmasデータクラスを作って、この内容をバインドさせます

Christmas.kt
data class Christmas(
    val title: String,
    val drawable: Drawable?
)

ViewHolder

レイアウトに定義したデータタグに、Christmasデータクラスの内容を渡すために、RecyclerView.ViewHolderを継承したViewHolderクラスを作ります。

ItemViewHolder.kt
internal class ItemViewHolder(
    itemView: View,
    private val binding: ViewChristmasBinding
) : RecyclerView.ViewHolder(itemView) {
    companion object {
        fun create(
            inflater: LayoutInflater,
            container: ViewGroup,
            attachToRoot: Boolean
        ): ItemViewHolder {
            val binding = ViewChristmasBinding.inflate(inflater, container, attachToRoot)
            return ItemViewHolder(binding.root, binding)
        }
    }

    fun bind(item: Christmas) {
        binding.apply {
            christmas = item
            executePendingBindings()
        }
    }
}

Adapter

表示させたいデータクラスのリストをViewHolderに受け渡すために、RecyclerView.Adapterを継承したアダプタークラスを作ります。

ViewPagerAdapter.kt
internal class ViewPagerAdapter : RecyclerView.Adapter<ItemViewHolder>() {
    private var list: List<Christmas> = listOf()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return ItemViewHolder.create(inflater, parent, false)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(list[position])
    }

    fun setItem(list: List<Christmas>) {
        this.list = list
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int = list.size
}

onBindViewHolderで、リストの1つ1つを受け渡します。

Fragment

Frafmentでアダプターを初期化します
今回はサンプルなので、Fragmentでadapterに直接文字とDrawableリソースを入れたリストを突っ込みました。

MainFragment.kt
class MainFragment : Fragment() {
    companion object {
        fun newInstance(): MainFragment = MainFragment()
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val christmasItems = listOf(
            Christmas(
                title = "サンタ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.santa)
            ),
            Christmas(
                title = "ツリー",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.tree)
            ),
            Christmas(
                title = "トナカイ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.reindeer)
            ),
            Christmas(
                title = "プレゼント",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.presents)
            ),
            Christmas(
                title = "ケーキ",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.cake)
            ),
            Christmas(
                title = "チキン",
                drawable = ContextCompat.getDrawable(requireContext(), R.drawable.chicken)
            )
        )

        val binding = FragmentMainBinding.inflate(
            inflater,
            container,
            false
        )

        val adapter = ViewPagerAdapter()

        binding.apply {
            binding.viewpager.adapter = adapter
            adapter.setItem(christmasItems)
            TabLayoutMediator(
                tabLayout,
                viewpager,
                TabLayoutMediator.TabConfigurationStrategy { tab, position -> }
            ).attach()
        }

        return binding.root
    }
}

TabLayoutMediator

TabLayoutMediatorにTabLaoutとViewPager2を渡してあげることで、ViewPagerのスクロールとTabLayoutのインジケーターがリンクします

TabLayoutMediator(
                tabLayout,
                viewpager,
                TabLayoutMediator.TabConfigurationStrategy { tab, position -> }
            ).attach()

リファレンス

これでViewPager2が使えます!RecyclerViewなので結構スムーズですね:bulb:

ViewPager2でできること

縦スクロール

ViewPager2はRecyclerViewなので、なんと、ViewPager2自体にorientationを設定するだけで超かんたんに縦方向のスクロールに変更することもできます!

android:orientation="vertical"

画面切り替えアニメーション

ViewPager2に対して、setPageTransformerを呼ぶと、画面切り替え時のアニメーションを設定できます。

今回はリファレンスに載っているZoomOutPageTransformerアニメーションをそのまま実装してみました。

binding.viewpager.setPageTransformer(ZoomOutPageTransformer())

スワイプ時のアニメーションがこんなにリッチになるとかなりテンションが上りますね🥰

リスナー

ViewPager2に対してregisterOnPageChangeCallbackを呼ぶと、リスナーが拾えます

 binding.viewpager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                super.onPageScrollStateChanged(state)
            }

            override fun onPageScrolled(
                position: Int,
                positionOffset: Float,
                positionOffsetPixels: Int
            ) {
                super.onPageScrolled(position, positionOffset, positionOffsetPixels)
            }

            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
            }
        })

onPageScrollStateChanged

onPageScrollStateChangedは、スクロール状態の変更が取得できます。

SCROLL_STATE_IDLE SCROLL_STATE_DRAGGING SCROLL_STATE_SETTLING
何もしていないとき ドラッグ開始時 指が離れたとき

onPageScrolled

onPageScrolled は、スクロールされているときに、画面がどれくらい動いているかを取得できます。

position positionOffset positionOffsetPixels
ページのインデックス番号 スクロールで動いた距離 スクロールで動いた距離のピクセル数

onPageSelected

onPageSelectedは、今表示されている画面のインデックス番号を取得できます。

おわりに

今回はViewPager2について調べる機会があったので、まとめてみました!
まだQiita上にも情報が少なかったので、どなたかの役に立てれば幸いです:christmas_tree:

サンプルのリポジトリをGitHubに置いておいたので全体が見たい場合はこちらをどうぞ!
:point_down::point_down::point_down:
https://github.com/mii-chang/ViewPager2Sample
:point_up_2::point_up_2::point_up_2:

DMMグループ Advent Calendar 2019明日の担当は、 @karayok さんです!:santa:

参考

https://developer.android.com/jetpack/androidx/releases/viewpager2
https://medium.com/google-developer-experts/exploring-the-view-pager-2-86dbce06ff71
https://proandroiddev.com/look-deep-into-viewpager2-13eb8e06e419
https://developer.android.com/reference/com/google/android/material/tabs/TabLayoutMediator

35
30
0

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
35
30