ListView优化及万能适配器

一、ListView优化

ListView、Adapter以及Data source之间的关系

ListView的显示缓存机制原理图:需要才显示, 显示完就回收,节省系统资源

如下图所示,一屏幕显示了5个Item,通过Logcat也能看出来只有5个Log,分别打印的是convertView.toString()。

当滑动时(这里演示的是第三种getView的写法),滑动到第6个第7个时,Logcat的显示


全部Item加载完毕,也就是convertView全部不为空,可以看到,item7就是复用的item1的convertView。为了更明显的观察,将Item的个数增加到11个。

从编号为6开始,就是在复用0-5的convertView,这个个数是屏幕一屏能显示5个,而且会再预先加载1个,因此前6个是不重复的convertView,像下图也是类似


下面介绍三种写法由无优化到谷歌推荐用法。

第一种getView方法,效率最低下,完全没有用到ListView的缓存机制,每次都是新建一个View去绑定itemLayout,每次都进行一个findViewByID,都去遍历视图树寻找id,这个步骤是很消耗系统资源的,因此强烈不推荐。

public View getView(int position, View convertView, ViewGroup parent) {
		
		View view = mInflater.inflate(R.layout.item_layout, null);
		
		TextView a, b, c, d;
		a = (TextView) view.findViewById(R.id.title);
		b = (TextView) view.findViewById(R.id.desc);
		c = (TextView) view.findViewById(R.id.time);
		d = (TextView) view.findViewById(R.id.phone);
		
		a.setText(mDatas.get(position).getTitle());
		b.setText(mDatas.get(position).getDesc());
		c.setText(mDatas.get(position).getTime());
		d.setText(mDatas.get(position).getPhone());
		
		return view;
}
第二种,则用到了getView中的convertView参数,当判断到convertView时null时,则会去创建,当判断非空时,表示已经缓存过了,那么直接从缓存中取出这个convertView即可,避免重复创建。

public View getView(int position, View convertView, ViewGroup parent) {
		
		if (convertView == null) {
			convertView = mInflater.inflate(R.layout.item_layout, null);
		}
		TextView a, b, c, d;
		a = (TextView) convertView.findViewById(R.id.title);
		b = (TextView) convertView.findViewById(R.id.desc);
		c = (TextView) convertView.findViewById(R.id.time);
		d = (TextView) convertView.findViewById(R.id.phone);
		
		a.setText(mDatas.get(position).getTitle());
		b.setText(mDatas.get(position).getDesc());
		c.setText(mDatas.get(position).getTime());
		d.setText(mDatas.get(position).getPhone());
		
		return convertView;
}
第三种,也是Google推荐也是效率最高的实现方式,此方式的优化是在findViewByID上,通过ViewHolder来避免重复的findViewByID。首先创建一个ViewHolder内部类,内部定义的是ItemLayout中所包含的各个控件。这样只会在第一次初始化时关联控件,此后,都是从ViewHolder中取出这个关联关系。

以下是我自己的语言叙述,可能不太准确。

其实就是将view和layout的通道给用ViewHolder给保存下来了,
如果convertView没有被创建过则先创建,并把保存路径的ViewHolder给设置成convertView的Tag,

当发现convertView已经存在,则只需要取出Tag中保存的ViewHolder就能找到路径,

有了这个所谓的路径,就可以将内容轻松设置到控件上。

效率高,用到了ListView提供的缓存机制,避免重复创建,避免重复寻找控件。

private class ViewHolder{
		TextView mTitle, mDesc, mTime, mPhone;
	}
public View getView(int position, View convertView, ViewGroup parent) {
		
		ViewHolder viewHolder = null;
		if (convertView == null) {
			viewHolder = new ViewHolder();
			convertView = mInflater.inflate(R.layout.item_layout, null);
			viewHolder.mTitle = (TextView) convertView.findViewById(R.id.title);
			viewHolder.mDesc = (TextView) convertView.findViewById(R.id.desc);
			viewHolder.mTime = (TextView) convertView.findViewById(R.id.time);
			viewHolder.mPhone = (TextView) convertView.findViewById(R.id.phone);
			convertView.setTag(viewHolder);
		}else{
			viewHolder = (ViewHolder) convertView.getTag();
		}
		
		Bean bean = mDatas.get(position);
		viewHolder.mTitle.setText(bean.getTitle());
		viewHolder.mDesc.setText(bean.getDesc());
		viewHolder.mTime.setText(bean.getTime());
		viewHolder.mPhone.setText(bean.getPhone());
		return convertView;
		
	}
下面是itemLayout布局
<?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="match_parent"
    android:padding="10dp" >

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:singleLine="true"
        android:text="Android新技能"
        android:textColor="#444"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/title"
        android:layout_marginTop="10dp"
        android:maxLines="2"
        android:minLines="1"
        android:text="Android打造万能ListView和GridView适配器"
        android:textColor="#898989"
        android:textSize="16sp" />

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/desc"
        android:layout_marginTop="10dp"
        android:text="2016-7-11"
        android:textColor="#898989"
        android:textSize="12sp" />

    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_below="@id/desc"
        android:layout_marginTop="10dp"
        android:background="#2ED667"
        android:drawableLeft="@drawable/stat_sys_phone_call"
        android:drawablePadding="5dp"
        android:padding="3dp"
        android:text="188****1028"
        android:textColor="#fff"
        android:textSize="12sp" />

</RelativeLayout>
接下来是完整的自定义Adapter
package com.example.commonadapter;

import java.util.List;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter {

	private LayoutInflater mInflater;
	private List<Bean> mDatas;
	
	public MyAdapter(Context context, List<Bean> data){
		mInflater = LayoutInflater.from(context);
		this.mDatas = data;
	}
	
	@Override
	public int getCount() {
		return mDatas.size();
	}

	@Override
	public Object getItem(int position) {
		return mDatas.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		
		ViewHolder viewHolder = null;
		if (convertView == null) {
			//其实就是将view和layout的通道给用ViewHolder给保存下来了
			//如果convertView没有被创建过则先创建,并把保存路径的ViewHolder给设置成convertView的Tag
			//当发现convertView已经存在,则只需要取出Tag中保存的ViewHolder就能找到路径
			viewHolder = new ViewHolder();
			convertView = mInflater.inflate(R.layout.item_layout, null);
			viewHolder.mTitle = (TextView) convertView.findViewById(R.id.title);
			viewHolder.mDesc = (TextView) convertView.findViewById(R.id.desc);
			viewHolder.mTime = (TextView) convertView.findViewById(R.id.time);
			viewHolder.mPhone = (TextView) convertView.findViewById(R.id.phone);
			convertView.setTag(viewHolder);
		}else{
			viewHolder = (ViewHolder) convertView.getTag();
		}
		
		Bean bean = mDatas.get(position);
		viewHolder.mTitle.setText(bean.getTitle());
		viewHolder.mDesc.setText(bean.getDesc());
		viewHolder.mTime.setText(bean.getTime());
		viewHolder.mPhone.setText(bean.getPhone());
		return convertView;
		
	}
	
	private class ViewHolder{
		TextView mTitle, mDesc, mTime, mPhone;
	}

}
最后,可以看出只有赋值操作是必须重复写的,其他的代码则可以进行抽取,这也是万能适配器的原理。

二、万能适配器

  • ViewHolder的优化抽取

跟普通的优化没有两样,只是将ViewHolder类抽取出来,并在此类中完成任意控件的获取,以及convertView的复用。而不是简单的定义Item控件变量,以及存储Item控件的引用。

因为这是个通用类,所以不能确定每个ItemLayout中包含哪种控件,所以不使用以前的方法在ViewHolder中定义控件类型为成员变量。取而代之的是Map,key值是控件ID,value值是确定的view,针对这个特性,最终决定使用SparseArray,也是一个特殊的Map,不过key值指定了为int,效率要比Map高。value值为View,但是事先并不知道View是哪种类型,所以才会使用以泛型为返回值的getView方法。

public class MyViewHolder {
    
    private SparseArray<View> mViews;
    private Context mContext;
    private int mPosition;
    private View mConvertView;
    
    private MyViewHolder(Context context, int layoutId, ViewGroup parent, int position){
        this.mPosition = position;
        this.mViews = new SparseArray<View>();
        mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);;
        
        mConvertView.setTag(this);
    }
    
    public static MyViewHolder get(Context context, View convertView, int layoutId, ViewGroup parent, int position){
        
        if (convertView == null) {
            return new MyViewHolder(context, layoutId, parent, position);
        }else{
            MyViewHolder holder = (MyViewHolder) convertView.getTag();
            holder.mPosition = position;
            return holder;
        }
    }
    
    /**
     * 通过viewId获取控件
     * @param viewId
     * @return
     */
    public <T extends View> T getView(int viewId){
        View view = mViews.get(viewId);
        
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }        
        return (T) view;
    }
    
    public View getConvertView(){
        return mConvertView;
    }
}
可以看到这个类将构造方法私有化,并提供静态方法像以前的一样来初始化和复用convertView,当为空时就new一个ViewHolder并同样用LayoutInflater来绑定ItemLayout,然后setTag。当不为空时,同样的步骤通过getTag方法取出viewHolder。最后position虽然没有用到,但还是应该让其保存正确的数据。
再看getView方法,利用了泛型,通过传入viewId来将ViewHolder和具体控件之间建立引用关系。以前为了避免重复的findViewByID是将holder的参数赋值为这种引用,此时却不行,上面的一段话也说过,因为不明确ItemLayout中到底包含了什么控件,所以用容器来代替以前的方法。getView时先在容器中查找,查找不到则findViewByID后添加到容器中,当再需要getView的时候不需要遍历视图树。


在adapter中也摆脱了繁杂的对每一个控件的findViewByID,只留下对具体控件设置的具体内容,因为这必须是自定义的。

@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		
		MyViewHolder holder = MyViewHolder.get(mContext, convertView, R.layout.item_layout, parent, position);
		Bean bean = mDatas.get(position);
		
		((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
		((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
		((TextView)holder.getView(R.id.time)).setText(bean.getTime());
		((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
		
		return holder.getConvertView();
	}

  • CommonAdapter的优化抽取

分析发现每一个Adapter都要实现的方法getCount,getItem,getItemId以及getView方法,因此想自建一个CommonAdapter,在这里实现大同小异的方法。而用户只需要实现getView方法就行。当然因为要继承此类,所以又要用泛型来代替具体的List元素。

public abstract class CommonAdapter<T> extends BaseAdapter {

	protected Context mContext;
	protected List<T> mDatas;
	protected LayoutInflater mInflater;
	
	public CommonAdapter(Context context, List<T> datas){
		this.mContext = context;
		this.mDatas = datas;
		mInflater = LayoutInflater.from(context);
	}
	
	@Override
	public int getCount() {
		return mDatas.size();
	}

	@Override
	public T getItem(int position) {
		return mDatas.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

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

}
public class MyAdapterCommon extends CommonAdapter<Bean> {

	public MyAdapterCommon(Context context, List<Bean> datas){
		super(context, datas);
	}

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

		MyViewHolder holder = MyViewHolder.get(mContext, convertView, R.layout.item_layout, parent, position);
		Bean bean = mDatas.get(position);
		
		((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
		((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
		((TextView)holder.getView(R.id.time)).setText(bean.getTime());
		((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
		return holder.getConvertView();
	}
}
可以发现实现代码已经少了许多。
接下来还可以继续提取优化。在CommonAdapter构造方法中加入ItemLayout的id,方便更好的适配,而不是仅支持此一个的item。

public CommonAdapter(Context context, List<T> datas, int layoutId){
		this.mContext = context;
		this.mDatas = datas;
		this.mInflater = LayoutInflater.from(context);
		this.mLayoutId = layoutId;
	}
同时取消getView方法的抽象,就在CommonAdapter中实现,具体的实现跟在子类中的实现是相同的。
@Override
	public View getView(int position, View convertView, ViewGroup parent){
		MyViewHolder holder = MyViewHolder.get(mContext, convertView, mLayoutId, parent, position);
		convert(holder, getItem(position));
		return holder.getConvertView();
	}
再分析,还是只有TextView的设置文本是自己动手写的,于是抽象一个方法,用来设置控件的具体内容。
public abstract void convert(MyViewHolder holder, T t);

@Override
	public void convert(MyViewHolder holder, Bean bean) {
		((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
		((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
		((TextView)holder.getView(R.id.time)).setText(bean.getTime());
		((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
	}
越来越精简的MyAdapter。还可以更加的简化。在ViewHolder中定义各个控件的主要内容设置的方法。比如说TextView的setText。

public MyViewHolder setText(int viewId, String text){
		TextView tv = getView(viewId);
		tv.setText(text);
		return this;
	}
为了使用链式编程,所以返回值类型就是MyViewHolder。设置文本的语句也变得更加简便,只有一句,所以没有必要再新建一个类继承CommonAdapter了,只需要在MainActivity中写一个匿名内部类就解决了。

holder.setText(R.id.title, bean.getTitle())
	.setText(R.id.desc, bean.getDesc())
	.setText(R.id.time, bean.getTime())
	.setText(R.id.phone, bean.getPhone());

mListView.setAdapter(new CommonAdapter<Bean>(MainActivity.this, mDatas, R.layout.item_layout) {
			
			@Override
			public void convert(MyViewHolder holder, Bean bean) {
				holder.setText(R.id.title, bean.getTitle())
				.setText(R.id.desc, bean.getDesc())
				.setText(R.id.time, bean.getTime())
				.setText(R.id.phone, bean.getPhone());
			}
		});
虽然说匿名内部类更简单,但是如果有些控件含有各种事件,点击事件等,还是新建个类继承CommonAdapter,在新的类中去实现。

当然如果ItemLayout中包含ImageView,则需要在ViewHolder中添加如下方法

public MyViewHolder setImageResource(int viewId, int resId){
		ImageView img = getView(viewId);
		img.setImageResource(resId);
		
		return this;
	}
当然,可以添加很多很多的辅助方法,来简化用户使用ViewHolder和设置控件内容,当每一次使用发现没有这个辅助方法时,就添加进去,久而久之,这个ViewHolder类就会越来越完善。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值