RecyclerView刷新和上拉加载
说到上拉加载,总是离不开刷新。没有刷新,哪有后面的上拉加载。所以“刷新”和“上拉加载”是有时间的先后顺序,有关联性的。故“刷新”和“上拉加载”要结合起来一起说。
RecyclerView的刷新有两种:
-
界面上放置一个按钮用于刷新
界面上放置一个按钮用于刷新.png
-
RecyclerView配合SwipeRefreshLayout用于刷新
<?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的上拉加载功能
想要实现的效果
每次联网加载完数据后:
- 返回数据条数<PageSize,底布局显示“没有更多数据”;
- 返回数据条数=PageSize,后面可能还有数据,底布局显示“加载中”
- 返回数据异常,底布局显示“加载失败”
通过分析我们的目标,我们可以得出的结论是:其实就是需要给RecyclerView添加一个底布局。【按照我们预期的效果,无论什么时候(加载成功,还是加载失败),始终都要添加一个底布局说明本次加载数据的情况】
Q:如何给RecyclerView添加底布局
A: getItemCount()、getItemViewType()、onCreateViewHolder()、onBindViewHolder()配合使用
- getItemCount()
@Override
public int getItemCount() {
// 无论什么时候(加载成功,还是加载失败),始终都要添加一个底布局
return mDatas.size() + 1;
}
- getItemViewType()
@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);
}
}
- onCreateViewHolder()
//实例化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;
}
- 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;
}
}
此时显示的效果是:无论加载多少条数据,底布局都是相同的。而我们需要的是根据不同的情况显示不同的布局
![](https://img.haomeiwen.com/i1836565/33dd07898d01b6de.png)
![](https://img.haomeiwen.com/i1836565/f963930e045ff507.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表示:当前没有数据)
- getData_update
/**
* 得到刷新数据
* 第一页数据
*/
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);
}
});
}
- getData_loadMore
/**
* 上拉加载更多数据
* 上拉加载
*/
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. 刷新、加载,同一时刻只进行一个网络请求获取数据
- 正在刷新,但结果未返回时,点击“刷新” 按钮,这个动作导致又要联网查询数据;
- 正在刷新,但结果未返回时,将现有数据滑动到底部,显示出了“加载中”底布局,这个动作导致又要联网加载下页数据
- 正在加载下页数据,但结果未返回时,点击“刷新” 按钮,这个动作导致又要联网查询数据;
- 正在加载下页数据,但结果未返回时,将现有数据滑动到底部,显示出了“加载中”底布局,会不停的触发RecyclerView的滑动事件加载下页数据。
上述情况中,描述的是当前查询结果未返回时,又进行了下一次查询请求,这样的操作没有必要,浪费资源,且会造成数据错乱。
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);
// ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
}
源码位置
参考文献
你想知道的关于RecyclerView的秘密都在这里
项目需求讨论-RecycleView分页加载实现分析
SwipeRefreshLayout详解和自定义上拉加载更多