BaseRecyclerViewAdapterHelper原始碼解讀(四) 上拉載入更多
上拉載入
上拉載入無需監聽滑動事件,可自定義載入佈局,顯示異常提示,自定義異常提示。
此篇文章為BaseRecyclerViewAdapterHelper原始碼解讀第四篇,開源庫地址,如果沒有看過之前3篇文章的同學可以先去看看,大神可直接跳過.
今天給大家帶來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項時,就開始去重新整理,並顯示載入佈局和回撥介面,讓外部去實現重新整理.
道理雖然很簡單,但是實現起來的話,有很多很多細節在裡面,很多很多的坑,再次感謝開源庫的作者們.