Hey people! Welcome to my blog. In this article you are going to learn and build how to implement how user can navigate from RecyclerView items to ViewPager2 and swipe between those items.

  • ViewPager2 lets you swipe between fragments with or without TabLayout and also swipe items inside RecyclerView in both horizontal and vertical direction.
  • In order to implement ViewPager2 in any project, first make sure the project uses AndroidX or-else you have to migrate to AndroidX first, link is provided at the end for migration guide.
  • ViewPager2 comes with right-to-left support, orientation changes, now you can use RecyclerView with ViewPager which can enable usage of DiffUtils, LayoutManager, PageTransformations, Fragments as pages which are modifiable etc.

Overview

1. Add dependencies to your project

Start with new android studio project choosing Empty Activity.
Give project and package name and select language Kotlin.
Minimum SDK as API 21: Lollipop and finish

gradle.build(Module: app):

// ConstraintLayout
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
// Material Design Components
implementation 'com.google.android.material:material:1.3.0'
// ViewPager2
implementation "androidx.viewpager2:viewpager2:1.0.0"
// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.2.0'

2. Add resources to show data in list

We need data such as string resources to show in layout, let’s copy the strings for showing titles and other data, colors, drawables and styles for the app.
Get all the strings and paste into your strings.xml file.
Get all the colors and paste into your colors.xml file.
Get all the styles and paste into your styles.xml file.
Get all drawables and paste into your drawables folder.

Now let’s create a data model class Sports.kt which helps in making a list.

Sports.kt:

import android.content.Context
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Sports(
    val id: Int,
    val banner: Int,
    val title: String,
    val subtitle: String,
    val about: String
) : Parcelable {

    companion object {
        fun sportsList(context: Context): List<Sports> {
            return listOf(
                Sports(0, R.drawable.ic_rugby,
                    context.getString(R.string.title_rugby),
                    context.getString(R.string.subtitle_rugby),
                    context.getString(R.string.about_rugby)
                ),
                Sports(1, R.drawable.ic_cricket,
                    context.getString(R.string.title_cricket),
                    context.getString(R.string.subtitle_cricket),
                    context.getString(R.string.about_cricket)
                ),
                Sports(2, R.drawable.ic_hockey,
                    context.getString(R.string.title_hockey),
                    context.getString(R.string.subtitle_hockey),
                    context.getString(R.string.about_hockey)
                ),
                Sports(3, R.drawable.ic_basketball,
                    context.getString(R.string.title_basketball),
                    context.getString(R.string.subtitle_basketball),
                    context.getString(R.string.about_basketball)
                ),
                Sports(4, R.drawable.ic_volleyball,
                    context.getString(R.string.title_volleyball),
                    context.getString(R.string.subtitle_volleyball),
                    context.getString(R.string.about_volleyball)
                ),
                Sports(5, R.drawable.ic_esports,
                    context.getString(R.string.title_esports),
                    context.getString(R.string.subtitle_esports),
                    context.getString(R.string.about_esports)
                ),
                Sports(6, R.drawable.ic_kabaddi,
                    context.getString(R.string.title_kabbadi),
                    context.getString(R.string.subtitle_kabbadi),
                    context.getString(R.string.about_kabbadi)
                ),
                Sports(7, R.drawable.ic_baseball,
                    context.getString(R.string.title_baseball),
                    context.getString(R.string.subtitle_baseball),
                    context.getString(R.string.about_baseball)
                ),
                Sports(8, R.drawable.ic_mma,
                    context.getString(R.string.title_mma),
                    context.getString(R.string.subtitle_mma),
                    context.getString(R.string.about_mma)
                ),
                Sports(9, R.drawable.ic_soccer,
                    context.getString(R.string.title_soccer),
                    context.getString(R.string.subtitle_soccer),
                    context.getString(R.string.about_soccer)
                ),
                Sports(10, R.drawable.ic_handball,
                    context.getString(R.string.title_handball),
                    context.getString(R.string.subtitle_handball),
                    context.getString(R.string.about_handball)
                ),
                Sports(11, R.drawable.ic_tennis,
                    context.getString(R.string.title_tennis),
                    context.getString(R.string.subtitle_tennis),
                    context.getString(R.string.about_tennis)
                )
            )
        }
    }
}

Create item_list layout file to show each item in list with adapter.

item_list.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    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="wrap_content"
    app:cardCornerRadius="0dp"
    app:cardUseCompatPadding="true">

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

        <ImageView
            android:id="@+id/banner_item_image_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="16dp"
            android:scaleType="fitCenter"
            app:layout_constraintDimensionRatio="2:2"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:ignore="ContentDescription"
            tools:src="@drawable/ic_hockey" />

        <TextView
            android:id="@+id/title_item_text_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:gravity="center"
            android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
            android:textColor="?attr/colorOnSurface"
            app:layout_constraintBottom_toTopOf="@id/subtitle_item_text_view"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/banner_item_image_view"
            tools:text="Title" />

        <TextView
            android:id="@+id/subtitle_item_text_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:gravity="center"
            android:textColor="?attr/colorOnSurface"
            android:textSize="16sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/title_item_text_view"
            tools:text="Subtitle" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</com.google.android.material.card.MaterialCardView>

Finally create an RecyclerView adapter for fragment to show a list.

SportsAdapter.kt :

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class SportsAdapter(
    private val sportsList: List<Sports>,
    private val onClickListener: OnClickListener
) :
    RecyclerView.Adapter<SportsAdapter.SportsViewHolder>() {

    class SportsViewHolder(
        itemView: View
    ) : RecyclerView.ViewHolder(itemView) {

        private val banner: ImageView = itemView.findViewById(R.id.banner_item_image_view)
        private val title: TextView = itemView.findViewById(R.id.title_item_text_view)
        private val subtitle: TextView = itemView.findViewById(R.id.subtitle_item_text_view)

        fun bind(
            sports: Sports,
            onClickListener: OnClickListener
        ) {
            banner.setImageResource(sports.banner)
            title.text = sports.title
            subtitle.text = sports.subtitle
            itemView.setOnClickListener {
                onClickListener.onClick(sports)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SportsViewHolder {
        return SportsViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_list, parent, false
            )
        )
    }

    override fun onBindViewHolder(holder: SportsViewHolder, position: Int) {
        val sports: Sports = sportsList[position]
        holder.bind(sports, onClickListener)
    }

    override fun getItemCount() = sportsList.size

    class OnClickListener(val clickListener: (sports: Sports) -> Unit) {
        fun onClick(sports: Sports) = clickListener(sports)
    }
}

3. Add RecyclerView widget to MainActivity

With the model class, adapter and item layout we proceed to show data in MainActivity class.
Since our app theme style is NoActionBar we add our own widget for Toolbar, add this below code to MainActivity layout and class to show RecyclerView.

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    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"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:liftOnScroll="true"
        app:liftOnScrollTargetViewId="@id/recycler_view">

        <androidx.appcompat.widget.Toolbar
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"
            app:title="@string/list_title"
            app:titleTextAppearance="@style/TextAppearance.Toolbar.Title"
            app:titleTextColor="?attr/colorOnPrimary" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:orientation="vertical"
        app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        app:spanCount="2"
        tools:itemCount="6"
        tools:listitem="@layout/item_list" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

MainActivity.kt:

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

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

        val sportsList: List<Sports> = Sports.sportsList(applicationContext)
        val recyclerView: RecyclerView = findViewById(R.id.recycler_view)

        val adapter = SportsAdapter(sportsList, sportsItemListener)
        recyclerView.adapter = adapter
    }

    private val sportsItemListener = SportsAdapter.OnClickListener { sports ->
        val intent = Intent(this, DetailActivity::class.java)
        intent.putExtra("Intent to Detail Activity", sports)
        startActivity(intent)
    }
}

We have added ClickListener in MainActivity which sends to DetailActivity with intent and also takes data which is Sports model class.
Let’s create DetailActivity for showing ViewPager2.

4. Create DetailActivity for ViewPager2

activity_detail.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/detail_view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />

</androidx.constraintlayout.widget.ConstraintLayout>

ViewPager2 is the only widget inside layout and items will be shown with ViewPager RecyclerView which enables to swipe inside the layout.
Let’s attach the ViewPager to our activity and then create adapter for our ViewPager.

DetailActivity.xml:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.viewpager2.widget.ViewPager2

class DetailActivity : AppCompatActivity() {

    private lateinit var viewPager2: ViewPager2

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

        val sports: Sports = intent.getParcelableExtra("Intent to Detail Activity")!!
        val sportsList: List<Sports> = Sports.sportsList(applicationContext)

        viewPager2 = findViewById(R.id.detail_view_pager)

        val viewPagerAdapter = DetailViewPagerAdapter(sportsList, this)
        viewPager2.adapter = viewPagerAdapter
        viewPager2.setCurrentItem(sports.id, false)
    }
}

Using setCurrentItem( ) we can call which ever item we want. When we click 3rd item in MainActivity then item is set to directly visible in MainActivity when user opens.

setCurrentItem(Position, SmoothScroll)

If you pass value as true for SmoothScroll then user will be taken to 3rd item which scrolls from first position to 3rd with visible animation.

5. Create ViewPager adapter

Create Adapter and item layout for DetailActivity for ViewPager to show it’s items using RecyclerView

DetailViewPagerAdapter.kt:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView

class DetailViewPagerAdapter(
    private val sportsList: List<Sports>,
    private val activity: DetailActivity
) : RecyclerView.Adapter<DetailViewPagerAdapter.DetailViewHolder>() {

    class DetailViewHolder(
        itemView: View
    ) : RecyclerView.ViewHolder(itemView) {

        val toolbar: Toolbar = itemView.findViewById(R.id.detail_toolbar)
        private val banner: ImageView = itemView.findViewById(R.id.detail_image_view)
        private val title: TextView = itemView.findViewById(R.id.title_detail_text_view)
        private val subtitle: TextView = itemView.findViewById(R.id.subtitle_detail_text_view)
        private val about: TextView = itemView.findViewById(R.id.about_detail_text_view)

        fun bind(
            sport: Sports
        ) {
            banner.setImageResource(sport.banner)
            title.text = sport.title
            subtitle.text = sport.subtitle
            about.text = sport.about
            toolbar.title = sport.title
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder {
        return DetailViewHolder(
            LayoutInflater.from(parent.context).inflate(
                R.layout.item_detail_adapter, parent, false
            )
        )
    }

    override fun onBindViewHolder(holder: DetailViewHolder, position: Int) {
        val sportsArgs: Sports = sportsList[position]
        holder.bind(sportsArgs)

        holder.toolbar.setNavigationOnClickListener {
            activity.finish()
        }
    }

    override fun getItemCount() = sportsList.size
}

Adapter class now requires a item layout and views to show one item for user at once. Let’s create layout xml file for items.

item_detail_adapter.xml:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    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"
    android:fitsSystemWindows="true"
    tools:context=".DetailActivity">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        app:liftOnScroll="true"
        app:liftOnScrollTargetViewId="@id/detail_view_scroll_view">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/detail_toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_collapseMode="pin"
            app:navigationIcon="@drawable/ic_up_button"
            app:titleTextAppearance="@style/TextAppearance.Toolbar.DetailTitle" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:id="@+id/detail_view_scroll_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipToPadding="false"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp">

            <ImageView
                android:id="@+id/detail_image_view"
                android:layout_width="match_parent"
                android:layout_height="256dp"
                android:scaleType="fitCenter"
                app:layout_constraintTop_toTopOf="parent"
                tools:ignore="ContentDescription"
                tools:src="@drawable/ic_hockey" />

            <TextView
                android:id="@+id/subtitle_detail_text_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="20dp"
                android:gravity="center"
                android:textAllCaps="true"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
                android:textColor="?attr/colorSecondaryVariant"
                android:textStyle="bold"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/detail_image_view"
                tools:text="Subtitle" />

            <TextView
                android:id="@+id/title_detail_text_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginTop="12dp"
                android:gravity="center"
                android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
                android:textColor="?attr/colorOnPrimary"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/subtitle_detail_text_view"
                tools:text="Title" />

            <TextView
                android:id="@+id/about_detail_text_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="28dp"
                android:textColor="?attr/colorOnPrimary"
                android:textSize="20sp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/title_detail_text_view"
                tools:text="@string/about_tennis" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_detail"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|bottom"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_menu_search"
        app:rippleColor="?attr/colorOnSecondary" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

6. Material ViewPager2

Since ViewPager2 makes use of RecyclerView as adapter to show items we can customize it’s performance and design in many ways.
We can make use of DiffUtils, layout direction such as right-to left, both vertical and horizontal direction to show items, PageTransformations etc.

Below i have added two material changes to the ViewPager2 which you can add to the previous classes with less changes.

Sample PageTransformers are available in official Android Documentation here .
Create a kotlin file for simple PageTransformer from below code.

ZoomOutPageTransformer.kt:

import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlin.math.abs
import kotlin.math.max

private const val MIN_SCALE = 0.85f
private const val MIN_ALPHA = 0.5f

class ZoomOutPageTransformer : ViewPager2.PageTransformer {

    override fun transformPage(view: View, position: Float) {
        view.apply {
            val pageWidth = width
            val pageHeight = height
            when {
                position < -1 -> { // [-Infinity,-1)
                    // This page is way off-screen to the left.
                    alpha = 0f
                }
                position <= 1 -> { // [-1,1]
                    // Modify the default slide transition to shrink the page as well
                    val scaleFactor = max(MIN_SCALE, 1 - abs(position))
                    val vertMargin = pageHeight * (1 - scaleFactor) / 2
                    val horzMargin = pageWidth * (1 - scaleFactor) / 2
                    translationX = if (position < 0) {
                        horzMargin - vertMargin / 2
                    } else {
                        horzMargin + vertMargin / 2
                    }

                    // Scale the page down (between MIN_SCALE and 1)
                    scaleX = scaleFactor
                    scaleY = scaleFactor

                    // Fade the page relative to its size.
                    alpha = (MIN_ALPHA +
                            (((scaleFactor - MIN_SCALE) / (1 - MIN_SCALE)) * (1 - MIN_ALPHA)))
                }
                else -> { // (1,+Infinity]
                    // This page is way off-screen to the right.
                    alpha = 0f
                }
            }
        }
    }
}

DetailActivity.kt:

viewPager2 = findViewById(R.id.detail_view_pager)
viewPager2.setPageTransformer(ZoomOutPageTransformer())

If you check the swipe between items you will notice pages is being zoomed out while user swipes, whereas before the swipe used to be horizontal. You can check the video on top i have added to see how it looks.

One more change i have added is hiding the views while user swipes the items. For this we need a Callback listener for ViewPager to listen when user swipes them.

Hide Fab and Up-button

Since the fab and up-button views are only accessible from adapter class we shall make those changes over there.
In DetailActivity class we need to pass two more arguments to adapter class which is activity reference and viewpager2, so let’s make those changes.

DetailActivity.kt:

val viewPagerAdapter = DetailViewPagerAdapter(sportsList, this, viewPager2)

Run the app and check swipe you will see views will be hidden while scrolling, this same preview also shown in above video. You can make more changes for items similarly.

7. External links, project Github and download

That’s it guys! Thanks for the read. If you found difficulty in following along with me you can download the project from my GitHub or from link directly I have added below. More useful articles are coming soon so subscribe now.
If you want me to write article on other android components then let me know through contact form. Until then Happy Trails.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.