爲RecyclerView打造通用Adapter 讓RecyclerView更加好用

原文出處: 張鴻洋 (Granker,@鴻洋_ )

一、概述

記得好久以前針對ListView類控件寫過一篇打造萬能的ListView GridView 適配器,如今RecyclerView異軍突起,其Adapter的用法也與ListView類似,那麼我們也可以一步一步的爲其打造通用的Adapter,使下列用法書寫更加簡單:

  • 簡單的數據綁定(單種Item)

  • 多種Item Type 數據綁定

  • 增加onItemClickListener , onItenLongClickListener

  • 優雅的添加分類header

二、使用方式和效果圖

在一步一步完成前,我們先看下使用方式和效果圖:

(1)簡單的數據綁定

首先看我們最常用的單種Item的書寫方式:

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

mRecyclerView.setAdapter(new CommonAdapter(thisR.layout.item_listmDatas)

{

    @Override

    public void convert(ViewHolder holderString s)

    {

        holder.setText(R.id.id_item_list_titles);

    }

});

是不是相當方便,在convert方法中完成數據、事件綁定即可。

(2)多種ItemViewType

多種ItemViewType,正常考慮下,我們需要根據Item指定ItemType,並且根據ItemType指定相應的佈局文件。我們通過MultiItemTypeSupport完成指定:

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

MultiItemTypeSupport  multiItemSupport new MultiItemTypeSupport()

{

    @Override

    public int getLayoutId(int itemType)

    {

       //根據itemType返回item佈局文件id

    }

 

    @Override

    public int getItemViewType(int postionChatMessage msg)

    {

       //根據當前的bean返回item type

    }

}

剩下就簡單了,將其作爲參數傳入到MultiItemCommonAdapter即可。

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

mRecyclerView.setAdapter(new SectionAdapter(thismDatasmultiItemSupport)

{

    @Override

    public void convert(ViewHolder holderString s)

    {

        holder.setText(R.id.id_item_list_titles);

    }

});

貼個效果圖:

(3)添加分類header

其實屬於多種ItemViewType的一種了,只是比較常用,我們就簡單封裝下。

依賴正常考慮下,這種方式需要額外指定header的佈局,以及佈局中顯示標題的TextView了,以及根據Item顯示什麼樣的標題。我們通過SectionSupport對象指定:

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

SectionSupport sectionSupport new SectionSupport()

{

    @Override

    public int sectionHeaderLayoutId()

    {

        return R.layout.header;

    }

 

    @Override

    public int sectionTitleTextViewId()

    {

        return R.id.id_header_title;

    }

 

    @Override

    public String getTitle(String s)

    {

        return s.substring(01);

    }

};

3個方法,一個指定header的佈局文件,一個指定佈局文件中顯示title的TextView,最後一個用於指定顯示什麼樣的標題(根據Adapter的Bean)。

接下來就很簡單了:

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

mRecyclerView.setAdapter(new SectionAdapter(thisR.layout.item_listmDatassectionSupport)

{

    @Override

    public void convert(ViewHolder holderString s)

    {

        holder.setText(R.id.id_item_list_titles);

    }

});

這樣就完了,效果圖如下:

ok,看完上面簡單的介紹,相信你已經基本瞭解了,沒錯,和我上篇ListView萬能Adapter的使用方式基本一樣,並且已經封裝到同一個庫了,鏈接爲:https://github.com/hongyangAndroid/base-adapter,此外還提供了ItemClick,ItemLongClick,添加EmptyView等支持。

說了這麼多,下面進入正題,看我們如何一步步完成整個封裝的過程。

三、通用的ViewHolder

RecyclerView要求必須使用ViewHolder模式,一般我們在使用過程中,都需要去建立一個新的ViewHolder然後作爲泛型傳入Adapter。那麼想要建立通用的Adapter,必須有個通用的ViewHolder。

首先我們確定下ViewHolder的主要的作用,實際上是通過成員變量存儲對應的convertView中需要操作的字View,避免每次findViewById,從而提升運行的效率。

那麼既然是通用的View,那麼對於不同的ItemType肯定沒有辦法確定創建哪些成員變量View,取而代之的只能是個集合來存儲了。

那麼代碼如下:

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

public class ViewHolder extends RecyclerView.ViewHolder

{

    private SparseArray mViews;

    private View mConvertView;

    private Context mContext;

 

    public ViewHolder(Context contextView itemViewViewGroup parent)

    {

        super(itemView);

        mContext context;

        mConvertView itemView;

        mViews new SparseArray();

    }

 

    public static ViewHolder get(Context contextViewGroup parentint layoutId)

    {

 

        View itemView LayoutInflater.from(context).inflate(layoutIdparent,

                false);

        ViewHolder holder new ViewHolder(contextitemViewparentposition);

        return holder;

    }

 

    /**

     * 通過viewId獲取控件

     *

     * @param viewId

     * @return

     */

    public  getView(int viewId)

    {

        View view mViews.get(viewId);

        if (view == null)

        {

            view mConvertView.findViewById(viewId);

            mViews.put(viewIdview);

        }

        return (Tview;

    }

}

代碼很簡單,我們的ViewHolder繼承自RecyclerView.ViewHolder,內部通過SparseArray來緩存我們itemView內部的子View,從而得到一個通用的ViewHolder。每次需要創建ViewHolder只需要傳入我們的layoutId即可。

ok,有了通用的ViewHolder之後,我們的通用的Adapter分分鐘就出來了。

四、通用的Adapter

我們的每次使用過程中,針對的數據類型Bean肯定是不同的,那麼這裏肯定要引入泛型代表我們的Bean,內部通過一個List代表我們的數據,ok,剩下的看代碼:

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

package com.zhy.base.adapter.recyclerview;

 

import android.content.Context;

import android.support.v7.widget.RecyclerView;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

 

import com.zhy.base.adapter.ViewHolder;

 

import java.util.List;

 

/**

* Created by zhy on 16/4/9.

*/

public abstract class CommonAdapter extends RecyclerView.Adapter

{

    protected Context mContext;

    protected int mLayoutId;

    protected List mDatas;

    protected LayoutInflater mInflater;

 

    public CommonAdapter(Context contextint layoutIdList datas)

    {

        mContext context;

        mInflater LayoutInflater.from(context);

        mLayoutId layoutId;

        mDatas datas;

    }

 

    @Override

    public ViewHolder onCreateViewHolder(final ViewGroup parentint viewType)

    {

        ViewHolder viewHolder ViewHolder.get(mContextparentmLayoutId);

        return viewHolder;

    }

 

    @Override

    public void onBindViewHolder(ViewHolder holderint position)

    {

        holder.updatePosition(position);

        convert(holdermDatas.get(position));

    }

 

    public abstract void convert(ViewHolder holdert);

 

    @Override

    public int getItemCount()

    {

        return mDatas.size();

    }

}

繼承自RecyclerView.Adapter,需要複寫的方法還是比較少的。首先我們使用過程中傳輸我們的數據集mDatas,和我們item的佈局文件layoutId。

onCreateViewHolder時,通過layoutId即可利用我們的通用的ViewHolder生成實例。

onBindViewHolder這裏主要用於數據、事件綁定,我們這裏直接抽象出去,讓用戶去操作。可以看到我們修改了下參數,用戶可以拿到當前Item所需要的對象和viewHolder去操作。

那麼現在用戶的使用是這樣的:

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

mRecyclerView.setAdapter(new CommonAdapter(thisR.layout.item_listmDatas)

{

    @Override

    public void convert(ViewHolder holderString s)

    {

        TextView tv holder.getView(R.id.id_item_list_title);

        tv.setText(s);

    }

});

看到這裏,爽了很多,目前我們僅僅寫了很少的代碼,但是我們的通用的Adapter感覺已經初步完成了。

可以看到我們這裏通過viewholder根據控件的id拿到控件,然後再進行數據綁定和事件操作,我們還能做些什麼簡化呢?

恩,我們可以通過一些輔助方法簡化我們的代碼,所以繼續往下看。

五、進一步封裝ViewHolder

我們的Item實際上使用的控件較多時候可能都是TextView,ImageView等,我們一般在convert方法都是去設置文本,圖片什麼的,那麼我們可以在ViewHolder裏面,寫上如下的一些輔助方法:

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

class ViewHolder extends RecyclerView.AdapterViewHolder>

{

    //...

    public ViewHolder setText(int viewIdString text)

    {

        TextView tv getView(viewId);

        tv.setText(text);

        return this;

    }

 

    public ViewHolder setImageResource(int viewIdint resId)

    {

        ImageView view getView(viewId);

        view.setImageResource(resId);

        return this;

    }

 

    public ViewHolder setOnClickListener(int viewId,

                                         View.OnClickListener listener)

    {

        View view getView(viewId);

        view.setOnClickListener(listener);

        return this;

    }

}

當然上面只給出了幾個方法,你可以把常用控件的方法都寫進去,並且在使用過程中不斷完善即可。

有了一堆輔助方法後,我們的操作更加簡化了一步。

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

mRecyclerView.setAdapter(new CommonAdapter(thisR.layout.item_listmDatas)

{

    @Override

    public void convert(ViewHolder holderString s)

    {

        //TextView tv = holder.getView(R.id.id_item_list_title);

        //tv.setText(s);

        holder.setText(R.id.id_item_list_title,s);

    }

});

ok,到這裏,我們的針對單種ViewItemType的通用Adapter就完成了,代碼很簡單也很少,但是簡化效果非常明顯。

ok,接下來我們考慮多種ItemViewType的情況。

六、多種ItemViewType

多種ItemViewType,一般我們的寫法是:

  • 複寫getItemViewType,根據我們的bean去返回不同的類型

  • onCreateViewHolder中根據itemView去生成不同的ViewHolder

如果大家還記得,我們的ViewHolder是通用的,唯一依賴的就是個layoutId。那麼上述第二條就變成,根據不同的itemView告訴我用哪個layoutId即可,生成viewholder這種事我們通用adapter來做。

於是,引入一個接口:

 

 

 

 

Java

 

1

2

3

4

5

6

public interface MultiItemTypeSupport

{

    int getLayoutId(int itemType);

 

    int getItemViewType(int positiont);

}

可以很清楚的看到,這個接口實際就是完成我們上述的兩條工作。用戶在使用過程中,通過實現上面兩個方法,指明不同的Bean返回什麼itemViewType,不同的itemView所對應的layoutId.

ok,有了上面這個接口,我們的參數就夠了,下面開始我們的MultiItemCommonAdapter的編寫。

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

public abstract class MultiItemCommonAdapterTextends CommonAdapterT>

{

    protected MultiItemTypeSupport mMultiItemTypeSupport;

 

    public MultiItemCommonAdapter(Context contextList datas,

                                  MultiItemTypeSupport multiItemTypeSupport)

    {

        super(context-1datas);

        mMultiItemTypeSupport multiItemTypeSupport;

    }

 

    @Override

    public int getItemViewType(int position)

    {

        return mMultiItemTypeSupport.getItemViewType(positionmDatas.get(position));

    }

 

    @Override

    public ViewHolder onCreateViewHolder(ViewGroup parentint viewType)

    {

        int layoutId mMultiItemTypeSupport.getLayoutId(viewType);

        ViewHolder holder ViewHolder.get(mContextparentlayoutId;

        return holder;

    }

 

}

幾乎沒有幾行代碼,感覺簡直不需要消耗腦細胞。getItemViewType用戶的傳入的MultiItemTypeSupport.getItemViewType完成,onCreateViewHolder中根據MultiItemTypeSupport.getLayoutId返回的layoutId,去生成ViewHolder即可。

ok,這樣的話,我們的多種ItemViewType的支持也就完成了,一路下來感覺還是蠻輕鬆的~~~

最後,我們還有個添加分類的header,爲什麼想起來封裝這個呢,這個是因爲我看到了這麼個項目:https://github.com/ragunathjawahar/simple-section-adapter,這個項目給了種類似裝飾者模式的方法,爲ListView添加了header,有興趣可以看下。我想我們的RecylerView也來個吧,不過我們這裏直接通過繼承Adapter完成。

七、添加分類Header

話說添加分類header,其實就是我們多種ItemViewType的一種,那麼我們需要知道哪些參數呢?

簡單思考下,我們需要:

  1. header所對應的佈局文件

  2. 顯示header的title對應的TextView

  3. 顯示的title是什麼(一般肯定根據Bean生成)

ok,這樣的話,我們依然引入一個接口,用於提供上述3各參數

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

public interface SectionSupport

{

    public int sectionHeaderLayoutId();

 

    public int sectionTitleTextViewId();

 

    public String getTitle(t);

}

方法名應該很明確了,這裏引入泛型,對應我們使用時的數據類型Bean。

剛纔也說了我們的分類header是多種ItemViewType的一種,那麼直接繼承MultiItemCommonAdapter實現。

 

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

public abstract class SectionAdapter extends MultiItemCommonAdapter

{

    private SectionSupport mSectionSupport;

    private static final int TYPE_SECTION 0;

    private LinkedHashMap mSections;

 

    private MultiItemTypeSupport headerItemTypeSupport new MultiItemTypeSupport()

    {

        @Override

        public int getLayoutId(int itemType)

        {

            if (itemType == TYPE_SECTION)

                return mSectionSupport.sectionHeaderLayoutId();

            else

                return mLayoutId;

        }

        @Override

        public int getItemViewType(int positiono)

        {

             return mSections.values().contains(position?

                    TYPE_SECTION :

                    1;

        }

    };

 

    @Override

    public int getItemViewType(int position)

    {

        return mMultiItemTypeSupport.getItemViewType(positionnull);

    }

 

    final RecyclerView.AdapterDataObserver observer new RecyclerView.AdapterDataObserver()

    {

        @Override

        public void onChanged()

        {

            super.onChanged();

            findSections();

        }

    };

 

    public SectionAdapter(Context contextint layoutIdList datasSectionSupport sectionSupport)

    {

        super(contextdatasnull);

        mLayoutId layoutId;

        mMultiItemTypeSupport headerItemTypeSupport;

        mSectionSupport sectionSupport;

        mSections new LinkedHashMap();

        findSections();

        registerAdapterDataObserver(observer);

    }

 

    @Override

    protected boolean isEnabled(int viewType)

    {

        if (viewType == TYPE_SECTION)

            return false;

        return super.isEnabled(viewType);

    }

 

    @Override

    public void onDetachedFromRecyclerView(RecyclerView recyclerView)

    {

        super.onDetachedFromRecyclerView(recyclerView);

        unregisterAdapterDataObserver(observer);

    }

 

    public void findSections()

    {

        int mDatas.size();

        int nSections 0;

        mSections.clear();

 

        for (int 0get(i));

 

            if (!mSections.containsKey(sectionName))

            {

                mSections.put(sectionNamenSections);

                nSections++;

            }

        }

 

    }

 

    @Override

    public int getItemCount()

    {

        return super.getItemCount(mSections.size();

    }

 

    public int getIndexForPosition(int position)

    {

        int nSections 0;

 

        SetentrySet mSections.entrySet();

        for (Map.Entry entry entrySet)

        {

            if (entry.getValue(return position nSections;

    }

 

    @Override

    public void onBindViewHolder(ViewHolder holderint position)

    {

        position getIndexForPosition(position);

        if (holder.getItemViewType(== TYPE_SECTION)

        {

            holder.setText(mSectionSupport.sectionTitleTextViewId()mSectionSupport.getTitle(mDatas.get(position)));

            return;

        }

        super.onBindViewHolder(holderposition);

    }

}

根據我們之前的代碼,使用MultiItemCommonAdapter,需要提供一個MultiItemTypeSupport,我們這裏當然也不例外。可以看到上述代碼,我們初始化了成員變量headerItemTypeSupport,分別對getLayoutIdgetItemViewType進行了實現。

  • getLayoutId如果type是header類型,則返回mSectionSupport.sectionHeaderLayoutId();否則則返回mLayout.

  • getItemViewType根據位置判斷,如果當前是header所在位置,返回header類型常量;否則返回1.

ok,可以看到我們構造方法中調用了findSections(),主要爲了存儲我們的title和對應的position,通過一個MapmSections來存儲。

那麼對應的getItemCount()方法,我們多了幾個title肯定總數會增加,所以需要複寫。

onBindViewHolder中我們有一行重置position的代碼,因爲我們的position變大了,所以在實際上綁定我們數據時,這個position需要還原,代碼邏輯見getIndexForPosition(position)

最後一點就是,每當我們的數據發生變化,我們的title集合,即mSections就可能會發生變化,所以需要重新生成,本來準備複寫notifyDataSetChanged方法,在裏面重新生成,沒想到這個方法是final的,於是利用了registerAdapterDataObserver(observer);,在數據發生變化回調中重新生成,記得在onDetachedFromRecyclerView裏面對註冊的observer進行解註冊。

ok,到此我們的增加Header就結束了~~

恩,上面是針對普通的Item增加header的代碼,如果是針對多種ItemViewType呢?其實也很簡單,這種方式需要傳入MultiItemTypeSupport。那麼對於headerItemTypeSupport中的getItemViewType等方法,不是header類型時,交給傳入的MultiItemTypeSupport即可,大致的代碼如下:

 

 

 

 

Java

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

headerItemTypeSupport new MultiItemTypeSupport()

{

    @Override

    public int getLayoutId(int itemType)

    {

        if (itemType == TYPE_SECTION)

            return mSectionSupport.sectionHeaderLayoutId();

        else

            return multiItemTypeSupport.getLayoutId(itemType);

    }

 

    @Override

    public int getItemViewType(int positiono)

    {

        int positionVal getIndexForPosition(position);

        return mSections.values().contains(position?

                TYPE_SECTION :

                multiItemTypeSupport.getItemViewType(positionValo);

    }

};

那麼這樣的話,今天的博客就結束了,有幾點需要說明下:

本來是想接着以前的萬能Adapter後面寫,但是爲了本文的獨立和完整性,還是儘可能沒有去依賴上篇博客的內容了。

此外,文章最後給出的開源代碼與上述代碼存在些許的差異,因爲開源部分源碼整合了ListView,RecyclerView等,而本文上述代碼完全針對RecyclerView進行編寫。

QQ技術交流羣290551701 http://cxy.liuzhihengseo.com/537.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章