RecycleView的封裝實現上拉載入更多,可以在有多種RecycleView的佈局,特別有listview存在時使用。
在我們開發app的時候,列表元件總是最常用的。目前下拉重新整理和上拉載入的元件有很多。Github一搜索,大部分的開源專案都只實現了下拉重新整理而沒有上拉載入,也有部分專案把上拉載入更多實現了,但是這樣做其實並不好,因為在app實際的執行中當戶滑動到底部就應該自動載入下一頁的內容(決大部分app都是這樣做的),而不是要讓使用者手動去上拉才會去載入,這樣會嚴重影響到使用者體驗,當然,個別特殊情況除外。
今天我要講的內容可能很多人都知道怎麼做,搜一搜也會出現很多相應的內容。我這次分享主要的目的是聊聊實現方案和在其中遇到的一些問題,如果有更好的實現方案,望不吝賜教。
1.常規版
我們都知道RecyclerView可以監聽滾動事件的Listener:我們只需實現裡面對應的事件就可以實現滑動到底部載入更多了
程式碼實現大概是這樣的(這裡只考慮了佈局管理器是LinearLayoutManager的情況,其他的也類似):
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); int totalItemCount = recyclerView.getAdapter().getItemCount(); int lastVisibleItemPosition = lm.findLastVisibleItemPosition(); int visibleItemCount = recyclerView.getChildCount(); if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItemPosition == totalItemCount - 1 && visibleItemCount > 0) { //載入更多 } } });
上面的程式碼我相信做過自動載入的朋友都很熟悉, 這樣做確實能做到自動載入更多,但是如果我們想顯示載入的狀態(載入中,加載出錯,沒有更多了)怎麼辦呢?只是用這種方案肯定是不能解決的。
2.進階版
我們也都知道RecyclerView不能像ListView那樣直接就有addHeadView和addFooterView之類的方法,要想實現載入狀態的顯示必須要在Adapter上動手腳才行。洋神對此也有了一種實現LoadmoreWrapper(只是單純的載入更多,並沒有狀態的處理),程式碼大概是這樣的(我只留下了關鍵部分的程式碼,想看程式碼完整實現的請直接點選上面的連結。):
public class LoadMoreWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public static final int ITEM_TYPE_LOAD_MORE = Integer.MAX_VALUE - 2; private RecyclerView.Adapter mInnerAdapter; private View mLoadMoreView; private int mLoadMoreLayoutId; public LoadMoreWrapper(RecyclerView.Adapter adapter) { mInnerAdapter = adapter; } ... @Override public int getItemViewType(int position) { if (isShowLoadMore(position)) { return ITEM_TYPE_LOAD_MORE; } return mInnerAdapter.getItemViewType(position); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == ITEM_TYPE_LOAD_MORE) { ViewHolder holder; if (mLoadMoreView != null) { holder = ViewHolder.createViewHolder(parent.getContext(), mLoadMoreView); } else { holder = ViewHolder.createViewHolder(parent.getContext(), parent, mLoadMoreLayoutId); } return holder; } return mInnerAdapter.onCreateViewHolder(parent, viewType); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (isShowLoadMore(position)) { if (mOnLoadMoreListener != null) { mOnLoadMoreListener.onLoadMoreRequested(); } return; } mInnerAdapter.onBindViewHolder(holder, position); } ... private OnLoadMoreListener mOnLoadMoreListener; public LoadMoreWrapper setOnLoadMoreListener(OnLoadMoreListener loadMoreListener) { if (loadMoreListener != null) { mOnLoadMoreListener = loadMoreListener; } return this; } ... }
可以看到這裡巧妙的用到了裝飾者模式,完全與資料展示的邏輯分隔,在相應的方法裡做了對應判斷。載入更多的呼叫就在onBindViewHolder這個方法中,意思就是當這個View顯示出來了,我們就觸發載入更多這個方法。
但是這裡還是沒有實現狀態的處理。這確實是一個好的方案這樣做還是有點不能滿足實際開發中的一些需求。並且這樣做還有一個潛在的問題:如果你的請求沒有延時,也就是說我們直接add的一批資料,然後直接呼叫了notifyDataSetChanged()方法,就會出下下面的錯誤:
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
3.綜合版
上面兩種方式各有各的優點,如果我們能將這兩種方式結合在一起,那就好了。第一種方式服務載入更多,第二種方式負責狀態的顯示。下面來看看程式碼的實現(有省略):
public class LoadMoreWrapper extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int ITEM_TYPE_LOAD_FAILED_VIEW = Integer.MAX_VALUE - 1;
public static final int ITEM_TYPE_NO_MORE_VIEW = Integer.MAX_VALUE - 2;
public static final int ITEM_TYPE_LOAD_MORE_VIEW = Integer.MAX_VALUE - 3;
public static final int ITEM_TYPE_NO_VIEW = Integer.MAX_VALUE - 4;//不展示footer view
private Context mContext;
private RecyclerView.Adapter mInnerAdapter;
private View mLoadMoreView;
private View mLoadMoreFailedView;
private View mNoMoreView;
private int mCurrentItemType = ITEM_TYPE_LOAD_MORE_VIEW;
private LoadMoreScrollListener mLoadMoreScrollListener;
private boolean isLoadError = false;//標記是否加載出錯
private boolean isHaveStatesView = true;
public LoadMoreWrapper(Context context, RecyclerView.Adapter adapter) {
this.mContext = context;
this.mInnerAdapter = adapter;
mLoadMoreScrollListener = new LoadMoreScrollListener() {
@Override
public void loadMore() {
if (mOnLoadListener != null && isHaveStatesView) {
if (!isLoadError) {
showLoadMore();
mOnLoadListener.onLoadMore();
}
}
}
};
}
public void showLoadMore() {
mCurrentItemType = ITEM_TYPE_LOAD_MORE_VIEW;
isLoadError = false;
isHaveStatesView = true;
notifyItemChanged(getItemCount());
}
public void showLoadError() {
mCurrentItemType = ITEM_TYPE_LOAD_FAILED_VIEW;
isLoadError = true;
isHaveStatesView = true;
notifyItemChanged(getItemCount());
}
public void showLoadComplete() {
mCurrentItemType = ITEM_TYPE_NO_MORE_VIEW;
isLoadError = false;
isHaveStatesView = true;
notifyItemChanged(getItemCount());
}
public void disableLoadMore() {
mCurrentItemType = ITEM_TYPE_NO_VIEW;
isHaveStatesView = false;
notifyDataSetChanged();
}
//region Get ViewHolder
private ViewHolder getLoadMoreViewHolder() {
if (mLoadMoreView == null) {
mLoadMoreView = new TextView(mContext);
mLoadMoreView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
mLoadMoreView.setPadding(20, 20, 20, 20);
((TextView) mLoadMoreView).setText("正在載入中");
((TextView) mLoadMoreView).setGravity(Gravity.CENTER);
}
return ViewHolder.createViewHolder(mContext, mLoadMoreView);
}
private ViewHolder getLoadFailedViewHolder() {
if (mLoadMoreFailedView == null) {
mLoadMoreFailedView = new TextView(mContext);
mLoadMoreFailedView.setPadding(20, 20, 20, 20);
mLoadMoreFailedView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
((TextView) mLoadMoreFailedView).setText("載入失敗,請點我重試");
((TextView) mLoadMoreFailedView).setGravity(Gravity.CENTER);
}
return ViewHolder.createViewHolder(mContext, mLoadMoreFailedView);
}
private ViewHolder getNoMoreViewHolder() {
if (mNoMoreView == null) {
mNoMoreView = new TextView(mContext);
mNoMoreView.setPadding(20, 20, 20, 20);
mNoMoreView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT));
((TextView) mNoMoreView).setText("--end--");
((TextView) mNoMoreView).setGravity(Gravity.CENTER);
}
return ViewHolder.createViewHolder(mContext, mNoMoreView);
}
//endregion
@Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1 && isHaveStatesView) {
return mCurrentItemType;
}
return mInnerAdapter.getItemViewType(position);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == ITEM_TYPE_NO_MORE_VIEW) {
return getNoMoreViewHolder();
} else if (viewType == ITEM_TYPE_LOAD_MORE_VIEW) {
return getLoadMoreViewHolder();
} else if (viewType == ITEM_TYPE_LOAD_FAILED_VIEW) {
return getLoadFailedViewHolder();
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder.getItemViewType() == ITEM_TYPE_LOAD_FAILED_VIEW) {
mLoadMoreFailedView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnLoadListener != null) {
mOnLoadListener.onRetry();
showLoadMore();
}
}
});
return;
}
mInnerAdapter.onBindViewHolder(holder, position);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
...
recyclerView.addOnScrollListener(mLoadMoreScrollListener);
}
...
@Override
public int getItemCount() {
return mInnerAdapter.getItemCount() + (isHaveStatesView ? 1 : 0);
}
...
}
程式碼的實現也還是很簡單,定義了四種itemType,分別對應載入失敗、沒有更多了、載入中、不顯示四種狀態,同時也實現了錯誤重試的處理。在onAttachedToRecyclerView回撥中去新增LoadMoreScrollListener。
完整版程式碼地址:LoadMoreWrapper
說了這麼多,怎麼用?
//把你用的adapter傳進去
LoadMoreWrapper mLoadMoreWrapper=new LoadMoreWrapper (mAdapter);
mLoadMoreWrapper.setOnLoadListener(new LoadMoreWrapper.OnLoadListener() {
@Override
public void onRetry() {
//重試處理
}
@Override
public void onLoadMore() {
//載入更多
}
});
mRecyclerView.setAdapter(mLoadMoreWrapper);
在重新整理資料的時候呼叫一下showLoadMore(),資料加載出錯的時候呼叫一下showLoadError(),資料載入完成的時候呼叫showLoadComplete()。
想看Demo的請看這裡:AbsListFragment,這是我封裝了一個通用的列表展示Fragment。
Over.