android BaseAdapter优化
1.getCount()方法:
android提供了N多已经封装好的适配器,但用得最多还是BaseAdapter。如果写一个类继承BaseAdapter,则会看到它至少要覆写四个方法:
public class MAdapter extends BaseAdapter{ @Override public int getCount() { // TODO Auto-generated method stub return 0; } @Override public Object getItem(int position) { // TODO Auto-generated method stub return null; } @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub return null; } }
首先说一下getCount()方法,该方法返回一个int型的值,表示你的组件(ListView,GridView等)的长度,也就是要显示的item的数量。如果你的getCount()返回值是0的话,列表一行都不会显示,如果返回1,就只显示一行。返回几则显示几行。我们一般会向adapter传递我们的数据项list(比如mList),一般getCount()方法会这么写:
@Override public int getCount() { // TODO Auto-generated method stub if (mList != null) return mList.size(); else return 0; }
但有时,比如你使用ListView,布局给它的位置能显示10个childitem,当传入的mList数量小于10,这个listView就只显示当前的子项数目而无法占满空间。有时为了美观,会让ListView至少显示10个(此时要自己处理,把多出来的子项显示为默认样式),所以也会这样写:
private static final int ITEM_COUNT = 10; @Override public int getCount() { // TODO Auto-generated method stub if(mList != null && mList.size() > 10) return mList.size(); else { return ITEM_COUNT; } }
2.getView方法(重点是convertView参数的缓存机制)
那么以ListView为例,来说下系统如何绘制ListView(也就是如何加载,其他需要适配器的组件均类似)。ListView 针对每个item,要求 adapter “返回一个视图” (getView()方法),也就是说ListView在开始绘制的时候,系统首先调用getCount()函数,根据他的返回值得到ListView的长度,然后根据这个长度,调用getView()一行一行的绘制ListView的每一项。于是,我们来看看getView()方法:
@Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub return null; }
这个方法会返回一个View,显示出来就是ListView的一个子项(Child)。该方法有3个参数:
position:从0开始,可以理解为adapter中数据集合的下标。通常情况下,这个position和你传入adapter的mList.get(position)中的position对应使用。
convertView:这个convertView其实就是最关键的部分了。原理上讲,当ListView滑动的过程中 会有item被滑出屏幕 而不再被使用 这时候Android会回收这个条目的view,这个view也就是这里的convertView。
其实到此为止我们可以总结出convertview的机制了,就是在初始显示的时候,每次显示一个item都调用一次getview方法但是每次调用的时候covertview为空(因为还没有旧的view),当显示完了之后。如果屏幕移动了之后,并且导致有些Item(也可以说是view)跑到屏幕外面,此时如果还有新的item需要产生,则这些item显示时调用的getview方法中的convertview参数就不是null,而是那些移出屏幕的view(旧view),我们所要做的就是将需要显示的item填充到这些回收的view(旧view)中去,最后注意convertview为null的不仅仅是初始显示的那些item,还有一些是已经开始移入屏幕但是还没有view被回收的那些item。
上图中,当item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个View。convertView此时不是空值了,它的值是item1。你只需设定新的数据然后返回convertView,不必重新new一个View。
最后一个parent参数暂时没用到过。
不使用convertView的写法,每次都会new一个View,逻辑上没有任何问题,但是数据量很大的话,动不动就会OOM:
public View getView(int position, View convertView, ViewGroup parent) {
View view = new View(); //通过inflate等找到布局 然后findViewById等 设置各个显示的item return view; }
使用convertView,节省了new View的大量开销:
public View getView(int position, View convertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; //复用了回收的view 只需要直接作内容填充的修改就好了 } else { view = new Xxx(...); //没有供复用的view 按一般的做法新建view } return view; }
3.ViewHolder和getTag()、setTag()方法:
先上一份使用ViewHolder自定义adapter的典型写法:
public class MarkerItemAdapter extends BaseAdapter { private Context mContext = null; private List<MarkerItem> mMarkerData = null; public MarkerItemAdapter(Context context, List<MarkerItem> markerItems) { mContext = context; mMarkerData = markerItems; } public void setMarkerData(List<MarkerItem> markerItems) { mMarkerData = markerItems; } @Override public int getCount() { int count = 0; if (null != mMarkerData) { count = mMarkerData.size(); } return count; } @Override public MarkerItem getItem(int position) { MarkerItem item = null; if (null != mMarkerData) { item = mMarkerData.get(position); } return item; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder = null; if (null == convertView) { viewHolder = new ViewHolder(); LayoutInflater mInflater = LayoutInflater.from(mContext); convertView = mInflater.inflate(R.layout.item_marker_item, null); viewHolder.name = (TextView) convertView.findViewById(R.id.name); viewHolder.description = (TextView) convertView .findViewById(R.id.description); viewHolder.createTime = (TextView) convertView .findViewById(R.id.createTime); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } // set item values to the viewHolder: MarkerItem markerItem = getItem(position); if (null != markerItem) { viewHolder.name.setText(markerItem.getName()); viewHolder.description.setText(markerItem.getDescription()); viewHolder.createTime.setText(markerItem.getCreateDate()); } return convertView; } private static class ViewHolder { TextView name; TextView description; TextView createTime; } }
getTag()、setTag()方法很简单:View中的setTag(Object)表示给View添加一个额外的数据,以后可以用getTag()将这个数据取出来。
可以看到ViewHolder就是一个内部类。当convertView为null时,老老实实加载xml布局文件,然后用findViewById把布局文件中的各个组件对应到ViewHolder的各个属性,并把ViewHolder用setTag()方法绑定到convertView。当convertView不为空可以复用时,直接使用getTag()方法取出绑定的ViewHolder,省去了findViewById()的开销。
4.getItemViewType(int position)和getViewTypeCount()方法
但是上面的程序仍然有缺陷——当我们的ListView中填充的item有多种形式时,比如微博中,有的item中包含图片,有的item包含视频,那么必然的,我们需要用到2种item的布局方式。此时如果只是单纯判断convertView是否存在,会造成回收的view不符合你当前需要的布局,而类似转换失败出错退出。这里要提到Adapter中的另外2个方法:
public int getItemViewType(int position)
{}
public int getViewTypeCount() {}
从方法名上
就可以比较明显的明白这2个的作用
下面附上一个demo代码:
class MyAdapter extends BaseAdapter{ Context mContext; LinearLayout linearLayout = null; LayoutInflater inflater; TextView tex; final int VIEW_TYPE = 2; final int TYPE_1 = 0; final int TYPE_2 = 1; public MyAdapter(Context context) { mContext = context; inflater = LayoutInflater.from(mContext); } @Override public int getCount() { return listString.size(); } //每个convert view都会调用此方法,获得当前所需要的view样式 @Override public int getItemViewType(int position) { int p = position%6; if(p == 0) return TYPE_1; else if(p < 3) return TYPE_2; else return TYPE_1; } @Override public int getViewTypeCount() { return 2; } @Override public Object getItem(int arg0) { return listString.get(arg0); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { viewHolder1 holder1 = null; viewHolder2 holder2 = null; int type = getItemViewType(position); //无convertView,需要new出各个控件 if(convertView == null) { //按当前所需的样式,确定new的布局 switch(type) { case TYPE_1: convertView = inflater.inflate(R.layout.listitem1, parent, false); holder1 = new viewHolder1(); holder1.textView = (TextView)convertView.findViewById(R.id.textview1); holder1.checkBox = (CheckBox)convertView.findViewById(R.id.checkbox); convertView.setTag(holder1); break; case TYPE_2: convertView = inflater.inflate(R.layout.listitem2, parent, false); holder2 = new viewHolder2(); holder2.textView = (TextView)convertView.findViewById(R.id.textview2); holder2.imageView = (ImageView)convertView.findViewById(R.id.imageview); convertView.setTag(holder2); break; } } else { //有convertView,按样式,取得不用的布局 switch(type) { case TYPE_1: holder1 = (viewHolder1) convertView.getTag(); break; case TYPE_2: holder2 = (viewHolder2) convertView.getTag(); break; } //设置资源 switch(type) { case TYPE_1: holder1.textView.setText(Integer.toString(position)); holder1.checkBox.setChecked(true); break; case TYPE_2: holder2.textView.setText(Integer.toString(position)); holder2.imageView.setBackgroundResource(R.drawable.icon); break; } } return convertView; } } //各个布局的控件资源 class viewHolder1{ CheckBox checkBox; TextView textView; } class viewHolder2{ ImageView imageView; TextView textView; }
以上基本就是主要的内容了,下面再补充实际操作当中的一些Tips
*如果convertView上用Type区分有些繁琐,或者不需要那么复杂,只是很少有出现不同的情况,那么还可以在取得convertView后,通过java提供的instanceof来判断是否可以强转,如果不能强转,就去新建一个View的做法,但是其实这种做法并不规范,所以还是推荐上面的做法。
*第二个是关于ListView,对于纯色的item背景,其实可以直接设置BackgroundColor,而不要使用图片,这一部分其实可以有不小的提升,同样的,对于任何纯色的背景,应该尽量去设置RGB颜色,而不是全用一张图片做背景返回。
参考博文:1.http://blog.chinaunix.net/uid-11798215-id-3407345.html
2.http://mzh3344258.blog.51cto.com/1823534/889879
3.http://www.cnblogs.com/over140/archive/2011/03/23/1991100.html
4.http://www.cnblogs.com/mengdd/p/3254323.html