1. Introduction
ViewPager2 is known as an alternative version of ViewPager. It addresses some of ViewPager’s pain points, including support for right-to-left layouts, vertical scrolling, and modifiable Fragment collections. ViewPager2 internal is to use RecyclerView to achieve.
So it inherits the advantages of RecyclerView, including but not limited to the following:
- Supports horizontal and vertical layout
- Nested sliding is supported
- Support for ItemPrefetch
- Support for three-level caching
ViewPager2 relative to RecyclerView, it also extends the following functions
- Supports shielding user touch functionsetUserInputEnabled
- Support analog drag and dropfakeDragBy
- Supports off-screen displaysetOffscreenPageLimit
- An adapter that supports displaying fragmentsFragmentStateAdapter
If you’re familiar with RecyclerView, getting started with ViewPager2 should be very simple. You can simply think of ViewPager2 as a full-screen RecyclerView for every ItemView. This article focuses on ViewPager2’s off-screen display and fragmentState Adapter-based caching.
2. Review RecyclerView cache mechanism
In this chapter, we briefly review the RecyclerView cache mechanism. RecyclerView has three levels of cache. For simplicity, only mViewCaches and mRecyclerPool are introduced here. More about the principle of RecyclerView cache, please move to the public number related articles.
-
MViewCaches: The cache is closer to UI and more efficient. Its feature is that ViewHolder can be reused directly without rebinding as long as position can be matched. The cache pool is realized by queue, first-in, first-out, and the default size is 2. The size of the cache pool is 2+ the number of precaptures. The default number of precaptures is 1. Therefore, the size of the pre-capture cache pool is 3 by default.
-
MRecyclerPool: This cache pool has the longest UI and is less efficient than mViewCaches. The ViewHolder recycled to the cache pool will unbind data. When the ViewHolder is reused, the data needs to be rebound. Its data structure is similar to a HashMap. The key is itemType, the value is an array, and the value stores ViewHolder. The default size of the array is 5. A maximum of 5 ViewHolder can be stored for each itemType.
3. The principle of offscreenPageLimit
/ / androidx. Viewpager2: viewpager2:1.0.0 @ aar / / 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; mRecyclerView.requestLayout(); }Copy the code
Call the setOffscreenPageLimit method to set the number of off-screen displays for ViewPager2. The default value is -1. If the Settings are not correct, exceptions will be thrown. We see that the method just assigns mOffscreenPageLimit. Why can achieve off-screen display function? The following code
/ / androidx. Viewpager2: viewpager2:1.0.0 @ aar / / viewpager2 $LinearLayoutManagerImpl @ Override protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, @NonNull int[] extraLayoutSpace) { int pageLimit = getOffscreenPageLimit(); if (pageLimit == OFFSCREEN_PAGE_LIMIT_DEFAULT) { super.calculateExtraLayoutSpace(state, extraLayoutSpace); return; } final int offscreenSpace = getPageSize() * pageLimit; extraLayoutSpace[0] = offscreenSpace; extraLayoutSpace[1] = offscreenSpace; }Copy the code
For example, slide ViewPager2 horizontally: getPageSize() indicates the width of ViewPager2, and the off-screen space is getPageSize() * pageLimit. ExtraLayoutSpace [0] represents the size on the left, and extraLayoutSpace[1] represents the size on the right.
Assuming offscreenPageLimit is set to 1, Android simply increases the canvas width by 3 times by default. There’s an off-screen ViewPager2 width on each side.
4. Principle of FragmentStateAdapter and cache mechanism
4.1 Simple Use
FragmentStateAdapter inherits from recyclerView. Adapter. It has an abstract method, createFragment(). It’s a perfect combination of Fragments and ViewPager2.
public abstract class FragmentStateAdapter extends RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter { public abstract Fragment createFragment(int position); }Copy the code
Using FragmentStateAdapter is very simple, as shown in the following Demo
class ViewPager2WithFragmentsActivity : AppCompatActivity() { private lateinit var mViewPager2: ViewPager2 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_recycler_view_view_pager2) mViewPager2 = findViewById(R.id.viewPager2) (mViewPager2.getChildAt(0) as RecyclerView).layoutManager? .apply { // isItemPrefetchEnabled = false } mViewPager2.orientation = ViewPager2.ORIENTATION_VERTICAL mViewPager2.adapter = MyAdapter(this) // mViewPager2.offscreenPageLimit = 1 } inner class MyAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) { override fun getItemCount(): Int { return 100 } override fun createFragment(position: Int): Fragment { return MyFragment("Item $position") } } class MyFragment(val text: String) : Fragment() { init { println("MyFragment $text") } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle? ) : View? { var view = layoutInflater.inflate(R.layout.view_item_view_pager_snap, container) view.findViewById<TextView>(R.id.text_view).text = text return view; }}}Copy the code
4.2 the principle
First, the ViewHolder corresponding to the FragmentStateAdapter is defined as follows, which simply returns a FrameLayout with an ID. As you can see, the FragmentStateAdapter does not reuse fragments; it simply reuses FrameLayout.
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)); container.setId(ViewCompat.generateViewId()); container.setSaveEnabled(false); return new FragmentViewHolder(container); } @NonNull FrameLayout getContainer() { return (FrameLayout) itemView; }}Copy the code
I then introduce two very important data structures in the FragmentStateAdapter:
final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
private final LongSparseArray<Integer> mItemIdToViewHolder = new LongSparseArray<>();
Copy the code
- MFragments: is a mapping table of position and Fragment.As position grows, fragments are constantly created. Fragments can be cached and cannot be reused when recycled.
When will fragments be recycled?
- MItemIdToViewHolder: is a mapping between position and the Id of the ViewHolder.Because ViewHolder is the carrier of RecyclerView cache mechanism. Therefore, with the growth of position, ViewHolder will not be constantly created like Fragment, but will make full use of RecyclerView reuse mechanism. So, in the figure below, a big question mark is placed in Position 4. The specific value is uncertain, which is determined by the size of the cache and the number of off-screen.
So let’s go on to onViewRecycled(). This method is called when ViewHolder is moved from the mViewCaches cache to the mRecyclerPool cache
@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 a ViewHolder is recycled into RecyclerPool, the viewholer-related information is removed from the two tables.
For example, when ViewHolder1 is recycled, the information corresponding to Position 0 is deleted from the two tables
Finally, the onBindViewHolder method is explained
@Override public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) { 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.put(itemId, viewHolderId); // this might overwrite an existing entry ensureFragment(position); /** Special case when {@link RecyclerView} decides to keep the {@link container} * attached to the window, but not to the view hierarchy (i.e. parent is null) */ final FrameLayout container = holder.getContainer(); if (ViewCompat.isAttachedToWindow(container)) { if (container.getParent() ! = null) { throw new IllegalStateException("Design assumption violated."); } 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); placeFragmentInViewHolder(holder); }}}); } gcFragments(); }Copy the code
The method can be divided into three parts:
- Check whether the reused ViewHolder has any residual data in both tables, and remove it from both tables if it does.
- Create a Fragment and register the ViewHolder with the Fragment and position information in the two tables
- Display the Fragment on ViewPager2 when appropriate.
This is the general outline, but other detailed and important methods are not listed to avoid redundancy
5. Explain the recycling mechanism
5.1 Default Settings
Default: offscreenPageLimit = -1. The prefetching function is enabled
MViewCaches are 3 because prefetching is enabled.
- You just go into ViewPager2, no Touch event is triggered, no prefetch is triggered, so only Fragment1
- Sliding on Fragment2 triggers a Fragment3 pre-grab, and since offscreenPageLimit = -1, only Fragment2 is displayed on ViewPager2, 1 and 3 go into the mViewCaches cache
- Slide to Fragment3. 1, 2, 4 go to mViewCaches cache
- Slide to Fragment4. 2, 3, and 5 go to the mViewCaches cache. Since there are 3 caches, 1 is squeezed into the mRecyclerPool cache, and Fragment1 is removed from the mFragments
- Slide to Fragment5. Fragment6 will reuse the ViewHolder corresponding to Fragment1. 3, 4, 6 go into the mViewCaches cache, 2 is squeezed out into the mRecyclerPool cache
5.2 offscreenPageLimit = 1
OffscreenPageLimit =1, ViewPager2 can display 3 fragments at once, one on the left and one on the right
- Fragment1 has no data to the left, so the screen is just 1 and 2
- 1, 2, 3 are displayed on the screen while prefetching 4 into the mViewCaches
- 2, 3, 4 are displayed on the screen, 1 and 5 are placed in the mViewCaches
- 3, 4, 5 are displayed on the screen, 1, 2, 6 are placed in mViewCaches
- 4, 5, 6 are displayed on the screen, 2, 3, 7 are added to the mViewCaches, and 1 is recycled to the mRecyclerPool cache. Fragment1 is also removed from mFragments