Working with ViewPager2

Anik Dey
4 min readMay 8, 2021

Recently I was working with a project where it was required to implement a TabLayout where I used ViewPager2. I had to explore the internet regarding few issues and thought of sharing it with you guys and for future reference just to copy & paste. Here is the git link where you can find the source code.

https://github.com/anikdey/ViewPager2Example

Here is what we will be creating.

Let us have a look into the code.

For TabLayout. The xml file

<?xml version="1.0" encoding="utf-8"?>
<layout>
<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=".withtablayout.TabLayoutExampleActivity">

<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabTextColor="@color/black_light"
app:tabSelectedTextColor="@color/teal_700"
app:tabBackground="@drawable/tab_indicator_color"
app:tabIndicatorColor="@color/red"
app:tabIndicatorHeight="5dp"
app:layout_constraintTop_toTopOf="parent"/>

<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/tabLayout"
app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here is the Activity. Please checkout the git repo for the complete code. I am not adding the code here for fragments.

class TabLayoutExampleActivity : AppCompatActivity() {

private lateinit var binding: ActivityTabLayoutExampleBinding
private lateinit var pagerAdapter: ExampleViewPagerAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTabLayoutExampleBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpViewPager()
}

private fun setUpViewPager() {
pagerAdapter = ExampleViewPagerAdapter(this)
pagerAdapter.addItem("First", FirstFragment.newInstance())
pagerAdapter.addItem("Second", SecondFragment.newInstance())
pagerAdapter.addItem("Third", ThirdFragment.newInstance())
binding.viewPager.setPageTransformer(ZoomOutPageTransformer())
binding.viewPager.adapter = pagerAdapter
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = pagerAdapter.getPageTitle(position)
}.attach()
}

companion object {

@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, TabLayoutExampleActivity::class.java)
}

}

}

Here is the adapter

class ExampleViewPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {

var fragmentList = ArrayList<Fragment>()
var titleList = ArrayList<String>()

fun getPageTitle(position: Int): String {
return titleList[position]
}

fun addItem(title: String, fragment: Fragment) {
fragmentList.add(fragment)
titleList.add(title)
}

override fun getItemCount(): Int {
return fragmentList.count()
}

override fun createFragment(position: Int): Fragment {
return fragmentList[position]
}

}

Now Let us look into the code for the sliding implementation for a login guide. Here is the xml file

<?xml version="1.0" encoding="utf-8"?>
<layout>
<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=".slider.LoginGuideActivity">

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

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
android:background="#8C000000">

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/prevButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="Prev"/>

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/nextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="Next"/>

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/doneButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:visibility="gone"
android:text="Done"/>

</RelativeLayout>

<LinearLayout
android:id="@+id/indicatorsContainer"
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#8C000000">

</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Here is the activity

class LoginGuideActivity : AppCompatActivity() {

private lateinit var binding: ActivityLoginGuideBinding
private lateinit var loginGuideAdapter: LoginGuideAdapter

private var onPageChangeCallbackListener = object : ViewPager2.OnPageChangeCallback() {

override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if(position == 0) {
controlButtonVisibility(View.GONE, View.VISIBLE, View.GONE)
} else if(position>0 && position<loginGuideAdapter.itemCount-1) {
controlButtonVisibility(View.VISIBLE, View.VISIBLE, View.GONE)
} else if(position == loginGuideAdapter.itemCount-1) {
controlButtonVisibility(View.VISIBLE, View.GONE, View.VISIBLE)
}
addIndicators(binding.viewPager.currentItem)
}

// override fun onPageScrollStateChanged(state: Int) {
// }
//
// override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
// }
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginGuideBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.doneButton.setOnClickListener {
showToast("Implement as per your need..")
}

binding.prevButton.setOnClickListener {
showPreviousItem()
}

binding.nextButton.setOnClickListener {
showNextItem()
}

setUpViewPager()
//addIndicators(0)
//setUpUsingCustomView(0)
}

private fun showPreviousItem() {
var currentPosition = binding.viewPager.currentItem
if(currentPosition>0) {
binding.viewPager.currentItem = currentPosition-1
}
}

private fun showNextItem() {
var currentPosition = binding.viewPager.currentItem
if(currentPosition<loginGuideAdapter.itemCount) {
binding.viewPager.currentItem = currentPosition+1
}
}

private fun setUpViewPager() {
loginGuideAdapter = LoginGuideAdapter()
loginGuideAdapter.addItems(getItems())
binding.viewPager.adapter = loginGuideAdapter
binding.viewPager.setPageTransformer(DepthPageTransformer())
binding.viewPager.registerOnPageChangeCallback(onPageChangeCallbackListener)
//binding.viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL
// Fix for the first time showing the dots....
binding.viewPager.post {
onPageChangeCallbackListener.onPageSelected(0)
}
}

private fun addIndicators(position: Int) {
binding.indicatorsContainer.removeAllViews()
setUpUsingCustomView(position)
//setUpUsingTextView(position)
}

private fun setUpUsingCustomView(position: Int) {
binding.indicatorsContainer.removeAllViews()
for (i in 0 until loginGuideAdapter.itemCount) {
var layout = layoutInflater.inflate(R.layout.circular_dot_using_custom_view, null) as LinearLayout
var dottedCircle = layout.findViewById<DottedCircle>(R.id.circle)
layout.removeView(dottedCircle)
if(position == i) {
dottedCircle.setOuterCircleColor(ContextCompat.getColor(this, R.color.red))
dottedCircle.setInnerCircleColor(ContextCompat.getColor(this, R.color.purple_500))
}
binding.indicatorsContainer.addView(dottedCircle)
}
}

private fun setUpUsingTextView(position: Int) {
for (i in 0 until loginGuideAdapter.itemCount) {
var textView = layoutInflater.inflate(R.layout.circular_dot, null) as AppCompatTextView
//textView.text = getText()
if(position == i) {
textView.setTextColor(ContextCompat.getColor(this, R.color.red))
} else {
textView.setTextColor(ContextCompat.getColor(this, R.color.green))
}
binding.indicatorsContainer.addView(textView)
}
}

private fun getText(): Spanned? {
var html = "&#8226;"
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
} else {
Html.fromHtml(html);
}
}

private fun controlButtonVisibility(prevVisibility: Int, nextVisibility: Int, doneVisibility: Int) {
binding.prevButton.visibility = prevVisibility
binding.nextButton.visibility = nextVisibility
binding.doneButton.visibility = doneVisibility
}

private fun getItems(): ArrayList<SliderItem> {
var list = ArrayList<SliderItem>()
list.add(
SliderItem(
Constants.VIEW_TYPE_ONLY_TEXT,
"Your first instruction goes here...",
null
)
)
list.add(
SliderItem(
Constants.VIEW_TYPE_TEXT_IMAGE,
"Your second instruction goes here...",
R.drawable.man_circle
)
)
list.add(
SliderItem(
Constants.VIEW_TYPE_TEXT_IMAGE,
"Your fourth instruction goes here...",
R.drawable.child
)
)
list.add(
SliderItem(
Constants.VIEW_TYPE_ONLY_TEXT,
"Your third instruction goes here...",
null
)
)
list.add(
SliderItem(
Constants.VIEW_TYPE_TEXT_IMAGE,
"Your fourth instruction goes here...",
R.drawable.child
)
)
return list
}

private fun showToast(message: String?) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

override fun onDestroy() {
super.onDestroy()
binding.viewPager.unregisterOnPageChangeCallback(onPageChangeCallbackListener)
}

companion object {

@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, LoginGuideActivity::class.java)
}

}
}

Here is the model for slider item

data class SliderItem (
val type: Int,
val instruction: String,
var resId: Int?
)

Here is the adapter

class LoginGuideAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

var items = ArrayList<SliderItem>()

fun addItems(list: ArrayList<SliderItem>) {
items = list
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if(viewType == Constants.VIEW_TYPE_ONLY_TEXT) {
var binding = ItemDemoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DemoViewHolder(binding)
} else {
var binding = ItemTextImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TextImageViewHolder(binding)
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(holder is DemoViewHolder) {
var demoViewHolder = holder as DemoViewHolder
demoViewHolder.setUpView(item = items[position])
} else if(holder is TextImageViewHolder) {
var textImageViewHolder = holder as TextImageViewHolder
textImageViewHolder.setUpView(item = items[position])
}
}

override fun getItemCount(): Int {
return items.count()
}

override fun getItemViewType(position: Int): Int {
return items[position].type
}

class DemoViewHolder(private val binding: ItemDemoBinding) : RecyclerView.ViewHolder(binding.root) {

fun setUpView(item: SliderItem) {
binding.instructionTextView.text = item.instruction
}

}

class TextImageViewHolder(private val binding: ItemTextImageBinding) : RecyclerView.ViewHolder(binding.root) {

fun setUpView(item: SliderItem) {
binding.instructionTextView.text = item.instruction
item.resId?.let {
binding.imageView.setImageResource(it)
}
}
}

}

You will be able to set up different kind of transformer for ViewPager2. You will find a package named allcollectedtransformers where I added few transformer after exploring the web. Thanks to the original authors.

You can checkout this link from where I took most of the help.

Hope that helps you. Thanks for reading.

--

--