代码优化>>>Android ListView适配器三级优化详解

转载本专栏每一篇博客请注明转载出处地址,尊重原创。此博客转载链接地址:点击打开链接  http://blog.csdn.net/qq_32059827/article/details/52718489

对ListView的优化,也就是对其封装:抽取方法共性,封装 BaseAdapter 和 ViewHolder

大多App都会使用到的基本控件 ——- Listiew,特别像新闻浏览类的比如说“今日关注”,或者“应用宝”这种汇集手机软件集合的。而且大家都知道 需要给每个单独的 ListView 搭配相应的适配器 Adapter 。如果你的项目中使用ListView 的频率很少甚至没有,那我不建议你对 ListView 进行抽取封装,但是!如果它的使用渗透到App中大多页面时,你必须考虑 对Adapter的公共方法进行抽取封装到单独的类中,来避免整个项目代码的冗杂,使代码结构规范化

首先,我们在写ListView的时候一般会这么写:

一. 未封装版 ListView

@Override
public View onCreateSuccessView() {
    ListView view = new ListView(UIUtils.getContext());
    initData();
    view.setAdapter(new MyListViewAdapter());
    return view;
}

private void initData() {
    for (int i = 0; i < 50; i++) {
        mList.add("测试数据" + i);
    }
}

class MyListViewAdapterextends BaseAdapter {

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public String getItem(int position) {
        return mList.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            convertView = View.inflate(UIUtils.getContext(),
                    R.layout.list_item_home, null);
            holder = new ViewHolder();
            holder.mListTextView= (TextView) convertView
                    .findViewById(R.id.tv_content);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.mListTextView.setText(getItem(position));

        return convertView;
    }
}

static class ViewHolder {
    public TextView mListTextView;
}

这里我们主要 把重点放在 AdapterViewHolder 的抽取封装,首先简单分析其逻辑组成

1.首先看到我自定义的 HomeAdapter 继承的是 BaseAdapter 。继承的四个方法中,前三个:getCountgetItemgetItemId 看的出来方法及其简单,

但是getView方法中步骤略复杂,首先梳理清楚方法里的逻辑,才好进一步的封装:
(1)加载布局文件,布局转换(xml —> view)
(2)初始化控件(finViewById)
(3)给ViewHolder设置标记(setTag),便于下次复用
(4)给控件设置数据

那么就开始对市面上60%的项目中的封装方法,进行普通封装。

封装一》》》普通封装:方式,保留getView(),getCountgetItemgetItemId 先封装。

方法:抽取类名:MyBaseAdapter。基类是需要一个数据源的,因此通过构造方法得到这个数据源;注意:数据源是什么类型,通过泛型指定。

public class MyBaseAdapter<T> extends BaseAdapter {//在类旁边声明一下泛型
	
	private ArrayList<T> mList;
	
	/**
	 * 我需要一个集合,那么就由孩子传递过来。但是孩子这么多,集合可以有好多类型,那么集合到底写什么类型?泛型更好用,孩子什么类型,我就什么类型
	 */
	public MyBaseAdapter(ArrayList<T> list){
		this.mList = list;
	}

	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return mList.size();
	}

	@Override
	public T getItem(int position) {
		// TODO Auto-generated method stub
		return mList.get(position);
	}

	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// TODO Auto-generated method stub
		return null;
	}

}

因为抽取了一些方法,我们原来的Adapter类继承自MyBaseAdapter的代码就简单了一些,如下:

private class MyListViewAdapter extends MyBaseAdapter<String>{//在这里告知父亲,我继承你我要给你的集合传递什么类型

		//通过构造函数,把孩子的集合传给父亲
		public MyListViewAdapter(ArrayList<String> list) {
			super(list);
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			// 自定义listview界面
			ViewHolder holder = null;
			if(convertView == null){
				holder = new ViewHolder();
				//1、加载item布局
				convertView = UIUtils.inflate(R.layout.homefragment_listview_item);
				//2、获取item布局实例
				holder.mListTextView = (TextView) convertView.findViewById(R.id.tv_listview_item_text);
				//3、viewholder设置到tag
				convertView.setTag(holder);
			}else{
				//若不为空,在缓存对象里取出
				holder = (ViewHolder) convertView.getTag();
			}
			
			//4、设置listview布局上的控件数据
			holder.mListTextView.setText(getItem(position));
			return convertView;
		}
		
	}

同时,原来实例化适配器代码也要稍作修改,传递数据源到基类里面:

//获取适配器对象
MyListViewAdapter listAdapter = new MyListViewAdapter(mListDatas);

现在运行程序,跟以前的效果是一样的。如下:

到目前为止,叫做是普通抽取,也就是60%应用可能是这么做的,对于每个子类的getView()都是自己实现自己的。

但是要想再高大上一些,要想根那些牛逼点的app一样,就要抽取getView(),对于这一点,比较复杂。接下来就一点点的演变整个过程。

在演变之前,要对ListView的加载流程几个问题要清楚,只有理解了ListView的加载流程,才能更好的而理解抽取getView()的思想。

HomeHolder抽取前思考问

  1. 为什么想到去抽取?
  2. convertView的作用 ?
  3. ViewHolder是什么 ?
  4. ViewHolder的作用 ?
  5. ViewHolder里面需要持有什么对象?
  6. 做ViewHolder需要什么条件?
  7. 一个ListView会创建几个convertView以及几个ViewHolder的思考?
  8. getView其实主要是做啥?

HomeHolder抽取前思考答

  1. 为什么想到去抽取?

    1. 总是要创建ViewHolder
    2. 有重复代码
  2. convertView的作用 ?

    1. 减少view对象的创建
  3. ViewHolder是什么 ?
    1. 就是一个普通的类,成员变量有根布局里面的孩子对象.
  4. ViewHolder的作用 ?
    1. 减少孩子对象的创建,减少findViewById的调用
  5. ViewHolder里面需要持有什么对象?
    1. 持有根布局里面的孩子对象
  6. 做ViewHolder需要什么条件?
    1. 一个类持有根布局里面的孩子对象即可
    2. 其实只要能有根布局就可以了,有根布局就有了对应的孩子.孩子无非就是调用根布局的findViewById初始化即可
  7. 一个ListView会创建几个convertView以及几个ViewHolder的思考
    1. 如果一个屏幕正好显示6个itemView那么会创建6+1个convertView和6+1个ViewHolder
  8. getView其实主要是做啥?
    1. 决定根布局
    2. 得到数据
    3. 填充数据

相信您已经理解了LitView的加载流程,就看一下第一次演变:

对ViewHolder抽取,定义为HomeHolder。

getView()有两层:视图层V、数据源模型层M。其实属于典型的MVC。抽取的过程,也按照MVC模式

整体的步骤,都详细的注释在代码中了,一目了然:

public class HomeHolder {
	//根据getView的设置过程来写这里的代码
	
	//getView()过程:
	//0、初始化ViewHolder
	//1、初始化布局
	//2、viewholder设置到根布局的tag。这里根布局给了mHolderView【viewHolder可以绑定tag的条件:该viewHolder要有孩子的布局,或者所有根布局的控件实例】
	//3、初始化孩子对象,即item布局上的控件实例
	//4、给孩子(item的布局控件实例)设置数据
	

	/*******************初始化视图************************/
	public View mHolderView;
	
	//条件:做Holder需要有孩子对象。该viewHolder要有孩子所有根布局的控件实例,该布局只有一个控件,就是展示文本
	TextView mListTextView;

	private String mData;
	
	//0、初始化ViewHolder
	public HomeHolder(){/**可见,构造方法一调用,初始化了viewholder、初始化了布局、布局控件实例化、此viewHolder绑定到了当前布局的tag中**/
		mHolderView = initView();
		//2、viewholder设置到根布局的tag。这里根布局给了mHolderView
		mHolderView.setTag(this);//this指当前的viewHolder
	}
	
	//1、初始化布局
	private View initView() {
		//return UIUtils.inflate(R.layout.homefragment_listview_item);
		View view = UIUtils.inflate(R.layout.homefragment_listview_item);
		
		//3、初始化孩子对象,即item布局上的控件实例
		mListTextView = (TextView) view.findViewById(R.id.tv_listview_item_text);
		
		return view;
	}


        /***********************初始化数据***********************/
	//4、给孩子(item的布局控件实例)设置数据
	public void setDataAndRefreshHolderView(String data) {
		//保存数据
		this.mData = data;
		//刷新显示,设置数据
		refreshHolderView(data);
	}

	private void refreshHolderView(String data) {
		// 刷新数据显示
		mListTextView.setText(data);
	}
}

经过抽取ViewHolder后,再看一看getView()代码怎么写:

                @Override
		public View getView(int position, View convertView, ViewGroup parent) {
			/**********************初始化视图 决定根布局***********************/
			HomeHolder holder = null;
			if(convertView == null){
				holder = new HomeHolder();
			}else{
				holder = (HomeHolder) convertView.getTag();
			}
			
			/**********************数据刷新显示***********************/
			
			holder.setDataAndRefreshHolderView(getItem(position));
			return holder.mHolderView;
		}
		
	}

可见,getView()明显的少了好多代码,一下子变得轻松了许多。运行程序,结果一模一样。

这个时候,明显轻松了很多,但是上边的抽取仅仅是针对HomeHolder的,如果页面很多的话,显然要写很多的Holder类。为了节省Holder的代码,在网上抽取。

》》》》HomeHolder抽取成BaseHolder。该类的抽取,是基于上边HomeHolder做修改的。把觉得基类取不到的具体东西定义为抽象方法。例如:加载具体的布局、设置刷新具体的数据。在基类里面都无法得知,交给子类实现。并且,由于成了基类,所以具体的设置的数据类型也不清楚,所以设置传递数据的时候,使用泛型

public abstract class BaseHolder<T> {
	//根据getView的设置过程来写这里的代码
	
		//getView()过程:
		//0、初始化ViewHolder
		//1、初始化布局
		//2、viewholder设置到根布局的tag。这里根布局给了mHolderView【viewHolder可以绑定tag的条件:该viewHolder要有孩子的布局,或者所有根布局的控件实例】
		//3、初始化孩子对象,即item布局上的控件实例
		//4、给孩子(item的布局控件实例)设置数据
		
		
		/*******************初始化视图************************/
		public View mHolderView;
		
		//条件:做Holder需要有孩子对象。该viewHolder要有孩子所有根布局的控件实例,该布局只有一个控件,就是展示文本
		//TextView mListTextView;基类不知道孩子对象

		private T mData;
		
		//0、初始化ViewHolder
		public BaseHolder(){/**可见,构造方法一调用,初始化了viewholder、初始化了布局、布局控件实例化、此viewHolder绑定到了当前布局的tag中**/
			mHolderView = initHolderView();
			//2、viewholder设置到根布局的tag。因为能获取item的根布局也可以设置tag
			mHolderView.setTag(this);//this指当前的viewHolder
		}
		
		/**
		 * 初始化holderView/根视图
		 * @call BaseHolder初始化的时候调用
		 * @return
		 */
		//1、初始化布局
		public abstract View initHolderView();

		
		
		/***********************初始化数据***********************/
		/**
		 * 设置数据和刷新视图
		 * @call 需要设置数据和刷新数据的时候调用
		 * @param data
		 */
		//4、给孩子(item的布局控件实例)设置数据
		public void setDataAndRefreshHolderView(T data) {//只有HomeHolder子类是String类型,其他的孩子不一定也是String,有可能还是bean等。因此使用泛型
			//保存数据
			this.mData = data;
			//刷新显示,设置数据
			refreshHolderView(data);
		}

		/**
		 * 刷新Holder视图
		 * @call setDataAndRefreshHolderView(T data) 调用的时候就被调用了吧
		 * 具体的布局我不知道,布局实例更不可能知道。定义为抽象
		 * @param data
		 */
		public abstract void refreshHolderView(T data);
	}
此时,HomeHolder继承自BaseHolder。看一下HomeHolder的代码简单了多少吧!

public class HomeHolder extends BaseHolder<String> {

	private TextView mListTextView;

	@Override
	public View initHolderView() {
		View view = UIUtils.inflate(R.layout.homefragment_listview_item);
		mListTextView = (TextView) view
				.findViewById(R.id.tv_listview_item_text);
		return view;
	}

	@Override
	public void refreshHolderView(String data) {
		// 刷新数据显示
		mListTextView.setText(data);
	}
}
只需要实现两个方法,就搞定了。运行程序,还是跟原来一样的效果。

那么最后,再回到getView()所在的MyListViewAdapter适配器类。当然记得,它也是有父类的MyBaseAdapter。

父类里面还有一个getView()没去写代码,最后就去搞一搞父亲的这个方法,让两个基类MyBaseAdapter和BaseHolder打招呼整合一下。把子类的getView()放到基类里面去

在基类里面做如下修改(与BaseHolder建立了连接)

public abstract class MyBaseAdapter<T> extends BaseAdapter {
	
	private ArrayList<T> mList;
	
	public MyBaseAdapter(ArrayList<T> list){
		this.mList = list;
	}

	@Override
	public int getCount() {
		// TODO Auto-generated method stub
		return mList.size();
	}

	@Override
	public T getItem(int position) {
		// TODO Auto-generated method stub
		return mList.get(position);
	}

	@Override
	public long getItemId(int position) {
		// TODO Auto-generated method stub
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		/**********************初始化视图 决定根布局***********************/
		//基类根基类交谈,不能使用具体的实现类
		BaseHolder<T> holder = null;
		if(convertView == null){
			//该holder应该是具体哪个Holder不清楚。如:HomeHolder、AppHolder等。因此定义抽象方法获取
			holder = getSpecialHolder();
		}else{
			holder = (BaseHolder) convertView.getTag();//注意:从缓存里面取tag
		}
		
		/**********************数据刷新显示***********************/
		
		holder.setDataAndRefreshHolderView(getItem(position));
		return holder.mHolderView;
	}

	/**
	 * 返回具体的BaseHolder的子类
	 * @call getView()方法中,如果没有converView的时候被创建
	 * @return
	 */
	public abstract BaseHolder<T> getSpecialHolder();

}

Adapter的基类抽象方法,且在获取Holder对象时,调用子类具体哪个Holder,只需要在子类中实现这个方法,并且返回具体的哪个Holder就好了。代码如下:

private class MyListViewAdapter extends MyBaseAdapter<String>{//在这里告知父亲,我继承你我要给你的集合传递什么类型

		//通过构造函数,把孩子的集合传给父亲
		public MyListViewAdapter(ArrayList<String> list) {
			super(list);
		}

		@Override
		public BaseHolder<String> getSpecialHolder() {
			// TODO Auto-generated method stub
			return new HomeHolder();
		}
	}
这个时候,您发现getView方法的代码以及适配器里面的代码少了太多太多了。而且运行结果还是一样的。

可能仅仅一个Adapter看不出有多强大,如果说有100个类需要适配器的话,那么只需要一个基类,其他的只要继承自基类,每个适配器类里面的方法就上边那几行,很显然,简化了大量的冗余的代码。

最后再总结一下此时的调用加载流程。

当ListView想要关联适配器的时候,创建自己的adapter适配器类对象,同时把集合数据源数据传递过去。

例如此例中的MyListViewAdapter listAdapter = new MyListViewAdapter(mListDatas);这样把mListDatas传给了adapter的基类,是通过带参构造函数的形式传给adapter父类的,他把以前孩子自己要写的getCountgetItemgetItemId方法帮孩子完成,孩子简写三大方法。同时,在ListVIew的item加载布局和数据的时候,getView方法被调用,此时的getView()方法在adapter基类里面呢,对于item的布局的显示和布局实例的数据刷新,都封装在了对应的Holder类里面;先执行holder = getSpecialHolder();方法来看该adapter配套的Holder是谁。马上调用该adapter的实现方法

        @Override
        public BaseHolder<String> getSpecialHolder() {
            // TODO Auto-generated method stub
            return new HomeHolder();
        }

看到返回的是HomeHolder,在new HomeHolder();的同时BaseHolder的构造也会被加载(子类初始化先初识化父类构造),此时父类的构造函数

           public BaseHolder(){/**可见,构造方法一调用,初始化了viewholder、初始化了布局、布局控件实例化、此viewHolder绑定到了当前布局的tag中**/
            mHolderView = initHolderView();
            //2、viewholder设置到根布局的tag。因为能获取item的根布局也可以设置tag
            mHolderView.setTag(this);//this指当前的viewHolder
        }

加载了布局、设置了tag。注意此时public abstract View initHolderView();是调用对应子类实现类HomeHolder的具体方法(调用父类抽象实际调用子类实现方法)。

此时HomeHolder中的
@Override
    public View initHolderView() {
        View view = UIUtils.inflate(R.layout.homefragment_listview_item);
        mListTextView = (TextView) view
                .findViewById(R.id.tv_listview_item_text);
        return view;
    }

被调用,终于看到在哪里初始化布局了!

这一系列方法走完之后,再回到adapter基类的getView()方法,继续往下执行到holder.setDataAndRefreshHolderView(getItem(position));。同上,先调用holder的
       //4、给孩子(item的布局控件实例)设置数据
        public void setDataAndRefreshHolderView(T data) {//只有HomeHolder子类是String类型,其他的孩子不一定也是String,有可能还是bean等。因此使用泛型
            //保存数据
            this.mData = data;
            //刷新显示,设置数据
            refreshHolderView(data);
        }

 refreshHolderView(data);抽象,调用实现类HomeHolder的实现类方法,完成了数据的设置和刷新。

到此,此次ListView的优化,以及详细的解析终于结束了。

9点半开始写博客,现在0:36,搞了3个多小时。由衷佩服自己,看完您记得关注本博客,或者点赞留下包括意见哦。


原文地址:https://www.cnblogs.com/wanghang/p/6299577.html