懒加载,就是滑动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();
    }
}

注意事项

  1. 如果想要让 fragment 的布局复用成功,需要重写 viewpager 的适配器里的 

destroyItem()

  1.  方法,将 super 去掉,也就是不销毁 view。
  2. 如果出现切换回来或不相邻的Tab切换时导致空白界面的问题,解决方法:在 onCreateView中复用布局 + ViewPager 的适配器中复写 destroyItem() 方法去掉 super。