一、概述
記得好久以前針對ListView類控件寫過一篇打造萬能的ListView GridView 適配器,如今RecyclerView異軍突起,其Adapter的用法也與ListView類似,那么我們也可以一步一步的為其打造通用的Adapter,使下列用法書寫更加簡單:
- 簡單的數據綁定(單種Item)
- 多種Item Type 數據綁定
- 增加onItemClickListener , onItenLongClickListener
- 優雅的添加分類header
二、使用方式和效果圖
在一步一步完成前,我們先看下使用方式和效果圖:
(1)簡單的數據綁定
首先看我們最常用的單種Item的書寫方式:
1
2
3
4
5
6
7
8
|
mRecyclerView.setAdapter(new CommonAdapter(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String s)
{
holder.setText(R.id.id_item_list_title, s);
}
});
|
是不是相當方便,在convert方法中完成數據、事件綁定即可。
(2)多種ItemViewType
多種ItemViewType,正常考慮下,我們需要根據Item指定ItemType,並且根據ItemType指定相應的布局文件。我們通過MultiItemTypeSupport
完成指定:
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 postion, ChatMessage msg)
{
//根據當前的bean返回item type
}
}
|
剩下就簡單了,將其作為參數傳入到MultiItemCommonAdapter
即可。
1
2
3
4
5
6
7
8
|
mRecyclerView.setAdapter(new SectionAdapter(this, mDatas, multiItemSupport)
{
@Override
public void convert(ViewHolder holder, String s)
{
holder.setText(R.id.id_item_list_title, s);
}
});
|
貼個效果圖:
(3)添加分類header
其實屬於多種ItemViewType的一種了,只是比較常用,我們就簡單封裝下。
依賴正常考慮下,這種方式需要額外指定header的布局,以及布局中顯示標題的TextView了,以及根據Item顯示什么樣的標題。我們通過SectionSupport
對象指定:
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(0, 1);
}
};
|
3個方法,一個指定header的布局文件,一個指定布局文件中顯示title的TextView,最后一個用於指定顯示什么樣的標題(根據Adapter的Bean)。
接下來就很簡單了:
1
2
3
4
5
6
7
8
|
mRecyclerView.setAdapter(new SectionAdapter(this, R.layout.item_list, mDatas, sectionSupport)
{
@Override
public void convert(ViewHolder holder, String s)
{
holder.setText(R.id.id_item_list_title, s);
}
});
|
這樣就完了,效果圖如下:
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,取而代之的只能是個集合來存儲了。
那么代碼如下:
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 context, View itemView, ViewGroup parent)
{
super(itemView);
mContext = context;
mConvertView = itemView;
mViews = new SparseArray();
}
public static ViewHolder get(Context context, ViewGroup parent, int layoutId)
{
View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
false);
ViewHolder holder = new ViewHolder(context, itemView, parent, position);
return holder;
}
/**
* 通過viewId獲取控件
*
* @param viewId
* @return
*/
public T getView(int viewId)
{
View view = mViews.get(viewId);
if (view == null)
{
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
}
|
代碼很簡單,我們的ViewHolder繼承自RecyclerView.ViewHolder
,內部通過SparseArray來緩存我們itemView內部的子View,從而得到一個通用的ViewHolder。每次需要創建ViewHolder只需要傳入我們的layoutId即可。
ok,有了通用的ViewHolder之后,我們的通用的Adapter分分鍾就出來了。
四、通用的Adapter
我們的每次使用過程中,針對的數據類型Bean肯定是不同的,那么這里肯定要引入泛型代表我們的Bean,內部通過一個List代表我們的數據,ok,剩下的看代碼:
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 context, int layoutId, List datas)
{
mContext = context;
mInflater = LayoutInflater.from(context);
mLayoutId = layoutId;
mDatas = datas;
}
@Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType)
{
ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId);
return viewHolder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position)
{
holder.updatePosition(position);
convert(holder, mDatas.get(position));
}
public abstract void convert(ViewHolder holder, T t);
@Override
public int getItemCount()
{
return mDatas.size();
}
}
|
繼承自RecyclerView.Adapter
,需要復寫的方法還是比較少的。首先我們使用過程中傳輸我們的數據集mDatas,和我們item的布局文件layoutId。
onCreateViewHolder
時,通過layoutId即可利用我們的通用的ViewHolder生成實例。
onBindViewHolder
這里主要用於數據、事件綁定,我們這里直接抽象出去,讓用戶去操作。可以看到我們修改了下參數,用戶可以拿到當前Item所需要的對象和viewHolder去操作。
那么現在用戶的使用是這樣的:
1
2
3
4
5
6
7
8
9
|
mRecyclerView.setAdapter(new CommonAdapter(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String s)
{
TextView tv = holder.getView(R.id.id_item_list_title);
tv.setText(s);
}
});
|
看到這里,爽了很多,目前我們僅僅寫了很少的代碼,但是我們的通用的Adapter感覺已經初步完成了。
可以看到我們這里通過viewholder根據控件的id拿到控件,然后再進行數據綁定和事件操作,我們還能做些什么簡化呢?
恩,我們可以通過一些輔助方法簡化我們的代碼,所以繼續往下看。
五、進一步封裝ViewHolder
我們的Item實際上使用的控件較多時候可能都是TextView
,ImageView
等,我們一般在convert方法都是去設置文本,圖片什么的,那么我們可以在ViewHolder里面,寫上如下的一些輔助方法:
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 viewId, String text)
{
TextView tv = getView(viewId);
tv.setText(text);
return this;
}
public ViewHolder setImageResource(int viewId, int 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;
}
}
|
當然上面只給出了幾個方法,你可以把常用控件的方法都寫進去,並且在使用過程中不斷完善即可。
有了一堆輔助方法后,我們的操作更加簡化了一步。
1
2
3
4
5
6
7
8
9
10
|
mRecyclerView.setAdapter(new CommonAdapter(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String 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來做。
於是,引入一個接口:
1
2
3
4
5
6
|
public interface MultiItemTypeSupport
{
int getLayoutId(int itemType);
int getItemViewType(int position, T t);
}
|
可以很清楚的看到,這個接口實際就是完成我們上述的兩條工作。用戶在使用過程中,通過實現上面兩個方法,指明不同的Bean返回什么itemViewType,不同的itemView所對應的layoutId.
ok,有了上面這個接口,我們的參數就夠了,下面開始我們的MultiItemCommonAdapter
的編寫。
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 MultiItemCommonAdapterT> extends CommonAdapterT>
{
protected MultiItemTypeSupport mMultiItemTypeSupport;
public MultiItemCommonAdapter(Context context, List datas,
MultiItemTypeSupport multiItemTypeSupport)
{
super(context, -1, datas);
mMultiItemTypeSupport = multiItemTypeSupport;
}
@Override
public int getItemViewType(int position)
{
return mMultiItemTypeSupport.getItemViewType(position, mDatas.get(position));
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
int layoutId = mMultiItemTypeSupport.getLayoutId(viewType);
ViewHolder holder = ViewHolder.get(mContext, parent, layoutId;
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的一種,那么我們需要知道哪些參數呢?
簡單思考下,我們需要:
- header所對應的布局文件
- 顯示header的title對應的TextView
- 顯示的title是什么(一般肯定根據Bean生成)
ok,這樣的話,我們依然引入一個接口,用於提供上述3各參數
1
2
3
4
5
6
7
8
|
public interface SectionSupport
{
public int sectionHeaderLayoutId();
public int sectionTitleTextViewId();
public String getTitle(T t);
}
|
方法名應該很明確了,這里引入泛型,對應我們使用時的數據類型Bean。
剛才也說了我們的分類header是多種ItemViewType的一種,那么直接繼承MultiItemCommonAdapter
實現。
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 position, T o)
{
return mSections.values().contains(position) ?
TYPE_SECTION :
1;
}
};
@Override
public int getItemViewType(int position)
{
return mMultiItemTypeSupport.getItemViewType(position, null);
}
final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver()
{
@Override
public void onChanged()
{
super.onChanged();
findSections();
}
};
public SectionAdapter(Context context, int layoutId, List datas, SectionSupport sectionSupport)
{
super(context, datas, null);
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 n = mDatas.size();
int nSections = 0;
mSections.clear();
for (int i = 0; i get(i));
if (!mSections.containsKey(sectionName))
{
mSections.put(sectionName, i + nSections);
nSections++;
}
}
}
@Override
public int getItemCount()
{
return super.getItemCount() + mSections.size();
}
public int getIndexForPosition(int position)
{
int nSections = 0;
Set> entrySet = mSections.entrySet();
for (Map.Entry entry : entrySet)
{
if (entry.getValue() return position - nSections;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position)
{
position = getIndexForPosition(position);
if (holder.getItemViewType() == TYPE_SECTION)
{
holder.setText(mSectionSupport.sectionTitleTextViewId(), mSectionSupport.getTitle(mDatas.get(position)));
return;
}
super.onBindViewHolder(holder, position);
}
}
|
根據我們之前的代碼,使用MultiItemCommonAdapter
,需要提供一個MultiItemTypeSupport
,我們這里當然也不例外。可以看到上述代碼,我們初始化了成員變量headerItemTypeSupport
,分別對getLayoutId
和getItemViewType
進行了實現。
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
即可,大致的代碼如下:
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 position, T o)
{
int positionVal = getIndexForPosition(position);
return mSections.values().contains(position) ?
TYPE_SECTION :
multiItemTypeSupport.getItemViewType(positionVal, o);
}
};
|
那么這樣的話,今天的博客就結束了,有幾點需要說明下:
本來是想接着以前的萬能Adapter后面寫,但是為了本文的獨立和完整性,還是盡可能沒有去依賴上篇博客的內容了。
此外,文章最后給出的開源代碼與上述代碼存在些許的差異,因為開源部分源碼整合了ListView,RecyclerView等,而本文上述代碼完全針對RecyclerView進行編寫。
QQ技術交流群290551701 http://cxy.liuzhihengseo.com/537.html