ViewPager2的使用(生命周期以及懒加载)

::: hljs-center
基本使用方法
:::
1.添加引用
implementation 'androidx.viewpager2:viewpager2:1.0.0'
2.添加布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
3.需要注意
ViewPager2中加载Fragment时的Adapter类需要继承自FragmentStateAdapter,而不是ViewPager中的FragmentStatePagerAdapter。
public class MyFragmentPagerAdapter extends FragmentStateAdapter {
private List<Fragment> mFragments;
public MyFragmentPagerAdapter(@NonNull FragmentActivity fragmentActivity, List<Fragment> fragments) {
super(fragmentActivity);
this.mFragments = fragments;
}
@NonNull
@Override
public Fragment createFragment(int position) {
return mFragments.get(position);
}
@Override
public int getItemCount() {
return mFragments.size();
}
}
4.为ViewPager2设置Adapter
ViewPager2 mViewPager2 = findViewById(R.id.view_pager2);
List<Fragment> mFragments = new ArrayList<>();
mFragments .add(new FirstFragment());
mFragments .add(new SecondFragment());
mFragments .add(new ThirdFragment());
MyFragmentPagerAdapter mAdapter = new MyFragmentPagerAdapter(this, mFragments);
mViewPager2.setAdapter(mAdapter);
## 运行观察每个fragment的生命周期发现===》
- 可以看出此时只创建出了第一个Fragment,生命周期方法执行到了onResume(),其他的几个Fragment并没有创建。
- 此时创建出了第二个Fragment,生命周期方法同样执行到onResume(),同时,第一个Fragment执行onPause()方法。
- 切换到第三个Fragment, 创建出第三个Fragment,执行到onResume()方法,同时第二个Fragment执行onPause()方法。
- 切换到第四个Fragment, 和前两种情况相同,同样是创建出当前Fragment,生命周期方法执行到onResume(),并且上一个 Fragment执行onPause()方法。不同的是,此时会销毁第一个Fragment,依次执行onStop()、onDestroyView()、onDestroy()和onDetach()方法。
- 切换到第五个Fragment, 和上一种情况相同,创建出第五个Fragment,生命周期方法执行到onResume(),第四个Fragment执行onPause()方法,同时销毁第二个Fragment。
- 切换到第六个(最后一个)Fragment,可以看出此时创建出了第六个Fragment,生命周期方法执行到onResume(),第五个Fragment执行onPause()方法.
如果按照上面两种情况的执行结果来看,此时应该会销毁第三个Fragment,但实际上并没有。这个是为什么?
通过示例中的执行结果我们可以发现ViewPager2默认情况下不会像ViewPager那样预先加载出两侧的Fragment,这是为什么呢,我们可能会想到ViewPager中预加载相关的一个方法:setOffscreenPageLimit(),ViewPager2中也定义了该方法,我们来看一下它们的区别。 1. 来看ViewPager中的setOffscreenPageLimit()方法: 方法传入一个整型数值,表示当前Fragment两侧的预加载数量,很多人可能都知道,ViewPager默认的预加载数量为1,也就是会预先创建出当前Fragment左右两侧的一个Fragment。从代码中我们可以看出,如果我们传入的数值小于1,依然会将预加载数量设置为1,这也导致了ViewPager无法取消预加载,也因此才会需要Fragment的懒加载方案。 2. ViewPager2中默认的预加载数量mOffscreenPageLimit为OFFSCREEN_PAGE_LIMIT_DEFAULT也就是-1,我们可以通过传入该默认值或者大于1的整数来设置预加载数量。所以他默认就是懒加载的模式。如果改成1,就和viewpager一样,预加载左右两边各一个的fragment。但是为什么最后一个加载完成后上上一个fragment却没有销毁呢》其实这就和recycleview的机制有关系了。
因为我们的viewpager是基于recycleview的——>recycleview的内部缓存原理:
RecyclerView的缓存机制算是老生常谈的问题了,核心在它的一个内部类Recycler中,Item的回收和复用相关工作都是Recycler来进行的,RecyclerView的缓存可以分为多级,由于我了解得非常浅显,这里就不详细介绍了,大家可以自行查看相关文章。我们直接来看和ViewPager2中Fragment回收相关的缓存——mCachedViews,它的类型是ArrayList,移出屏幕的Item对应的ViewHolder都会被优先缓存到该容器中。Recycler类中有一个成员变量mViewCacheMax,表示mCachedViews最大的缓存数量,默认值为2,我们可以通过调用RecyclerView的setItemViewCacheSize()方法来设置缓存大小。
回到我们的具体场景中,通过查看FragmentStateAdapter类的源码,我们可以看到,此时mCachedViews中保存的ViewHolder类型为FragmentViewHolder,它的视图根布局是一个FrameLayout,Fragment会被添加到对应的FrameLayout中,因此缓存ViewHolder其实就相当于缓存了Fragment,为了简明,我后面就都说成缓存Fragment了,大家清楚这样说是不准确的就好了。在上面的示例中,我们使用ViewPager2加载了6个Fragment,当切换到第四个Fragment时,由于最多只能缓存两个Fragment,此时mCachedViews中缓存的是第二个Fragment和第三个Fragment,因此第一个Fragment就要被销毁,之后切换到第五个Fragment的情况同理,此时会缓存第三个和第四个Fragment,因此第二个Fragment被销毁。接下来问题就来了,如果按照这样的解释,当切换到第六个Fragment时应该销毁第三个Fragment,上面的示例中很明显没有啊,这又是为什么呢? 这就涉及到RecyclerView的预取(Prefetch)机制了,它是官方在support v25版本包中引入的功能,具体表现为在RecyclerView滑动时会预先加载出下一个Item,准确地说是预先创建出下一个Item对应的ViewHolder。默认情况下预取功能是开启的,我们可以调用下面的代码来关闭:
mRecyclerView.getLayoutManager().setItemPrefetchEnabled(false);
这个预取机制对我们的fragment的销毁是有影响的,因为我们滑到最后一个就预取不到下一个了(分析源码可知) 当切换到第六个也就是最后一个Fragment时,不需要再预取下一个Fragment了,但是此时mCachedViews的最大缓存数量依然为3,所以第三个Fragment也可以被添加到缓存中,不会被销毁。 为了验证得出的结论,我们首先通过代码取消ViewPager2内部RecyclerView的预取机制:
((RecyclerView) mViewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
可以看出当切换到最后一个Fragment时会销毁掉第三个Fragment,此时缓存的Fragment为第四和第五个,这是由于我们关闭了预取机制,在执行LayoutPrefetchRegistryImpl中的collectPrefetchPositionsFromView()方法时不满足layout.isItemPrefetchEnabled()为true的条件,不会执行后面的逻辑,因此mCachedViews的最大缓存数量始终为2,这就验证了我们的结论是没错的。 解释完毕。。。。。。
懒加载需要注意的地方
由于ViewPager2默认情况下不会预加载出两边的Fragment,相当于默认就是懒加载的,因此如果我们如果没有通过setOffscreenPageLimit()方法设置预加载数量,完全可以不做任何额外处理。但是对于Fragment很多的情况,由于ViewPager2中的RecyclerView可以缓存Fragment的数量是有限的,因此会造成Fragment的多次销毁和创建,如何解决这个问题呢?下面就介绍一下我的解决方案。 首先设置ViewPager2的预加载数量,让ViewPager2预先创建出所有的Fragment,防止切换造成的频繁销毁和创建。
mViewPager2.setOffscreenPageLimit(mFragments.size());
通过此前示例中Fragment切换时生命周期方法的执行情况我们不难发现不管Fragment是否会被预先创建,只有可见时才会执行到onResume()方法,我们正好可以利用这一规律来实现懒加载 将Fragment加载数据的逻辑放到onResume()方法中,这样就保证了Fragment可见时才会加载数据。 声明一个变量标记是否是首次执行onResume()方法,因为每次Fragment由不可见变为可见都会执行onResume()方法,需要防止数据的重复加载。 按照以上两点就可以封装我们的懒加载Fragment了,完整代码如下:
public abstract class LazyFragment extends Fragment {
private Context mContext;
private boolean isFirstLoad = true; // 是否第一次加载
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = LayoutInflater.from(mContext).inflate(getContentViewId(), null);
initView(view);
return view;
}
@Override
public void onResume() {
super.onResume();
if (isFirstLoad) {
// 将数据加载逻辑放到onResume()方法中
initData();
initEvent();
isFirstLoad = false;
}
}
/**
* 设置布局资源id
*
* @return
*/
protected abstract int getContentViewId();
/**
* 初始化视图
*
* @param view
*/
protected void initView(View view) {
}
/**
* 初始化数据
*/
protected void initData() {
}
/**
* 初始化事件
*/
protected void initEvent() {
}
}