效果圖
簡述
這篇文章主要描述如何擴(kuò)展原生的Adapter
,讓其支持添加header泥耀、footer饺汹,以及item的事件處理,這將會是一篇很長的文章痰催。
首先的考慮是我需要一個(gè)支持添加任意數(shù)量的header和footer兜辞,并且互相不干擾的Adapter
迎瞧。主要表現(xiàn)在客戶端的調(diào)用上數(shù)據(jù)是數(shù)據(jù),header是header逸吵,footer是footer凶硅,而且他們的順序關(guān)系也是很顯然的。另外再將header和footer進(jìn)行一下擴(kuò)展扫皱,將給客戶端使用的和庫內(nèi)部本身需要擴(kuò)展的header足绅、footer進(jìn)行區(qū)分,這樣就產(chǎn)生了下面這個(gè)順序關(guān)系(已經(jīng)在第一篇進(jìn)行了闡述):
{
item_sys_header - item_header -
item_data -
item_footer - item_sys_footer
}
這個(gè)順序以及每個(gè)域的數(shù)量都是很關(guān)鍵韩脑。
有了上面這個(gè)順序的關(guān)系圖氢妈,這時(shí)候數(shù)據(jù)結(jié)構(gòu)也就定下來了:
protected List<View> headerViews = new ArrayList<>();
protected List<View> footerViews = new ArrayList<>();
//邏輯上設(shè)計(jì)為系統(tǒng)頭部也可以是多個(gè) ,但是實(shí)現(xiàn)上系統(tǒng)頭部實(shí)現(xiàn)為僅有一個(gè)
private List<View> sysHeaderViews = new ArrayList<>();
private View sysFooterView;
//真實(shí)的數(shù)據(jù)部分
protected List<T> datas;
這里需要對上面的數(shù)據(jù)結(jié)構(gòu)補(bǔ)充說明一下:
數(shù)據(jù)結(jié)構(gòu)的訪問權(quán)限是為了擴(kuò)展用段多;系統(tǒng)header有計(jì)劃進(jìn)行擴(kuò)展(聲明中可以看到)首量,但是目前來說只支持一個(gè)(將會在稍后的實(shí)現(xiàn)中看到);系統(tǒng)footer沒有打算擴(kuò)展(聲明中可以看到)衩匣,所以只支持一個(gè)蕾总。
既然有了上面這個(gè)關(guān)系圖,那么接下來的思路就很明顯了琅捏,我們按照這個(gè)順序進(jìn)行生百。
1. 系統(tǒng)頭部
這部分對應(yīng)到item_sys_header部分,起初的考慮是用于內(nèi)部擴(kuò)展用柄延。
數(shù)據(jù)操作
首先目測需要這么幾個(gè)方法對系統(tǒng)頭部的操作
int getSysHeaderViewCount();
void setSysHeaderView(View view);
void removeSysHeaderView(View view);
這里的setSysHeaderView()
方法為什么用一個(gè)view參數(shù)而不是一個(gè)layoutId呢蚀浆,我的考慮是如果用了id,那么內(nèi)部就要去創(chuàng)建這個(gè)view以及viewHolder搜吧,這樣客戶端需要通過viewHolder去綁定數(shù)據(jù)市俊,這樣的操作不是我希望的。移除的操作removeSysHeaderView()
與設(shè)置操作相對應(yīng)滤奈。
前面已經(jīng)說了摆昧,系統(tǒng)頭部在設(shè)計(jì)的時(shí)候是支持任意數(shù)量的,并且系統(tǒng)頭部的是在最頂層的蜒程。所以調(diào)用設(shè)置方法時(shí)將會清除所有的系統(tǒng)header(這么做的目的是可能在某個(gè)版本中將會進(jìn)行多個(gè)的擴(kuò)充)绅你,然后才進(jìn)行添加(這時(shí)候一定只有一個(gè),索引就是0)昭躺,接著是局部刷新(這些是Adapter
的常規(guī)操作)忌锯。下面就是相關(guān)代碼:
final
public int getSysHeaderViewCount(){
return sysHeaderViews.size();
}
final
public void setSysHeaderView(View view) {
if (null == view) {
return;
}
int index = getSysHeaderViewCount();
if (index > 0) {
sysHeaderViews.clear();
notifyItemRangeRemoved(0, index);
}
sysHeaderViews.add(view);
notifyItemInserted(0);
}
對于移除操作就不太一樣,因?yàn)槊鎸Φ氖嵌鄠€(gè)领炫,所以具體要?jiǎng)h除哪一個(gè)需要先檢索出在sysHeaderViews
中的位置index偶垮,然后進(jìn)行相應(yīng)索引位置的移除操作,接著還是局部刷新。下面就是相關(guān)代碼:
final
public void removeSysHeaderView(View view) {
if (null == view || !sysHeaderViews.contains(view)) {
return;
}
int index = sysHeaderViews.indexOf(view);
sysHeaderViews.remove(view);
notifyItemRemoved(index);
}
onCreateViewHolder過程
系統(tǒng)頭部數(shù)據(jù)的操作已經(jīng)結(jié)束似舵,我們再調(diào)用了刷新操作后脚猾,Adapter
將會處理一系列操作,首先我們看看onCreateViewHolder()
啄枕,這里將會根據(jù)viewType創(chuàng)建對應(yīng)的viewHolder婚陪。在我們這個(gè)Adapter
的擴(kuò)展中很明顯是item的類型已經(jīng)不止一種了,所以我們需要復(fù)寫getItemViewType()
方法為系統(tǒng)頭部關(guān)聯(lián)一個(gè)類型频祝。系統(tǒng)header可能很多泌参,每一個(gè)都是單獨(dú)的view對象,在創(chuàng)建viewHolder的時(shí)候也是需要區(qū)分的常空,因此每一個(gè)系統(tǒng)header都是一個(gè)單獨(dú)的類型沽一,所以我們這里采取了將view的hashCode作為與之對應(yīng)的類型。
@Override
final
public int getItemViewType(int position) {
int shc = getSysHeaderViewCount();
//處理系統(tǒng)頭部
if (shc > 0 && position < shc) {
return sysHeaderViews.get(position).hashCode();
}
return 0;
}
在onCreateViewHolder()
的過程中我們需要根據(jù)viewType創(chuàng)建viewHolder漓糙,首先檢查viewType是否是系統(tǒng)header以及是哪一個(gè)系統(tǒng)header
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//處理系統(tǒng)頭部
View sysHeaderView = getSysFooterViewByHashCode(viewType);
if (null != sysHeaderView) {
return new BaseViewHolder(sysHeaderView);
}
return null;
}
private View getSysFooterViewByHashCode(int hashCode) {
return this.getViewByHashCodeFromList(sysHeaderViews, hashCode);
}
private View getViewByHashCodeFromList(List<View> views, int hashCode) {
if (null == views) {
return null;
}
for (View v : views) {
if (v.hashCode() == hashCode) {
return v;
}
}
return null;
}
onBindViewHolder過程
viewHolder創(chuàng)建完畢之后铣缠,按照流程就到了onBindViewHolder
進(jìn)行數(shù)據(jù)的綁定操作,但是看看我們向客戶端提供的設(shè)置方法昆禽,傳入的是一個(gè)view蝗蛙,這就意味著系統(tǒng)header數(shù)據(jù)的綁定操作是在客戶端進(jìn)行的,換句話說就是在這里針對系統(tǒng)header沒有要數(shù)據(jù)綁定的操作(傳入的position在系統(tǒng)頭部范圍內(nèi)不做任何處理)醉鳖。那么代碼就是下面這個(gè)樣子:
@Override
final
public void onBindViewHolder(BaseViewHolder holder, int position) {
int shc = getSysHeaderViewCount();
//item_sys_header
if (0 != shc && position < shc) {
return;
}
}
到這里捡硅,系統(tǒng)header的處理部分就結(jié)束了,這時(shí)候已經(jīng)可以添加和刪除一個(gè)系統(tǒng)header了盗棵。這里可能有的同學(xué)就會問了壮韭,最重要的三個(gè)方法為什么都加了final,這樣不是都沒法操作了嗎纹因,至于為什么要這樣做喷屋,將會在稍后慢慢說明。
2. 用戶頭部
這部分對應(yīng)到item_header部分瞭恰,是提供給客戶端使用的屯曹。
流程仍然按照上面系統(tǒng)header部分。
數(shù)據(jù)操作
首先對于這部分功能提供一下部分幾個(gè)方法進(jìn)行頭部的操作
int getHeaderViewCount();
void addHeaderView(View view);
void removeHeaderView(View view);
以上參數(shù)的說明與系統(tǒng)header一致惊畏,這里不再贅述是牢。
這部分在按順序在系統(tǒng)header(item_sys_header)后面,所以在數(shù)據(jù)操作上需要考慮item_sys_header部分陕截。下面是具體的操作代碼:
final
public int getHeaderViewCount() {
return headerViews.size();
}
final
public void addHeaderView(View view) {
if (null != view && headerViews.contains(view)) {
headerViews.remove(view);
}
headerViews.add(view);
int shc = getSysHeaderViewCount();
int index = shc + headerViews.indexOf(view);
notifyItemInserted(index);
}
final
public void removeHeaderView(View view) {
if (null == view || !headerViews.contains(view)) {
return;
}
int shc = getSysHeaderViewCount();
int index = shc + headerViews.indexOf(view);
headerViews.remove(view);
notifyItemRemoved(index);
}
onCreateViewHolder過程
這部分仍然與系統(tǒng)header一樣,item類型也是view對應(yīng)的hashCode批什,這里對getItemViewType()
和onCreateViewHolder()
進(jìn)行完善农曲,添加對用戶header的支持。代碼如下:
@Override
final
public int getItemViewType(int position) {
int shc = getSysHeaderViewCount();
int hc = getHeaderViewCount();
int hAll = shc + hc;
//處理系統(tǒng)頭部
if (shc > 0 && position < shc) {
return sysHeaderViews.get(position).hashCode();
}
//處理頭部
if (hc > 0 && position >= shc && position < hAll) {
position = position - shc;
return headerViews.get(position).hashCode();
}
return 0;
}
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//處理系統(tǒng)頭部
View sysHeaderView = getSysFooterViewByHashCode(viewType);
if (null != sysHeaderView) {
return new BaseViewHolder(sysHeaderView);
}
//處理頭部
View headerView = getHeaderViewByHashCode(viewType);
if (null != headerView) {
return new BaseViewHolder(headerView);
}
return null;
}
onBindViewHolder過程
用戶header的數(shù)據(jù)綁定處理仍然與處理系統(tǒng)header一致,增加了用戶header處理的代碼如下:
@Override
final
public void onBindViewHolder(BaseViewHolder holder, int position) {
int shc = getSysHeaderViewCount();
//item_sys_header
if (0 != shc && position < shc) {
return;
}
int hc = getHeaderViewCount();
int hAll = shc + hc;
//處理用戶header
if (0 != hc && position < hAll) {
return;
}
}
3. 正常的數(shù)據(jù)部分
這部分是除header乳规、footer外客戶端處理的真實(shí)數(shù)據(jù)部分形葬。
對于這部分的實(shí)現(xiàn)有以下幾方面的考慮:
多item類型支持
這個(gè)擴(kuò)展庫一定是支持多種item類型的,但是對于header和footer的類型已經(jīng)在內(nèi)部做了處理(本應(yīng)該是這樣)暮的,從上面來看多類型的支持的重載方法被設(shè)置為final笙以,所以需要額外提供一個(gè)方法來只針對數(shù)據(jù)部分多item類型的支持。在安全起見和必要性方面將getItemViewType()
方法做了屏蔽冻辩。額外增加的方法如下:
/**
* 初始化數(shù)據(jù)域類型,總是從0開始猖腕,已經(jīng)除去頭部
* @param positionOffsetHeaders
* @return
*/
protected int mapDataSectionItemViewTypeToItemLayoutId(int positionOffsetHeaders) {
return 0;
}
從方法名來看,除了header和footer外的數(shù)據(jù)域部分item類型對應(yīng)的都是item的布局文件id恨闪,這樣在onCreateViewHolder()
內(nèi)部封裝是有好處的倘感,再一次說明viewHolder的創(chuàng)建對于客戶端直接使用api是屏蔽的。
onCreateViewHolder
如果就單一般的展示用咙咽,那么這個(gè)方法也可以被屏蔽了老玛,但是為了更好的擴(kuò)展(在后面的滑動(dòng)菜單以及粘性頭部時(shí)),還是向上面一樣再提供一個(gè)下面這樣的方法供子類使用:
abstract
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType);
onBindViewHolder
同上面一樣钧敞,我們需要保護(hù)非數(shù)據(jù)域的邏輯蜡豹,但是數(shù)據(jù)的綁定操作一定是客戶端來做的,所以我們還是需要額外提供一個(gè)下面這樣的方法供客戶端使用:
/**
* 綁定數(shù)據(jù)
* @param holder
* @param position 數(shù)據(jù)域索引從0開始溉苛,已經(jīng)除去頭部
*/
abstract
public void convert(BaseViewHolder holder, int position);
僅在這篇文章中提及的功能而言镜廉,對于onBindViewHolder()
的操作我們只是又提取了一個(gè)convert()方法,但是如果翻看源代碼的話會發(fā)現(xiàn)對這塊的提取操作是下面這樣的(在粘性頭部的擴(kuò)展中會找到原因):
/**
* 為了子類擴(kuò)展
* @param holder
* @param position
*/
@CallSuper
protected void onBindHolder(BaseViewHolder holder, int position) {
this.convert(holder, position);
}
/**
* 綁定數(shù)據(jù)
* @param holder
* @param position 數(shù)據(jù)域索引從0開始炊昆,已經(jīng)除去頭部
*/
abstract
public void convert(BaseViewHolder holder, int position);
經(jīng)過上面的篇幅闡述桨吊,已經(jīng)簡單描述了header的添加刪除以及數(shù)據(jù)域部分的處理,屏蔽了部分非必要方法以及為進(jìn)一步擴(kuò)展等額外添加了一些處理方法凤巨,這么做的目的就是為了簡單视乐、易擴(kuò)展。
大概總結(jié)一下就是:
- 多類型的支持分為兩類:數(shù)據(jù)部分映射為item布局id敢茁;非數(shù)據(jù)部分映射為view的hashCode
- viewHolder創(chuàng)建分為兩種情況:對于客戶端使用直接屏佑淀;對于擴(kuò)展則有間接的映射方法
- 數(shù)據(jù)綁定分為兩類:數(shù)據(jù)域?qū)τ诳蛻舳擞斜仨殞?shí)現(xiàn)的間接映射方法;非數(shù)據(jù)域則直接屏蔽
相信看到這里已經(jīng)知道footer怎么加進(jìn)去了彰檬,與header和數(shù)據(jù)域都是類似的伸刃,主要抓住一點(diǎn)就是正確的計(jì)算索引,具體的就不在往下闡述了逢倍,可以去翻看源代碼捧颅。
4. 為數(shù)據(jù)域的item添加事件
我們知道RecyclerView
對于viewHolder有特殊的要求,必須是RecyclerView.ViewHolder
的子類才行较雕,而它內(nèi)部有個(gè)itemView屬性碉哑,指的就是我們創(chuàng)建的item根視圖挚币,有了這個(gè)萬事已經(jīng)具備。還記得代碼是這樣子寫的:
@Override
public BaseViewHolder onCreateHolder(ViewGroup parent, int viewType) {
View itemView = inflater.inflate(viewType, parent, false);
return new BaseViewHolder(itemView);
}
我們仿照ListView添加以下兩種事件扣典。
/**
* 點(diǎn)擊事件
*/
public interface OnItemClickListener {
void onItemClick(View itemView, int position);
}
/**
* 長按事件
*/
public interface OnItemLongClickListener {
void onItemLongClick(View itemView, int position);
}
接下來只需要在onCreateViewHolder()
里面添加事件回調(diào)就可以了妆毕,這里需要說明兩點(diǎn):1. 寫這么復(fù)雜的原因是還是為了擴(kuò)展 2. 計(jì)算正確的索引
@Override
final
public BaseViewHolder onCreateViewHolder(ViewGroup parent, final int viewType) {
//省略其他操作...
//事件只針對正常數(shù)據(jù)項(xiàng)
final BaseViewHolder holder = onCreateHolder(parent, viewType);
this.initItemListener(holder/*, viewType*/);
return holder;
}
protected void initItemListener(final BaseViewHolder holder/*, final int viewType*/){
if (null == holder) {
return;
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
HeaderFooterAdapter.this.onItemClick(holder, v);
}
});
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return HeaderFooterAdapter.this.onItemLongClick(holder, v);
}
});
}
protected void onItemClick(final BaseViewHolder holder, View view){
if (null == onItemClickListener) {
return;
}
int hAll = getHeaderViewCount() + getSysHeaderViewCount();
onItemClickListener.onItemClick(view, holder.getAdapterPosition() - hAll);
}
protected boolean onItemLongClick(final BaseViewHolder holder, View view){
if (null == onItemLongClickListener) {
return false;
}
int hAll = getHeaderViewCount() + getSysHeaderViewCount();
onItemLongClickListener.onItemLongClick(view, holder.getAdapterPosition() - hAll);
return true;
}
5. 支持GridLayoutManager與StaggeredGridLayoutManager
寫到這里我們擴(kuò)展的Adapter
已經(jīng)支持header、footer的增刪操作贮尖、更加簡化的api處理以及為數(shù)據(jù)域添加事件監(jiān)聽響應(yīng)笛粘。
寫一個(gè)demo測試后發(fā)現(xiàn)一切正常,但是這僅限于LayoutManager
為LinearLayoutManager
的情況湿硝,其他情況會發(fā)現(xiàn)是失效的狀態(tài)薪前。我們需要的是在任何情況下我們加進(jìn)去的header和footer一定要是橫向填充的,顯然還需要處理图柏。
發(fā)現(xiàn)Adapter
中并沒有提供相關(guān)可以使用的方法序六,其實(shí)這種布局的處理是交給LayoutManager
處理的,所以我們需要從LayoutManager
著手處理這些問題蚤吹,最后發(fā)現(xiàn)不同的LayoutManager
處理的方式不同例诀,那么我們需要分別對待。首先明確一點(diǎn)LayoutManager
是設(shè)置在RecyclerView
而不是Adapter
裁着,所以第一點(diǎn)我們需要在Adapter
中獲取RecyclerView
對象繁涂。
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (this.recyclerView == recyclerView) {
return;
}
this.recyclerView = recyclerView;
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
this.recyclerView = null;
}
獲取到RecyclerView
也就獲取到了設(shè)置的LayoutManager
對象,這樣我們就可以針對不同的LayoutManager
進(jìn)行處理二驰。在處理之前扔罪,首先有以下兩點(diǎn)需要考慮:
- 除了數(shù)據(jù)域(header和footer)一定是橫跨的,并且這部分處理需要內(nèi)部搞定且對外屏蔽
- 客戶端設(shè)置的數(shù)據(jù)域有可能有些item也是需要橫跨的桶雀,這時(shí)候我們需要額外提供一個(gè)方法供客戶端使用矿酵。很簡單方法,如下:
/**
* 設(shè)置是否橫跨
* @param position
* @return
*/
protected boolean isFullSpanWithItemView(int position) {
return false;
}
所以我們就有了以下的代碼來分別針對GridLayoutManager
和StaggeredGridLayoutManager
進(jìn)行處理矗积。
private void adapterGridLayoutManager() {
final RecyclerView.LayoutManager layoutManager = null == recyclerView ? null : recyclerView.getLayoutManager();
if (null == layoutManager) {
return;
}
if (layoutManager instanceof GridLayoutManager) {
final GridLayoutManager glm = (GridLayoutManager) layoutManager;
final GridLayoutManager.SpanSizeLookup ssl = glm.getSpanSizeLookup();
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
return !isDataItemView(position) ? glm.getSpanCount() : ssl.getSpanSize(position);
}
});
}}
private void adapterStaggeredGridLayoutManager(BaseViewHolder holder) {
final RecyclerView.LayoutManager layoutManager = null == recyclerView ? null : recyclerView.getLayoutManager();
if (null == layoutManager) {
return;
}
if (layoutManager instanceof StaggeredGridLayoutManager) {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
int position = holder.getAdapterPosition();
if (null != lp && lp instanceof StaggeredGridLayoutManager.LayoutParams && !isDataItemView(position)) {
((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(true);
}
}
}
/**
* 用來判斷item是否為真實(shí)數(shù)據(jù)項(xiàng)全肮,除了頭部、尾部棘捣、系統(tǒng)尾部等非真實(shí)數(shù)據(jù)項(xiàng)辜腺,結(jié)構(gòu)為: * item_header - item_data - item_footer - item_sys_footer
* @param position
* @return true:將保留LayoutManager的設(shè)置 false:該item將會橫跨整行(對GridLayoutManager,StaggeredLayoutManager將很有用)
*/
private boolean isDataItemView(int position) {
int shc = getSysHeaderViewCount();
int hc = shc + getHeaderViewCount();
int dc = getDataSectionItemCount();
boolean isHeaderOrFooter = position >= 0 && position >= hc && position < (hc + dc);
if (!isHeaderOrFooter) {
return isHeaderOrFooter;
}
return this.isFullSpanWithItemView(position - (shc + hc));
}
最后將以上處理加進(jìn)去,注意一下兩種方式在注冊的位置不太一樣:
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (this.recyclerView == recyclerView) {
return;
}
this.recyclerView = recyclerView;
this.adapterGridLayoutManager();
}
@Override
public void onViewAttachedToWindow(BaseViewHolder holder) {
super.onViewAttachedToWindow(holder);
this.adapterStaggeredGridLayoutManager(holder);
}
到這里乍恐,為Adapter
添加header评疗、footer、事件以及適配LayoutManager
就結(jié)束了茵烈,下一篇將闡述下數(shù)據(jù)域的操作百匆。