1. 前言
众所周知ViewPager2是ViewPager的替代版本。它解决了ViewPager的一些痛点,包括支持right-to-left布局,支持垂直方向滑动,支持可修改的Fragment集合等。ViewPager2内部是使用RecyclerView来实现的。
所以它继承了RecyclerView的优势,包含但不限于以下
:
- 支持横向和垂直方向布局
- 支持嵌套滑动
- 支持ItemPrefetch(预加载)功能
- 支持三级缓存
ViewPager2相对于RecyclerView,它又扩展出了以下功能
- 支持屏蔽用户触摸功能setUserInputEnabled
- 支持模拟拖拽功能fakeDragBy
- 支持离屏显示功能setOffscreenPageLimit
- 支持显示Fragment的适配器FragmentStateAdapter
如果熟悉RecyclerView,那么上手ViewPager2将会非常简单。可以简单把ViewPager2想象成每个ItemView都是全屏的RecyclerView。本文将重点讲解ViewPager2的离屏显示功能和基于FragmentStateAdapter的缓存机制。
2. 回顾RecyclerView缓存机制
本章节,简单回顾下RecyclerView缓存机制。RecyclerView有三级缓存,简单起见,这里只介绍mViewCaches和mRecyclerPool两种缓存池。更多关于RecyclerView的缓存原理,请移步公众号相关文章。
- mViewCaches:该缓存离UI更近,效率更高,它的特点是只要position能对应上,就可以直接复用ViewHolder,无需重新绑定,该缓存池是用队列实现的,先进先出,默认大小为2,如果RecyclerView开启了预抓取功能,则缓存池大小为2+预抓取个数,默认预抓取个数为1。所以默认开启预抓取缓存池大小为3。
- mRecyclerPool:该缓存池离UI最远,效率比mViewCaches低,回收到该缓存池的ViewHolder会将数据解绑,当复用该ViewHolder时,需要重新绑定数据。它的数据结构是类似HashMap。key为itemType,value是数组,value存储ViewHolder,数组默认大小为5,最多每种itemType的ViewHolder可以存储5个。
3. 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(); }
调用setOffscreenPageLimit方法就可以为ViewPager2设置离屏显示的个数,默认值为-1。如果设置不当,会抛异常。我们看到该方法,只是给mOffscreenPageLimit赋值。为什么就能实现离屏显示功能呢?如下代码
//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; }
以水平滑动ViewPager2为例:getPageSize()表示ViewPager2的宽度,离屏的空间大小为getPageSize() * pageLimit。extraLayoutSpace[0]表示左边的大小,extraLayoutSpace[1]表示右边的大小。
假设设置offscreenPageLimit为1,简单讲,Android系统会默认把画布宽度增加到3倍。左右两边各有一个离屏ViewPager2的宽度。
4. FragmentStateAdapter原理以及缓存机制
4.1 简单使用
FragmentStateAdapter继承自RecyclerView.Adapter。它有一个抽象方法,createFragment()。它能将Fragment与ViewPager2完美结合。
public abstract class FragmentStateAdapter extends RecyclerView.Adapter implements StatefulAdapter { public abstract Fragment createFragment(int position); }
使用FragmentStateAdapter非常简单,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(R.id.text_view).text = text return view; } } }
4.2 原理
首先
FragmentStateAdapter对应的ViewHolder定义如下,它只是返回一个简单的带有id的FrameLayout。由此可以看出,FragmentStateAdapter并不复用Fragment,它仅仅是复用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; } }
然后
介绍FragmentStateAdapter中两个非常重要的数据结构:
final LongSparseArray mFragments = new LongSparseArray<>(); private final LongSparseArray mItemIdToViewHolder = new LongSparseArray<>();
- mFragments:是position与Fragment的映射表。随着position的增长,Fragment是会不断的新建出来的。 Fragment可以被缓存起来,当它被回收后无法重复使用。
❝
Fragment什么时候会被回收掉呢?
mItemIdToViewHolder:是position与ViewHolder的Id的映射表。由于ViewHolder是RecyclerView缓存机制的载体。所以随着position的增长,ViewHolder并不会像Fragment那样不断的新建出来,而是会充分利用RecyclerView的复用机制。所以如下图,position 4处打上了一个大大的问号,具体的值是不确定的,它由缓存的大小以及离屏个数共同决定的。
接下来
我们讲解onViewRecycled()。当ViewHolder从mViewCaches缓存中移出到mRecyclerPool缓存中时会调用该方法
@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); } }
该方法的作用是,当ViewHolder回收到RecyclerPool中时,将ViewHolder相关的信息从上面两张表中移除。
举例
当ViewHolder1发生回收时,position 0对应的信息从两张表中删除
最后
讲解onBindViewHolder方法
@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(); }
该方法可以分成3个部分:
- 检查该复用的ViewHolder在两张表中是否还有残留的数据,如果有,将它从两张表中移除掉。
- 新建Fragment,并将ViewHolder与Fragment和position的信息注册到两张表中
- 在合适的时机把Fragment展示在ViewPager2上。
大概的脉络就是这样,为了避免文章冗余,其它的细支且也蛮重要的方法就没有列出来
5. 案例讲解回收机制
5.1 默认情况
❝
默认情况:offscreenPageLimit = -1,开启预抓取功能
因为开启了预抓取,所以mViewCaches大小为3。
- 刚开始进入ViewPager2,没有触发Touch事件,不会触发预抓取,所以只有Fragment1
- 滑动到Fragment2,会触发Fragment3预抓取,由于offscreenPageLimit = -1,所以只有Fragment2会展示在ViewPager2上,1和3进入mViewCaches缓存中
- 滑动到Fragment3。1、2、4进入mViewCaches缓存中
- 滑动到Fragment4。2、3、5进入mViewCaches缓存中,由于缓存数量为3,所以1被挤出到mRecyclerPool缓存中,同时把Fragment1从mFragments中移除掉
- 滑动到Fragment5。Fragment6会复用Fragment1对应的ViewHolder。3、4、6进入mViewCaches缓存中,2被挤出到mRecyclerPool缓存中
5.2 offscreenPageLimit=1
offscreenPageLimit=1,所以ViewPager2一下子能展示3屏Fragment,左右各显示一屏
- Fragment1左边没有数据,所以屏幕只有1和2
- 滑动到fragment2,1、2、3显示在屏幕上(1和3肉眼不可见,下同),同时预抓取4放入mViewCaches
- 滑动到fragment3,2、3、4显示在屏幕上,1和5放入mViewCaches
- 滑动到fragment4,3、4、5显示在屏幕上,1、2、6放入mViewCaches
- 滑动到fragment5,4、5、6显示在屏幕上,2、3、7放入mViewCaches,1被回收到mRecyclerPool缓存中。Fragment1同时从mFragments中删除掉
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/20222,转载请注明出处。
评论0