代码优化>>>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; }
这里我们主要 把重点放在 Adapter 和 ViewHolder 的抽取封装,首先简单分析其逻辑组成:
1.首先看到我自定义的 HomeAdapter 继承的是 BaseAdapter 。继承的四个方法中,前三个:getCount 、getItem 、getItemId 看的出来方法及其简单,
但是getView方法中步骤略复杂,首先梳理清楚方法里的逻辑,才好进一步的封装:
(1)加载布局文件,布局转换(xml —> view)
(2)初始化控件(finViewById)
(3)给ViewHolder设置标记(setTag),便于下次复用
(4)给控件设置数据
封装一》》》普通封装:方式,保留getView(),getCount 、getItem 、getItemId 先封装。
方法:抽取类名: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抽取前思考问
- 为什么想到去抽取?
- convertView的作用 ?
- ViewHolder是什么 ?
- ViewHolder的作用 ?
- ViewHolder里面需要持有什么对象?
- 做ViewHolder需要什么条件?
- 一个ListView会创建几个convertView以及几个ViewHolder的思考?
- getView其实主要是做啥?
HomeHolder抽取前思考答
-
为什么想到去抽取?
- 总是要创建ViewHolder
- 有重复代码
-
convertView的作用 ?
- 减少view对象的创建
-
ViewHolder是什么 ?
- 就是一个普通的类,成员变量有根布局里面的孩子对象.
-
ViewHolder的作用 ?
-
减少孩子对象的创建,减少findViewById的调用
-
减少孩子对象的创建,减少findViewById的调用
-
ViewHolder里面需要持有什么对象?
- 持有根布局里面的孩子对象
-
做ViewHolder需要什么条件?
- 一个类持有根布局里面的孩子对象即可
- 其实只要能有根布局就可以了,有根布局就有了对应的孩子.孩子无非就是调用根布局的findViewById初始化即可
-
一个ListView会创建几个convertView以及几个ViewHolder的思考
- 如果一个屏幕正好显示6个itemView那么会创建6+1个convertView和6+1个ViewHolder
-
getView其实主要是做啥?
- 决定根布局
- 得到数据
- 填充数据
相信您已经理解了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父类的,他把以前孩子自己要写的getCount 、getItem 、getItemId方法帮孩子完成,孩子简写三大方法。同时,在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个多小时。由衷佩服自己,看完您记得关注本博客,或者点赞留下包括意见哦。