Android 實現自己的RecyclerView加載更多
很多時候,項目中都會有列表加載更多的場景,這次我們讓RecyclerView輕松擁有加載更多的功能。雖然已有許多類似的輪子,但有的功能過于復雜,其實很多都用不到,所以不妨打造更適合自己的輪子。
我們的RecyclerView加載更多是通過其Adapter子類實現的,接下來我們一步步的構建Adapter吧!
1、編寫通用的Adapter、ViewHolder
一般情況下使用Adapter都要為其創建一個ViewHolder,既然要編寫通用的Adapter,首先要有一個通用的ViewHolder:
public class ViewHolder extends RecyclerView.ViewHolder {
private SparseArray<View> mViews;
private View mConvertView;
private ViewHolder(View itemView) {
super(itemView);
mConvertView = itemView;
mViews = new SparseArray<>();
}
public static ViewHolder create(Context context, int layoutId, ViewGroup parent) {
View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
return new ViewHolder(itemView);
}
public static ViewHolder create(View itemView) {
return new ViewHolder(itemView);
}
public <T extends View> T getView(int viewId) {
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
public View getConvertView() {
return mConvertView;
}
public void setText(int viewId, String text) {
TextView textView = getView(viewId);
textView.setText(text);
}
.......省略其它輔助方法.........
}
我們自定義的ViewHolder類可以根據布局文件的id或具體的itemView返回一個ViewHolder對象,并用SparseArray來緩存我們itemView中的子View,避免每次都要去解析子View,同時提供相關輔助方法設置itemView的內容。有了ViewHolder,接下來編寫Adapter就簡單了:
public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public static final int TYPE_COMMON_VIEW = 100001;
private OnItemClickListeners<T> mItemClickListener;
protected Context mContext;
protected List<T> mDatas;
protected abstract void convert(ViewHolder holder, T data);
protected abstract int getItemLayoutId();
public BaseAdapter(Context context, List<T> datas) {
mContext = context;
mDatas = datas == null ? new ArrayList<T>() : datas;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder = null;
switch (viewType) {
case TYPE_COMMON_VIEW:
viewHolder = ViewHolder.create(mContext, getItemLayoutId(), parent);
break;
}
return viewHolder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (holder.getItemViewType()) {
case TYPE_COMMON_VIEW:
bindCommonItem(holder, position);
break;
}
}
private void bindCommonItem(RecyclerView.ViewHolder holder, final int position) {
final ViewHolder viewHolder = (ViewHolder) holder;
convert(viewHolder, mDatas.get(position));
viewHolder.getConvertView().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mItemClickListener.onItemClick(viewHolder, mDatas.get(position), position);
}
});
}
@Override
public int getItemCount() {
return mDatas.size();
}
@Override
public int getItemViewType(int position) {
return TYPE_COMMON_VIEW;
}
public T getItem(int position) {
if (mDatas.isEmpty()) {
return null;
}
return mDatas.get(position);
}
public void setOnItemClickListener(OnItemClickListeners<T> itemClickListener) {
mItemClickListener = itemClickListener;
}
}
很簡單,繼承RecyclerView.Adapter,重寫相關方法,提供了 getItemLayoutId() 、 convert() 兩個抽象方法供BaseAdapter的子類實現,來初始化item的布局id,以及item內容,同時通過 OnItemClickListeners 接口為item綁定點擊事件。
編寫好了Adapter,我們在其構造方法中添加一個參數 isOpenLoadMore ,來表示是否開啟加載更多:
public BaseAdapter(Context context, List<T> datas, boolean isOpenLoadMore) {
mContext = context;
mDatas = datas == null ? new ArrayList<T>() : datas;
mOpenLoadMore = isOpenLoadMore;
}
這樣初級版本的Adapter就完成了。
2、添加Footer View
接下來就要添加Footer View,這樣才能有加載更多的視覺效果么。其實很簡單,如果當前item的position滿足如下條件:
private boolean isFooterView(int position) {
return mOpenLoadMore && position >= getItemCount() - 1;
}
即已經開啟加載更多、當前position在列表的尾部,則在 getItemViewType() 返回
@Override
public int getItemViewType(int position) {
if (isFooterView(position)) {
return TYPE_FOOTER_VIEW;
}
}
之后會創建Footer View對應的ViewHolder:
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder = null;
switch (viewType) {
case TYPE_FOOTER_VIEW:
if (mFooterLayout == null) {
mFooterLayout = new RelativeLayout(mContext);
}
viewHolder = ViewHolder.create(mFooterLayout);
break;
}
return viewHolder;
}
可以看到 mFooterLayout 是一個空的Container,因為要根據加載更多對應的狀態來更新 mFooterLayout ,這個稍后再說。
這樣Footer View就添加完了嗎?當然沒有,我們需要針對StaggeredGridLayoutManager、GridLayoutManager模式分別重寫 onViewAttachedToWindow() 、 onAttachedToRecyclerView() 方法,否則會出現Footer View不能在列表底部占據一行的問題:
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (isFooterView(holder.getLayoutPosition())) {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager gridManager = ((GridLayoutManager) layoutManager);
gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (isFooterView(position)) {
return gridManager.getSpanCount();
}
return 1;
}
});
}
}
到此無論是那種形式的列表都能正常添加Footer View了。
3、判斷列表是否滾動到了底部
按照常理,只有滑動到列表的底部才會觸發加載更多的操作,之前提到了 onAttachedToRecyclerView() 方法,通過該方法可以得到Adapter所綁定的RecyclerView,這樣就能監聽RecyclerView的滾動事件,進而判斷列表是否滾動了底部:
private void startLoadMore(RecyclerView recyclerView, final RecyclerView.LayoutManager layoutManager) {
if (!mOpenLoadMore || mLoadMoreListener == null) {
return;
}
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (!isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
scrollLoadMore();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isAutoLoadMore && findLastVisibleItemPosition(layoutManager) + 1 == getItemCount()) {
scrollLoadMore();
} else if (isAutoLoadMore) {
isAutoLoadMore = false;
}
}
});
}
我們單獨封裝了 startLoadMore() 方法,當列表滾動狀態改變會回調 onScrollStateChanged() 方法,如果狀態為 SCROLL_STATE_IDLE ,并且當前可見的item位置為列表最后一項,則開始加載更多數據。這里還重寫了 onScrolled() 方法,當列表滾動結束后會回調,重寫該方法有什么用呢?如果初始item不滿一屏幕,則可在該方法中加載更多數據,直到item占滿一屏幕,也就自動加載更多。我們用 isAutoLoadMore 來區分這種情況,如果 isAutoLoadMore 為true,則Footer View可見則自動加載更多。
再看一下 scrollLoadMore() 方法:
private void scrollLoadMore() {
if (mFooterLayout.getChildAt(0) == mLoadingView) {
mLoadMoreListener.onLoadMore(false);
}
}
如果當前的Footer View 是正在加載的狀態,則調用 OnLoadMoreListener 接口的 onLoadMore() 方法進行具體的加載操作,該方法有一個boolean類型的參數,表示是否重新加載,因為存在加載失敗的情況,這樣可方便使用。
4、更新Footer View布局樣式
到這里,我們已經明確了加載更多操作的觸發時機,接下來就是在加載更多的時候來更新Footer View,我們定義了三種狀態: 加載中、加載失敗、加載結束 ,通過如下方法將對應狀態的View或布局id添加到Footer View中:
public void setLoadingView(int loadingId) {
setLoadingView(Util.inflate(mContext, loadingId));
}
public void setLoadFailedView(int loadFailedId) {
setLoadFailedView(Util.inflate(mContext, loadFailedId));
}
public void setLoadEndView(int loadEndId) {
setLoadEndView(Util.inflate(mContext, loadEndId));
}
這三個方法時是通過布局id來給Footer View設置新樣式,當然還有通過View來設置的重載方法。在初始化Adapter時可以調用 setLoadingView() 來設置加載中的Footer View樣式,如果加載失敗了可調用 setLoadFailedView() 、如果加載結束沒有更多數據則可以調用 setLoadEndView() 設對應的布局樣式。其實就是先移除 mFooterLayout 的子View,然后將新的布局添加進去。
5、添加EmptyView
考慮一種情況,如果初始化時,需要先從網絡請求數據,然后再更新列表,則一般需要有一個加載提示,所以我們有必要將這個小功能也封裝到Adapter中,這樣就省去了修改界面布局或者手動顯示、隱藏加載提示的步驟。
實現也很簡單,先看如下代碼:
@Override
public int getItemCount() {
if (mDatas.isEmpty() && mEmptyView != null) {
return 1;
}
}
如果mData為空,且設置了EmptyView則 getItemCount() 直接返回1。同理返回的item類型為 TYPE_EMPTY_VIEW ,代表EmptyView:
@Override
public int getItemViewType(int position) {
if (mDatas.isEmpty() && mEmptyView != null) {
return TYPE_EMPTY_VIEW;
}
}
在 onCreateViewHolder() 方法中會創建對應的ViewHolder。
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder viewHolder = null;
switch (viewType) {
case TYPE_EMPTY_VIEW:
viewHolder = ViewHolder.create(mEmptyView);
break;
}
return viewHolder;
}
同時提供方法在初始化Adapter時設置EmptyView:
public void setEmptyView(View emptyView) {
mEmptyView = emptyView;
}
6、具體使用
完成了封裝,來看看具體的使用,首先創建一個 RefreshAdapter 繼承我們的BaseAdapter:
public class RefreshAdapter extends BaseAdapter<String> {
public RefreshAdapter(Context context, List<String> datas, boolean isLoadMore) {
super(context, datas, isLoadMore);
}
@Override
protected void convert(ViewHolder holder, final String data) {
holder.setText(R.id.item_title, data);
holder.setOnClickListener(R.id.item_btn, new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(mContext, "我是" + data + "的button", Toast.LENGTH_SHORT).show();
}
});
}
@Override
protected int getItemLayoutId() {
return R.layout.item_layout;
}
}
在 getItemLayoutId() 中返回item布局id,在 convert() 中初始化item的內容。有了RefreshAdapter,接下來看Activity的操作:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
//初始化adapter
mAdapter = new RefreshAdapter(this, null, true);
//初始化EmptyView
View emptyView = LayoutInflater.from(this).inflate(R.layout.empty_layout, (ViewGroup) mRecyclerView.getParent(), false);
mAdapter.setEmptyView(emptyView);
//初始化 開始加載更多的loading View
mAdapter.setLoadingView(R.layout.load_loading_layout);
//設置加載更多觸發的事件監聽
mAdapter.setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore(boolean isReload) {
loadMore();
}
});
//設置item點擊事件監聽
mAdapter.setOnItemClickListener(new OnItemClickListeners<String>() {
@Override
public void onItemClick(ViewHolder viewHolder, String data, int position) {
Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show();
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(mAdapter);
//延時3s刷新列表
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
List<String> data = new ArrayList<>();
for (int i = 0; i < 12; i++) {
data.add("item--" + i);
}
//刷新數據
mAdapter.setNewData(data);
}
}, 3000);
}
注釋已經很詳細了,就不多說了。其中 loadMore() 方法如下:
private void loadMore() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (mAdapter.getItemCount() > 15 && isFailed) {
isFailed = false;
//加載失敗,更新footer view提示
mAdapter.setLoadFailedView(R.layout.load_failed_layout);
} else if (mAdapter.getItemCount() > 17) {
//加載完成,更新footer view提示
mAdapter.setLoadEndView(R.layout.load_end_layout);
} else {
final List<String> data = new ArrayList<>();
for (int i = 0; i < 2; i++) {
data.add("item--" + (mAdapter.getItemCount() + i - 1));
}
//刷新數據
mAdapter.setLoadMoreData(data);
}
}
}, 2000);
}
就是延時2s更新列表數據,同時人為模擬加載失敗和結束的情況。
7、效果
運行后,看具體的效果:
EmptyView
loading
load_failed
load_end
auto_load
來自:http://www.jianshu.com/p/66c065874848