為recyclerview添加header footer

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需要修改的幅度就比較大了杈笔,比如getItemTypegetItemCount糕非、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的功能怀伦,使其支持addHeaderViewaddFooterView脆烟。這樣我們就可以不去改動(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向挖。

源碼地址:
https://github.com/hongyangAndroid/baseAdapter

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末炕舵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子幕侠,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件悼潭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡皆疹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門略就,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晃酒,“玉大人,你說(shuō)我怎么就攤上這事贝次〈扌耍” “怎么了蛔翅?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)山析。 經(jīng)常有香客問(wèn)我,道長(zhǎng)秆剪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任鸟款,我火速辦了婚禮茂卦,結(jié)果婚禮上何什,老公的妹妹穿的比我還像新娘等龙。我一直安慰自己,他們只是感情好蛛砰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荠诬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柑贞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天钧嘶,我揣著相機(jī)與錄音,去河邊找鬼闸拿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛新荤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播迟隅,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼励七,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奔缠!你這毒婦竟也來(lái)了掠抬?” 一聲冷哼從身側(cè)響起校哎,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闷哆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抱怔,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年局冰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了灌危。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勇蝙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情烫罩,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布贝攒,位于F島的核電站时甚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏荒适。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一刀诬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陕壹,春花似錦、人聲如沸嘶伟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)毕匀。三九已至,卻和暖如春皂岔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凤薛。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工缤苫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留速兔,地道東北人活玲。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓谍婉,卻偏偏與公主長(zhǎng)得像镀钓,于是被迫代替她去往敵國(guó)和親穗熬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子丁溅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容