在我們開發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()。
Over.