BaseRecyclerViewAdapterHelper源码解读(四) 上拉加载更多

来源:互联网 发布:网络渗透 pdf 编辑:程序博客网 时间:2024/06/03 03:24

上拉加载

上拉加载无需监听滑动事件,可自定义加载布局,显示异常提示,自定义异常提示。

此篇文章为BaseRecyclerViewAdapterHelper源码解读第四篇,开源库地址,如果没有看过之前3篇文章的同学可以先去看看,大神可直接跳过.

BaseRecyclerViewAdapterHelper源码解读(一) 封装简单的adapter和万能的BaseViewHolder

BaseRecyclerViewAdapterHelper源码解读(二) 添加header和footer

BaseRecyclerViewAdapterHelper源码解读(三) 添加动画

今天给大家带来BaseRecyclerViewAdapterHelper是如何添加动画的.由于本人才学尚浅,如有有不对的地方,欢迎指正,谢谢.

开源库使用自动加载方法

上拉加载

// 滑动最后一个Item的时候回调onLoadMoreRequested方法setOnLoadMoreListener(RequestLoadMoreListener);

默认第一次加载会回调onLoadMoreRequested()加载更多这个方法,如果不需要可以配置如下:

mQuickAdapter.disableLoadMoreIfNotFullPage();

回调处理代码

mQuickAdapter.setOnLoadMoreListener(new BaseQuickAdapter.RequestLoadMoreListener() {            @Override public void onLoadMoreRequested() {                mRecyclerView.postDelayed(new Runnable() {                    @Override                    public void run() {                        if (mCurrentCounter >= TOTAL_COUNTER) {                            //数据全部加载完毕                            mQuickAdapter.loadMoreEnd();                        } else {                            if (isErr) {                                //成功获取更多数据                                mQuickAdapter.addData(DataServer.getSampleData(PAGE_SIZE));                                mCurrentCounter = mQuickAdapter.getData().size();                                mQuickAdapter.loadMoreComplete();                            } else {                                //获取更多数据失败                                isErr = true;                                Toast.makeText(PullToRefreshUseActivity.this, R.string.network_err, Toast.LENGTH_LONG).show();                                mQuickAdapter.loadMoreFail();                            }                        }                    }                }, delayMillis);            }        }, mReyclerView);这里可能看的不是很清楚,详情请看官方demo,https://github.com/CymChad/BaseRecyclerViewAdapterHelper/blob/d296d1fb4e7a64b9fa8a2601f3f896d3a9518be5/app/src/main/java/com/chad/baserecyclerviewadapterhelper/PullToRefreshUseActivity.java

加载完成(注意不是加载结束,而是本次数据加载结束并且还有下页数据)

mQuickAdapter.loadMoreComplete();

加载失败

mQuickAdapter.loadMoreFail();

加载结束

mQuickAdapter.loadMoreEnd();

设置监听器,开启监听上拉加载

    /**     * 设置监听RecyclerView上拉加载更多  并设置监听器     * @param requestLoadMoreListener     * @param recyclerView     */    public void setOnLoadMoreListener(RequestLoadMoreListener requestLoadMoreListener,                                      RecyclerView recyclerView) {        openLoadMore(requestLoadMoreListener);        if (getRecyclerView() == null) {            setRecyclerView(recyclerView);        }    }    /**     *     * 开启上拉加载更多     * @param requestLoadMoreListener     */    private void openLoadMore(RequestLoadMoreListener requestLoadMoreListener) {        this.mRequestLoadMoreListener = requestLoadMoreListener;        mNextLoadEnable = true;        mLoadMoreEnable = true;        mLoading = false;    }

设置什么时候回调?

    /**     * 设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法     * @param preLoadNumber     */    public void setPreLoadNumber(int preLoadNumber) {        if (preLoadNumber > 1) {            mPreLoadNumber = preLoadNumber;        }    }

先来说简单的,上面这个方法比较简单,属于配置型的方法.
就是设置当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法.待会儿下面会用到这个参数,先放着.
另外,这个方法可以在使用时不必调用,因为已经有默认值了.

    /**     * To bind different types of holder and solve different the bind events     *     * @param holder     * @param position     * @see #getDefItemViewType(int)     */    @Override    public void onBindViewHolder(K holder, int position) {        //Add up fetch logic, almost like load more, but simpler.        //这里是判断是否需要下拉加载更多        autoUpFetch(position);        //Do not move position, need to change before LoadMoreView binding        //判断是否需要进行上拉加载更多        autoLoadMore(position);        int viewType = holder.getItemViewType();        switch (viewType) {            case 0:                convert(holder, getItem(position - getHeaderLayoutCount()));                break;            case LOADING_VIEW:                mLoadMoreView.convert(holder);                break;            case HEADER_VIEW:                break;            case EMPTY_VIEW:                break;            case FOOTER_VIEW:                break;            default:                convert(holder, getItem(position - getHeaderLayoutCount()));                break;        }    }    /**     * 根据position位置判断当前是否需要进行加载更多     *     * @param position 当前onBindViewHolder()的Position     */    private void autoLoadMore(int position) {        // 判断是否可以进行加载更多的逻辑        if (getLoadMoreViewCount() == 0) {            return;        }        //只有在当前列表的倒数mPreLoadNumber个item开始绑定数据时才进行加载更多的逻辑        if (position < getItemCount() - mPreLoadNumber) {            return;        }        //判断当前加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载        if (mLoadMoreView.getLoadMoreStatus() != LoadMoreView.STATUS_DEFAULT) {            return;        }        //设置当前状态:加载中        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_LOADING);        if (!mLoading) {            mLoading = true;            if (getRecyclerView() != null) {                getRecyclerView().post(new Runnable() {                    @Override                    public void run() {                        //回调  让调用者去处理加载更多的逻辑                        mRequestLoadMoreListener.onLoadMoreRequested();                    }                });            } else {                mRequestLoadMoreListener.onLoadMoreRequested();            }        }    }    /**     * Load more view count     * 判断是否可以进行加载更多的逻辑     * @return 0 or 1     */    public int getLoadMoreViewCount() {        //参数合法性    加载更多状态        if (mRequestLoadMoreListener == null || !mLoadMoreEnable) {            return 0;        }        //可加载下一页               有无更多数据        if (!mNextLoadEnable && mLoadMoreView.isLoadEndMoreGone()) {            return 0;        }        //当前数据项个数        if (mData.size() == 0) {            return 0;        }        return 1;    }

重点来了,加载更多的主要逻辑就在这里:当在onBindViewHolder()的时候,根据当前item的position位置,然后去判断是否应该执行加载更多.
具体判断逻辑:当一个item第一次进入window界面时,会调用onBindViewHolder()去绑定数据,这个时候我们知道该position的位置,
于是我们就可以这样干:设置一个mPreLoadNumber标志位置( 当列表滑动到倒数第N个Item的时候(默认是1)回调onLoadMoreRequested()方法 ),
当onBindViewHolder()在绑定数据时的position是最后mPreLoadNumber个时,我们即进行加载更多的回调,然后让调用者去处理.

当然,在回调之前,我们需要进行一些判断,确定当前是否可以进行加载更多.
- mRequestLoadMoreListener监听器是否为null,当前是否处于可以加载更多的状态(mLoadMoreEnable标志位控制)
- 当前有无更多数据(这个由外界调用者决定)
- 当前的数据项个数是否为0,如果没有数据项,那就不必加载更多
- 是否进入倒数的那mPreLoadNumber区域
- 判断当前(mLoadMoreView 这是加载更多的View )加载状态,如果不是默认状态(可能正处于 正在加载中 的状态),则不进行加载

好吧,细心的观众可能已经发现了,上面的这种方式其实有一个缺点:当数据项个数小于1屏幕,那么最后倒数的mPreLoadNumber个肯定是可见的,既然可见那么肯定会执行该item的onBindViewHolder(),执行该方法即会判断是否需要执行加载更多,显然这时是符合条件的,于是就会出现数据未满一屏幕会自动回调onLoadMoreRequested()并且还在那里显示正在加载中.

明显,这时不符合我们的需求的.于是官方有一个解决方案.往下看.

/**     * bind recyclerView {@link #bindToRecyclerView(RecyclerView)} before use!     *     * @see #disableLoadMoreIfNotFullPage(RecyclerView)     */    public void disableLoadMoreIfNotFullPage() {        //检查当前RecyclerView是否为null        checkNotNull();        disableLoadMoreIfNotFullPage(getRecyclerView());    }    /**     * check if full page after {@link #setNewData(List)}, if full, it will enable load more again.     * <p>     * 不是配置项!!     * <p>     * 这个方法是用来检查是否满一屏的,所以只推荐在 {@link #setNewData(List)} 之后使用     * 原理:先关闭 load more,检查完了再决定是否开启     * 数据项个数未满一屏幕,则不开启load more     * 数据项个数 > 一屏幕,则继续开启load more     * <p>     * 不是配置项!!     *     * @param recyclerView your recyclerView     * @see #setNewData(List)     */    public void disableLoadMoreIfNotFullPage(RecyclerView recyclerView) {        // 设置加载状态为false        setEnableLoadMore(false);        if (recyclerView == null) return;        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();        if (manager == null) return;        if (manager instanceof LinearLayoutManager) {            final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) manager;            recyclerView.postDelayed(new Runnable() {                @Override                public void run() {                    //数据项个数 > 一屏幕,则继续开启load more                    if ((linearLayoutManager.findLastCompletelyVisibleItemPosition() + 1) !=                            getItemCount()) {                        setEnableLoadMore(true);                    }                }            }, 50);        } else if (manager instanceof StaggeredGridLayoutManager) {            final StaggeredGridLayoutManager staggeredGridLayoutManager =                    (StaggeredGridLayoutManager) manager;            recyclerView.postDelayed(new Runnable() {                @Override                public void run() {                    //返回StaggeredGridLayoutManager布局的跨度数                    final int[] positions = new int[staggeredGridLayoutManager.getSpanCount()];                    //返回每一个跨度(列)的最后一个可见的item的位置  赋值到该数组里面                    staggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(positions);                    //找出数组中最大的数(即StaggeredGridLayoutManager布局的当前可见的最下面那个item)                    int pos = getTheBiggestNumber(positions) + 1;                    // 数据项个数 > 一屏幕,则继续开启load more                    if (pos != getItemCount()) {                        setEnableLoadMore(true);                    }                }            }, 50);        }    }    /**     * 返回数组中的最大值     * @param numbers     * @return     */    private int getTheBiggestNumber(int[] numbers) {        int tmp = -1;        if (numbers == null || numbers.length == 0) {            return tmp;        }        for (int num : numbers) {            if (num > tmp) {                tmp = num;            }        }        return tmp;    }    /**     * Set the enabled state of load more.     * 设置上拉加载更多是否可用     *     * @param enable True if load more is enabled, false otherwise.     */    public void setEnableLoadMore(boolean enable) {        //之前的状态需要和现在的状态做对比        int oldLoadMoreCount = getLoadMoreViewCount();        mLoadMoreEnable = enable;        int newLoadMoreCount = getLoadMoreViewCount();        if (oldLoadMoreCount == 1) {            if (newLoadMoreCount == 0) {                //之前有 现在没有 需要移除                notifyItemRemoved(getLoadMoreViewPosition());            }        } else {            if (newLoadMoreCount == 1) {                //将加载布局插入                mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);                notifyItemInserted(getLoadMoreViewPosition());            }        }    }

这段代码我看到在开源项目的讨论区异常热门,好像很多人都遇到了使用disableLoadMoreIfNotFullPage()无效的事件.
可能是他们用错了吧,可能.disableLoadMoreIfNotFullPage()是需要在setNewData()之后调用才有效.
disableLoadMoreIfNotFullPage()里面想做的事情就是:判断是否需要load more,他判断的依据是:
查看当前屏幕内的最底部的那个item的索引是否与总的数据项个数相等.
- 如果相等,那么说明未满一屏幕,不需要开启load more
- 如果不相等,那么说明满了一屏幕,需要开启laod more

创建加载布局item 并 设置加载布局的点击事件

 @Override    public K onCreateViewHolder(ViewGroup parent, int viewType) {        K baseViewHolder = null;        this.mContext = parent.getContext();        this.mLayoutInflater = LayoutInflater.from(mContext);        switch (viewType) {            case LOADING_VIEW:                baseViewHolder = getLoadingView(parent);                break;            case HEADER_VIEW:                baseViewHolder = createBaseViewHolder(mHeaderLayout);                break;            case EMPTY_VIEW:                baseViewHolder = createBaseViewHolder(mEmptyLayout);                break;            case FOOTER_VIEW:                baseViewHolder = createBaseViewHolder(mFooterLayout);                break;            default:                baseViewHolder = onCreateDefViewHolder(parent, viewType);                bindViewClickListener(baseViewHolder);        }        baseViewHolder.setAdapter(this);        return baseViewHolder;    }    private K getLoadingView(ViewGroup parent) {        //加载 加载布局        View view = getItemView(mLoadMoreView.getLayoutId(), parent);        //生成baseviewholder        K holder = createBaseViewHolder(view);        //设置加载布局的点击事件        holder.itemView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_FAIL) {                    //之前是加载失败状态时   前去刷新                    notifyLoadMoreToLoading();                }                if (mEnableLoadMoreEndClick && mLoadMoreView.getLoadMoreStatus() == LoadMoreView                        .STATUS_END) {                    //加载更多布局可以被点击  并且  之前状态是结束状态                    notifyLoadMoreToLoading();                }            }        });        return holder;    }    /**     * The notification starts the callback and loads more     * 通知启动回调并加载更多     */    public void notifyLoadMoreToLoading() {        //如果当前正在加载中,则不用管        if (mLoadMoreView.getLoadMoreStatus() == LoadMoreView.STATUS_LOADING) {            return;        }        //将加载更多布局的状态设置为默认状态  这样当下面刷新adapter时会回调onBindViewHolder()从而触发        //autoLoadMore()方法去判断是否需要加载更多,这时候刚好又是默认状态是可以更新的,于是就去回调onLoadMoreRequested()方法        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);        notifyItemChanged(getLoadMoreViewPosition());    }

这里的目标是给加载更多布局设置点击事件,可以看到其实在代码里面把加载更多布局是直接设置了点击事件的,只是根据不同的状态决定是否需要执行加载更多的逻辑.只有下面2种情况需要去加载更多.
- 之前是加载失败状态时 加载布局被点击
- 之前是结束状态 并且 加载更多布局可以被点击

满足这两种情况时,就把加载布局view的状态设置成默认状态,并且刷新adapter的最后一项(即加载更多布局那一项),这样adapter会回调onBindViewHolder(),而在onBindViewHolder()又调用了autoLoadMore()方法去判断是否需要加载更多,
显然此时是符合条件的,需要刷新,于是回调onLoadMoreRequested(),并且把加载布局的状态改为STATUS_LOADING正在加载的状态,这样加载布局的样式也跟着改变了.

加载完成

注意不是加载结束,而是本次数据加载结束并且还有下页数据

    /**     * Refresh complete     * 刷新完成时调用     *     */    public void loadMoreComplete() {        if (getLoadMoreViewCount() == 0) {            return;        }        //将当前加载状态改为false  表示未在加载        mLoading = false;        //可进行下一页加载        mNextLoadEnable = true;        // 恢复加载更多布局的状态        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_DEFAULT);        // 告知加载更多布局被更新了,需要刷新一下        notifyItemChanged(getLoadMoreViewPosition());    }    /**     * Gets to load more locations     * 获取加载更多的布局的索引     * @return     */    public int getLoadMoreViewPosition() {        return getHeaderLayoutCount() + mData.size() + getFooterLayoutCount();    }

刷新完成之后,需要做一些善后操作,如上所示,代码注释已经很清楚了.

加载失败

    /**     * Refresh failed     * 加载失败     */    public void loadMoreFail() {        if (getLoadMoreViewCount() == 0) {            return;        }        //当前加载状态  切换为未在加载中        mLoading = false;        //加载布局设置为加载失败        mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_FAIL);        //通知加载布局更新了,需要刷新        notifyItemChanged(getLoadMoreViewPosition());    }

就是简单地做一下判断,是否可以继续加载,并且更新布局.

加载结束

    /**     * Refresh end, no more data     * 加载更多,并且没有更多数据了    调用此方法即表示无更多数据了     * 这里设置加载更多布局依然可见     */    public void loadMoreEnd() {        loadMoreEnd(false);    }    /**     * Refresh end, no more data     * 加载更多,并且没有更多数据了   调用此方法即表示无更多数据了     * gone:设置加载更多布局是否可见   true:不可见   false:可见     * @param gone if true gone the load more view     */    public void loadMoreEnd(boolean gone) {        if (getLoadMoreViewCount() == 0) {            return;        }        ////当前加载状态  切换为未在加载中        mLoading = false;        //不能再加载下一页了  因为已经没有更多数据了        mNextLoadEnable = false;        //设置加载更多布局是否可见        mLoadMoreView.setLoadMoreEndGone(gone);        if (gone) {            //如果布局不可见,则更新            notifyItemRemoved(getLoadMoreViewPosition());        } else {            //如果布局可见,则先更新布局(切换为STATUS_END状态那种布局)            mLoadMoreView.setLoadMoreStatus(LoadMoreView.STATUS_END);            //并更新adapter            notifyItemChanged(getLoadMoreViewPosition());        }    }

设置加载结束,即表示没有更多的数据可以加载了,于是把mNextLoadEnable标志位设为false,表示无法再加载下一页.
然后根据是否需要显示加载布局,进行刷新adapter.

上拉加载布局

在源码里面有一个抽象类LoadMoreView.

public abstract class LoadMoreView {    public static final int STATUS_DEFAULT = 1;    /**     * 加载中     */    public static final int STATUS_LOADING = 2;    /**     * 加载失败     */    public static final int STATUS_FAIL = 3;    /**     * 加载结束  没有更多数据     */    public static final int STATUS_END = 4;    /**     * 当前加载更多的状态     */    private int mLoadMoreStatus = STATUS_DEFAULT;    private boolean mLoadMoreEndGone = false;    public void setLoadMoreStatus(int loadMoreStatus) {        this.mLoadMoreStatus = loadMoreStatus;    }    public int getLoadMoreStatus() {        return mLoadMoreStatus;    }    public void convert(BaseViewHolder holder) {        //根据不同的状态        switch (mLoadMoreStatus) {            case STATUS_LOADING:                visibleLoading(holder, true);                visibleLoadFail(holder, false);                visibleLoadEnd(holder, false);                break;            case STATUS_FAIL:                visibleLoading(holder, false);                visibleLoadFail(holder, true);                visibleLoadEnd(holder, false);                break;            case STATUS_END:                visibleLoading(holder, false);                visibleLoadFail(holder, false);                visibleLoadEnd(holder, true);                break;            case STATUS_DEFAULT:                visibleLoading(holder, false);                visibleLoadFail(holder, false);                visibleLoadEnd(holder, false);                break;        }    }    private void visibleLoading(BaseViewHolder holder, boolean visible) {        holder.setVisible(getLoadingViewId(), visible);    }    private void visibleLoadFail(BaseViewHolder holder, boolean visible) {        holder.setVisible(getLoadFailViewId(), visible);    }    private void visibleLoadEnd(BaseViewHolder holder, boolean visible) {        final int loadEndViewId = getLoadEndViewId();        if (loadEndViewId != 0) {            holder.setVisible(loadEndViewId, visible);        }    }    /**     * 设置标志  有无更多数据     * @param loadMoreEndGone true:无更多数据     */    public final void setLoadMoreEndGone(boolean loadMoreEndGone) {        this.mLoadMoreEndGone = loadMoreEndGone;    }    public final boolean isLoadEndMoreGone() {        if (getLoadEndViewId() == 0) {            return true;        }        return mLoadMoreEndGone;    }    /**     * No more data is hidden     *     * @return true for no more data hidden load more     * @deprecated Use {@link BaseQuickAdapter#loadMoreEnd(boolean)} instead.     */    @Deprecated    public boolean isLoadEndGone() {        return mLoadMoreEndGone;    }    /**     * load more layout     *     * @return     */    public abstract    @LayoutRes    int getLayoutId();    /**     * loading view     *     * @return     */    protected abstract    @IdRes    int getLoadingViewId();    /**     * load fail view     *     * @return     */    protected abstract    @IdRes    int getLoadFailViewId();    /**     * load end view, you can return 0     *     * @return     */    protected abstract    @IdRes    int getLoadEndViewId();}

该类是用于管理加载布局的,不同的状态显示不同的布局.
源码里面已经给我们提供了一个默认的加载布局,可以直接使用,当然了,是支持自定义的,只需要继承LoadMoreView就行.

默认的加载布局如下:

public final class SimpleLoadMoreView extends LoadMoreView {    @Override    public int getLayoutId() {        return R.layout.quick_view_load_more;    }    @Override    protected int getLoadingViewId() {        return R.id.load_more_loading_view;    }    @Override    protected int getLoadFailViewId() {        return R.id.load_more_load_fail_view;    }    @Override    protected int getLoadEndViewId() {        return R.id.load_more_load_end_view;    }}

下面是xml

<?xml version="1.0" encoding="utf-8"?><FrameLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="@dimen/dp_40">    <LinearLayout        android:id="@+id/load_more_loading_view"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:gravity="center"        android:orientation="horizontal">        <ProgressBar            android:id="@+id/loading_progress"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            style="?android:attr/progressBarStyleSmall"            android:layout_marginRight="@dimen/dp_4"/>        <TextView            android:id="@+id/loading_text"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginLeft="@dimen/dp_4"            android:text="@string/loading"            android:textColor="@android:color/black"            android:textSize="@dimen/sp_14"/>    </LinearLayout>    <FrameLayout        android:id="@+id/load_more_load_fail_view"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:visibility="gone">        <TextView            android:id="@+id/tv_prompt"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center"            android:text="@string/load_failed"/>    </FrameLayout>    <FrameLayout        android:id="@+id/load_more_load_end_view"        android:layout_width="match_parent"        android:layout_height="match_parent"        android:visibility="gone">        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_gravity="center"            android:text="@string/load_end"            android:textColor="@android:color/darker_gray"/>    </FrameLayout></FrameLayout>

其实就是一个布局,根据根据状态,动态的显示和隐藏某一种容器就行.

总结

这一块,感觉稍微复杂一些,用了2天的琐碎时间才看完,可能是因为比较菜吧.
大体实现思路就是当检测到滑动到RecyclerView的最后倒数N项时,就开始去刷新,并显示加载布局和回调接口,让外部去实现刷新.
道理虽然很简单,但是实现起来的话,有很多很多细节在里面,很多很多的坑,再次感谢开源库的作者们.

原创粉丝点击