Introduce a ViewPager2

ViewPager2(hereinafter referred to as VP2) is an improved version of ViewPager(hereinafter referred to as VP) library. It is realized internally by RecyclerView. VP2 can be understood as a RecyclerView that every ItemView is full of full-screen. VP2 provides enhanced functionality and solves some of the common problems encountered when using VP.

1.1 ViewPager2 features

  • Horizontal and vertical layout support

Android: Orientation =”vertical” for the VP2 layout.

  • RTL(right-to-left)Right-to-left layout support

Set VP2 layout to Android :layoutDirection=” RTL “.

  • One-click disable user sliding support

Use setUserInputEnabled() to set whether to disable user swiping.

  • Can be modifiedFragmentA collection of

VP2 supports paging through a collection of modifiable fragments, calling notifyDatasetChanged() to update the interface when the underlying collection changes. This means that your application can dynamically modify the Fragment collection at run time, and VP2 will display the modified collection correctly.

  • Support DiffUtil

VP2 is built on top of RecyclerView, which means it has access to the DiffUtil utility class. So VP2 supports partial updates when data changes, rather than notifyDatasetChanged() updates in full.

  • Support for simulated drag and dropfakeDragBy

Two ViewPager2 use

2.1 Effect picture of Banner library based on ViewPager2

function The sample
The basic use
Imitation Taobao search bar up and down round broadcast

See lib_viewPager2 for the source code of the above sample effect. Only the implementation image is listed here, which will be described in detail in the next article.

2.2 ViewPager2 Basic Use

  • Different from VP, VP2 needs to be introduced separately:
Dependencies {implementation "androidx viewpager2: viewpager2:1.0.0"}Copy the code

Declare the XML layout:

<androidx.viewpager2.widget.ViewPager2
            android:id="@+id/view_pager2"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintDimensionRatio="2:3"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
Copy the code
  • Set the Adapter:

Because VP2 internal RecyclerView is realized, so the simple interface directly inherit RecyclerView.Adapter:

Class VpAdapter: recyclerview.adapter < vpAdapter.vpViewholder >() {// Adapter private var data: MutableList<HouseItem> = mutableListOf() fun setData(list: MutableList<HouseItem>) { data.clear() data.addAll(list) notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VpViewHolder { //...... } override fun onBindViewHolder(holder: VpViewHolder, position: Int) { } override fun getItemCount() = data.size class VpViewHolder(_itemView: View) : RecyclerView.ViewHolder(_itemView) { //...... }}Copy the code

If you use fragments, you need to use FragmentStateAdapter:

const val PAGES_NUM = 4 class ViewPager2Adapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { private val mItems: ArrayList<VP2Model> = arrayListOf() override fun getItemCount(): Int = PAGES_NUM override fun createFragment(position: Int): Fragment { log("pos:$position: createFragment()") return VP2Fragment(position) } override fun onBindViewHolder( holder: FragmentViewHolder, position: Int, payloads: MutableList<Any> ) { super.onBindViewHolder(holder, position, payloads) log("pos:$position: onBindViewHolder()") } override fun getItemId(position: Int): Long { return super.getItemId(position) } override fun containsItem(itemId: Long): Boolean { return super.containsItem(itemId) } fun setModels(newItems: List<VP2Model>) {// update data without DiffUtil // mitems.clear () // mitems.addall (newItems) //notifyDataSetChanged() // Update data with DiffUtil  val callback = PageDiffUtil(mItems, newItems) val difResult = DiffUtil.calculateDiff(callback) mItems.clear() mItems.addAll(newItems) difResult.dispatchUpdatesTo(this) } }Copy the code
  • Call in Activity/Fragment:
//mVP2Adapter = VpAdapter() //RecyclerView.Adapter
mVP2Adapter = ViewPager2Adapter(this) //FragmentStateAdapter
VP2.adapter = mVP2Adapter
Copy the code

It is very simple to use and the renderings are no longer posted

2.3 Advanced use

2.3.1 Fragment Lazy loading

When VP2 uses FragmentStateAdapter to load the Fragment, setOffscreenPageLimit(int limit) is used to set the number of off-screen cache. When the limit is less than 1, the Fragment is not preloaded, that is, the corresponding life cycle of the Fragment is not called back. Otherwise, the Fragment is preloaded and the corresponding life cycle of the Fragment is called back. The default value OFFSCREEN_PAGE_LIMIT_DEFAULT is -1, that is, lazy loading is performed by default. This is different from VP, where the default value is 1, which means that the left and right fragments are loaded by default.

If in VP2 you want to cache the Fragment(set setOffscreenPageLimit() >=1) and lazyload the data (request the data only when the Fragment is visible), you can do something like this:

/** */ abstract class BaseLazyFragment: Fragment() {private var mIsFirstLoad = true override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View? { if (container ! = null) { val rootView = inflater.inflate(getLayoutId(), container, false) initViews(rootView) return rootView } return super.onCreateView(inflater, container, savedInstanceState) } override fun onResume() { super.onResume() if (mIsFirstLoad) { initData() mIsFirstLoad = false } }  @LayoutRes protected abstract fun getLayoutId(): Int protected fun initViews(view: View) {} protected fun initData() {} }Copy the code

OnResume () is executed only when the Fragment is visible, so use a Boolean field to control the data request to be executed only once.

PS: Influence of offscreenPageLimit on mCachedViews

  • When not setoffscreenPageLimitWhen off screen cache,VP2In theRecyclerViewThe default inmCachedViewsThe first two in the cacheItemAnd the one that I pre-grabbedItem.
  • If you set it upoffscreenPageLimitIf the value is 1, a cache is added for the left and right off-screenItemIncrease the width of the canvas by 3 times (the left and right are not visible by default) and addRecyclerViewDefault cache of 3, in addition to the current displayItemAnd it will cache a total of fiveItem.

2.3.2 Multiple Pages on one Screen

The key code for setting one screen with multiple pages is as follows:

Var recyclerView = getChildAt(0) as recyclerView recyclerView.apply {val padding = 50 // setting padding on inner RecyclerView puts overscroll effect in the right place setPadding(padding, 0, padding, 0) clipToPadding = false } adapter = Adapter() }Copy the code

In VP2 source code internal line 254, RecyclerView fixed index is 0:

attachViewToParent(mRecyclerView, 0, mRecyclerView.getLayoutParams());
Copy the code

So you can go throughVP2.getChildAt(0)Direct access toVP2The inside of theRecyclerView, and then through the SettingspaddingTo achieve a screen of multiple pages, the running effect is as follows:

2.3.3 ViewPager2 nested sliding Conflicts

Because VP2 is realized internally by RecyclerView, sliding related processing is mainly carried out in RecyclerView, and its internal implementation is as follows:

private class RecyclerViewImpl extends RecyclerView { RecyclerViewImpl(@NonNull Context context) { super(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return isUserInputEnabled() && super.onInterceptTouchEvent(ev); }}Copy the code

OnInterceptTouchEvent will be used for event interception. The onInterceptTouchEvent is only isUserInputEnabled, and the other events are not handled. Therefore, the nested sliding of VP2 is not handled officially. Need developers to dispose, here can pass through the event in the internal intercept method (requestDisallowInterceptTouchEvent ()) for processing, if the internal control of nested sliding need slide, can’t control the external parent control seizures, Set to requestDisallowInterceptTouchEvent (true); Let father external control seizures, conversely set to requestDisallowInterceptTouchEvent (false). NestedScrollableHost:

class NestedScrollableHost : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) private var touchSlop = 0 private var initialX = 0f private var initialY = 0f private val parentViewPager: ViewPager2? get() { var v: View? = parent as? View while (v ! = null && v ! is ViewPager2) { v = v.parent as? View } return v as? ViewPager2 } private val child: View? get() = if (childCount > 0) getChildAt(0) else null init { touchSlop = ViewConfiguration.get(context).scaledTouchSlop } private fun canChildScroll(orientation: Int, delta: Float): Boolean { val direction = -delta.sign.toInt() return when (orientation) { 0 -> child? .canScrollHorizontally(direction) ? : false 1 -> child? .canScrollVertically(direction) ? : false else -> throw IllegalArgumentException() } } override fun onInterceptTouchEvent(e: MotionEvent): Boolean { handleInterceptTouchEvent(e) return super.onInterceptTouchEvent(e) } private fun handleInterceptTouchEvent(e: MotionEvent) { val orientation = parentViewPager? .orientation ? : return // Early return if child can't scroll in same direction as parent if (! canChildScroll(orientation, -1f) && ! canChildScroll(orientation, 1f)) { return } if (e.action == MotionEvent.ACTION_DOWN) { initialX = e.x initialY = e.y parent.requestDisallowInterceptTouchEvent(true) } else if (e.action == MotionEvent.ACTION_MOVE) { val dx = e.x - initialX val dy = e.y - initialY val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL // assuming ViewPager2 touch-slop is 2x touch-slop of child val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f if (scaledDx > touchSlop || scaledDy > touchSlop) { if (isVpHorizontal == (scaledDy > scaledDx)) { // Gesture is perpendicular, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } else { // Gesture is parallel, query child if movement in that direction is possible if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { // Child can scroll, disallow all parents to intercept parent.requestDisallowInterceptTouchEvent(true) } else { // Child cannot scroll, allow all parents to intercept parent.requestDisallowInterceptTouchEvent(false) } } } } } }Copy the code

2.3.4 Incremental DiffUtil update is supported

VP2 internal by RecyclerView, so support DiffUtil incremental update, so as to improve performance; Try to avoid using notifyDatasetChanged() for full updates. DiffUtil is used as follows:

class PageDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) : Callback() {override fun getOldListSize(): Int = oldModels. Size /** * new data */ Override fun getNewListSize(): Int = newModels.size /** * DiffUtil call to determine whether two objects represent the same Item. True means two items are the same (which means views can be reused), false means different (which means views cannot be reused) * For example, if your items have unique ids, this method should check whether their ids are equal. */ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java } /** * * This method is called only if areItemsTheSame (int, int) returns true to compare whether two items have the same contents. */ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {return oldModels[oldItemPosition] == newModels[newItemPosition]} /** * AreItemsTheSame (int, int) returns true and areContentsTheSame(int, int) returns false. Override Fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { return super.getChangePayload(oldItemPosition, newItemPosition) } }Copy the code

Call method:

Val callback = PageDiffUtil(mItems, newItems) val difResult = DiffUtil.calculateDiff(callback) mItems.clear() mItems.addAll(newItems) difResult.dispatchUpdatesTo(adapter)Copy the code

Note: if you want to compare data asynchronously, you can use AsyncListDiffer or RecyclerView#ListAdapter.

2.3.5 Support transition animation Transformer

Call way: ViewPager2 setPageTransformer (transformer), if want to perform multiple transformer at the same time, can be written like this:

val multiTransformer = CompositePageTransformer()
multiTransformer.addTransformer(ScaleInTransformer())
multiTransformer.addTransformer(MarginPageTransformer(10))
ViewPager2.setPageTransformer(multiTransformer)
Copy the code

Three source code analysis

3.1 RecyclerView Cache mechanism

Because VP2 is internally based on RecyclerView, so THE cache of VP2 is also based on RecyclerView cache mechanism. Directly look at the cache mechanism of RecyclerView:

The cache Involves the object role Recreate the View View(onCreateViewHolder) Rebind data (onBindViewHolder)
Level 1 cache mAttachedScrap Cache the ViewHolder of the visible scope on the screen false false
The second level cache mCachedViews Cache sliding ViewHolder to be separated from RecyclerView, according to the position or ID of the child View cache, the default maximum storage of 2 false false
Three levels of cache mViewCacheExtension Developer-implemented caching
Four levels of cache mRecyclerPool The ViewHolder cache pool is essentially a SparseArray, where key is ViewType(int type) and value holds ArrayList< ViewHolder>. By default, each ArrayList can hold up to five Viewholders false true

The RecyclerView cache mechanism is used to create the RecyclerView cache. In VP2, mCachedViews and mRecyclerPool are mainly used:

  • mCachedViews: cache slides when about to be withRecyclerViewpage-detachedViewHolderAccording to theThe child ViewthepositionoridCache, the default store 2, can passsetItemViewCacheSize(int size)Example Modify the number of caches. ifRecyclerViewIf precapture is enabled (the default number of precapture is 1), the default size of the cache pool is 3(mCachedViews cache 2 + Precapture number 1).
  • mRecyclerPool:ViewHolderA cache pool, essentially, is aSparseArray, includingkeyisViewType (int).valueDeposit isArrayList< ViewHolder>By default, eachArrayListIs used to store a maximum of fiveViewHolder. Recycled to the cache poolViewHolderWill unbind the data when reusing theViewHolder, the data needs to be rebound (that is, rewalk (onBindViewHolder).

3.2 offscreenPageLimit Off-screen cache

//ViewPager2.java public void setOffscreenPageLimit(@OffscreenPageLimit int limit) { if (limit < 1 && limit ! = OFFSCREEN_PAGE_LIMIT_DEFAULT) { throw new IllegalArgumentException( "Offscreen page limit must be OFFSCREEN_PAGE_LIMIT_DEFAULT or a number > 0"); } mOffscreenPageLimit = limit; // Trigger layout so prefetch happens through getExtraLayoutSize() mRecyclerView.requestLayout(); }Copy the code

SetOffscreenPageLimit sets the number of off-screen display of VP2. The default value is -1, because the layout in RecyclerView is through LayoutManager. So the real off-screen calculation is in VP2 LinearLayoutManagerImpl# calculateExtraLayoutSpace (), the method to calculate the LinearLayoutManager extra space layout, LinearLayoutManagerImpl inherits from LinearLayoutManager:

protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state,
        @NonNull int[] extraLayoutSpace) {
    int pageLimit = getOffscreenPageLimit();
    if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) {
        // Only do custom prefetching of offscreen pages if requested
        super.calculateExtraLayoutSpace(state, extraLayoutSpace);
        return;
    }
    final int offscreenSpace = getPageSize() * pageLimit;
    extraLayoutSpace[0] = offscreenSpace;
    extraLayoutSpace[1] = offscreenSpace;
}
Copy the code

getPageSize()saidViewPager2Width, left and right off-screen size aregetPageSize() * pageLimit.extraLayoutSpace[0]On the left,extraLayoutSpace[1]On the right. Such as setting upoffscreenPageLimitIt’s 1, so you can think of it as expanding the screen 3 times. There’s one off-screen on the left and one off-screen on the rightPageSizeWidth (left and right not visible), as shown:

3.3 principles of FragmentStateAdapter caching

The use of FragmentStateAdapter has been described previously. Because FragmentStateAdapter inherits recyclerview. Adapter, it can be set directly to VP2 using setAdapter. We know that when a FragmentStateAdapter is a Adapter, every Item is a Fragment. How is a Fragment associated with a FragmentStateAdapter? Here’s a try:

//FragmentStateAdapter.java final LongSparseArray<Fragment> mFragments = new LongSparseArray<>(); private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>(); public abstract @NonNull Fragment createFragment(int position); @NonNull @Override public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return FragmentViewHolder.create(parent); } //FragmentViewHolder.java public final class FragmentViewHolder extends ViewHolder { private FragmentViewHolder(@NonNull FrameLayout container) { super(container); } @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) { FrameLayout container = new FrameLayout(parent.getContext()); container.setLayoutParams( new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); / / set a unique ID container. SetId (ViewCompat. GenerateViewId ()); container.setSaveEnabled(false); return new FragmentViewHolder(container); } @NonNull FrameLayout getContainer() { return (FrameLayout) itemView; }}Copy the code

Inside onCreateViewHolder is a ViewHolder called FragmentViewHolder, and the internal root layout is a FrameLayout, and we give that FrameLayout a unique ID, Used later when reusing ViewHolder and Fragment layouts. There are two useful data structures inside the FragmentStateAdapter:

 final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
 private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
Copy the code
  • mFragmentsIs:positionwithFragmentMapping table of. As thepositionThe growth,FragmentIt’s going to be constantly being created.FragmentIt can be cached, recycled, not reused, but recreated.
  • mItemIdToViewHolderIs:positionwithViewHolder#IdMapping table of. Due to theViewHolderisRecyclerViewCaching mechanism of the carrier, so along withpositionThe growth,ViewHolderIt’s going to be reused.

When VP2 slides, the last two items displayed on the current screen will be cached in mCachedViews. When more than two items are displayed, they will be deleted from mCachedViews and transferred to RecyclerPool. In this case, onViewRecycled() will be called as follows:

@Override
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}
Copy the code

When the ViewHolder is recycled into RecyclerPool, the information related to the ViewHolder is deleted. In the previous introduction, we know that onBindViewHolder is not executed when ViewHolder is fetched from mCachedViews. OnBindViewHolder is only executed when ViewHolder is fetched from RecyclerPool. Then take a look at onBindViewHolder:

@Override public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, Int position) {// If mItemIdToViewHolder has the same ID as the current ViewHolder, delete the ID from mItemIdToViewHolder. Final Long itemId = holder.getitemId (); final Long itemId = Holder.getitemId (); final int viewHolderId = holder.getContainer().getId(); final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH if (boundItemId ! = null && boundItemId ! = itemId) { removeFragment(boundItemId); mItemIdToViewHolder.remove(boundItemId); MItemIdToViewHolder mitemidToViewholder. put(itemId, viewHolderId);} // Add viewHolerId back to mItemIdToViewHolder; // This might overwrite an existing entry // Create a Fragment and add it to mFragments ensureFragment(position); final FrameLayout container = holder.getContainer(); if (ViewCompat.isAttachedToWindow(container)) { //... The other... container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { if (container.getParent() ! = null) { container.removeOnLayoutChangeListener(this); / / to the layout of the new fragments attached to the ViewHolder placeFragmentInViewHolder (holder); }}}); } gcFragments(); } private void ensureFragment(int position) {long itemId = getItemId(position);} private void ensureFragment(int position) {long itemId = getItemId(position); if (! Mfragments.containskey (itemId)) {// Create a Fragment Fragment newFragment = createFragment(position); newFragment.setInitialSavedState(mSavedStates.get(itemId)); mFragments.put(itemId, newFragment); }}Copy the code

You can see that the Fragment was created in onBindViewHolder() and added to the mFragments, thus associating the Fragment with the FragmentStateAdapter.

By default, the first two fragments of the current Item and the last one (RecyclerView enables prefetching by default: isItemPrefetchEnabled is true by default). A total of three fragments are cached in mCachedViews. Fragments created at more than 2 places will be destroyed. There is a special case to note: when VP2 slides to the end, the first 3 fragments of the current Item will be cached, because the last Fragment will be moved to the front. When it is first loaded, the subsequent prefetching will not take place at this time because VP2’s onTouch operation has not yet been triggered.

Comparison of ViewPager and ViewPager2

function ViewPager ViewPager2
Listener addPageChangeListener RegisterOnPageChangeCallback (OnPageChangeCallback callback), including OnPageChangeCallback is an abstract class, is different from the interface, use your which overwrite which can be abstract classes
Fragment FragmentPagerAdapter, FragmentStatePagerAdapter FragmentStateAdapter
setOffscreenPageLimit(int num) Off-screen cache: When the value is less than 1, the value is forcibly set to 1, that is, one cache is forcibly set to the left and right OFFSCREEN_PAGE_LIMIT_DEFAULT The default value is -1 and does not leave the screen cache by default
Adapter PagerAdapter RecyclerView.Adapter
Other operating / Supports RTL sorting from right to left, vertical sliding, and stopping user operations

Five reference

[1] official: Use ViewPager2 to slide between fragments 【4】 Talk about the cache and reuse mechanism in ViewPager2