懒加载,就是滑动Viewpager的过程中,当fragment显示的时候才去加载数据,但是由于ViewPager的预加载机制,会提前初始化左右两边的fragment,那么,要想实现懒加载,就需要一些骚操作了
骚一、既然Viewpager有个setOffscreenPageLimit(int limit)方法,那么是不是我们就可以直接将里面的参数设置为0,让他不预加载页面不就可以嘿嘿嘿了,我只想说,小伙子,你的思想很危险啊,看源码
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
+ DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
}
DEFAULT_OFFSCREEN_PAGES//这个的默认值是1
也就是说最少预加载一个页面,次操作失败
骚二、既然不能控制Viewpager的预加载,那么只能另辟蹊径,还有另外一个函数,setUserVisibleHint(boolean isVisibleToUser);,这个就是fragment是否可见的回调函数,但是,经过测试,这个函数也有玄机,这个函数的调用时机是两个:一个是fragment初始化之前会被调用,那个时候view还没被创建出来,一个是fragment从可见到不可见和由不可见到可见的时候会被调用,这个时候就需要做一些判断了,好了,先看代码吧
这次封装的支持以下的功能:
1.支持数据的懒加载并且只加载一次
2.提供 Fragment 可见与不可见时回调,支持你在这里进行一些 ui 操作,如显示/隐藏加载框
3.支持 view 的复用,防止与 ViewPager 使用时出现重复创建 view 的问题
第一点应该是比较需要且常用的一点,之前那篇博客里没有考虑到这点应用场景是我的疏忽。稍微讲解一下,有些时候,我们打开一个 Fragment 页面时,希望它是在可见时才去加载数据,也就是不要在后台就开始加载数据,而且,我们也希望加载数据的操作只是第一次打开该 Fragment 时才进行的操作,以后如果再重新打开该 Fragment 的话,就不要再重复的去加载数据了。
onCreat()
或者 onCreateView()
里去跟服务器交互,下载界面数据,那么这时这些已经被创建的 Fragment,就都会出现在后台下载数据的情况了。所以我们通常需要在 setUserVisibleHint()
setUserVisibleHint()
做了很多判断,实现了可见时加载并且只有第一次可见时才加载,可能还是会遇到其他问题。比如说,我下载完数据就直接需要对 ui 进行操作,将数据展示出来,但有时却报了 ui 控件 null 异常,这是因为 setUserVisibleHint()
有可能在 onCreateView()
除了懒加载,只加载一次的需求外,可能我们还需要每次 Fragment 的打开或关闭时显示数据加载进度。对吧,我们打开一个 Fragment 时,如果数据还没下载完,那么应该给个下载进度或者加载框提示,如果这个时候打开了新的 Fragment 页面,然后又重新返回时,如果数据还没加载完,那么也还应该继续给提示,对吧。这就需要有个 Fragment 可见与不可见时触发的回调方法,并且该方法还得保证是在 view 创建完后才触发的,这样才能支持对 ui 进行操作。
以上,就是我们封装的 BaseFragment 基类要干的活了。下面上代码。
代码
/**
* Created by dasu on 2016/9/27.
*
* Fragment基类,封装了懒加载的实现
*
* 1、Viewpager + Fragment情况下,fragment的生命周期因Viewpager的缓存机制而失去了具体意义
* 该抽象类自定义新的回调方法,当fragment可见状态改变时会触发的回调方法,和 Fragment 第一次可见时会回调的方法
*
* @see #onFragmentVisibleChange(boolean)
* @see #onFragmentFirstVisible()
*/
public abstract class BaseFragment extends Fragment {
private static final String TAG = BaseFragment.class.getSimpleName();
private boolean isFragmentVisible;
private boolean isReuseView;
private boolean isFirstVisible;
private View rootView;
//setUserVisibleHint()在Fragment创建时会先被调用一次,传入isVisibleToUser = false
//如果当前Fragment可见,那么setUserVisibleHint()会再次被调用一次,传入isVisibleToUser = true
//如果Fragment从可见->不可见,那么setUserVisibleHint()也会被调用,传入isVisibleToUser = false
//总结:setUserVisibleHint()除了Fragment的可见状态发生变化时会被回调外,在new Fragment()时也会被回调
//如果我们需要在 Fragment 可见与不可见时干点事,用这个的话就会有多余的回调了,那么就需要重新封装一个
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
//setUserVisibleHint()有可能在fragment的生命周期外被调用
if (rootView == null) {
return;
}
if (isFirstVisible && isVisibleToUser) {
onFragmentFirstVisible();
isFirstVisible = false;
}
if (isVisibleToUser) {
onFragmentVisibleChange(true);
isFragmentVisible = true;
return;
}
if (isFragmentVisible) {
isFragmentVisible = false;
onFragmentVisibleChange(false);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariable();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
//如果setUserVisibleHint()在rootView创建前调用时,那么
//就等到rootView创建完后才回调onFragmentVisibleChange(true)
//保证onFragmentVisibleChange()的回调发生在rootView创建完成之后,以便支持ui操作
if (rootView == null) {
rootView = view;
if (getUserVisibleHint()) {
if (isFirstVisible) {
onFragmentFirstVisible();
isFirstVisible = false;
}
onFragmentVisibleChange(true);
isFragmentVisible = true;
}
}
super.onViewCreated(isReuseView ? rootView : view, savedInstanceState);
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
initVariable();
}
private void initVariable() {
isFirstVisible = true;
isFragmentVisible = false;
rootView = null;
isReuseView = true;
}
/**
* 设置是否使用 view 的复用,默认开启
* view 的复用是指,ViewPager 在销毁和重建 Fragment 时会不断调用 onCreateView() -> onDestroyView()
* 之间的生命函数,这样可能会出现重复创建 view 的情况,导致界面上显示多个相同的 Fragment
* view 的复用其实就是指保存第一次创建的 view,后面再 onCreateView() 时直接返回第一次创建的 view
*
* @param isReuse
*/
protected void reuseView(boolean isReuse) {
isReuseView = isReuse;
}
/**
* 去除setUserVisibleHint()多余的回调场景,保证只有当fragment可见状态发生变化时才回调
* 回调时机在view创建完后,所以支持ui操作,解决在setUserVisibleHint()里进行ui操作有可能报null异常的问题
*
* 可在该回调方法里进行一些ui显示与隐藏,比如加载框的显示和隐藏
*
* @param isVisible true 不可见 -> 可见
* false 可见 -> 不可见
*/
protected void onFragmentVisibleChange(boolean isVisible) {
}
/**
* 在fragment首次可见时回调,可在这里进行加载数据,保证只在第一次打开Fragment时才会加载数据,
* 这样就可以防止每次进入都重复加载数据
* 该方法会在 onFragmentVisibleChange() 之前调用,所以第一次打开时,可以用一个全局变量表示数据下载状态,
* 然后在该方法内将状态设置为下载状态,接着去执行下载的任务
* 最后在 onFragmentVisibleChange() 里根据数据下载状态来控制下载进度ui控件的显示与隐藏
*/
protected void onFragmentFirstVisible() {
}
protected boolean isFragmentVisible() {
return isFragmentVisible;
}
}
使用方法
使用很简单,新建你需要的 Fragment 类继承自该 BaseFragment,然后重写两个回调方法,根据你的需要在回调方法里进行相应的操作比如下载数据等即可。
例如:
public class CategoryFragment extends BaseFragment {
private static final String TAG = CategoryFragment.class.getSimpleName();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_category, container, false);
initView(view);
return view;
}
@Override
protected void onFragmentVisibleChange(boolean isVisible) {
if (isVisible) {
//更新界面数据,如果数据还在下载中,就显示加载框
notifyDataSetChanged();
if (mRefreshState == STATE_REFRESHING) {
mRefreshListener.onRefreshing();
}
} else {
//关闭加载框
mRefreshListener.onRefreshFinish();
}
}
@Override
protected void onFragmentFirstVisible() {
//去服务器下载数据
mRefreshState = STATE_REFRESHING;
mCategoryController.loadBaseData();
}
}
注意事项
- 如果想要让 fragment 的布局复用成功,需要重写 viewpager 的适配器里的
destroyItem()
- 方法,将 super 去掉,也就是不销毁 view。
- 如果出现切换回来或不相邻的Tab切换时导致空白界面的问题,解决方法:在 onCreateView中复用布局 + ViewPager 的适配器中复写 destroyItem() 方法去掉 super。