Wen

Android ListView 介绍和 Holder 设计模式

在Android开发过程中ListView是一个非常常见的类,但是它的用法又和普通的TextViewImageView不太一样,本篇介绍一下ListView的基本用法,以及实现过程中用到的Holder设计模式。

ListView基本组成


ListView可以以列表的方式来显示用户的数据。它继承于AdapterView, 可以用M(Model)V(View)C(Controller)的模式来理解ListView,Model是数据来源,可以是从数据库读取或者网上传送来的数据,最简单的例子就是一个hard coded的数组。Controller就是ListView中的Adapter,一个数据和View之间的中介,负责将数据和View联系起来,比如哪些数据显示在View的哪些地方、怎么显示等问题,Android中有专门的adapter类,只要继承这个类并实现某些函数就可以了。最后再在ListView中运行setAdapter()。View就是ListView,来定义这个view的大小位置等信息。

理解Adapter


理解ListView,重点是理解adapter。在Android中,Adapter类是AdapterView(ListView的父类)和数据之间的桥梁,类似于ListView的config文件。比如有ArrayAdapter, SimpleAdapter等,对应于不同的数据传入方式。来看一个最简单的ArrayAdapter的例子。

本例中想用ListView展示各种不同的菜谱,为了简单,这些菜谱数据就存在数组里。ListView在Fragment里初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CookBookFragment extends Fragment {
ListView recipeList;
public CookBookFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.cookbook, container, false);
recipeList = (ListView) v.findViewById(R.id.recipe_list);
RecipeItem[] recipesData = new RecipeItem[1];
recipesData[0] = new RecipeItem("Hello Title", "Hello Content");
RecipeListAdapter adapter = new RecipeListAdapter(container.getContext(), R.layout.recipe_item, recipesData);
recipeList.setAdapter(adapter);
return v;
}
}

cookbook.xml里,只有一个叫recipe_list的ListView。上面这段代码只做了一件事,就是给这个listview setAdapter。重点就是这个adapter了,初始化的时候传入了一个layout和一个数组,这个layout就是list中每一行的layout,这个数组就是要的数据,这个初始化方式是ArrayAdapter的初始化方式。下面来看看这个RecipeListAdapter是怎么写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RecipeListAdapter extends ArrayAdapter<RecipeItem>{
Context context;
int layoutResourceId;
RecipeItem[] recipesData;
public RecipeListAdapter(Context context, int layoutResourceId, RecipeItem[] recipesData) {
super(context, layoutResourceId);
this.context = context;
this.layoutResourceId = layoutResourceId;
this.recipesData = recipesData;
}
@Override
public int getCount() {
return recipesData.length;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
...
return row;
}
}

上面这段代码里,除了构造函数,override了两个函数。构造函数把传进来的类存为成员变量,方便其他函数引用。override的两个函数,是ArrayAdapter必须知道的两个函数,一个是getCount(),告诉这个listview一共要显示几个item。 然后是geiView(int position, ...), 返回的是在listview上position这个位置要显示的View。也就是说在listview中每个位置的item可以分别单独定义view,通过getView()来实现这一点,它return的那个view就是在position位置显示的view,这里省略了实现细节,具体解释下面讲。

getView()和Holder Pattern


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
public class RecipeListAdapter extends ArrayAdapter<RecipeItem>{
...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
RecipeItemHolder holder = null;
if(row == null) {
LayoutInflater inflater = ((Activity)context).getLayoutInflater();
row = inflater.inflate(layoutResourceId, parent, false);
holder = new RecipeItemHolder();
holder.title = (TextView)row.findViewById(R.id.recipe_title);
holder.content = (TextView)row.findViewById(R.id.recipe_content);
row.setTag(holder);
}
else {
holder = (RecipeItemHolder)row.getTag();
}
RecipeItem recipe = recipesData[position];
holder.title.setText(recipe.title);
holder.content.setText(recipe.content);
return row;
}
static class RecipeItemHolder {
TextView title;
TextView content;
}
}

ListView里的item可能有很多,在ListView的实现中,不可能对每个item都建立一个view,然后当view在屏幕位置的时候就显示,不在的时候就存在内存里面,因为这样太浪费内存了。事实上,Android内部用了回收机制,将没有在当前屏幕上显示的item的view放进RecycleBin,如果滑动到应该显示该item时再从RecycleBin中复用这个View。例如,一个屏幕只能够显示5个item,但有10个item在这个list里,那么初始化时,Android就创建5个view来显示前5个item,当向下滑把第一个item滑出屏幕,第6个要出现时,系统就从RecycleBin中重用第一个view,只不过重新设置为第6个item的数据,而不会去重新创建一个view。所以不管ListView里面有再多的item,系统只要创建5个view就可以了。

明白了这个原理后,来看传入的三个参数。第一个是要显示view的position,如果的listview里一共有10个item的话,那么这个getView就会调用10次,position从0到9。第二个参数convertView就是刚刚说的系统回收利用的view,如果它是null的时候可以新建,如果它不是null说明就是系统回收后的view,可以重新设置它而不必重新创建。第三个参数是parent view,调用inflater.inflate()的时候可作为参数传进去(见上面getView()内具体代码)。

好了,现在不明白的可能就是getView()里Holder模式的运用。在android编程中,findViewById()这种操作是很消耗时间的,而getView()会被非常频繁的调用到,所以不希望每次调用getView()时都运行findViewById()来找到相应的view。根据ListView的recycle原理,绝大部分view都是被回收利用的,所以完全可以在view里保存对其child view的引用,这样就不用每次都findViewById来找其child view了。

Android里每个view都可以通过setTag(Object tag)来保存一个Object到view本身,可以每次新建view的时候把上面的child view通过findViewById()找到,然后把它们的reference存到这个view的tag里,比如上面的代码,每个item的view里都有两个TextView,就可以只在新建item view的时候通过findViewById来找到这两个TextView,然后存到这个item的tag里。然后当这个view再次被利用的时候,就不必再调用findViewById了,而是直接调用getTag来得到这两个TextView的reference。大家可以再看看上面getView()的代码来加深理解。这个存child view并传入setTag()的Object就成为Holder. Holder模式就是通过这种方式来提高app的运行效率。

关于ListView就介绍到这里,如果有什么不明白的地方欢迎留言讨论。

转载请注明出处:http://zhaowen.io/post/Android_ListView_Introduction_And_Holder_Design_Pattern/