Android笔记之BaseAdapter适配器的使用与优化

ListView缓存机制

什么是数据适配器

适配器是AdapterView视图(如ListView - 列表视图控件、Gallery - 缩略图浏览器控件、GridView - 网格控件、Spinner - 下拉列表控件、AutoCompleteTextView - 自动提示文本框、ExpandableListView - 支持展开/收缩功能的列表控件等)与数据之间的桥梁,用来处理数据并将数据绑定到AdapterView上。

AdapterView对象有两个主要任务

  1. 在布局中显示数据
  2. 处理用户的选择

数据源的来源是各种各样的,而ListView所展示的格式是有一定要求的。数据适配器正是建立了数据源与ListView之间的适配关系,将数据源转换成ListView能够显示的数据格式,从而将数据的来源和数据的显示进行了解耦,降低了程序的耦合性,让程序更加容易拓展。
android提供多种适配器,开发时可以针对数据源的不同采用最方便的适配器,也可以自定义适配器完成复杂功能。


BaseAdapter一般的适配器基类可用于将数据绑定到listview、Gallery、GridView 、spinner、AutoCompleteTextView上,当然也可以绑定到ExpandableListView上。
BaseExpandableListAdapter可扩展的适配器基类可用于将数据绑定到支持展开/收缩功能的列表控件ExpandableListView上,ExpandableListView继承自ListView。
更多关于适配器的内容可参考这篇文章

ListView的显示与缓存机制


ListView并不会一次把所有的数据都加载出来,而是只加载显示在屏幕上的数据。如图所示,当向上滑动屏幕,将Item1移除屏幕时,Item1被放入对象池中,原本在对象池中的Item8放到了Item7的下方。

BaseAdapter的简单用法

BaseAdapter基本结构

  1. public int getCount():适配器里数据集中数据的个数
  2. public Object getItem(int position):获取数据集中与指定索引对应的数据项;
  3. public long getItemId(int position):获取指定对应数据项的ID;
  4. public View getView(int postion, View convertView, ViewGroup parent):获取每一个Item的显示内容。

创建布局文件

在主布局文件中新建一个ListView控件:

1
2
3
4
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

新建一个xml文件,命名为item。
注意将布局设置的高改为适配自己,即android:layout_height="wrap_content"

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">


<ImageView
android:id="@+id/img0_1"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@drawable/pic"/>

<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_toRightOf="@+id/img0_1"
android:text="title"
android:textSize="25sp"
android:gravity="center"/>

<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_toRightOf="@+id/img0_1"
android:layout_below="@+id/tv_title"
android:text="content"/>


</RelativeLayout>

创建数据源

新建一个类MyItem作为ListView适配器的适配类型。

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
package com.demoniaccube.chobits.study_baseadapter;

public class MyItem//数据项类
{

private int imageID;//图片的ID
private String title;//数据项的标题
private String content;//数据项的内容
public MyItem (int imageID, String title, String content)
{

this.imageID = imageID;
this.title = title;
this.content = content;
}

public int getImageID()
{

return imageID;
}

public String getTitle()
{

return title;
}

public String getContent()
{

return content;
}
}

数据适配器初解

创建一个自定义的适配器MyBaseAdapter来继承BaseAdapter。

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
public class MyBaseAdapter extends BaseAdapter
{

private List<MyItem> list;
private LayoutInflater mInflater;
public MyBaseAdapter(Context context, List<MyItem> list)
{

this.list = list;//数据源和适配器关联
mInflater = LayoutInflater.from(context);//Context要使用当前的Adapter的界面对象inflater布局装载器对象

}
@Override
public int getCount()
{

return list.size();//返回ListView需要显示的数据数量
}

@Override
public Object getItem(int position)
{

return list.get(position);//获取数据项
}

@Override
public long getItemId(int position)
{

return position;//返回指定索引对应的数据项
}

@Override
public View getView(int position, View convertView, ViewGroup parent)
{

View view = mInflater.inflate(R.layout.item, null);//将xml文件转化为我们需要的View
//获取View中的控件;
ImageView imageView = (ImageView)view.findViewById(R.id.img0_1);
TextView tv_title = (TextView)view.findViewById(R.id.tv_title);
TextView tv_content = (TextView)view.findViewById(R.id.tv_content);
MyItem mItem = list.get(position);//利用索引得到数据项
//设置控件
imageView.setImageResource(mItem.getImageID());
tv_title.setText(mItem.getTitle());
tv_content.setText(mItem.getContent());
return view;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends Activity
{


@Override
protected void onCreate(Bundle savedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//实例化MyItem数据项类
List<MyItem> myItemList = new ArrayList<>();
for (int i = 0; i < 20; i++)
{
//注意这里的图片Id不是xml中设置的id,而是图片加载到资源文件夹中的id
myItemList.add(new MyItem(R.drawable.pic, "标题:" + i, "内容:" + i));
}
//获取ListView并设置适配器
ListView listView = (ListView)findViewById(R.id.lv_main);
listView.setAdapter(new MyBaseAdapter(this, myItemList));
}
}

最终效果:

总结:
对于ListView的用法,我并不是一下子就理解和使用的。可能会有人觉得简单吧。但我开始就是用不好,往往是忘了下一步该怎么做?其中用到的LayoutInflater类不太理解;或者某个细节处理的不对,运行出错等等情况。亦或是换了一个复杂的数据项。不知道这个数据项的类该如何写,对应的Adapter该怎么继承。

下面对于ListView的简单用法做个简单的思路总结:
1、在主xml文件(main.xml)中新建一个ListView控件;然后再新建一个xml文件(item.xml),这个xml文件是你自定义的数据项的布局。比如上面xml代码中,它的数据项布局就包含一个图片,以及图片的主题和内容,当然你也可以设计一个你喜欢的数据项。比如:一个数据项就是某张图片。

2、布局搞定后,就要为刚刚设计的数据项布局新建一个对应的类MyItem,写需要用到的属性,比如这里的图片ID,标题、内容的String。

3、最后,在MainActivity中实例化数据项,再获取并配置ListView适配器即可。由于数据源的来源是各种各样的,比如这里,数据源就是图片+标题+内容的数据类型。这并不能直接传入ListView中,而且这也不利于拓展程序。这时候就需要一个“中间人”来帮忙,它就是适配器。新建一个类来继承BaseAdapter(这里我把它命名为MyBaseAdapter),在构造函数里关联数据源和适配器(即private一个数据项类MyItem数组,利用构造函数将外面的MyItem数组传入到这个MyItem数组中;另外,我们还需要利用LayoutInflater.from(context)方法获取到当前的Adapter的界面对象的inflater布局装载器对象,具体看代码部分),重写Adapter适配器里的默认方法。在这一步中最关键的是对getView方法的重写:利用构造函数中获取的Adapter界面对象的inflater布局装载器对象用inflater方法将xml文件转化为我们需要的View。最后获取View中的控件并设置控件,返回view。

虽然这样也能做出一个效果来,但这种方式没有任何优化处理,也没有考虑适配器的缓存机制,每次创建新的View,设置控件。效率极其低下。

对BaseAdapter进行优化

修改MyBaseAdapter代码

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
public class MyBaseAdapter extends BaseAdapter
{

private List<MyItem> list;
private LayoutInflater mInflater;
public MyBaseAdapter(Context context, List<MyItem> list)
{

this.list = list;//数据源和适配器关联
mInflater = LayoutInflater.from(context);//Context要使用当前的Adapter的界面对象inflater布局装载器对象

}
@Override
public int getCount()
{

return list.size();//返回ListView需要显示的数据数量
}

@Override
public Object getItem(int position)
{

return list.get(position);//获取数据项
}

@Override
public long getItemId(int position)
{

return position;//返回指定索引对应的数据项
}

//以下修改部分:
@Override
public View getView(int position, View convertView, ViewGroup parent)
{

if (convertView == null)
{
convertView = mInflater.inflate(R.layout.item, null);//将xml文件转化为我们需要的View
}
//获取View中的控件;
ImageView imageView = (ImageView)convertView.findViewById(R.id.img0_1);
TextView tv_title = (TextView)convertView.findViewById(R.id.tv_title);
TextView tv_content = (TextView)convertView.findViewById(R.id.tv_content);
MyItem mItem = list.get(position);//利用索引得到数据项
//设置控件
imageView.setImageResource(mItem.getImageID());
tv_title.setText(mItem.getTitle());
tv_content.setText(mItem.getContent());
return convertView;
}
}

可以看到简单的用法和优化后的方法差别只是改了一个

if (convertView == null)//如果没有缓存才创建新的View
{
    convertView = mInflater.inflate(R.layout.item, null);
}

但正是这样一个处理,我们利用了BaseAdapter的缓存机制。避免了重复的去创建View对象。通过inflate对象将一个xml布局转化为View时,这个操作是十分耗时耗资源的。但通过这样一个判断,就避免了大量去创建View对象。但是,findViewById方法依然会浪费大量时间。所以我们还要进一步优化。

BaseAdapter的再优化

只需要对MyBaseAdapter类的getView方法做修改,并增加一个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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 @Override
public View getView(int position, View convertView, ViewGroup parent)
{

//优化前的代码:
// View view = mInflater.inflate(R.layout.item, null);//将xml文件转化为我们需要的View
// //获取View中的控件;
// ImageView imageView = (ImageView)view.findViewById(R.id.img0_1);
// TextView tv_title = (TextView)view.findViewById(R.id.tv_title);
// TextView tv_content = (TextView)view.findViewById(R.id.tv_content);
// MyItem mItem = list.get(position);//利用索引得到数据项
// //设置控件
// imageView.setImageResource(mItem.getImageID());
// tv_title.setText(mItem.getTitle());
// tv_content.setText(mItem.getContent());
// return view;

//优化后代码:
ViewHolder viewHolder;
if (convertView == null)
{
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item, null);
viewHolder.imageView = (ImageView) convertView.findViewById(R.id.img0_1);
viewHolder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
viewHolder.tv_content = (TextView)convertView.findViewById(R.id.tv_content);
convertView.setTag(viewHolder);

}
else
{
viewHolder = (ViewHolder)convertView.getTag();
}
MyItem myItem = list.get(position);
viewHolder.imageView.setImageResource(myItem.getImageID());
viewHolder.tv_title.setText(myItem.getTitle());
viewHolder.tv_content.setText(myItem.getContent());
return convertView;

}

class ViewHolder
{

public ImageView imageView;
public TextView tv_title;
public TextView tv_content;
}

在MyBaseAdapter类中新建一个ViewHolder内部类,新建属性(这个属性是根据数据项布局的控件决定的,比如这里就public ImageView、两个TextView),这里新建一个ViewHolder类就是为了避免重复的findViewById;convertView为空时,像上面一样先将xml文件转化为我们需要的View,实例化一个ViewHolder,用于保存布局中的三个控件;再用convertView.setTag方法建立convertView与viewHolder的关系。在if外面的接下来的代码中使用viewHolder中的成员变量来找到控件,而避免了通过findViewById来实例化这个控件。这样就节省了资源,提高了效率。
这种方法不仅利用了listView的缓存,更通过ViewHolder类来实现显示数据的视图的缓存,避免多次通过findViewById寻找控件。

ViewHolder优化BaseAdapter的思路:

  1. 创建内部类ViewHolder
  2. 判断convertView是否为空,为空则实例化ViewHolder,并设置setTag,将ViewHolder与convertView绑定;否则通过getTag取出ViewHolder。
  3. 通过ViewHolder对象找到对应控件,给ViewHolder中的控件设置数据。