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 modified
Fragment
A 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 drop
fakeDragBy
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 set
offscreenPageLimit
When off screen cache,VP2
In theRecyclerView
The default inmCachedViews
The first two in the cacheItem
And the one that I pre-grabbedItem
. - If you set it up
offscreenPageLimit
If the value is 1, a cache is added for the left and right off-screenItem
Increase the width of the canvas by 3 times (the left and right are not visible by default) and addRecyclerView
Default cache of 3, in addition to the current displayItem
And 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 toVP2
The inside of theRecyclerView
, and then through the Settingspadding
To 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 withRecyclerView
page-detachedViewHolder
According to theThe child View
theposition
orid
Cache, the default store 2, can passsetItemViewCacheSize(int size)
Example Modify the number of caches. ifRecyclerView
If 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
:ViewHolder
A cache pool, essentially, is aSparseArray
, includingkey
isViewType (int)
.value
Deposit isArrayList< ViewHolder>
By default, eachArrayList
Is used to store a maximum of fiveViewHolder
. Recycled to the cache poolViewHolder
Will 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()
saidViewPager2
Width, left and right off-screen size aregetPageSize() * pageLimit
.extraLayoutSpace[0]
On the left,extraLayoutSpace[1]
On the right. Such as setting upoffscreenPageLimit
It’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 rightPageSize
Width (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
mFragments
Is:position
withFragment
Mapping table of. As theposition
The growth,Fragment
It’s going to be constantly being created.Fragment
It can be cached, recycled, not reused, but recreated.mItemIdToViewHolder
Is:position
withViewHolder#Id
Mapping table of. Due to theViewHolder
isRecyclerView
Caching mechanism of the carrier, so along withposition
The growth,ViewHolder
It’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