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