美文网首页
ViewHolder那些事儿

ViewHolder那些事儿

作者: 一笨正经的小屁孩 | 来源:发表于2020-07-06 22:23 被阅读0次
    ViewHolder那些事儿

    前言

    我们都知道ListView和RecyclerView都能实现列表,实现的过程略微有些差别,但都离不开ViewHolder,既然ListView和RecyclerView已经帮我们实现了Item的复用,那么ViewHolder又是用来干嘛的,它能解决什么问题?弄懂了这些,我们对如何实现一个ViewHolder会有一个更深的了解。

    ListView实现单类型Item

    我们先来看看ViewHolder在ListView单类型Item中的使用情况,下面是Java伪代码:

    public class SingleItemAdapter extends BaseAdapter {
       private List<XXX> datas = new ArrayList();
       private Context context;
       private LayoutInflater layoutInflater;
      
       public SingleItemAdapter(Context context,List<XXX> datas) {
         this.context = context;
         this.datas = datas
         this.layoutInflater = LayoutInflater.from(context);
         // or
         // this.layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
       }
      
         @Override
       public int getCount() {
         return datas.size();
       }
       
       @Override
       public Object getItem(int position) {
         return datas.get(position);
       }
      
       @Override
       public long getItemId(int position) {
         return position;
       }
      
       @Override
       public View getView(int position,View convertView,ViewGroup parent) {
         XXX data = getItem(position);
         ViewHolder holder;
         if(convertView == null) {
           holder = new ViewHolder()
           convertView = this.layoutInflater.inflate(R.layout.xxx, parent, false);
           // 这里需要findViewById拿到Item布局中的子控件
           holder.tvName = convertView.findViewById(R.id.xxx);
           holder.ivImage = convertView.findViewById(R.id.xxxx);
           // ...
           // 把ViewHolder作为TAG设置给convertView
           convertView.setTag(holder);
         } else {
           holder = (ViewHolder)convertView.getTag();
         }
         
         // 这部分代码基本都是对ViewHolder中的控件进行操作了,比如给TextView设置文本,给ImageView设置图片等等
         
         return convertView;
       }
      
       class ViewHolder {
         TextView tvName;
         ImageView ivImage;
         // ...
       }
    }
    

    从上面的实现来看,ViewHolder真正的作用不就是省略了findViewById的步骤嘛,是不是这样呢?我们再来实现一个不用ViewHolder的版本:

    public class SingleItemAdapter extends BaseAdapter {
       private List<XXX> datas = new ArrayList();
       private Context context;
       private LayoutInflater layoutInflater;
      
       public SingleItemAdapter(Context context,List<XXX> datas) {
         this.context = context;
         this.datas = datas
         this.layoutInflater = LayoutInflater.from(context);
         // or
         // this.layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
       }
      
         @Override
       public int getCount() {
         return datas.size();
       }
       
       @Override
       public Object getItem(int position) {
         return datas.get(position);
       }
      
       @Override
       public long getItemId(int position) {
         return position;
       }
      
       @Override
       public View getView(int position,View convertView,ViewGroup parent) {
         XXX data = getItem(position);
         if(convertView == null) {
           convertView = this.layoutInflater.inflate(R.layout.xxx, parent, false);
         }
         // 使用findViewById拿到Item布局中的子控件
         holder.tvName = convertView.findViewById(R.id.xxx);
         holder.ivImage = convertView.findViewById(R.id.xxxx);
         // 接下来就是操作控件...
         
         return convertView;
       }
    }
    

    我们知道在ListView中,随着列表的不断滚动,getView(...)方法会不断调用,上面代码中没有用到ViewHolder,就意味着每次调用getView(...)方法都需要findViewById一次子View,如果列表Item布局比较复杂,需要findViewById的子View特别多,这会严重影响列表滚动的流畅性。

    所以,我们在这里就能得出一个结论:

    ViewHolder是用来减少findViewById次数的,它只需findViewById有限次后,在列表滚动过程中不断复用ViewHolder中已经缓存的View,提高列表滚动的流畅性。

    ListView实现多类型Item

    前面我们得出了ViewHolder是为了减少findViewById次数的结论,那只是在ListView实现单类型Item时的情况,在多类型Item时是不是也是这样呢?下面我们将来看看在ListView多类型Item中ViewHolder的实现方式。

    public class MultiItemAdapter extends BaseAdapter {
      private List<XXX> datas = new ArrayList();
      private Context context;
      private LayoutInflater layoutInflater;
      
      public MultiItemAdapter(Context context, List<XXX> datas) {
        this.context = context;
        this.datas = datas;
        this.layoutInflater = LayoutInflater.from(context);
      }
      
      @Override
       public int getCount() {
         return datas.size();
       }
       
       @Override
       public Object getItem(int position) {
         return datas.get(position);
       }
      
       @Override
       public long getItemId(int position) {
         return position;
       }
      
       @Override
       public int getItemViewType(int position) {
         XXX data = getItem(position);
         // 注意:返回值必须从0开始返回,不然会抛出异常
         switch(data.getItemType()) {
           case XXX1:
             return 0;
           case XXX2:
             return 1;
           case XXX3:
             return 2
           // 其他类型...
           default:
             return 0; 
         }
       }
      
       @Override
       public int getViewTypeCount() {
         // 有多少种类型的Item就写多少种类型
         return 3;
       }
      
       // 多类型Item比单类型Item多覆写了上面两个方法
      
       @Override
       public View getView(int position, View convertView, ViewGroup parent) {
         XXX data = getItem(position);
         
         ViewHolder1 holder1;
         ViewHolder2 holder2;
         ViewHolder3 holder3;
         if(convertView == null) {
           switch(data.getItemType()) {
             case XXX1:
               holder1 = new ViewHolder1();
               convertView = this.layoutInflater.inflate(R.layout.xx1, parent, false);
               holder1.tvName = convertView.findViewById(R.id.tvName);
               holder1.ivImage = convertView.findViewById(R.id.ivImage);
               // ...
               convertView.setTag(holder1);
               break;
             case XXX2:
               holder2 = new ViewHolder2();
               convertView = this.layoutInflater.inflate(R.layout.xx2, parent, false);
               holder2.tvName = convertView.findViewById(R.id.tvName);
               holder2.ivImage = convertView.findViewById(R.id.ivImage);
               // ...
               convertView.setTag(holder2);
               break;
             case XXX3:
               holder3 = new ViewHolder3();
               convertView = this.layoutInflater.inflate(R.layout.xx3, parent, false);
               holder3.tvName = convertView.findViewById(R.id.tvName);
               holder3.ivImage = convertView.findViewById(R.id.ivImage);
               // ...
               convertView.setTag(holder3);
               break;
             default:
               break;
           }
         } else {
           // convertView不等于null的情况
           switch(data.getItemType()) {
             case XXX1:
               holder1 = (ViewHolder1)convertView.getTag();
               break;
             case XXX2:
               holder2 = (ViewHolder2)convertView.getTag();
               break;
             case XXX3:
               holder3 = (ViewHolder3)convertView.getTag();
               break;
             default:
               break;
           }
         }
         
         // 这一部分最终还是操作ViewHolder中的View
         switch(data.getItemType()) {
           case XXX1:
             // 操作ViewHolder1中的View
             break;
           case XXX2:
             // 操作ViewHolder2中的View
             break;
           case XXX3:
             // 操作ViewHolder3中的View
             break;
           default:
             break;
         }
         
         return convertView;
       }
      
       class ViewHolder1 {
         TextView tvName;
         ImageView ivImage;
         // ...
       }
      
       class ViewHolder2 {
         TextView tvName;
         ImageView ivImage;
         //...
       }
      
       class ViewHolder3 {
         TextView tvName;
         ImageView ivImage;
         // ...
       }
    }
    

    从上面的代码中我们知道实际对Item子View的操作还是需要借助ViewHolder,不论是单类型Item还是多类型Item所有的子View都缓存在ViewHolder中,然后ViewHolder缓存在Item根布局的tag中,换句话说:

    ListView中每个Item的子View都缓存在这个Item的根布局的tag中。

    在多类型和单类型Item的ListView中,ViewHolder的作用都是相同的,就是为了缓存Item的子View,减少findViewById的次数,提高ListView滚动流畅性。

    思考

    既然我们知道了ListView中每个Item的子View都是通过ViewHolder缓存在根布局的tag字段中,那么我们是否不借助ViewHolder而把子View也缓存在根布局的tag中呢?答案是可以的。

    我们来想一下,在ListView中,ViewHolder不就相当于子View的集合么,那么我们再找个其他集合代替它不就行了,比如SparseArray,我们把findViewById出来的子View存放在SparseArray中,然后把SparseArray当做tag设置给Item根布局,这样每次取子View的时候先从SparseArray中拿,没有的通过findViewById查找,然后存入SparseArray中,这样就避免了回回调用findViewById的尴尬了,有了这个思路后,接下来我们看看具体的实现。

    ListView中ViewHolder的另类实现

    根据前面的思路,我们可以实现下面的代码(ViewHolder另类写法):

    public class ViewHolder {    
        @SuppressWarnings("unchecked")  
        public static <T extends View> T getView(View convertView, int viewId) {  
            SparseArray<View> viewHolder = (SparseArray<View>) convertView.getTag();  
            if (viewHolder == null) {  
                viewHolder = new SparseArray<View>();  
                convertView.setTag(viewHolder);  
            }  
            View childView = viewHolder.get(viewId);  
            if (childView == null) {  
                childView = view.findViewById(viewId);  
                viewHolder.put(viewId, childView);  
            }  
            return (T) childView;  
        }  
    }  
    

    我们看看换了这种写法后,多类型Item的ListView能节省多少代码吧:

    public class MultiItemAdapter extends BaseAdapter {
      private List<XXX> datas = new ArrayList();
      private Context context;
      private LayoutInflater layoutInflater;
      
      public MultiItemAdapter(Context context, List<XXX> datas) {
        this.context = context;
        this.datas = datas;
        this.layoutInflater = LayoutInflater.from(context);
      }
      
      @Override
       public int getCount() {
         return datas.size();
       }
       
       @Override
       public Object getItem(int position) {
         return datas.get(position);
       }
      
       @Override
       public long getItemId(int position) {
         return position;
       }
      
       @Override
       public int getItemViewType(int position) {
         XXX data = getItem(position);
         switch(data.getItemType()) {
           case XXX1:
             return 0;
           case XXX2:
             return 1;
           case XXX3:
             return 2
           // 其他类型...
           default:
             return 0; 
         }
       }
      
       @Override
       public int getViewTypeCount() {
         // 有多少种类型的Item就写多少种类型
         return 3;
       }
      
       @Override
       public View getView(int position, View convertView, ViewGroup parent) {
         XXX data = getItem(position);
         
         if(convertView == null) {
           switch(data.getItemType()) {
             case XXX1:
               convertView = this.layoutInflater.inflate(R.layout.xx1, parent, false);
               break;
             case XXX2:
               convertView = this.layoutInflater.inflate(R.layout.xx2, parent, false);
               break;
             case XXX3:
               convertView = this.layoutInflater.inflate(R.layout.xx3, parent, false);
               break;
             default:
               break;
           }
         }
         
         switch(data.getItemType()) {
           case XXX1:
             // 操作ViewHolder1中的View
             // 通过ViewHolder.getView()方法获取子View操作
             break;
           case XXX2:
             // 操作ViewHolder2中的View
             // 通过ViewHolder.getView()方法获取子View操作
             break;
           case XXX3:
             // 操作ViewHolder3中的View
             // 通过ViewHolder.getView()方法获取子View操作
             break;
           default:
             break;
         }
         
         return convertView;
       }
    }
    

    效果还是非常明显的,getView中少些了大概有一半的代码,这在复杂布局的多类型Item非常有优势。

    Kotlin版的ListView中ViewHolder的另类实现

    在Kotlin中,我们可以借助Kotlin的语言特性-扩展函数,进一步简化前面的代码,让使用者完全感觉不到ViewHolder这种机制的存在:

    @Suppress("UNCHECKED_CAST")
    fun <V : View> View.findViewOften(viewId: Int): V {
        val childViews: SparseArray<View> = tag as? SparseArray<View> ?: SparseArray()
        val childView = childViews.get(viewId)
        childView?.let { return it as V }
        val child = findViewById<V>(viewId)
        childViews.put(viewId, child)
        return child
    }
    

    我们给View添加了一个扩展函数,这个扩展函数的作用就是缓存复用当前View的子View,这样我们就可以直接在Adapter的getView方法中调用convertView.findViewOften(R.id.xxx)来获取某个子View了,完全感知不到有ViewHolder这个机制的存在。

    RecyclerView实现单类型Item

    我们知道RecyclerView是自带ViewHolder的,而且在代码实现上必须要有ViewHolder这么一个东西,那么我们前面说的ViewHolder另类写法的思路在这里还能应用吗?当然可以。

    单类型Item的RecyclerView.Adapter:

    public class SingleItemRVAdapter extends RecyclerView.Adapter<CustomViewHolder> {
      private Context context;
      private List<XXX> datas = new ArrayList();
      private LayoutInflater layoutInflater;
      
      public SingleItemRVAdapter(Context context,List<XXX> datas) {
        this.context = context;
        this.datas = datas;
        this.layoutInflater = LayoutInflater.from(context);
      }
      
      @Override
      public int getItemCount() {
        return datas.size();
      }
      
      @Override
      public CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new CustomViewHolder(this.layoutInflater.inflate(R.layout.item_xxx, parent, false));
      }
      
      @Override
      public void onBindViewHolder(CustomViewHolder holder, int position) {
        XXX data = datas.get(position);
        holder.ivImage.setImageResource(data.xxx);
        holder.tvText.setText(data.text);
        // ...
      }
      
      class CustomViewHolder extends RecyclerView.ViewHolder {
         
         ImageView ivImage;
         TextView tvText;
        
         public CustomViewHolder(View view) {
           super(view);
           this.ivImage = view.findViewById(R.id.xxx1);
           this.tvText = view.findViewById(R.id.xxx2);
         }
      }
    }
    

    从RecyclerView实现单类型Item的Adapter上就能看出来它比ListView的Adapter要更简洁,RecyclerView把ViewHolder的复用也封装进源码里面了(RecyclerView本身实现了对ViewHolder的缓存),所以我们在使用RecyclerView的时候离不开ViewHolder。

    这样看起来,我们再使用前面章节说的ViewHolder另类实现有点多此一举了:能把子View缓存在RecyclerView的ViewHolder中,为什么还要把子View缓存在根布局的tag中呢?

    貌似单类型Item的RecyclerView上面这种写法挺好的,无非就是多写了一个ViewHolder子类而已,没什么毛病,下面我们看看多类型Item的RecyclerView的ViewHolder实现,看看这样的做法还是不是没有太大的毛病。

    RecyclerView实现多类型Item

    多类型Item的RecyclerView.Adapter:

    public class MultiItemRVAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
      private Context context;
      private List<XXX> datas = new ArrayList();
      private LayoutInflater layoutInflater;
      
      public MultiItemRVAdapter(Context context, List<XXX> datas) {
        this.context = context;
        this.datas = datas;
        this.layoutInflater = LayoutInflater.from(context);
      }
      
      @Override
      public int getItemCount() {
        return this.datas.size();
      }
      
      @Override
      public int getItemViewType(int position) {
        return this.datas.get(position).getItemViewType();
      }
      
      @Override
      public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch(viewType) {
          case XXX.TYPE1:
            return new ViewHolder1(this.layoutInflter.inflate(R.layout.xxx1, parent, false));
          case XXX.TYPE2:
            return new ViewHolder2(this.layoutInflater.inflate(R.layout.xxx2, parent, false));
          case XXX.TYPE3:
            return new ViewHolder3(this.layoutInflater.inflate(R.layout.xxx3, parent, false));
          default:
            return new RecyclerView.ViewHolder(new View(this.context));
        }
      }
      
      @Override
      public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        XXX data = datas.get(position);
        if(holder instanceOf ViewHolder1) {
          ((ViewHolder1)holder).tvText.setText(data.text);
          ((ViewHolder1)holder).ivImage.setImageResource(data.image_res);
          // ...
        } else if(holder instanceOf ViewHolder2) {
          // ...
        } else if(holder instanceOf ViewHolder3) {
          // ...
        }
      }
      
      
      class ViewHolder1 extends RecyclerView.ViewHolder {
        TextView tvText;
        ImageView ivImage;
        
        public ViewHolder1(View view) {
          super(view)
          this.tvText = view.findViewById(R.id.xxx1);
          this.ivImage = view.findViewById(R.id.xxx2);
        }
      }
      
      class ViewHolder2 extends RecyclerView.ViewHolder {
        TextView tvText;
        ImageView ivImage;
        
        public ViewHolder1(View view) {
          super(view)
          this.tvText = view.findViewById(R.id.xxx1);
          this.ivImage = view.findViewById(R.id.xxx2);
        }
      }
      
      class ViewHolder3 extends RecyclerView.ViewHolder {
        TextView tvText;
        ImageView ivImage;
        
        public ViewHolder1(View view) {
          super(view)
          this.tvText = view.findViewById(R.id.xxx1);
          this.ivImage = view.findViewById(R.id.xxx2);
        }
      }
    }
    

    我们可以发现,多类型Item比单类型Item实现多了几个ViewHolder子类型,在onCreateViewHolder中需要根据Item的类型来构建不同类型的ViewHolder,然后在onBindViewHolder中要判断当前的holder是什么类型的ViewHolder,然后强转调用子View进行逻辑处理。

    思考

    onCreateViewHolderonBindViewHolder方法中的条件判断我们是否可以通过某种方式把它们去掉,再观察一下这两个方法中的逻辑,我们发现这些条件判断语句都是跟ViewHolder有关,onCreateViewHolder是创建不同类型的ViewHolder,onBindViewHolder是根据不同类型ViewHolder进行逻辑处理,如果我们把ViewHolder统一了,这些条件判断语句自然就没了,那么现在的问题就是统一了ViewHolder之后,ViewHolder中缓存的子View怎么办呢?原来是不同类型的ViewHolder缓存不同的子View,现在统一了ViewHolder,子View该如何缓存呢?我们可以借鉴前面ListView的ViewHolder另类实现的思路,把子View缓存在SparseArray中,通过子View的id来获取对应的子View,这样既统一了ViewHolder类型,也统一了ViewHolder中子View的缓存方式,这就是对ViewHolder的一种抽象。

    RecyclerView中ViewHolder的另类实现

    按照上面的思路,我们进行通用ViewHolder的封装:

    public class CommonViewHolder extends RecyclerView.ViewHolder {
      
      private SparseArray<View> mViews = new SparseArray();
      
      public CommonViewHolder(View view) {
        super(view);
      }
      
      // 通过这个方法获取item子view
      public <V extends View> V getViewById(int viewId) {
        View childView = this.mViews.get(viewId);
        if(childView == null) {
          childView = itemView.findViewById(viewId);
          if(childView == null) {
            throw new NullPointerException("childView is null, view id is " + viewId);
          }
          this.mViews.put(viewId, childView);
        }
        return (V) childView;
      }
    }
    

    下面我们再来用封装的通用ViewHolder来重写多类型Item的Adapter实现:

    public class MultiItemRVAdapter extends RecyclerView.Adapter<CommonViewHolder> {
      private Context context;
      private List<XXX> datas = new ArrayList();
      private LayoutInflater layoutInflater;
      
      public MultiItemRVAdapter(Context context, List<XXX> datas) {
        this.context = context;
        this.datas = datas;
        this.layoutInflater = LayoutInflater.from(context);
      }
      
      @Override
      public int getItemCount() {
        return this.datas.size();
      }
      
      @Override
      public int getItemViewType(int position) {
        return this.datas.get(position).getItemViewType();
      }
      
      @Override
      public CommonViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view;
        // 通过item类型加载不同的item布局文件
        switch(viewType) {
          case XXX.TYPE1:
            view = this.layoutInflter.inflate(R.layout.xxx1, parent, false));
            break;
          case XXX.TYPE2:
            view = this.layoutInflater.inflate(R.layout.xxx2, parent, false));
            break;
          case XXX.TYPE3:
            view = this.layoutInflater.inflate(R.layout.xxx3, parent, false));
            break;
          default:
            break;
        }
        // 封装在统一的ViewHolder中,通过getViewById()获取和缓存子View
        return new CommonViewHolder(view);
      }
      
      @Override
      public void onBindViewHolder(CommonViewHolder holder, int position) {
        XXX data = datas.get(position);
        // 不需要再区分某个ViewHolder了,拿到CommonViewHolder调用getViewById()方法取子View就行了
        // 没有具体的ViewHolder了,但我们还需要根据某个item类型来进行对应的逻辑处理
        switch(getItemViewType(position)) {
          case XXX.TYPE1:
            TextView tvText = holder.getViewById(R.id.xxx);
            tvText.setText(data.text);
            // ...
            break;
          case XXX.TYPE2:
            // ...
            break;
          case XXX.TYPE3:
            // ...
            break;
          default:
            break;
        }
      }
    }
    

    这么一写,我们发现确实没有了各种各样的ViewHolder了,代码也相对精干。

    所以我们总结:

    RecyclerView对ViewHolder做了缓存复用处理,我们就应该利用这个“平台”,没太大必要跟ListView一样再占用根布局的tag字段做子View的缓存复用工作。

    总结

    • ListView中每个item的子View都是缓存在item根布局的tag字段上;
    • 利用前面这一条特性我们可以结合SparseArray实现通用的ViewHolder,实际上这里应该不叫ViewHolder了,只是对子View缓存复用实现;
    • 在Kotlin中利用Kotlin中的扩展函数进一步简化了子View的缓存复用实现,让使用更简单,用者基本感知不到这个机制的存在;
    • RecyclerView中使用ViewHolder是必然的,因为RecyclerView的实现就包含了ViewHolder,内部对ViewHolder已经做了缓存复用处理,所以我们应该借鉴ListView实现item子View缓存复用实现的方式来进行通用ViewHolder的封装。
    • 如果真要把ListView中实现的那套item子View缓存复用机制用在RecyclerView上也是可以的,但个人觉得没必要,既然RecyclerView中已经有了ViewHolder了,何必再占用其他资源做缓存复用呢。

    相关文章

      网友评论

          本文标题:ViewHolder那些事儿

          本文链接:https://www.haomeiwen.com/subject/znknqktx.html