ListView缓存机制
什么是数据适配器
适配器是AdapterView视图(如ListView - 列表视图控件、Gallery - 缩略图浏览器控件、GridView - 网格控件、Spinner - 下拉列表控件、AutoCompleteTextView - 自动提示文本框、ExpandableListView - 支持展开/收缩功能的列表控件等)与数据之间的桥梁,用来处理数据并将数据绑定到AdapterView上。
AdapterView对象有两个主要任务
- 在布局中显示数据
- 处理用户的选择
数据源的来源是各种各样的,而ListView所展示的格式是有一定要求的。数据适配器正是建立了数据源与ListView之间的适配关系,将数据源转换成ListView能够显示的数据格式,从而将数据的来源和数据的显示进行了解耦,降低了程序的耦合性,让程序更加容易拓展。
android提供多种适配器,开发时可以针对数据源的不同采用最方便的适配器,也可以自定义适配器完成复杂功能。
BaseAdapter一般的适配器基类可用于将数据绑定到listview、Gallery、GridView 、spinner、AutoCompleteTextView上,当然也可以绑定到ExpandableListView上。
BaseExpandableListAdapter可扩展的适配器基类可用于将数据绑定到支持展开/收缩功能的列表控件ExpandableListView上,ExpandableListView继承自ListView。
更多关于适配器的内容可参考这篇文章
ListView的显示与缓存机制
ListView并不会一次把所有的数据都加载出来,而是只加载显示在屏幕上的数据。如图所示,当向上滑动屏幕,将Item1移除屏幕时,Item1被放入对象池中,原本在对象池中的Item8放到了Item7的下方。
BaseAdapter的简单用法
BaseAdapter基本结构
public int getCount():
适配器里数据集中数据的个数;
public Object getItem(int position):
获取数据集中与指定索引对应的数据项;
public long getItemId(int position):
获取指定对应数据项的ID;
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; 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);
} @Override public int getCount() { return list.size(); }
@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); 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);
List<MyItem> myItemList = new ArrayList<>(); for (int i = 0; i < 20; i++) { myItemList.add(new MyItem(R.drawable.pic, "标题:" + i, "内容:" + i)); } 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);
} @Override public int getCount() { return list.size(); }
@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); } 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)
{
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) {
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的思路:
- 创建内部类ViewHolder
- 判断convertView是否为空,为空则实例化ViewHolder,并设置setTag,将ViewHolder与convertView绑定;否则通过getTag取出ViewHolder。
- 通过ViewHolder对象找到对应控件,给ViewHolder中的控件设置数据。