RecyclerView 大家肯定很熟悉,平常使用的时候也难免会遇到下拉刷新和上拉加载更多,网上相关的控件也是多如牛毛。我特别期待谷歌什么时候能够自己开发一个,可惜一直到现在,也只有一个SwipeRefreshLayout下拉刷新控件,上拉加载连个影子都没。。。
如果不想使用第三方的控件,又想有上拉加载更多功能怎么办呢?于是上网查了相关资料,发现RecyclerView有一个监听方法addOnScrollListener,可以监听到RecyclerView的滑动状态,欣喜若狂,好好研究了一番,并且自己封装了下
Demo 地址
https://github.com/linqinen708/MyDatabindingRecyclerView
使用方式:
首先在xml布局中使用
<MyRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
然后在Activity中初始化,大功告成
其中adapter 就是RecyclerView的adapter
private void initRefreshLayout() {
mRefreshLayout.setAdapter(mAdapter);
mRefreshLayout.setRefreshListener(new MyRefreshLayout.RefreshListener() {
@Override
public void loadMore() {
}
@Override
public void refresh() {
}
});
}
那么其中的原理是什么呢?
下面简单说一下:
/**最后一个可见的item的索引*/
private int lastVisibleItemPosition;
/**
* 是否正在上拉加载更多
*/
private boolean isLoadingMore;
***
***
***
private void initLoadMoreListener() {
mLayoutManager = new LinearLayoutManager(this);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// LogT.i("停止滑动:");
int visibleItemCount = mLayoutManager.getChildCount();
int totalItemCount = mLayoutManager.getItemCount();
LogT.i("visibleItemCount:" + visibleItemCount + ", totalItemCount:" + totalItemCount + ",lastVisibleItemPosition:" + lastVisibleItemPosition);
if (!isLoadingMore && lastVisibleItemPosition == totalItemCount - 1) {
isLoadingMore = true;
}
}
}
/**仅对LinearLayoutManager 有效,其他LayoutManager未验证*/
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
}
});
}
首先需要通过onScrolled方法,获得屏幕中可见的最后一个item的position,然后在onScrollStateChanged方法中,判断RecyclerView是否已经停止滚动,也就是newState == RecyclerView.SCROLL_STATE_IDLE,
然后判断最后一个item的position是否是总数的最后一个lastVisibleItemPosition == totalItemCount - 1,如果是,则说明滑到底部了,整个逻辑就是这么简单粗暴
然后我就自己封装了下RecyclerView,变成了LoadMoreRecyclerView
public class LoadMoreRecyclerView extends RecyclerView {
private LinearLayoutManager mLayoutManager;
private int lastVisibleItemPosition;
/**
* 是否可以上拉加载更多
* 比如当暂无更多数据之后,就没必要再支持上拉加载更多了
* 可以关闭上拉功能
*/
private boolean isLoadMoreEnable = true;
public boolean isLoadMoreEnable() {
return isLoadMoreEnable;
}
public void setLoadMoreEnable(boolean loadMoreEnable) {
isLoadMoreEnable = loadMoreEnable;
}
/**
* 是否正在上拉加载更多,
* 如果不做额外判断,而用户连续快速上拉,则会出现多次请求
*/
private boolean isLoadingMore;
public boolean isLoadingMore() {
return isLoadingMore;
}
public void completeLoadMore() {
isLoadingMore = false;
}
private LoadMoreListener mLoadMoreListener;
public void setLoadMoreListener(LoadMoreListener loadMoreListener) {
init();
mLoadMoreListener = loadMoreListener;
}
public interface LoadMoreListener {
void loadMore();
}
public LoadMoreRecyclerView(@NonNull Context context) {
super(context);
initView();
}
public LoadMoreRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView();
}
public LoadMoreRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
/**如果不取消动画,会导致数据不一致的报错
* IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter
* */
private void initView(){
setItemAnimator(null);
}
private void init() {
LogT.i("初始化:" + getLayoutManager());
if (getLayoutManager() != null && getLayoutManager() instanceof LinearLayoutManager) {
mLayoutManager = (LinearLayoutManager) getLayoutManager();
}
if (mLayoutManager != null) {
addOnScrollListener(new RecyclerView.OnScrollListener() {
/**
* 当显示的item数量不够多,无法撑满屏幕高度时
* 无论是上拉还是下拉,都会触发该方法,
* 所以下拉需要满足条件
* 否则下拉也会触发加载更多
* 所以当 visibleItemCount < totalItemCount 才触发加载更多
* 即 item的数量够多,并超过屏幕高度时,才触发加载更多
* */
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (isLoadMoreEnable && newState == RecyclerView.SCROLL_STATE_IDLE) {
// LogT.i("停止滑动:");
int visibleItemCount = mLayoutManager.getChildCount();
int totalItemCount = mLayoutManager.getItemCount();
// LogT.i(":" + getAdapter().getItemViewType(0));
// LogT.i("isLoadingMore:"+isLoadingMore + ",visibleItemCount:" + visibleItemCount + ", totalItemCount:" + totalItemCount + ",lastVisibleItemPosition:" + lastVisibleItemPosition);
if (!isLoadingMore && visibleItemCount < totalItemCount && lastVisibleItemPosition == totalItemCount - 1) {
isLoadingMore = true;
if (getAdapter() != null && getAdapter() instanceof BaseBindingAdapter) {
LogT.i("加载更多:");
((BaseBindingAdapter) getAdapter()).showLoadMore(true);
}
LogT.i("mLoadMoreListener:" + mLoadMoreListener);
if (mLoadMoreListener != null) {
mLoadMoreListener.loadMore();
}
}
}
}
/**仅对LinearLayoutManager 有效,其他LayoutManager未验证*/
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
// LogT.i("dx:" +dx + ", dy:" + dy);
if (isLoadMoreEnable) {
lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
}
}
});
}
}
}
使用方式:
mRecyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
@Override
public void loadMore() {
httpRequest();
}
});
不过这样子只完成了一半,还不是一个完整的控件,于是我自己也封装了一个控件MyRefreshLayout,整个控件非常简单,就是把SwipeRefreshLayout和LoadMoreRecyclerView 整合在一起,这样,就是一个简单的下拉刷新和上拉加载更多控件
public class MyRefreshLayout extends FrameLayout implements SwipeRefreshLayout.OnRefreshListener {
private Context mContext;
private SwipeRefreshLayout mSwipeRefreshLayout;
private LoadMoreRecyclerView mRecyclerView;
private BaseBindingAdapter mAdapter;
private RefreshListener mRefreshListener;
/**
* 页数
*/
private int page = 1;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public void setRefreshListener(RefreshListener refreshListener) {
mRefreshListener = refreshListener;
mSwipeRefreshLayout.setEnabled(true);
mSwipeRefreshLayout.setOnRefreshListener(this);
mRecyclerView.setLoadMoreListener(new LoadMoreRecyclerView.LoadMoreListener() {
@Override
public void loadMore() {
page++;
if (mRefreshListener != null) {
mRefreshListener.loadMore();
}
}
});
}
public interface RefreshListener {
void loadMore();
void refresh();
}
public MyRefreshLayout(@NonNull Context context) {
super(context);
mContext = context;
initView();
}
public MyRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
initView();
}
public MyRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initView();
}
/**
* 初始化控件
*/
private void initView() {
mSwipeRefreshLayout = new SwipeRefreshLayout(mContext);
mSwipeRefreshLayout.setEnabled(false);
mRecyclerView = new LoadMoreRecyclerView(mContext);
mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
/*因为自定义的LoadMoreListener 需要LinearLayoutManager支持
* 所以setLoadMoreListener 需要在setLayoutManager 之后 设置
* */
mSwipeRefreshLayout.addView(mRecyclerView);
addView(mSwipeRefreshLayout);
}
@Override
public void onRefresh() {
page = 1;
if (mRefreshListener != null) {
mRefreshListener.refresh();
}
}
/**
* 下拉刷新完成
*/
public void completeRefresh() {
/*如果下拉刷新后,mRecyclerView 自动加载到底部,则让其返回到顶部*/
mRecyclerView.scrollToPosition(0);
mSwipeRefreshLayout.setRefreshing(false);
}
/**
* 上拉加载更多完成
*/
public void completeLoadMore() {
mRecyclerView.completeLoadMore();
}
public void complete() {
completeLoadMore();
completeRefresh();
}
public void completeHttpRequest(Collection collection) {
if (mAdapter == null) {
return;
}
if (page == 1) {
mAdapter.getItems().clear();
completeRefresh();
mAdapter.toggleFootView(collection);
setLoadMoreEnable(true);
} else {
completeLoadMore();
setLoadMoreEnable(mAdapter.toggleLoadMore(collection));
}
}
public void setAdapter(BaseBindingAdapter adapter) {
mAdapter = adapter;
mRecyclerView.setAdapter(adapter);
}
public void addItemDecoration(@NonNull RecyclerView.ItemDecoration decor) {
mRecyclerView.addItemDecoration(decor);
}
public void setLoadMoreEnable(boolean loadMoreEnable) {
mRecyclerView.setLoadMoreEnable(loadMoreEnable);
}
}
坑1:
在使用的过程当中,如果用的是notifyItemRangeInserted 或则 notifyItemRangeRemoved 刷新数据,而不是notifyDataSetChanged(),有可能会出错IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter,上网查了很多方法,解决的方式是mRecyclerView.setItemAnimator(null);真是一脸懵逼,加个动画居然还有问题。。。还是RecyclerView默认的自带动画。。。
坑2:
如果mAdapter.getItems().clear()之后,再加入数据,很有可能会发现RecyclerView仍然在底部,所以需要mRecyclerView.scrollToPosition(0); 返回顶部
坑3:
当显示的item数量不够多,无法撑满屏幕高度时无论是上拉还是下拉,都会触发onScrollStateChanged该方法,所以下拉需要满足条件否则下拉也会触发加载更多 所以当 visibleItemCount < totalItemCount 才触发加载更多 即 item的数量够多,并超过屏幕高度时,才触发加载更多
坑4:
如果有人连续上拉加载更多,可能会导致数据错乱,所以加一个boolean值isLoadMoreEnable,当上拉加载更多还没有结束时,不让其继续上拉加载更多
温馨提示:
自己封装的RefreshLayout采用的adapter 是自己封装的BaseBindingAdapter
我自己又额外封装了一个方法
当请求后台数据,获得List后,直接调用completeHttpRequest方法,
就可以动态的实现FooterView 是 “正在加载…” 还是 “暂无更多数据”
非常方便
mRefreshLayout.completeHttpRequest(bean.getRooms());
MVVM模式下RecyclerView与databinding的结合
MVVM模式下RecyclerView与databinding的结合(2)
参考资料 https://blog.csdn.net/weixin_37577039/article/details/79214663