ViewPager2 under the Hood
Feb 8, 2019 · 8 minute readcode
android
Today, Google released the first alpha of ViewPager2. I had been looking at the code as it was developed in the Android X repository for a while now, and wanted to write a bit about how it works.
Motivations
ViewPager2
builds on top of the versatile RecyclerView
to make ViewPager2
a worthy successor to ViewPager
.
Per the release notes, there are 3 major features that ViewPager2 adds:
- RTL support - ViewPager’s never natively supported RTL. In order to support it, you’d either have to manually reverse the pages yourself, or use a library that does it for you.
- Vertical orientation - while you could always use a
RecyclerView
or the like to achieve this, you don’t get the snapping behavior out of the box without implementing it yourself. notifyDataSetChanged
now works - the oldViewPager
’snotifyDataSetChanged
didn’t recreate the page (a common workaround was to overridegetItemPosition
to returnPOSITION_NONE
).
I’d argue that a 4th major feature is the ability to inter-op adapters with RecyclerView
s. The replacement for PagerAdapter
is now RecyclerView.Adapter
, which means that any RecyclerView
adapter can just be used as is within ViewPager2
. It also opens the door to using the more granular notifyDataSetChanged
type functions (notifyItemInserted
, notifyItemChanged
, etc) directly or via DiffUtil
.
A Quick Sample
Before diving in to how ViewPager2
is implemented, below is a really quick example of how to use it with views. For more detailed samples, please see the the official ViewPager2 Demos.
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
class MainActivity : AppCompatActivity() {
private val data = intArrayOf(Color.BLUE, Color.RED, Color.GRAY, Color.GREEN, Color.YELLOW)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
val viewPager = findViewById<ViewPager2>(R.id.viewpager)
// optionally, for vertical orientation
// viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL
viewPager.adapter = object : RecyclerView.Adapter<ItemHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemHolder {
val view = View(parent.context)
// limitation: ViewPager2 pages need to fill the page for now
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT)
return ItemHolder(view)
}
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: ItemHolder, position: Int) {
holder.itemView.setBackgroundColor(data[position])
}
}
}
class ItemHolder(view: View) : RecyclerView.ViewHolder(view)
}
main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager2.widget.ViewPager2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
ViewPager2 can also be used with fragments, using the FragmentStateAdapter
. It has 2 required methods - one for the number of items, and one for getting the item.
For more examples, see the official ViewPager2 Demos that are part of AndroidX.
Under the Hood
In order to get an overview of how ViewPager2
is built, let’s start by going back in history to the very first commit for it in AndroidX. The initial version of ViewPager2
was only 175 lines long and serves to give us a good idea of how it is built and how it works.
ViewPager2’s Initial Commit
View Construction
ViewPager2
is a ViewGroup
. All of its constructors call an initialize
method, which creates a RecyclerView
, sets its layout manager to a LinearLayoutManager
, and attaches the RecyclerView
to itself. It also creates and attaches a PageSnapHelper
(this is what supports page snapping).
Measure and Layout
As we’d imagine, there’s an implementation of onMeasure
to measure the view. This first asks its internal RecyclerView
to measure itself. It adjusts the width and height to add any padding set on the ViewPager2
, and uses the maximum between the calculated width/height and the suggested minimum width and height (these suggestions come either from the background (if one is set, and in this case, one isn’t set), or from android:minWidth
and android:minHeight
if they are defined).
There’s also an onLayout
, which lays out the internal RecyclerView
while respecting padding (though a note in the source says that this may potentially be delegated to the RecyclerView
itself one day to avoid page transition bugs) and setting a gravity of start and top.
Other
There are 2 remaining functions in the initial ViewPager2
implementation that remain in the current master version - onViewAdded
, which is called when a view is added to the ViewGroup
- this currently throws an exception (with a TODO to add support for decor views).
Finally, there is a setAdapter
- this just sets the adapter
on the RecyclerView
. (In the old initial commit, this wrapped the Adapter
with one that ensured that the width and height of the child are MATCH_PARENT
. Today, this enforcement is in an enforceChildFillListener
method, which it does via a RecyclerView.OnChildAttachStateChangeListener
).
(Note - for completion’s sake, the initial commit also had a method to addOnScrollListener
, which was just handed down to the RecyclerView
).
ViewPager2 Today
Today, ViewPager2
supports many of ViewPager
’s APIs that weren’t supported in the initial commit. The vast majority of the new code in today’s ViewPager2
is for adding support for:
- saving and restoring state
- setting orientation (this just passes the parameter to the
LinearLayoutManager
). - support for
getCurrentItem
andsetCurrentItem
- ability to add an
OnPageChangeCallback
(similar toViewPager.OnPageChangeListener
from the legacyViewPager
). setPageTransformer
, which lets transformations be applied to each page while it is scrolling.
Saving and Restoring state
In onSaveInstanceState
, the ViewPager2
saves:
- the id of the
RecyclerView
- theRecyclerView
’sid
was set in the constructor toViewCompat.generateViewId
, which generates an id that won’t clash with existingR.id
values. - the orientation
- the current item position
- whether or not a scroll was in progress - this is done by asking the
LayoutManager
for the first completely visible item on the screen and comparing it to the current item. - an optional
Parcelable
containing the adapter’s state if the adapter is aStatefulAdapter
.
A method called onDispatchRestoreInstanceState
is what ultimately calls restoreInstanceState
during restoration. This method gets the state
as a Parcelable
from its parent and reads the old RecyclerView
’s id. It then replaces the mapping for the RecyclerView
’s data from its old id to its current id (since the id is generated by the constructor). This allows the new RecyclerView
to restore using the old one’s state.
restoreInstanceState
restores the saved values variables back. If the ViewPager2
died while it was scrolling, it temporarily nulls out the OnPageChangeCallback
to avoid propagating events. It then posts a message to set the callback back, and calls a scrollToPosition
on the RecyclerView
to go back to the restored item position. The removing and re-adding of the callback is done to avoid sending out a scroll event when this happens.
Translating RecyclerView scrolls to ViewPager2 events
Like its predecessor, ViewPager2
provides support for getting and setting the current page. How does this work, especially when RecyclerView
has no such method for getCurrentItem
or setCurrentItem
?
RecyclerView
allows us to set an RecyclerView.OnScrollListener
. This interface has 2 methods:
// newState is one of RecyclerView's constants for:
// SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, or SCROLL_STATE_SETTLING
void onScrollStateChanged(RecyclerView recyclerView, int newState);
void onScrolled(RecyclerView recyclerView, int dx, int dy);
ViewPager2
has a helper class called ScrollEventAdapter
, which maps calls from RecyclerView.OnScrollListener
to calls to OnPageChangeCallback
(the ViewPager2
equivalent of ViewPager.OnPageChangeListener
). OnPageChangeCallback
has 3 methods:
public void onPageScrolled(int position,
float positionOffset,
@Px int positionOffsetPixels);
public void onPageSelected(int position);
// state is one of ViewPager2's constants for:
// SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING, or SCROLL_STATE_SETTLING
public void onPageScrollStateChanged(@ScrollState int state)
The ScrollEventAdapter
class maintains 3 values in an internal class called ScrollEventValues
- position (the adapter position of the first visible item in the
RecyclerView
- gotten usingLayoutManager
’sfindFirstVisibleItemPosition
) - the offset in pixels (this is just “how much of this view is off the screen?”)
- the offset as a percentage (dividing the offset by the width for horizontal pagers and by the height for vertical ones).
With a method to calculate the ScrollEventValues
, the class can then map the RecyclerView
scrolled events into the corresponding ViewPager2
page events.
This serves 2 main purposes:
- to maintain the
currentItem
(forgetCurrentItem
andsetCurrentItem
) by means of listening to this class internally, and - to dispatch these events to listeners who call
registerOnPageChangeCallback
. This also allowsPageTransformer
to work.
Scrolling (and smooth scrolling) are implemented by calling scrollToPosition
or smoothScrollToPosition
on the RecyclerView
(for longer jumps, it first scrolls to a nearby position and then smooth scrolls to the actual desired position).
Page Transformations
The ViewPager2
allows for transforming pages when they scroll by passing in a PageTransformer
through the setPageTransformer
method. This method is fired on every onPageScrolled
event. The PageTransformer
gets called with 2 parameters - the View
, and the position
- the position
is a value of where between -1 and 1 relative to the current page position. 0 means this is the current page. 1 means the page is one entire page to the right, and -1 means the page is one entire page to the left.
So if we scroll from page 1 to page 2 (in an LTR, horizontal ViewPager2
), the PageTransformer
for page 1 is fired for decreasing values between 0 and -1, and page 2 is fired for increasing values between 1 and 0. If we go the other way (from page 2 back to page 1), page 1 has its transformer fire for values between -1 and 0, and page 2 has its transformer fired for values between 0 and 1.
Here’s a simple example of fading the outgoing page out and the incoming view in:
val transformer = ViewPager2.PageTransformer { page, position ->
page.apply {
page.alpha = 0.25f + (1 - Math.abs(position))
}
}
viewPager.setPageTransformer(transformer)
Here’s a demo of this in action (animated gif):
A quick note about RTL
The RTL support for ViewPager2
comes directly from the existing LinearLayoutManager
used by a good portion of RecyclerView
s. Like LinearLayout
, LinearLayoutManager
will draw the views backwards if the RTL flag is set.
Summary
ViewPager2
is a nice replacement for ViewPager
built on top of RecyclerView
. In addition to the support for RTL, the fact that the adapters are RecyclerView.Adapter
s is amazing - not just for the fact that it’s easy to inter-op with RecyclerView
s, but for the ability to use specific methods like notifyItemChanged
, notifyItemInserted
, etc directly. DiffUtil
can also be used with ViewPager2
.
There are some limitations mentioned on the release page, but hopefully these will be resolved as time goes on (especially as this is just the first alpha).
As for features, I hope that one day, ViewPager2
will support more of RecyclerView
’s functionality, like ItemDecoration
s (useful for drawing gaps between pages, for example).