Android-2

RecyclerView刷新和上拉加载

2018-07-22  本文已影响172人  四喜汤圆

说到上拉加载,总是离不开刷新。没有刷新,哪有后面的上拉加载。所以“刷新”和“上拉加载”是有时间的先后顺序,有关联性的。故“刷新”和“上拉加载”要结合起来一起说。

RecyclerView的刷新有两种:

<?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:gravity="center"
    android:orientation="vertical">

    <android.support.v4.widget.SwipeRefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_LoadMoreAndRefreshRecycler_recycler"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_weight="match_parent" />
    </android.support.v4.widget.SwipeRefreshLayout>


</LinearLayout>

本文是在界面上放置一个按钮用于刷新,在此基础上再加入RecyclerView的上拉加载功能

想要实现的效果

每次联网加载完数据后:

  1. 返回数据条数<PageSize,底布局显示“没有更多数据”;
  2. 返回数据条数=PageSize,后面可能还有数据,底布局显示“加载中”
  3. 返回数据异常,底布局显示“加载失败”

通过分析我们的目标,我们可以得出的结论是:其实就是需要给RecyclerView添加一个底布局。【按照我们预期的效果,无论什么时候(加载成功,还是加载失败),始终都要添加一个底布局说明本次加载数据的情况】

Q:如何给RecyclerView添加底布局

A: getItemCount()、getItemViewType()、onCreateViewHolder()、onBindViewHolder()配合使用

@Override
    public int getItemCount() {
        // 无论什么时候(加载成功,还是加载失败),始终都要添加一个底布局
        return mDatas.size() + 1;
    }
@Override
    public int getItemViewType(int position) {
        //如果position加1正好等于所有item的总和,说明是最后一个item,将它设置为脚布局
        //返回的count也要加1,因为添加了一个脚布局
        if (position + 1 == getItemCount()) {
            return TYPE_FOOTER;
        } else {
            return TYPE_ITEM;
        }
    }

ViewHolder,不同布局对应不同的ViewHolder

/**
     * 脚布局的ViewHolder
     */
    static class FootViewHolder extends RecyclerView.ViewHolder {
        // 进度条
        @BindView(R.id.pb_LoadMoreAndRefreshRecycler_footer_loading)
        ProgressBar pb_LoadMoreAndRefreshRecycler_footer_loading;
        // 提示信息
        @BindView(R.id.tv_LoadMoreAndRefreshRecycler_footer_content)
        TextView tv_LoadMoreAndRefreshRecycler_footer_content;

        public FootViewHolder(View footerView) {
            super(footerView);
            ButterKnife.bind(this, footerView);
        }
    }

    /**
     * 正常布局的ViewHolder
     */
    static class MyViewHolder extends RecyclerView.ViewHolder {
        // 姓名
        @BindView(R.id.tv_LoadMoreAndRefreshRecycler_name)
        TextView tv_LoadMoreAndRefreshRecycler_name;
        // 年龄
        @BindView(R.id.tv_LoadMoreAndRefreshRecycler_age)
        TextView tv_LoadMoreAndRefreshRecycler_age;

        public MyViewHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }
    }
//实例化viewholder,设置item布局
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_ITEM) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
            MyViewHolder holder = new MyViewHolder(view);
            return holder;
        } else if (viewType == TYPE_FOOTER) {
            //脚布局
            View view = View.inflate(parent.getContext(), R.layout.item_end, null);
            FootViewHolder footViewHolder = new FootViewHolder(view);
            return footViewHolder;
        }
        return null;
    }
//绑定数据
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        if (holder instanceof MyViewHolder) {
            /*
            * 为Item绑定数据
             */
            //instanceof判断其左边对象是否为其右边类的实例,返回boolean类型的数据
            // 1,绑定数据
            MyViewHolder myViewHolder = (MyViewHolder) holder;
            final Info info = mDatas.get(position);
            myViewHolder.tv_LoadMoreAndRefreshRecycler_name.setText(info.getName());
            myViewHolder.tv_LoadMoreAndRefreshRecycler_age.setText(info.getAge());
        } else if (holder instanceof FootViewHolder) {
            FootViewHolder footViewHolder = (FootViewHolder) holder;            
        }

    }

此时显示的效果是:无论加载多少条数据,底布局都是相同的。而我们需要的是根据不同的情况显示不同的布局

后台返回4条数据.png 后台返回3条数据.png

Q:如何控制不同情况下加载不同的底布局

A:在RecyclerView中添加一变量loadState,值不同,显示的布局不同

 public static final int STATE_LOADING = 1;// 正在加载中
    public static final int STATE_LASTED = 2;// 没有更多数据
    public static final int STATE_ERROR = 3;// 加载失败
    public static final int STATE_OTHER = 4;// 什么都不显示
    private static final String STR_LOADING = "正在加载中";// 正在加载中
    private static final String STR_LASTED = "没有更多数据";// 没有更多数据
    private static final String STR_ERROR = "加载失败";// 加载失败
    // 标志底布局应该显示哪种(默认STATE_OTHER:什么都不显示)
    private int loadState=STATE_OTHER;

onBindViewHolder()中的代码发生了变化

 //绑定数据
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
        if (holder instanceof MyViewHolder) {
            /*
            * 为Item绑定数据
             */
            //instanceof判断其左边对象是否为其右边类的实例,返回boolean类型的数据
            // 1,绑定数据
            MyViewHolder myViewHolder = (MyViewHolder) holder;
            final Info info = mDatas.get(position);
            myViewHolder.tv_LoadMoreAndRefreshRecycler_name.setText(info.getName());
            myViewHolder.tv_LoadMoreAndRefreshRecycler_age.setText(info.getAge());
        } else if (holder instanceof FootViewHolder) {
            FootViewHolder footViewHolder = (FootViewHolder) holder;
            switch (loadState) {
                case STATE_LOADING:
                    /*
                     * 正在加载:
                     * 文字显示“加载中”,显示进度条
                      */
                    footViewHolder.tv_LoadMoreAndRefreshRecycler_footer_content.setText(STR_LOADING);
                    footViewHolder.itemView.setOnClickListener(null);
                    footViewHolder.pb_LoadMoreAndRefreshRecycler_footer_loading.setVisibility(View.VISIBLE);
                    break;
                case STATE_LASTED:
                    /*
                     * 没有更多数据
                     * 文字显示“没有更多数据”,隐藏进度条
                      */
                    footViewHolder.tv_LoadMoreAndRefreshRecycler_footer_content.setText(STR_LASTED);
                    footViewHolder.itemView.setOnClickListener(null);
                    footViewHolder.pb_LoadMoreAndRefreshRecycler_footer_loading.setVisibility(View.INVISIBLE);
                    break;
                case STATE_ERROR:
                    /*
                     * 加载失败,
                     * 文字显示“加载失败”,隐藏进度条
                      */
                    footViewHolder.tv_LoadMoreAndRefreshRecycler_footer_content.setText(STR_ERROR);
                    footViewHolder.pb_LoadMoreAndRefreshRecycler_footer_loading.setVisibility(View.INVISIBLE);
                    break;
                default:
                    /*
                    * 文字隐藏、进度条隐藏
                     */
                    footViewHolder.tv_LoadMoreAndRefreshRecycler_footer_content.setText("");
                    // 隐藏进度条
                    footViewHolder.pb_LoadMoreAndRefreshRecycler_footer_loading.setVisibility(View.INVISIBLE);
                    break;
            }
        }

    }

问题又来了

Q:1、在何处维护loadState的值

A:每次加载完数据后(刷新加载或上拉加载更多)设置该变量的值

Activity中设置一变量currentPage=0标志当前显示的是第几页的数据:从0开始(0表示:当前没有数据)

/**
     * 得到刷新数据
     * 第一页数据
     */
    private void getData_update() { 
        final HashMap<String, String> params = new HashMap<>();
        params.put("LX", "Query");
        params.put("page", 1 + "");
        NetApi.init(context, true).query(ConfigURL.GET_QUERY_DATA, params, new NetApi.OnCallBackListener() {

            @Override
            public void callSuccessResult(ResultBean response) {
                // 刷新完毕
                List<Info> list = GsonUtil.GsonToList(response.getData(), Info.class);
                Info[] a = list.toArray(new Info[0]);
                LogUtil.e(TAG, String.format("callSuccessResult();page=%s;update返回的数据:%s", 1 + "", Arrays.toString(a)));
                // 1,设置Adapter中loadState的值
                if (list.size() < RecyclerAdapter.PAGE_SIZE) {
                    // 说明后面没有数据了
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LASTED);
                    LogUtil.e(TAG, "后面无更多数据");
                } else {
                    // 说明后面还需加载
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LOADING);
                    LogUtil.e(TAG, "后面仍需加载");
                }
                // 2,用新数据替换原来的数据源
                mAdapter.upData(list);
                // 3,当前页数设置为1
                currentPage = 1;
            }

            @Override
            public void callFailResult(ResultBean response) {
                LogUtil.e(TAG, "callFailResult()");
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callError(Call call, Exception e, int id) {
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callExceptionResult(Context context, ResultBean response) {
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }
        });
    }
/**
     * 上拉加载更多数据
     * 上拉加载
     */
    private void getData_loadMore() {
        final HashMap<String, String> params = new HashMap<>();
        params.put("LX", "Query");
        params.put("page", currentPage + 1 + "");
        NetApi.init(context, false).query(ConfigURL.GET_QUERY_DATA, params, new NetApi.OnCallBackListener() {

            @Override
            public void callSuccessResult(ResultBean response) {
                // 上拉加载完毕
                List<Info> list = GsonUtil.GsonToList(response.getData(), Info.class);
                Info[] a = list.toArray(new Info[0]);
                LogUtil.e(TAG, String.format("callSuccessResult():page=%s;loadMore返回的数据:%s", currentPage + 1 + "", Arrays.toString(a)));
                // 1,设置Adapter中loadState的值
                if (list.size() < RecyclerAdapter.PAGE_SIZE) {
                    // 说明后面没有数据了
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LASTED);
                } else {
                    // 说明后面还需加载
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LOADING);
                }
                // 2,数据拼接在现有数据源后面
                mAdapter.appendData(list);
                // 3,当前页数加1
                currentPage++;
            }

            @Override
            public void callFailResult(ResultBean response) {
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callError(Call call, Exception e, int id) {
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callExceptionResult(Context context, ResultBean response) {
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }
        });
    }

Q2:在底布局显示不同文字时,要执行相应的动作。

底布局显示文字 相应动作
没有更多数据
加载中 联网加载下页数据
加载失败

问题转化为:如何知道RecyclerView当前滑动到底部,且底布局显示的是“加载中”

A :监听RecyclerView的滑动事件。找到最后一个完全可见的Item的position值,若该position+1==getItemCount(),说明最后一项完全显示出来了。如果此时loadState==STATE_LOADING,-------那么,时机到了,可以加载了。

// 监听RecyclerView的滑动事件
        rv_LoadMoreAndRefreshRecycler_recycler.addOnScrollListener(new MyOnScroolListener(this));
 /**
     * 静态内部类+弱引用
     * 防止内存泄漏
     */
    static class MyOnScroolListener extends RecyclerView.OnScrollListener {
        WeakReference<LoadMoreAndRefreshRecyclerActivity> mActivity;

        public MyOnScroolListener(LoadMoreAndRefreshRecyclerActivity activity) {
            this.mActivity = new WeakReference<LoadMoreAndRefreshRecyclerActivity>(activity);
        }

        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            LoadMoreAndRefreshRecyclerActivity activity = mActivity.get();
            if (activity != null) {
                RecyclerView.LayoutManager manager = activity.rv_LoadMoreAndRefreshRecycler_recycler.getLayoutManager();
                RecyclerAdapter adapter = (RecyclerAdapter) activity.rv_LoadMoreAndRefreshRecycler_recycler.getAdapter();
                if (null == manager) {
                    throw new RuntimeException("you should call setLayoutManager() first!!");
                }
                if (manager instanceof LinearLayoutManager) {
                    // 返回最后一个完全可见的item项的position值
                    int lastCompletelyVisibleItemPosition =
                            ((LinearLayoutManager) manager).findLastCompletelyVisibleItemPosition();
//                    lastCompletelyVisibleItemPosition+1 == adapter.getItemCount():表示已经滑动到底部
//                    adapter.getLoadState()==MyAdapter.STATE_LOADING:表示正在加载中
                    if (lastCompletelyVisibleItemPosition + 1 == adapter.getItemCount() &&
                            adapter.getLoadState() == RecyclerAdapter.STATE_LOADING) {
                        // 加载下页数据
                        activity.getData_loadMore();
                    }
                }
            }


        }
    }

至此,基本功能实现。但还有细节需完善

细节

1. 刷新、加载,同一时刻只进行一个网络请求获取数据

A:在Activity中创建变量isLoadMore、isRefreshing,在getData_update()和getData_loadMore()中进行相应判断,避免重复的查询请求。

// 标志是否正在上拉加载{true:正在上拉加载;false:没有}
    private boolean isLoadMore = false;
    // 表示是否正在下拉刷新{true:正在刷新;false“没有}
    private boolean isRefreshing = false;
/**
     * 得到刷新数据
     * 第一页数据
     */
    private void getData_update() {
        if (isLoadMore) {
            // 正在上拉加载,退出
            LogUtil.e(TAG, "正在上拉加载,取消本次操作");
            return;
        }
        if (isRefreshing) {
            // 正在刷新
            LogUtil.e(TAG, "正在刷新,取消本次操作");
            return;
        }
        isRefreshing = true;
        // 刷新时将底布局设置为什么都不显示
        mAdapter.setLoadState(RecyclerAdapter.STATE_OTHER);
        final HashMap<String, String> params = new HashMap<>();
        params.put("LX", "Query");
        params.put("page", 1 + "");
        NetApi.init(context, true).query(ConfigURL.GET_QUERY_DATA, params, new NetApi.OnCallBackListener() {

            @Override
            public void callSuccessResult(ResultBean response) {
                // 刷新完毕
                isRefreshing = false;
                List<Info> list = GsonUtil.GsonToList(response.getData(), Info.class);
                Info[] a = list.toArray(new Info[0]);
                LogUtil.e(TAG, String.format("callSuccessResult();page=%s;update返回的数据:%s", 1 + "", Arrays.toString(a)));
                // 1,设置Adapter中loadState的值
                if (list.size() < RecyclerAdapter.PAGE_SIZE) {
                    // 说明后面没有数据了
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LASTED);
                    LogUtil.e(TAG, "后面无更多数据");
                } else {
                    // 说明后面还需加载
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LOADING);
                    LogUtil.e(TAG, "后面仍需加载");
                }
                // 2,用新数据替换原来的数据源
                mAdapter.upData(list);
                // 3,当前页数设置为1
                currentPage = 1;
            }

            @Override
            public void callFailResult(ResultBean response) {
                LogUtil.e(TAG, "callFailResult()");
                // 刷新完毕
                isRefreshing = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callError(Call call, Exception e, int id) {
                LogUtil.e(TAG, "callError()");
                super.callError(call, e, id);
                // 刷新完毕
                isRefreshing = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callExceptionResult(Context context, ResultBean response) {
                super.callExceptionResult(context, response);
                LogUtil.e(TAG, "callExceptionResult()");
                // 刷新完毕
                isRefreshing = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }
        });
    }

    /**
     * 上拉加载更多数据
     * 上拉加载
     */
    private void getData_loadMore() {
        LogUtil.e(TAG, "正在执行getData_loadMore()");
        if (isLoadMore) {
            // 正在上拉加载,退出
            LogUtil.e(TAG, "正在上拉加载,取消本次操作");
            return;
        }
        if (isRefreshing) {
            // 正在刷新
            LogUtil.e(TAG, "正在刷新,取消本次操作");
            return;
        }
        final HashMap<String, String> params = new HashMap<>();
        params.put("LX", "Query");
        params.put("page", currentPage + 1 + "");
        NetApi.init(context, false).query(ConfigURL.GET_QUERY_DATA, params, new NetApi.OnCallBackListener() {

            @Override
            public void callSuccessResult(ResultBean response) {
                // 上拉加载完毕
                isLoadMore = false;
                List<Info> list = GsonUtil.GsonToList(response.getData(), Info.class);
                Info[] a = list.toArray(new Info[0]);
                LogUtil.e(TAG, String.format("callSuccessResult():page=%s;loadMore返回的数据:%s", currentPage + 1 + "", Arrays.toString(a)));
                // 1,设置Adapter中loadState的值
                if (list.size() < RecyclerAdapter.PAGE_SIZE) {
                    // 说明后面没有数据了
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LASTED);
                } else {
                    // 说明后面还需加载
                    mAdapter.setLoadState(RecyclerAdapter.STATE_LOADING);
                }
                // 2,数据拼接在现有数据源后面
                mAdapter.appendData(list);
                // 3,当前页数加1
                currentPage++;
            }

            @Override
            public void callFailResult(ResultBean response) {
                LogUtil.e(TAG, "callFailResult()");
                // 上拉加载完毕
                isLoadMore = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callError(Call call, Exception e, int id) {
                LogUtil.e(TAG, "callError()");
                super.callError(call, e, id);
                // 上拉加载完毕
                isLoadMore = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }

            @Override
            public void callExceptionResult(Context context, ResultBean response) {
                LogUtil.e(TAG, "callExceptionResult()");
                super.callExceptionResult(context, response);
                // 上拉加载完毕
                isLoadMore = false;
                // 设置Adapter中loadState的值,说明加载失败
                mAdapter.setLoadState(RecyclerAdapter.STATE_ERROR);
            }
        });
    }

2. 虽然底布局显示的是“加载中”,但并未按照预期联网加载数据

笔者在测试的过程中发现,若只是通过添加滑动监听事件,会出现“加载中”底布局已经完全可见,但若手指此时不滑动屏幕,不触发滑动事件,则不会加载下页数据。

故在Adapter的onBindViewHolder()中做文章。该方法当item滚动到屏幕中时被调用,当要加载“加载中”的底布局时,通过接口回调,调用上拉加载的逻辑。

switch (loadState) {
                case STATE_LOADING:
                    /*
                     * 正在加载:
                     * 文字显示“加载中”,显示进度条
                      */
                    footViewHolder.tv_LoadMoreAndRefreshRecycler_footer_content.setText(STR_LOADING);
                    footViewHolder.itemView.setOnClickListener(null);
                    footViewHolder.pb_LoadMoreAndRefreshRecycler_footer_loading.setVisibility(View.VISIBLE);
                    if (mOnMyItemClickListener != null) {
                        mOnMyItemClickListener.onLoadMore();
                    }
                    break;
// ...
}

3. 点击刷新时,去掉RecyclerView的底布局(此处是将底布局隐藏)

例如:正在刷新,但结果未返回时,这时把列表滑动到底部,发现底布局显示的还是上次数据加载设置的底布局
所以有必要在刷新数据时,把底布局隐藏,以防对用户造成误导。

private void getData_update() {
        // ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
        // 刷新时将底布局设置为什么都不显示
        mAdapter.setLoadState(RecyclerAdapter.STATE_OTHER);
        // ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
}

源码位置

SiXiWanZi_MyDemo

参考文献

你想知道的关于RecyclerView的秘密都在这里
项目需求讨论-RecycleView分页加载实现分析
SwipeRefreshLayout详解和自定义上拉加载更多

上一篇 下一篇

猜你喜欢

热点阅读