作为一个临近毕业的应届生,这是我第一次实习做android的相关开发,看书时候觉得自己学的差不多了,真正到做项目的时候,才发现,其实自己只是懂一些函数的用法,真正的实现细节根本不是很明白,特开始写下博客,一来记录自己的成长,二来回顾自己每周的收获,以加深印象,三来也是为了记录一些自己有用的资料,以便以后自己可以使用。现在说自己的使用心得,为时尚早,只能说收集更多资料,集百家之长。
这周接触最多的就是ListView这个控件,在安卓开发中,这也是个用的特别多,也是特别实用的控件。说一说关于ListView的工作原理。
ListView的工作原理如下:
ListView针对每个Item,要求adapter返回一个视图(getView()),也就是说ListView在开始绘制的时候,系统首先调用getCount()函数,根据他的返回值得到ListView的长度,然后根据这个长度,调用getView(),一行一行的绘制ListView的每一项,如果getCount返回值是0的话,列表一行都不会显示,返回几,则显示几行。当有几千几万甚至更多的Item时,不可能为每个Item创建一个新的View,实际上,Android早已缓存了这些视图,下面这个图是解释ListView工作原理最经典的图。(Android中有个叫做Recycler的构件,与Recycler相关的已经由Google做过N多优化的东西有:AbsListView.RecyclerListener、ViewDebug.RecyclerTraceType等待)
下面简单说下上图的原理:
1.如果你有几千几万甚至更多的选项(Item)时,其中只有可见的项目存在内存(一般说的的ListView优化,就是指在内存中的优化)中,其他的在Recycler中
2.ListView先请求一个type1视图(getView)然后请求其他可见的项目。convertView在getView中是空(null)的
3.当item1滚出屏幕时,并且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图,convertView此时不是空值了,它的值是item1。只需设定新的数据然后返回convertView,不必重新创建一个视图。
下面一个是示例的代码,以便加深理解:
public class MultipleItemsList extends ListActivity {
private MyCustomAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new MyCustomAdapter();
for (int i = 0; i < 50; i++) {
mAdapter.addItem("item " + i);
}
setListAdapter(mAdapter);
}
private class MyCustomAdapter extends BaseAdapter {
private ArrayList mData = new ArrayList();
private LayoutInflater mInflater;
public MyCustomAdapter() {
mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(final String item) {
mData.add(item);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mData.size();
}
@Override
public String getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
System.out.println("getView " + position + " " + convertView);
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item1, null);
holder = new ViewHolder();
holder.textView = (TextView)convertView.findViewById(R.id.text);
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.textView.setText(mData.get(position));
return convertView;
}
}
public static class ViewHolder {
public TextView textView;
}
}
执行程序,查看日志:
图上可见项目为13个,然后查看其对应的Logcat
getView一共被调用了13次,并且convertView对于所有的可见项目为null。
向下滚动直到item14出现,convertView仍为null。因为recycler中没有视图,其中item0的边缘任然可见,在顶端,继续滚动:
item15出现,item0消失在了顶端,此时的convertview也不为空了,item0消失去了recycler中去了,然后item15被创建
所以一般在getView方法中会看见这样一个判断:
if(convertView==null){
.......
}else{
.....
}
以上就是listview的原理。。。下面再来总结一下listview的绘制过程
ListView的绘制过程:
首先,系统在绘制ListView之前,将会先调用适配器中的getCount方法来获取item的个数,之后每绘制一个item就会调用一次getView方法,在此方法内就可以引用实现定义好的xml来确定显示的效果并返回一个View对象作为一个item显示出来,也正是在这个过程中完成了适配器的主要转换功能,把数据和资源以开发者想要的效果显示出来,也正是getView的重复调用,是得ListView的使用更加简单和灵活。这两个方法是自定ListView显示效果中最为重要的,同时只要重写好了这两个方法,ListView就能完全按开发者的要求显示,儿getItem和getItemId方法将会在调用ListView的响应方法的时候被调用到,所以要保证ListView的各个方法有效的话,这两个方法也得重写,比如:没有完成getItemId方法功能的实现的话,当调用ListView的getItemIdPosition方法时,将不会得到想要的结果,因为该方法就是调用了对应的适配器的getItemId方法。
接下来说一下如何创建一个ListView:
列表的显示需要的三个元素:
1.ListView:用来展示列表的View,通常是一个xml指定的。例如ListView中经常使用到的"android.R.layout.simple_list_item",就是Android内部定义好的一个xml文件。
2.适配器:用来把数据映射到ListView上。不同的数据对应不同的适配器,如BaseAdapter,ArrayAdapter,CursorAdapter,SimpleAdapter等,他们能够将数组,指针指向的数据,map等映射到view上。也正是由于适配器的存在,listview的使用相当灵活。
3.数据:具体的将被映射的字符串,图片,或者是基本组件。
listview 和适配器的关系如图:
创建一个listview的基本流程如下:
1.获取到listview控件的id
2.初始化适配器
3.为listview位置适配器(setAdapter方法)
下面说一下关于适配器:
适配器的继承关系图如下:
public abstract class BaseAdapter——抽象类,继承它需要实现较多的方法,所以就具有较高的灵活性,实现了ListAdapter和SpinnerAdapter。
BaseAdapter需要重写的方法:
getCount();
getItem(int position);
getItemId(int position);
getView(int position,View convertView,ViewGroup parent);
系统显示列表时,首先实例化一个适配器(这里将手动实例化自定义的适配器)。当手动完成适配时,必须手动映射数据,这需要重写getView()方法。系统在绘制列表的每一行的时候将调用此方法。getView()有三个参数,position表示将显示的是第几行,convertView是从布局文件中inflate来的布局。一般用LayoutInflate的方法将定义好的item.xml文件提取成view实例用来显示。然后将xml文件中的各个组件实例化(使用findViewById方法)。这样便可以将数据对应到各个组件上。但是按钮为了响应点击事件,需要为它添加点击监听器,这样就能捕获点击事件。
ListView优化:
Adapter的作用就是ListView界面与数据之间的桥梁,当列表里的每一项显示到页面的时候,都会调用Adapter的getView方法返回一个View。例如,在列表有1000000000项时,将会占有极大的系统资源。先看如下代码:
public View getView(int position, View convertView, ViewGroup parent)
{
View item = mInflater.inflate(R.layout.list_item_icon_text, null);
((TextView) item.findViewById(R.id.text)).setText(DATA[position]);
((ImageView) item.findViewById(R.id.icon)).setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return item;
}
如果这么写,后果将不堪设想。原因是有1000000000项列表的时候,每项调用一次getView时,就会产生1000000000个item的view。。。。。再看看下面的代码:
public View getView(int position, View convertView, ViewGroup parent)
{
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.item, null);
}
((TextView) convertView.findViewById(R.id.text)).setText(DATA[position]);
((ImageView) convertView.findViewById(R.id.icon)).setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return convertView;
}
上面的代码就明显会好很多(见前面分析ListView的原理时的图),系统将会减少创建很多的view,性能也就得到了极大的提升。。。。然后,还有更好的优化方法:再看看采用ViewHolder模式进行优化:
class ChatListAdapter extends BaseAdapter
{
static class ViewHolder
{
TextView text;
ImageView icon;
}
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder holder;
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.list_item_icon_text, null);
holder = new ViewHolder();
holder.text = (TextView) convertView.findViewById(R.id.text);
holder.icon = (ImageView) convertView.findViewById(R.id.icon);
convertView.setTag(holder);
}
else
{
holder = (ViewHolder) convertView.getTag();
}
holder.text.setText(DATA[position]);
holder.icon.setImageBitmap((position & 1) == 1 ? mIcon1 : mIcon2);
return convertView;
}
}
ViewHolder的作用:优化显示效率,即之前显示过的不用再从布局中读取,直接从缓存中读取。可以看到它只是一个静态类(1.通常一个普通类不允许声明为静态的,只有内部类才可以。声明为静态的内部类,可以作为一个普通类来使用,而不需要实例一个外部类2.这里设置ViewHolder为static,也就是静态的,静态类只会在第一次加载时会耗费比较长时间,但是后面就可以很好帮助加载,同时保证了内存中只有一个ViewHolder,节省了内存的开销。),它的作用在于减少不必要的调用findViewById,View的findViewById方式是比较耗时的,因此需要考虑值调用一次,然后把对底下的空间引用存在ViewHolder里面,再用convertView.setTag(holder)把它放在view里,下次就可以用(viewholder)convertView.getTag()直接提取了。。。。