android封装通用Adapter、ViewHolder实现ListView多条目混排

先看下效果图


处理多条目android给我们的方法是设置setViewTypeCount传入类型个数,RecycleBin会创建对应数量的mScrapViews集合数组,每种类型的View在对应的集合中管理。当要告诉ListView我要显示什么样的UI布局时就得调用getItemViewType,给每个position指定要使用的ViewType类型。
但是要注意如果返回错误就会有问题,例如你不能返回超过setViewTypeCount的值,否则会数组脚本越界。ListView根据getItemViewType就能找到缓存该ViewTypewhichScrap然后来渲染UI这就是android给我们提供的ListView多条目混排的实现方案,如果我们不进行封装,扩展的话,会导致一个Adapter中有N多个ViewType,getView的时候会根据positon返回N多种,然后再在getItemViewType方法中根据position返回不同的ViewType,还有定义不同的ViewHolder来管理。这样就会导致我们的一个Adapter类爆棚,满屏都是if else想想就可怕。
下面就是用传统的方法实现三种Item的adapter,我已经精简了很多。每次添加新条目,就要改动这个Adapter,扩展性很差,当代码多了,很容易出bug,也不好查找。不符合开放封闭原则

    public class ChatAdapter extends BaseAdapter {  
  
          //3种不同的布局  
        public static final int VALUE_TIME_TIP = 0;
        public static final int VALUE_LEFT_TEXT = 1;  
        public static final int VALUE_LEFT_IMAGE = 2;   

        public ChatAdapter(Context context, List<Message> myList) {  
            this.myList = myList;  
      
            mInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
        }  
      
        @Override  
        public int getCount() {  
            return myList.size();  
        }  
      
        @Override  
        public Object getItem(int arg0) {  
            return myList.get(arg0);  
        }  
      
        @Override  
        public long getItemId(int arg0) {  
            return arg0;  
        }  
      
        @Override  
        public View getView(int position, View convertView, ViewGroup arg2) {  
      
            if (convertView == null) {  
                switch (type) {  
                case VALUE_TIME_TIP:  
                    ..
                case VALUE_LEFT_TEXT:  
                    ...
                case VALUE_LEFT_IMAGE:  
                    ...             
            } else {  
                Log.d("baseAdapter", "Adapter_:"+(convertView == null) );  
                switch (type) {  
                case VALUE_TIME_TIP:  
                    ... 
                case VALUE_LEFT_TEXT:  
                    ...  
                case VALUE_LEFT_IMAGE:  
                    ...  
                }  
            }  
            return convertView;  
        }  
      
         //根据数据源的position返回需要显示的的layout的type 
        @Override  
        public int getItemViewType(int position) {  
      
            Message msg = myList.get(position);  
            int type = msg.getType();  
            return type;  
        }  
      
         //返回所有的layout的数量 
        @Override  
        public int getViewTypeCount() {  
            return 3;  
        }  
      
        class ViewHolderTime {  
           ...
        }  
      
        class ViewHolderRightText {  
           ...
        }  
      
        class ViewHolderRightImg {  
           ...
        }  
    }  

首先分析下ViewHolder的作用,通过convertView.setTag与convertView进行绑定,然后当convertView复用时,直接从与之对应的ViewHolder(getTag)中拿到convertView布局中的控件,省去了findViewById的时间。这里我们先打造一个通用的Adapter并抽出一个公用的ViewHolder

    public abstract class CommonListAdapter<T> extends BaseAdapter {
        protected Context mContext;
        protected List<T> mDatas;
        public CommonListAdapter(Context context, List<T> datas) {
            mDatas = datas;
            mContext = context;
        }
    
        /**
         * @return 这里应理解为UI的条目数量
         */
        @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 View getView(int position, View convertView, ViewGroup parent) {
            return getItemListView(position, convertView, parent);
        }
    
        private View getItemListView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = ViewHolder.get(mContext, convertView, parent, getItemLayoutId(position));
            onBindHolder(holder, position, parent);
            return holder.getItemView();
        }
        /**
         * 返回创建条目view所需的res
         *
         * @param position
         * @return
         */
        protected abstract int getItemLayoutId(int position);
        /**
         * 这里可以实现数据的加载,事件的添加,界面的显隐等操作
         * @param holder
         * @param position 对应的数据的位置
         * @param parent
         */
        public abstract void onBindHolder(ViewHolder holder, int position, ViewGroup parent);
    }

这样我们就打造了一个通用的adapter,并把ViewHolder与Adapter分离,我们再看看这个通用的ViewHolder。既然是通用的,那我们就不能像以前一样将成员变量写进去了,因为每个Item的布局是不同的,那我们怎么办呢?既然没有,那我们就提供一个方法,帮调用者查找,同时维护一个集合,键为控件的Id,值为控件引用,集合里找不到我们再去findViewByid,这里我们用SparseArray,他比Map集合更高效,内部是二分查找法。但是key只能是Integer,我们给每个mItemView设置一个Tag,再绑定一个ViewHolder对象(this)。

    public class ViewHolder {
        private SparseArray<View> mViews;
        private View mItemView;
    
        private ViewHolder(Context context, ViewGroup parent, int layoutId) {
            LayoutInflater inflater = LayoutInflater.from(context);
            mItemView = inflater.inflate(layoutId, parent, false);
            mViews = new SparseArray<View>();
    
            mItemView.setTag(R.id.item_holder, this);
        }
    
        public View getItemView() {
            return mItemView;
        }
    
        public static ViewHolder get(Context context, View itemView, ViewGroup parent, int layoutId) {
            if (itemView == null)
                return new ViewHolder(context, parent, layoutId);
            else
                return (ViewHolder) itemView.getTag(R.id.item_holder);
        }
    
        public <T extends View> T getView(int id) {
            View view = mViews.get(id);
            if (view == null) {
                view = mItemView.findViewById(id);
                mViews.put(id, view);
            }
            return (T) view;
        }
    
        public ViewHolder setText(int id, CharSequence sequence) {
            TextView view = getView(id);
            if (view != null)
                view.setText(sequence);
            return this;
        }
    
        public ViewHolder show(int id) {
            View view = getView(id);
            if (view != null)
                ViewUtils.setGone(view, false);
            return this;
        }
    
        public ViewHolder hide(int id) {
            View view = getView(id);
            if (view != null)
                ViewUtils.setGone(view, true);
            return this;
        }
    
    }

这样我们就打造了一个通用的ViewHolder,它负责管理每个ItemView控件,同时也可以看成一个工具类,以后可以随意添加。

有了通用的Adapter与通用的ViewHolder,那我们以后写代码就方便多了,接下来我们就开始实现多条目功能

其实我们仔细想想,不同的条目也就是我们的数据不同,需要的UI不同而已,每个数据对应一个UI,但是我们怎才能让ListView知道加载什么UI?它只认识getViewTypeCount与getItemViewType。那我们只有将不同UI的layoutId转换成ListView认识的getItemViewType,这就需要一个转换ViewTypeGenerator

    class ViewTypeGenerator implements IViewTypeInfo{
        private Map<Integer, Integer> mAdapterTypeMap;
    
        private int mNextType = 1;
    
        private boolean mNeedReGenerate;
    
        public ViewTypeGenerator() {
            mAdapterTypeMap = new HashMap<Integer, Integer>();
            mNeedReGenerate = true;
        }
    
        @Override
        public void setNeedReGenerate(boolean needReGenerate) {
            mNeedReGenerate = needReGenerate;
        }
    
        public int reGenViewType(int key) {
            if (!mNeedReGenerate) {
                return key;
            }
    
            Integer resultType = mAdapterTypeMap.get(key);
            if (resultType == null || resultType <= 0) {
                resultType = mNextType++;
                mAdapterTypeMap.put(key, resultType);
            }
    
            return resultType;
        }
    }

ViewTypeGenerator维护一个集合,它可以将我们的layoutId转换成连续的int值,满足getItemViewType方法
我们来看看使用方法:

    public class MixListAdapter extends CommonListAdapter<BaseBean> {
        private ViewTypeGenerator mTypeGenerator;
        private IViewHandler viewHandler;
        private BaseBean itemData;
    
        public MixListAdapter(Context context, List datas) {
            super(context, datas);
            mTypeGenerator = new ViewTypeGenerator();
        }
    
    
        /**
         * 那么此处的adapter只要保证返回一个独有的int值即可,不需要限制在[0, getViewTypeCount)之间
         * @return
         */
        @Override
        public int getItemViewType(int position) {
            IViewHandler viewHandler = ViewHandlerFactory.getViewHandler(getItem(position).getViewHandlerType());
            int viewHandlerItemViewType = viewHandler.getItemViewType();
            int itemViewType = mTypeGenerator.reGenViewType(viewHandlerItemViewType);
            if (itemViewType > getViewTypeCount())
                throw new RuntimeException("条目类型数量超过了预设值:" + getViewTypeCount() + ",请扩大预设值");
            return itemViewType ;
        }
    
        public int getViewTypeCount() {
            return 20;//根据需要,预设一个值,不要小于你的Item类型总数
        }
    
        @Override
        protected int getItemLayoutId(int position) {
            //注释1.获取数据
            itemData = getItem(position);
               //注释2.获取ViewHandler
            viewHandler = ViewHandlerFactory.getViewHandler(itemData.getViewHandlerType());
            return viewHandler.getItemViewType();
        }
    
        @Override
        public void onBindHolder(ViewHolder holder, int position, ViewGroup parent) {
            viewHandler.handle(parent, holder, itemData, position);
        }
    }

注释2处大家可能有点不明白,这也是最重要,最想说明的地方,我是不想把View绑定数据(handle方法),返回布局layoutId(getItemViewLayoutId方法)、设置ItemViewType(getItemViewType方法)都放在子类Adapter中所以抽出一个IViewHandler接口,统一处理.item数据绑定一个viewHandlerType属性,是IViewHandler子类的Class名,我们就可以在IViewHandler子类中返回LayoutId给View绑定数据,以后新加item类型,也不用改动adapter,只要
新建一个IViewHandler的子类再给数据绑定该类的Class文件名就可以了。

    /**
     * 处理ListView的不同类型条目的显示
     */
    public interface IViewHandler<T> {
        /**
         * 回调函数,用于处理ListView的条目显示
         */
        void handle(ViewGroup parent, ViewHolder holder, T data, int position);
    
        /**
         * 返回Item对应的布局资源
         */
        int getItemViewLayoutId();
    
        /**
         * 设置ItemViewType,用于ListView的标记回收
         */
        int getItemViewType();
    }

看看反射类

    /**
     * 统一生成ViewHandler的工厂类
     */
    public class ViewHandlerFactory {
        private static final String TAG = "[ViewHandlerFactory:wangsai]";
    
        /**
         * 缓存已存在的handler
         */
        private static Map<String, IViewHandler> mHandlerMap = new HashMap<String, IViewHandler>();
    
        /**
         * 通过类名动态加载创建、加载对应的类对象
         */
        public static IViewHandler getViewHandler(String viewHandlerClazz) {
            IViewHandler result = mHandlerMap.get(viewHandlerClazz);
    
            if (result == null) {
                try {
                    Class clazz = Class.forName(viewHandlerClazz);
                    result = (IViewHandler) clazz.newInstance();
                    mHandlerMap.put(viewHandlerClazz, result);
                } catch (Exception e) {
                    DebugLog.e(TAG, e.toString());
                }
            }
    
            if(result == null)
                throw new RuntimeException("IViewHandler创建失败:" + viewHandlerClazz);
    
            return result;
        }
    }

贴一个子类看看

    public class LeftViewHandler implements IViewHandler<BaseBean> {
        @Override
        public void handle(ViewGroup parent, ViewHolder holder, BaseBean data, int position) {
            holder.setText(R.id.tv,data.getName());
        }
    
        @Override
        public int getItemViewLayoutId() {
            return R.layout.simple_common_left;
        }
    
        @Override
        public int getItemViewType() {
            return R.layout.simple_common_left;
        }
    }

对数据的处理

    public List<BaseBean> getData(){
        data = new ArrayList<BaseBean>();
        for (int i = 0 ; i < 100 ; i++){
            BaseBean b = new BaseBean();
            b.setViewHandlerType(LeftViewHandler.class.getName());
            b.setName("item:"+i);
            data.add(b);
        }
        return data;
    }

我们以后对于多条目的混排,只需要给每个Data设定不同的Class文件名,然后在这个文件中绑定数据就Ok了

最后我们通过UML来梳理下


源码可能跟文章里面的不一样,注释换成英文的了
源码下载

声明:主要思想是剽窃赛哥的劳动成果(!_!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • 简介 在Android开发中ListView是比较常用的组件。 以列表的形式展示具体内容。 并且能够根据数据的长度...
    上善若水Ryder阅读 6,869评论 2 5
  • Android四大组件: activity: activity的生命周期:activity是context的子类,...
    梧桐树biu阅读 609评论 0 2
  • 1.开启图片上下文 2.获取当前上下文 3.把drawView的layer 渲染到当前上下文中,drawView为...
    金歌漫舞阅读 137评论 0 0
  • (小白) 花儿开得灿烂 经过一次次 看了一眼又一眼 砰动的心 如平静西湖的水煮沸 如老鼠与猫狭路相逢 无波无澜 以...
    cc1cc44bccf8阅读 209评论 0 1