前言:
每当使用ViewPager
时,对于选用什么适配器
,缓存多少页面
,是否需要懒加载
以及Fragment
的数据刷新
经常会有些疑问,网络上的答案很多,但是很少有一篇能够对一些疑问进行总结,本文主要在于记录,方便日后查看。
正文
1. FragmentPagerAdapter和FragmentPagerStateAdapter的区别及使用场景
setOffScreenPageLimit(int limit)设置viewpager左右预加载页
viewPager.setOffscreenPageLimit(1);
区别:
FragmentPagerAdapter
将每一个生成的Fragment
保存在内存
中,limit外Fragment
没有销毁,生命周期为onPause->onStop->onDestroyView , onCreateView ->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View,避免重复inflate
。
private View mRootView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(TAG, "onCreateView: page_" + mPosition);
if (mRootView == null) {
mRootView = inflater.inflate(R.layout.fragment_test, container, false);
initView(mRootView);
}
return mRootView;
}
FragmentStatePagerAdapter
对limit外的Fragment
销毁,生命周期为onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。
使用场景:
对于需要缓存在内存中的固定较少数量的静态页面使用FragmentPagerAdapter
,如引导页
,Tab页面
;
对于拥有大量页面的情况应使用FragmentStatePagerAdapter
避免占用大量内存,如图片预览
。
2.是否有必要在适配器的public Fragment getItem(int position)方法中返回缓存List<Fragment>中的Fragment
对于FragmentPagerAdapter
,instantiateItem()
先从FragmentManager.findFragmentByTag()
中查找FragmentManager
中List
缓存的Fragment
取不到则会调用getItem()
,所以对于缓存在内存中的FragmentPagerAdapter
没有必要再使用一个List
缓存Fragment
,因为FragmentPagerAdapter
会缓存
每一个加载过的Fragment
到内存
中。
对于FragmentStatePagerAdapter
的instantiateItem()
则会缓存limit左右的Fragment
,超过limit则会回收
,当Fragment
没有缓存时重新调用getItem()
,因为页面比较多,所以也没必要使用List
缓存Fragment
占用内存,否则FragmentStatePagerAdapter
没有意义。
3.ViewPager为什么要懒加载,什么情况适用?
ViewPager
的setOffScreenPageLimit()
方法默认limit
为1,即会预加载左右页面,而为了节省流量,理想情况是当用户切换到该界面时才会调用网络请求获取数据。相关方法为setUserVisibleHint()
,当前页面为true,预加载页面为false,只有Fragment
从可见到不可见或者从不可见到可见时会调用,Fragment
初次创建时setUserVisibleHint
先于onCreateView()
调用,所以可以由此判断Fragment
是否初始创建。
ViewPager
首次显示的页面经过方法调用setUserVisibleHint(false)->setUserVisibleHint(true)->onCreateView()...,所以该页面的数据加载放在onCreateView中;其它预加载页面预加载时setUserVisibleHint(false)->onCreateView()...,当选中该页面显示时调用setUserVisibleHint(true),所以预加载页面数据加载放在setUserVisibleHint
中。
/**
* 延迟加载Fragment
*/
public abstract class LazyLoadFragment extends BaseFragment {
protected boolean bIsViewCreated;
protected boolean bIsDataLoaded;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutResId(), container, false);
initView(view);
bIsViewCreated = true;
if (getUserVisibleHint() && !bIsDataLoaded) {
loadData();
bIsDataLoaded = true;
}
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bIsViewCreated = false;
bIsDataLoaded = false;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser && bIsViewCreated && !bIsDataLoaded) {
loadData();
bIsDataLoaded = true;
}
}
/**
* @return 布局资源id
*/
protected abstract int getLayoutResId();
/**
* 初始化View
*/
protected abstract void initView(View view);
/**
* 加载数据
*/
protected abstract void loadData();
}
因为懒加载
需要设置setOffScreenPageLimit
,所以适合有网络请求
、页面较少
且需要缓存
的Tab页面,配合FragmentPagerAdapter
使用,因为limit要包括所有的界面,在limit内FragmentStatePagerAdapter
和FragmentPagerAdapter
没有区别。
4.ViewPager刷新数据
一般使用PagerAdapter
的notifyDataSetChanged
方法来刷新数据,但是很多时候数据没有更新,先来看PagerAdapter
的notifyDataSetChanged
方法
观察者模式:
ViewPager中的PagerObserver实现了DateSetObserver
ViewPager中的dataSetChanged方法会根据adapter.getItemPosition返回的值来判断是否DestroyItem
getItemPosition默认会返回POSITION_UNCHANGED,而ViewPager中dataSetChanged只有当返回POSITION_NONE时才会销毁页面重新创建
继续看ViewPager中dataSetChanged方法
接着到populate方法
终于跑到adapter的instantiateItem方法了
所以如果想通过adapter.notifyDataSetChanged
来刷新页面时,必须继承FragmentStatePagerAdapter
,因为FragmentPagerAdapter
会缓存Fragment
,不会走getItem
方法,同时将所要刷新页面的getItemPosition
返回POSITION_NONE
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
还有其他的一种做法,拿到Fragment
,通过Fragment
中的public方法来刷新页面,由FragmentPagerAdapter
的instantiateItem
方法内部通过tag
查找Fragment,因此可以保存其相同的tag
private SparseArray<String> mTags = new SparseArray<>();
@Override
public Object instantiateItem(ViewGroup container, int position) {
mTags.put(position, makeFragmentName(container.getId(), position));
return super.instantiateItem(container, position);
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
mTags.remove(position);
super.destroyItem(container, position, object);
}
private String makeFragmentName(int viewId, int position) {
return "android:switcher:" + viewId + ":" + position;
}
然后获取Fragment
Fragment fragment = getSupportFragmentManager().findFragmentByTag(mTags.get(position));
fragment.XXX();
由第二节FragmentStatePagerAdapter
的instantiateItem
方法可知,其保存时没有对Fragment添加tag,ViewPager中的Fragment也不能指定id,只有通过调用
Fragment fragment = (Fragment)(fragmentStatePagerAdapter.instantiateItem(viewpager, position));
来获取Fragment
总结
有Tab时:
需要设置setOffScreenPageLimit,FragmentPageAdapter
和FragmentStatePageAdapter
效果相同,让Fragment
都缓存在内存
中,否则Fragment
销毁了再次点击Tab
选中又会重新创建会很突兀。需要网络请求时则执行延迟加载
策略,无需网络请求时可以正常创建Fragment。无Tab时:
无需设置SetOffScreenPageLimit,因为默认limit是1,会预加载左右界面,不会显得突兀。页面较多时则选用占用内存少的FragmentStatePageAdapter
,如浏览大图页面;页面较少时则选用加载到内存的FragmentPageAdapter
, 如引导页,需要注意的是FragmentPageAdapter在limit外的Fragment没有销毁,生命周期为onPause->onStop->onDestroyView, onCreateView->onStart->onResume,但Fragment的成员变量都没有变,所以可以缓存根View。如果需要刷新所有limit内的页面,继承
FragmentStatePagerAdapter
, 设置getItemPosition返回POSITION_NONE,再调用notifyDataSetChanged;如果只需要刷新单个页面,则通过获取Fragment的引用,再通过public方法来更新数据。