Adapter 详解

基础

适配器模式

将一个类的接口转换成客户希望的另外一个接口, Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

0026_adapter_design_pattern.png

Android 中适配器模式的运用

Android 中的 ListViewGridViewSpinnerAutoCompletedAdapterView 在做视图展示时需要填充数据,但是每个视图的显示效果,需要的数据等等都不一样,所以增加了一个 Adapter 层来应对变化。

0026_android_adapter-class.png

常用的 Adapter

  • BaseAdapter
    抽象类,自定义 Adapter 实现该类,拥有较高的灵活性
  • ArrayAdapter
    支持泛型操作,最为简单,常用来展示一行文本
  • SimpleCursorAdapter
    绑定查询到的 Cursor 游标到视图上,字段和 Viewid 对应起来
  • SimpleAdapter
    有最好的扩充性,可以自定义出各种效果,需要实现数据填充

Adapter 对应的常用系统自带布局文件

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
// 单行文本TextView  
android.R.layout.simple_list_item_1.xml
// 两行文本TextView,如名字-电话号码
android.R.layout.simple_list_item_2.xml
// 单行文本CheckedTextView,具有RadioButton样式
android.R.layout.simple_list_item_single_choice.xml
// 单行文本CheckedTextView,具有CheckBox样式
android.R.layout.simple_list_item_multiple_choice.xml
// 单行文本CheckedTextView,也是多选但是是"对勾"样式
android.R.layout.simple_list_item_checked.xml
// 两行文本TextView,加上一个RadioButton
android.R.layout.simple_list_item_2_single_choice.xml
// 单行文本TextView,选中整行高亮
android.R.layout.simple_list_item_activated_1.xml
// 两行文本TextView,选中整行高亮
android.R.layout.simple_list_item_activated_2.xml
// 单行文本TextView,spinner
android.R.layout.simple_spinner_item.xml
// 单行文本CheckedTextView,下拉样式,和simple_spinner_item区别不大
android.R.layout.simple_spinner_dropdown_item.xml
// 单行文本TextView,下拉样式,常用于自动填充,AutoCompleted
android.R.layout.simple_dropdown_item_1line.xml
// 单行文本TextView,expandalble样式(有左边距,左边空出一部分)
android.R.layout.simple_expandable_list_item_1.xml
// 两行文本TextView,expandalble样式
android.R.layout.simple_expandable_list_item_2.xml

ArrayAdapter 介绍

源码分析

用来显示至少包含一行 TextView 的字符串列表

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
// 定义,泛型操作,T 可以是简单数据类型,也可以是自定义的类(需要实现 toString )
public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {...}

// 构造函数必须要指定布局
// 不指定 TextView id,表示布局就是一个 TextView,初始化空 ArrayList
public ArrayAdapter(Context, int) {...}
// 组合布局,指定具体的 TextView id
public ArrayAdapter(Context, int, int) {...}
// 不指定 TextView id,布局就是一个 TextView,指定数据数组
public ArrayAdapter(Context, int, T[]) {...}
// 组合布局,指定具体的 TextView id,并指定数据数组
public ArrayAdapter(Context, int, int, T[]) {...}
// 不指定 TextView id,布局就是一个 TextView,指定数据 List
public ArrayAdapter(Context, int, List<T>) {...}
// 以上构造函数都会调用这个最详细的构造
public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
@IdRes int textViewResourceId, @NonNull List<T> objects) {...}
...
// getView 的具体实现
@Override
public @NonNull View getView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
return createViewFromResource(mInflater, position, convertView, parent, mResource);
}

private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
@Nullable View convertView, @NonNull ViewGroup parent, int resource) {
final View view;
final TextView text;

if (convertView == null) {
view = inflater.inflate(resource, parent, false);
} else {
view = convertView;
}

try {
if (mFieldId == 0) {
// If no custom field is assigned, assume the whole resource is a TextView
// 不指定 TextView id,则默认整个布局文件就是 TextView
text = (TextView) view;
} else {
// Otherwise, find the TextView field within the layout
// 至少包含一个 TextView
text = (TextView) view.findViewById(mFieldId);

if (text == null) {
throw new RuntimeException("Failed to find view with ID "
+ mContext.getResources().getResourceName(mFieldId)
+ " in item layout");
}
}
} catch (ClassCastException e) {
Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
throw new IllegalStateException(
"ArrayAdapter requires the resource ID to be a TextView", e);
}

// getItem 的实现是从 objects 中获取数据
final T item = getItem(position);
if (item instanceof CharSequence) {
text.setText((CharSequence) item);
} else {
// 数据转换为字符串。T 可以是简单数据类型,也可以是自定义的类
text.setText(item.toString());
}

return 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
41
42
43
44
45
46
47
48
49
50
// 展示三种 ArrayAdapter 的用法
private void initArrayAdapter(){
// 简单字符串显示,使用系统自带的布局 simple_list_item_1
ListView arrayListLeft = (ListView) findViewById(R.id.lv_array_adapter_left);
String[] arrayData1 = new String[]{"Left1", "Left2", "Left3", "Left4"};
ArrayAdapter stringAdapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, arrayData1);
arrayListLeft.setAdapter(stringAdapter);

// 自定义布局,布局 array_adapter_user_samples 文件中必须包含 TextView 和 Button
// 初始化 ArrayAdapter 时,需要指定 TextView 的 ID:tv_array_adapter_sample
ListView arrayListCenter = (ListView) findViewById(R.id.lv_array_adapter_center);
String[] arrayData2 = new String[]{"Center1", "Center2", "Center3", "Center4"};
ArrayAdapter userLayoutAdapter = new ArrayAdapter<String>(this,
R.layout.array_adapter_user_samples, R.id.tv_array_adapter_sample, arrayData2);
arrayListCenter.setAdapter(userLayoutAdapter);

// 自定义数据集合,使用系统自带布局 simple_list_item_checked
ListView arrayListRight = (ListView) findViewById(R.id.lv_array_adapter_right);
List<UserData> dataList = initUserDataList();
ArrayAdapter userDataAdapter = new ArrayAdapter<UserData>(this,
android.R.layout.simple_list_item_checked, dataList);
arrayListRight.setAdapter(userDataAdapter);
}

// 自定义数据类,必须重写 toString,根据 ArrayAdapter 源码分析
// 数据填充时,必须使用 toString 来赋值
private class UserData{
private String mStr;
private int mNum;

public UserData(String str, int num){
mStr = str;
mNum = num;
}

public String toString(){
return mStr + String.valueOf(mNum);
}
}
// 初始化数据集合
private List<UserData> initUserDataList(){
List<UserData> dataList = new ArrayList<UserData>();
String pre = "Right";
for(int i = 1; i <= 4; i++) {
UserData data = new UserData(pre, i);
dataList.add(data);
}
return dataList;
}

如上代码对应的效果图:

0026_arrayadapter_sample.png

SimpleCursorAdapter 介绍

类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类定义
public class SimpleCursorAdapter extends ResourceCursorAdapter {...}
public abstract class ResourceCursorAdapter extends CursorAdapter {...}
/**
* Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
* {@link android.widget.ListView ListView} widget.
* <p>
* The Cursor must include a column named "_id" or this class will not work.
* Additionally, using {@link android.database.MergeCursor} with this class will
* not work if the merged Cursors have overlapping values in their "_id"
* columns.
*/
public abstract class CursorAdapter extends BaseAdapter implements Filterable,
CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {...}

// 构造函数
public SimpleCursorAdapter(Context context, int layout, Cursor c,
String[] from, int[] to, int flags) {...}

注意CursorAdapter 中的 Cursor 必须包含一列 “_id”,也就是查询时 projection 中要包含 _id,但 UI 中并不需要显示这列

具体示例及错误分析

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
// 例子使用 CursorLoader 异步加载
// 构造函数初始化时传递的是一个 null 的 cursor,只需要显示姓名和号码
String[] from = new String[]{
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER};
int[] to = new int[]{android.R.id.text1, android.R.id.text2};
mSimpleCursorAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_2, null, from, to, 0);

// 定义 projections,必须包含一列 _id
String[] projections = new String[]{
ContactsContract.CommonDataKinds.Phone._ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER};
// 返回具体的实例,包含 Cursor
return new CursorLoader(getApplicationContext(),
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projections, null, null, null);

// 如果 projections 中不包含 id, 在使用 swapCursor 时会抛出如下异常
06-30 11:15:26.679 11899-11899/com.***.*** E/AndroidRuntime:
FATAL EXCEPTION: main
Process: com.***.***, PID: 11899
java.lang.IllegalArgumentException: column '_id' does not exist
at android.database.AbstractCursor.getColumnIndexOrThrow(AbstractCursor.java:303)
at android.database.CursorWrapper.getColumnIndexOrThrow(CursorWrapper.java:78)
at android.widget.CursorAdapter.swapCursor(CursorAdapter.java:342)
at android.widget.SimpleCursorAdapter.swapCursor(SimpleCursorAdapter.java:346)

swapCursor 的源码分析

CursorAdapter.java 文件中:

0026_swapCursor_code.png

SimpleAdapter 介绍

SimpleAdapter 的扩展性最好,可以定义各种各样的布局出来,可以显示比较复杂的列表,包括每行显示图片、文字等,只是简单的负责显示。
使用 SimpleAdapter 的数据用一般都是 HashMap 构成的 ListHashMap 的每个键值数据映射到布局文件中对应 id 的组件上。因为系统没有对应的布局文件可用,我们可以自己定义一个布局。

构造函数

1
2
public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,
@LayoutRes int resource, String[] from, @IdRes int[] to) {

参数说明:

  • context
  • data
    Map(String ,Object) 列表填充的数据集合
  • resource
    界面布局文件
  • from
    数据 Map 集合中的 key
  • to
    布局文件中各组件 id,和 Map 集合中的 key 需要一一对应,资源数据按照这个对应关系来填充值

系统默认支持的布局

默认布局中支持如下控件:

  • Checkable
  • TextView
  • ImageView

参考 bindview 源码:

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
// SimpleAdapter.java:: bindview,代码部分有删减
private void bindView(int position, View view) {
...
final ViewBinder binder = mViewBinder;
final String[] from = mFrom;
final int[] to = mTo;
...
boolean bound = false;
// 使用了系统默认不支持的控件,需要实现 ViewBinder
// 重写 setViewValue
if (binder != null) {
bound = binder.setViewValue(v, data, text);
}

// 系统默认支持的控件:Checkable,TextView,ImageView
// 如果 binder 不存在或者绑定不成功,使用系统默认方式绑定
if (!bound) {
if (v instanceof Checkable) {
if (data instanceof Boolean) {
((Checkable) v).setChecked((Boolean) data);
} else if (v instanceof TextView) {
// Note: keep the instanceof TextView check at the bottom of these
// ifs since a lot of views are TextViews (e.g. CheckBoxes).
setViewText((TextView) v, text);
} else {
throw new IllegalStateException(v.getClass().getName() +
" should be bound to a Boolean, not a " +
(data == null ? "<unknown type>" : data.getClass()));
}
} else if (v instanceof TextView) {
// Note: keep the instanceof TextView check at the bottom of these
// ifs since a lot of views are TextViews (e.g. CheckBoxes).
setViewText((TextView) v, text);
} else if (v instanceof ImageView) {
if (data instanceof Integer) {
setViewImage((ImageView) v, (Integer) data);
} else {
setViewImage((ImageView) v, text);
}
} else {
throw new IllegalStateException(v.getClass().getName() + " is not a " +
" view that can be bounds by this SimpleAdapter");
}
}
...
}

参考源码,按以下顺序绑定数据:

  • Checkable
    如果 View 实现了 Checkable(例如 CheckBox),期望绑定值是一个布尔类型
  • TextView
    期望绑定值是一个字符串类型,通过调用 setViewText(TextView, String) 绑定数据
  • ImageView
    期望绑定值是一个资源 id 或者一个字符串,通过调用 setViewImage(ImageView, int)setViewImage(ImageView, String) 绑定数据

如果没有一个合适的绑定发生将会抛出 IllegalStateException

自定义布局

自定义布局中如果包含了不是系统默认支持控件,需要实现 ViewBinder 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static interface ViewBinder {
/**
* Binds the specified data to the specified view.
*
* When binding is handled by this ViewBinder, this method must return true.
* If this method returns false, SimpleAdapter will attempts to handle
* the binding on its own.
*
* @param view the view to bind the data to
* @param data the data to bind to the view
* @param textRepresentation a safe String representation of the supplied data:
* it is either the result of data.toString() or an empty String but it
* is never null
*
* @return true if the data was bound to the view, false otherwise
*/
boolean setViewValue(View view, Object data, String textRepresentation);
}

BaseAdapter 介绍

类定义及重写

类定义:
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {...}

重写四个方法:

1
2
3
4
public int getCount() {...}
public Object getItem(int position){...}
public long getItemId(int position){...}
public View getView(int position, View convertView, ViewGroup parent) {...}

其中 getView 最关键,特别是数据很多时如果全部加载会非常消耗资源,导致 ListView 滑动慢,常用的解决方案是:convertView + ViewHolder 方案

ViewHolder 的意义

ViewHolder 是一个静态类, 用来缓存了显示数据的视图,加快 UI 的响应速度。

To work efficiently the adapter implemented here uses two techniques:
(译:提升 Adapter 性能的两种方法:)
-It reuses the convertView passed to getView() to avoid inflating View when it is not necessary
(译:重用缓存 convertView 传递给 getView() 方法来避免填充不必要的视图)
-It uses the ViewHolder pattern to avoid calling findViewById() when it is not necessary
(译:使用 ViewHolder 模式来避免没有必要的调用 findViewById():因为太多的 findViewById 也会影响性能)
ViewHolder类的作用
-The ViewHolder pattern consists in storing a data structure in the tag of the view
returned by getView().This data structures contains references to the views we want to bind data to, thus avoiding calling to findViewById() every time getView() is invoked
(译:ViewHolder 模式通过 getView() 方法返回的视图的标签 Tag 中存储一个数据结构,这个数据结构包含了指向我们要绑定数据的视图的引用,从而避免每次调用 getView() 的时候调用 findViewById())

示例

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
// 静态类ViewHolder
private static class ViewHolder{
ImageView img;
TextView tvName;
TextView tvDesc;
}

private class CustomAdapter extends BaseAdapter{

private LayoutInflater mLayoutInflater;
public CustomAdapter(Context context){
mLayoutInflater = LayoutInflater.from(context);
}

// 重写四个方法
@Override
public int getCount() {
return mAdapterData.size();
}

@Override
public Object getItem(int position) {
return mAdapterData.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 使用Holder
ViewHolder holder = null;
if(convertView == null){
convertView = mLayoutInflater.inflate(R.layout.simple_adapter_user_samples, null);
holder = new ViewHolder();
holder.img = (ImageView) convertView.findViewById(R.id.iv_simple_adapter);
holder.tvName = (TextView) convertView.findViewById(R.id.tv_simple_adapter_name);
holder.tvDesc = (TextView) convertView.findViewById(R.id.tv_simple_adapter_desc);
// Holder保存到converView中
convertView.setTag(holder);
}else {
holder = (ViewHolder) convertView.getTag();
}

Map<String, Object> map = mAdapterData.get(position);
holder.img.setImageResource((int)map.get(ADAPTER_KEY_IMG));
holder.tvName.setText((String)map.get(ADAPTER_KEY_NAME));
holder.tvDesc.setText((String)map.get(ADAPTER_KEY_DESC));

return convertView;
}
}

Spinner 介绍

标准用法

定义布局文件和下拉布局,并填充数据

1
2
3
4
5
6
7
Spinner spinner = (Spinner)findViewById(R.id.spinner_show_checkedtextview);
// Spinner 显示的样式为 simple_spinner_item,自定义的布局修改了系统的背景色
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.screen_orientations, R.layout.simple_spinner_item);
// Spinner 下拉菜单显示的样式为 simple_spinner_dropdown_item
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);

Spinner 显示样式和下拉菜单样式的区别:

0026_spinner_pattern_and_dropdown_diff.png

如果 Adapter 不设置 setDropDownViewResource ,默认下拉菜单和显示使用同一个布局。

两种模式

默认模式为 dropdown

1
2
android:spinnerMode="dialog"
android:spinnerMode="dropdown"
0%