之前我在GitHub上開源了一個可以實現RecyclerView列表分組的通用Adapter: GroupedRecyclerViewAdapter。也在部落格上寫了一篇專門介紹它的實現和使用的文章:《Android 可分組的RecyclerViewAdapter》。有一些朋友在看了我的博文和使用我的開源庫後,會主動的給我反饋一些問題和提出一些建議。我很高興,這使我覺得我所做的這些不僅可以提升自己,同時也能幫助到別人(雖然我很少寫部落格和開源東西)。前幾天就有朋友向我反饋,說他不僅需要一個分組的列表,同時還希望能給列表新增頭部和尾部。其實GroupedRecyclerViewAdapter的每個分組都是可以設定頭部和尾部的。如果只是給一個普通的列表新增頭部和尾部,只需要用GroupedRecyclerViewAdapter實現只有一個分組的列表就可以。但是他希望的是可以實現多個分組的同時,給整個大列表也設定頭部和尾部,這樣的需求是我以前從來沒有想過的,但我還是給出了自己的建議:列表的第一個分組只要頭部,不要尾部和子項,把它當做整個大列表的頭部,尾部的實現也一樣。這樣也能實現他的需求,但就是處理邏輯複雜了一點。其實我在設計GroupedRecyclerViewAdapter的時候,為了能讓它有更好的擴充套件性和能方便實現更多的複雜佈局,所以給它的頭部、尾部和子項都支援了多種型別的ViewTtype,有興趣的朋友歡迎去看一下。
ListView新增頭部和尾部的實現原理
在我們以前使用ListView的時候,ListView為我們提供了新增頭部(addHeaderView())和尾部(addFooterView())的方法,讓我們可以很方便的給列表新增頭部和尾部,而且頭部和尾部跟我們自己的ListView Adapter完全沒有任何關係。那麼ListView是如何做到的呢。讓我們一起開啟ListView的原始碼,看一下它是如何給列表新增頭部(addHeaderView())的。(新增尾部的原理是一樣的,這裡就不單獨說了)
public void addHeaderView(View v) {
addHeaderView(v, null, true);
}
public void addHeaderView(View v, Object data, boolean isSelectable) {
final FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mHeaderViewInfos.add(info);
mAreAllItemsSelectable &= isSelectable;
// 將ListView的Adapter包裝成HeaderViewListAdapter。
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}複製程式碼
上面就是ListView新增頭部的方法的原始碼,其中它會判斷當前ListView的Adapter是不是HeaderViewListAdapter,如果不是,這把當前的Adapter包裝成HeaderViewListAdapter。所以HeaderViewListAdapter就是實現ListView新增頭部的關鍵,那麼我們就去看一下HeaderViewListAdapter到底是個什麼東西。
public class HeaderViewListAdapter implements WrapperListAdapter, Filterable{
}複製程式碼
HeaderViewListAdapter實現了WrapperListAdapter介面,而WrapperListAdapter介面是ListAdapter的子介面,所以HeaderViewListAdapter就是ListAdapter的一個實現類,跟普通的ListView Adapter沒有太大區別。HeaderViewListAdapter接收一個普通的ListView Adapter和ListView頭部列表和尾部列表,並且對它們統一管理。下面分析一下它的核心程式碼。
//構造方法:接收從外部傳進來的頭部列表資訊和尾部列表資訊,還有被包裝的普通ListAdapter。
public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
ArrayList<ListView.FixedViewInfo> footerViewInfos,
ListAdapter adapter) {
}
//返回整個列表的item個數,其實就是普通Adapter的item個數加上頭尾部的個數。
public int getCount() {
if (mAdapter != null) {
return getFootersCount() + getHeadersCount() + mAdapter.getCount();
} else {
return getFootersCount() + getHeadersCount();
}
}
/返回當前列表項的ViewType。
public int getItemViewType(int position) {
int numHeaders = getHeadersCount();
if (mAdapter != null && position >= numHeaders) {
int adjPosition = position - numHeaders;
int adapterCount = mAdapter.getCount();
//如果當前列表項是普通的列表項,則交由mAdapter處理。
//傳給mAdapter的position需要除掉頭尾部的處理。
if (adjPosition < adapterCount) {
return mAdapter.getItemViewType(adjPosition);
}
}
//返回當前列表項時頭部或者尾部
return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
}
public View getView(int position, View convertView, ViewGroup parent) {
//如果當前列表項是頭部,則返回對應的頭部佈局。
int numHeaders = getHeadersCount();
if (position < numHeaders) {
return mHeaderViewInfos.get(position).view;
}
//如果當前列表項是普通的列表項,則交由mAdapter處理。
//傳給mAdapter的position需要除掉頭尾部的處理。
final int adjPosition = position - numHeaders;
int adapterCount = 0;
if (mAdapter != null) {
adapterCount = mAdapter.getCount();
if (adjPosition < adapterCount) {
return mAdapter.getView(adjPosition, convertView, parent);
}
}
//如果當前列表項是尾部,則返回對應的尾部佈局。
return mFooterViewInfos.get(adjPosition - adapterCount).view;
}複製程式碼
這就是HeaderViewListAdapter的核心程式碼,我們可以看到,它的程式碼非常的簡單,就是在getCount()、getItemViewType()、getView()中做了一些處理。在getCount()中它返回的個數里加上了頭部和尾部的個數。在getItemViewType()、getView()中,它判斷如果當前列表項是頭部或者尾部的時候自己處理,否則就交由被包裝的普通Adapter處理。所以HeaderViewListAdapter的主要作用就是管理ListView的頭部和尾部的。
HeaderViewListAdapter的設計非常的巧妙,只需要把我們設定給ListView的Adapter包裝一下,就可以讓我們的ListView具有了新增頭部和尾部的功能,而且絲毫不會影響到我們原來的Adapter。甚至於我們根本就不知道在我們給ListView新增頭部的時候,ListView已經將我們原來的Adapter包裝成HeaderViewListAdapter,我們也無需關心他的實現邏輯。
根據HeaderViewListAdapter的設計思路,我們是不是也可以給我們的RecyclerView.Adapter實現一個包裝類,只要對我們自己的Adapter包裝一下,就可以讓我們的列表具有了新增頭部和尾部的功能呢?帶著這樣的想法,於是我就自己動手寫了一個專門用來包裝RecyclerView的Adapter的包裝類:HeaderViewAdapter。
HeaderViewAdapter的程式碼實現
HeaderViewAdapter的設計思路和實現的效果跟HeaderViewListAdapter是完全一樣的,在程式碼的實現上會有所不同,畢竟ListView的Adapter和RecyclerView的Adapter是完全不同的兩個東西。
public class HeaderViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
//被包裝的Adapter。
private RecyclerView.Adapter mAdapter;
//用於存放HeaderView
private final List<FixedViewInfo> mHeaderViewInfos = new ArrayList<>();
//用於存放FooterView
private final List<FixedViewInfo> mFooterViewInfos = new ArrayList<>();
//用於監聽被包裝的Adapter的資料變化的監聽器。它將被包裝的Adapter的資料變化對映成HeaderViewAdapter的變化。
private RecyclerView.AdapterDataObserver mObserver = new RecyclerView.AdapterDataObserver() {
//這裡是具體的程式碼實現,因為篇幅的關係,在這裡就不放出來了。
};
public HeaderViewAdapter(RecyclerView.Adapter adapter) {
this.mAdapter = adapter;
if (mAdapter != null) {
//註冊mAdapter的資料變化監聽
mAdapter.registerAdapterDataObserver(mObserver);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 根據viewType查詢對應的HeaderView 或 FooterView。如果沒有找到則表示該viewType是普通的列表項。
View view = findViewForInfos(viewType);
if (view != null) {
return new ViewHolder(view);
} else {
//交由mAdapter處理。
return mAdapter.onCreateViewHolder(parent, viewType);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// 如果是HeaderView 或者是 FooterView則不繫結資料。
// 因為HeaderView和FooterView是由外部傳進來的,它們不由列表去更新。
if (isHeader(position) || isFooter(position)) {
return;
}
//將列表實際的position調整成mAdapter對應的position。
//交由mAdapter處理。
int adjPosition = position - getHeadersCount();
mAdapter.onBindViewHolder(holder, adjPosition);
}
@Override
public int getItemCount() {
return mHeaderViewInfos.size() + mFooterViewInfos.size()
+ (mAdapter == null ? 0 : mAdapter.getItemCount());
}
@Override
public int getItemViewType(int position) {
//如果當前item是HeaderView,則返回HeaderView對應的itemViewType。
if (isHeader(position)) {
return mHeaderViewInfos.get(position).itemViewType;
}
//如果當前item是HeaderView,則返回HeaderView對應的itemViewType。
if (isFooter(position)) {
return mFooterViewInfos.get(position - mHeaderViewInfos.size() - mAdapter.getItemCount()).itemViewType;
}
//將列表實際的position調整成mAdapter對應的position。
//交由mAdapter處理。
int adjPosition = position - getHeadersCount();
return mAdapter.getItemViewType(adjPosition);
}
/**
* 判斷當前位置是否是頭部View。
*
* @param position 這裡的position是整個列表(包含HeaderView和FooterView)的position。
* @return
*/
public boolean isHeader(int position) {
return position < getHeadersCount();
}
/**
* 判斷當前位置是否是尾部View。
*
* @param position 這裡的position是整個列表(包含HeaderView和FooterView)的position。
* @return
*/
public boolean isFooter(int position) {
return getItemCount() - position <= getFootersCount();
}
/**
* 獲取HeaderView的個數
*
* @return
*/
public int getHeadersCount() {
return mHeaderViewInfos.size();
}
/**
* 獲取FooterView的個數
*
* @return
*/
public int getFootersCount() {
return mFooterViewInfos.size();
}
/**
* 新增HeaderView
*
* @param view
*/
public void addHeaderView(View view) {
addHeaderView(view, generateUniqueViewType());
}
private void addHeaderView(View view, int viewType) {
//包裝HeaderView資料並新增到列表
FixedViewInfo info = new FixedViewInfo();
info.view = view;
info.itemViewType = viewType;
mHeaderViewInfos.add(info);
notifyDataSetChanged();
}
/**
* 新增FooterView
*
* @param view
*/
public void addFooterView(View view) {
addFooterView(view, generateUniqueViewType());
}
private void addFooterView(View view, int viewType) {
// 包裝FooterView資料並新增到列表
FixedViewInfo info = new FixedViewInfo();
info.view = view;
info.itemViewType = viewType;
mFooterViewInfos.add(info);
notifyDataSetChanged();
}
/**
* 生成一個唯一的數,用於標識HeaderView或FooterView的type型別,並且保證型別不會重複。
*
* @return
*/
private int generateUniqueViewType() {
int count = getItemCount();
while (true) {
//生成一個隨機數。
int viewType = (int) (Math.random() * Integer.MAX_VALUE) + 1;
//判斷該viewType是否已使用。
boolean isExist = false;
for (int i = 0; i < count; i++) {
if (viewType == getItemViewType(i)) {
isExist = true;
break;
}
}
//判斷該viewType還沒被使用,則返回。否則進行下一次迴圈,重新生成隨機數。
if (!isExist) {
return viewType;
}
}
}
/**
* 根據viewType查詢對應的HeaderView 或 FooterView。沒有找到則返回null。
*
* @param viewType 查詢的viewType
* @return
*/
private View findViewForInfos(int viewType) {
for (FixedViewInfo info : mHeaderViewInfos) {
if (info.itemViewType == viewType) {
return info.view;
}
}
for (FixedViewInfo info : mFooterViewInfos) {
if (info.itemViewType == viewType) {
return info.view;
}
}
return null;
}
/**
* 用於包裝HeaderView和FooterView的資料類
*/
private class FixedViewInfo {
//儲存HeaderView或FooterView
View view;
//儲存HeaderView或FooterView對應的viewType。
int itemViewType;
}
private static class ViewHolder extends RecyclerView.ViewHolder {
ViewHolder(View itemView) {
super(itemView);
}
}
}複製程式碼
為了讓大家看程式碼方便一點,我刪除了一些不是很重要的程式碼,而且把程式碼的實現細節講解寫到了每個方法的註釋上,相信大家都能很容易的看懂。如果大家想看完整的程式碼,請移步我的GitHub。
HeaderViewAdapter的使用
我已經把HeaderViewAdapter和相關的類打包成一個引用庫放到GitHub上,歡迎大家使用和star。
在ListView的設計上,對HeaderViewListAdapter的所有操作的是由ListView自己完成的。但是我們無法把對HeaderViewAdapter的操作交由RecyclerView來處理,所以需要自己對HeaderViewAdapter進行操作(包裝、新增頭部和尾部等),其實這也很簡單,幾句程式碼的事情而已。而且它可以適用於任何的RecyclerView,沒有任何使用上的限制。
//需要包裝的adapter
LinearAdapter adapter = new LinearAdapter(this);
//對adapter進行包裝。
HeaderViewAdapter headerViewAdapter = new HeaderViewAdapter(adapter);
//新增HeaderView和FooterView
headerViewAdapter.addHeaderView(headerView);
headerViewAdapter.addFooterView(footerView);
//設定Adapter
recyclerView.setAdapter(headerViewAdapter);複製程式碼
無論我們的RecyclerView使用什麼LayoutManager,HeaderViewAdapter都需要保證列表的頭部和尾部能佔滿一行,否則佈局就會很難看。使用LinearLayoutManager的時候不需要做特殊的處理,HeaderViewAdapter也已經幫我們處理了StaggeredGridLayoutManager的情況。至於GridLayoutManager的情況,我在HeaderViewAdapter的庫裡提供了一個HeaderViewGridLayoutManager的子類。所以大家在使用GridLayoutManager的時候,應該使用HeaderViewGridLayoutManager。
recyclerView.setLayoutManager(new HeaderViewGridLayoutManager(this, 2, headerViewAdapter));複製程式碼
為了讓我們的RecyclerView新增頭部和尾部的時候,更接近於ListView的體驗。所以我在庫裡提供了一個RecyclerView子類:HeaderRecyclerView。HeaderRecyclerView封裝了對HeaderViewAdapter的所以操作,這使我們只需要操作HeaderRecyclerView,而無需直接跟HeaderViewAdapter打交道,這使得我們使用HeaderRecyclerView的時候就如同以前使用ListView一樣。
HeaderRecyclerView rvList = (HeaderRecyclerView) findViewById(R.id.rv_list);
//這是普通的adapter
GridAdapter adapter = new GridAdapter(this);
rvList.setLayoutManager(new GridLayoutManager(this, 2));
//直接設定普通的adapter,不需要直接進行包裝。
rvList.setAdapter(adapter);
//新增HeaderView和FooterView。直接操作HeaderRecyclerView。
rvList.addHeaderView(headerView);
rvList.addFooterView(footerView);複製程式碼
效果圖: