1、概述
RecyclerView通過(guò)其高度的可定制性深受大家的青睞袱蜡,也有非常多的使用者開始對(duì)它進(jìn)行封裝或者改造叙淌,從而滿足越來(lái)越多的需求。
如果你對(duì)RecyclerView不陌生的話汉操,你一定遇到過(guò)這樣的情況再来,我想給RecyclerView加個(gè)headerView或者footerView,當(dāng)你敲出.addHeaderView
磷瘤,你會(huì)發(fā)現(xiàn)并沒(méi)有添加頭部或者底部View的相關(guān)API芒篷。
那么本文主要的內(nèi)容很明顯了,完成以下工作:
- 如何為RecyclerView添加HeaderView(支持多個(gè))
- 如何為RecyclerView添加FooterView(支持多個(gè))
- 如何讓HeaderView或者FooterView適配各種LayoutManager
恩采缚,其實(shí)本來(lái)我是想偷個(gè)懶的针炉,因?yàn)長(zhǎng)oader寫過(guò)一篇類似的文章,文章見文末參考鏈接扳抽。但是我發(fā)現(xiàn)被別的公眾號(hào)推送了~~
那我只能考慮自己換種思路來(lái)解決這個(gè)問(wèn)題篡帕,并且提供盡可能多的功能了~
本文首發(fā)于我的公眾號(hào),歡迎掃碼關(guān)注(二維碼見左側(cè)欄)贸呢。
2 思路
(1)原理
對(duì)于添加headerView或者footerView的思路
其實(shí)HeaderView實(shí)際上也是Item的一種镰烧,只不過(guò)顯示在頂部的位置,那么我們完全可以通過(guò)為其設(shè)置ItemType來(lái)完成楞陷。
有了思路以后拌滋,我們心里就妥了,最起碼我們的內(nèi)心中想想是可以實(shí)現(xiàn)的猜谚,接下來(lái)考慮一些細(xì)節(jié)败砂。
(2)一些細(xì)節(jié)
假設(shè)我們現(xiàn)在已經(jīng)完成了RecyclerView的編寫,忽然有個(gè)需求魏铅,需要在列表上加個(gè)HeaderView昌犹,此時(shí)我們?cè)撛趺崔k呢?
打開我們的Adapter览芳,然后按照我們上述的原理斜姥,添加特殊的ViewType,然后修改代碼完成沧竟。
這是比較常規(guī)的做法了铸敏,但是有個(gè)問(wèn)題是,如果需要添加viewType悟泵,那么可能我們的Adapter需要修改的幅度就比較大了杈笔,比如getItemType
、getItemCount
糕非、onBindViewHolder
蒙具、onCreateViewHolder
等球榆,幾乎所有的方法都要進(jìn)行改變。
這樣來(lái)看禁筏,出錯(cuò)率是非常高的持钉。
況且一個(gè)項(xiàng)目中可能多個(gè)RecyclerView都需要在其列表中添加headerView。
這么來(lái)看篱昔,直接改Adapter的代碼是非常不劃算的每强,最好能夠設(shè)計(jì)一個(gè)類,可以無(wú)縫的為原有的Adapter添加headerView和footerView州刽。
本文的思路是通過(guò)類似裝飾者模式空执,去設(shè)計(jì)一個(gè)類,增強(qiáng)原有Adapter的功能怀伦,使其支持addHeaderView
和addFooterView
脆烟。這樣我們就可以不去改動(dòng)我們之前已經(jīng)完成的代碼,靈活的去擴(kuò)展功能了房待。
我希望的用法是這樣的:
mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);
t1.setText("Header 1");
TextView t2 = new TextView(this);
mHeaderAndFooterWrapper.addHeaderView(t2);
在不改變?cè)械腁dapter基礎(chǔ)上去增強(qiáng)其功能邢羔。
3、初步的實(shí)現(xiàn)
(1) 基本代碼
public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
private static final int BASE_ITEM_TYPE_HEADER = 100000;
private static final int BASE_ITEM_TYPE_FOOTER = 200000;
private SparseArrayCompat<View> mHeaderViews = new SparseArrayCompat<>();
private SparseArrayCompat<View> mFootViews = new SparseArrayCompat<>();
private RecyclerView.Adapter mInnerAdapter;
public HeaderAndFooterWrapper(RecyclerView.Adapter adapter)
{
mInnerAdapter = adapter;
}
private boolean isHeaderViewPos(int position)
{
return position < getHeadersCount();
}
private boolean isFooterViewPos(int position)
{
return position >= getHeadersCount() + getRealItemCount();
}
public void addHeaderView(View view)
{
mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, view);
}
public void addFootView(View view)
{
mFootViews.put(mFootViews.size() + BASE_ITEM_TYPE_FOOTER, view);
}
public int getHeadersCount()
{
return mHeaderViews.size();
}
public int getFootersCount()
{
return mFootViews.size();
}
}
首先我們編寫一個(gè)Adapter的子類桑孩,我們叫做HeaderAndFooterWrapper拜鹤,然后再其內(nèi)部添加了addHeaderView,addFooterView等一些輔助方法流椒。
這里你可以看到敏簿,對(duì)于多個(gè)HeaderView,講道理我們首先想到的應(yīng)該是使用List<View>
宣虾,而這里我們?yōu)槭裁匆褂?code>SparseArrayCompat<View>呢惯裕?
SparseArrayCompat有什么特點(diǎn)呢?它類似于Map绣硝,只不過(guò)在某些情況下比Map的性能要好蜻势,并且只能存儲(chǔ)key為int的情況。
并且可以看到我們對(duì)每個(gè)HeaderView鹉胖,都有一個(gè)特定的key與其對(duì)應(yīng)握玛,第一個(gè)headerView對(duì)應(yīng)的是BASE_ITEM_TYPE_HEADER
,第二個(gè)對(duì)應(yīng)的是BASE_ITEM_TYPE_HEADER+1
甫菠;
為什么要這么做呢挠铲?
這兩個(gè)問(wèn)題都需要到復(fù)寫onCreateViewHolder的時(shí)候來(lái)說(shuō)明。
(2)復(fù)寫相關(guān)方法
public class HeaderAndFooterWrapper<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder>
{
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
if (mHeaderViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mHeaderViews.get(viewType));
return holder;
} else if (mFootViews.get(viewType) != null)
{
ViewHolder holder = ViewHolder.createViewHolder(parent.getContext(), mFootViews.get(viewType));
return holder;
}
return mInnerAdapter.onCreateViewHolder(parent, viewType);
}
@Override
public int getItemViewType(int position)
{
if (isHeaderViewPos(position))
{
return mHeaderViews.keyAt(position);
} else if (isFooterViewPos(position))
{
return mFootViews.keyAt(position - getHeadersCount() - getRealItemCount());
}
return mInnerAdapter.getItemViewType(position - getHeadersCount());
}
private int getRealItemCount()
{
return mInnerAdapter.getItemCount();
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
if (isHeaderViewPos(position))
{
return;
}
if (isFooterViewPos(position))
{
return;
}
mInnerAdapter.onBindViewHolder(holder, position - getHeadersCount());
}
@Override
public int getItemCount()
{
return getHeadersCount() + getFootersCount() + getRealItemCount();
}
}
- getItemViewType
由于我們?cè)黾恿薶eaderView和footerView首先需要復(fù)寫的就是getItemCount和getItemViewType寂诱。
getItemCount很好理解拂苹;
對(duì)于getItemType,可以看到我們的返回值是mHeaderViews.keyAt(position)刹衫,這個(gè)值其實(shí)就是我們addHeaderView時(shí)的key醋寝,footerView是一樣的處理方式搞挣,這里可以看出我們?yōu)槊恳粋€(gè)headerView創(chuàng)建了一個(gè)itemType带迟。
- onCreateViewHolder
可以看到音羞,我們分別判斷viewType,如果是headview或者是footerview仓犬,我們則為其單獨(dú)創(chuàng)建ViewHolder嗅绰,這里的ViewHolder是我之前寫的一個(gè)通用的庫(kù)里面的類,文末有鏈接搀继。當(dāng)然窘面,你也可以自己寫一個(gè)ViewHolder的實(shí)現(xiàn)類,只需要將對(duì)應(yīng)的headerView作為itemView傳入ViewHolder的構(gòu)造即可叽躯。
這個(gè)方法中财边,我們就可以解答之前的問(wèn)題了:
- 為什么我要用SparseArrayCompat而不是List?
- 為什么我要讓每個(gè)headerView對(duì)應(yīng)一個(gè)itemType点骑,而不是固定的一個(gè)酣难?
對(duì)于headerView假設(shè)我們有多個(gè),那么onCreateViewHolder返回的ViewHolder中的itemView應(yīng)該對(duì)應(yīng)不同的headerView黑滴,如果是List憨募,那么不同的headerView應(yīng)該對(duì)應(yīng)著:list.get(0),list.get(1)等。
但是問(wèn)題來(lái)了袁辈,該方法并沒(méi)有position參數(shù),只有itemType參數(shù)晚缩,如果itemType還是固定的一個(gè)值,那么你是沒(méi)有辦法根據(jù)參數(shù)得到不同的headerView的荞彼。
所以,我利用SparseArrayCompat卿泽,將其key作為itemType莺债,value為我們的headerView,在onCreateViewHolder中签夭,直接通過(guò)itemType,即可獲得我們的headerView第租,然后構(gòu)造ViewHolder對(duì)象。而且我們的取值是從100000開始的慎宾,正常的itemType是從0開始取值的丐吓,所以正常情況下浅悉,是不可能發(fā)生沖突的。
需要說(shuō)明的是术健,這里的意思并非是一定不能用List,通過(guò)一些特殊的處理荞估,List也能達(dá)到上述我描述的效果。
- onBindViewHolder
onBindViewHolder比較簡(jiǎn)單勘伺,發(fā)現(xiàn)是HeaderView或者FooterView直接return即可褂删,因?yàn)閷?duì)于頭部和底部我們僅僅做展示即可飞醉,對(duì)于事件應(yīng)該是在addHeaderView等方法前設(shè)置屯阀。
這樣就初步完成了我們的裝飾類,我們分別添加兩個(gè)headerView和footerView:
我們看看運(yùn)行效果:
[圖片上傳失敗...(image-f6435e-1530872000836)]
感覺還是不錯(cuò)的蹲盘,再不改動(dòng)原來(lái)的Adapter的基礎(chǔ)上,我們完成了初步的實(shí)現(xiàn)召衔。
大家都知道RecyclerView比較強(qiáng)大,可以設(shè)置不同的LayoutManager趣席,那么我們換成GridLayoutMananger再看看效果。
[圖片上傳失敗...(image-51108-1530872000836)]
好像發(fā)現(xiàn)了不對(duì)的地方宣肚,我們的headerView果真被當(dāng)成普通的Item處理了悠栓,不過(guò)由于我們的編寫方式霉涨,出現(xiàn)上述情況是可以理解的惭适。
那么我們?cè)撊绾翁幚砟兀孔屆總€(gè)headerView獨(dú)立的占據(jù)一行癞志?
4、進(jìn)一步的完善
好在RecyclerView里面為我們提供了一些方法错洁。
(1)針對(duì)GridLayoutManager
在我們的HeaderAndFooterWrapper中復(fù)寫onAttachedToRecyclerView方法秉宿,如下:
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView)
{
innerAdapter.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager)
{
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup()
{
@Override
public int getSpanSize(int position)
{
int viewType = getItemViewType(position);
if (mHeaderViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
} else if (mFootViews.get(viewType) != null)
{
return layoutManager.getSpanCount();
}
if (oldLookup != null)
return oldLookup.getSpanSize(position);
return 1;
}
});
gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
}
}
當(dāng)發(fā)現(xiàn)layoutManager為GridLayoutManager時(shí)描睦,通過(guò)設(shè)置SpanSizeLookup,對(duì)其getSpanSize方法酌摇,返回值設(shè)置為layoutManager.getSpanCount();
現(xiàn)在看一下運(yùn)行效果:
[圖片上傳失敗...(image-b47f64-1530872000836)]
哈嗡载,終于正常了。
(3)對(duì)于StaggeredGridLayoutManager
在剛才的代碼中我們好像沒(méi)有發(fā)現(xiàn)StaggeredGridLayoutManager的身影洼滚,StaggeredGridLayoutManager并沒(méi)有setSpanSizeLookup這樣的方法,那么該如何處理呢遥巴?
依然不復(fù)雜享幽,重寫onViewAttachedToWindow方法铲掐,如下:
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder)
{
mInnerAdapter.onViewAttachedToWindow(holder);
int position = holder.getLayoutPosition();
if (isHeaderViewPos(position) || isFooterViewPos(position))
{
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null
&& lp instanceof StaggeredGridLayoutManager.LayoutParams)
{
StaggeredGridLayoutManager.LayoutParams p =
(StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
這樣就完成了對(duì)StaggeredGridLayoutManager的處理摆霉,效果圖就不貼了。
到此携栋,我們就完成了整個(gè)HeaderAndFooterWrapper的編寫,可以在不改變?cè)瑼dapter代碼的情況下婉支,為其添加一個(gè)或者多個(gè)headerView或者footerView,以及完成了如何讓HeaderView或者FooterView適配各種LayoutManager向挖。