ViewHolder那些事儿

ViewHolder那些事儿

前言

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

ListView实现单类型Item

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

public class SingleItemAdapter extends BaseAdapter {
   private List datas = new ArrayList();
   private Context context;
   private LayoutInflater layoutInflater;
  
   public SingleItemAdapter(Context context,List 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 datas = new ArrayList();
   private Context context;
   private LayoutInflater layoutInflater;
  
   public SingleItemAdapter(Context context,List 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 datas = new ArrayList();
  private Context context;
  private LayoutInflater layoutInflater;
  
  public MultiItemAdapter(Context context, List 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 getView(View convertView, int viewId) {  
        SparseArray viewHolder = (SparseArray) convertView.getTag();  
        if (viewHolder == null) {  
            viewHolder = new SparseArray();  
            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 datas = new ArrayList();
  private Context context;
  private LayoutInflater layoutInflater;
  
  public MultiItemAdapter(Context context, List 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  View.findViewOften(viewId: Int): V {
    val childViews: SparseArray = tag as? SparseArray ?: SparseArray()
    val childView = childViews.get(viewId)
    childView?.let { return it as V }
    val child = findViewById(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 {
  private Context context;
  private List datas = new ArrayList();
  private LayoutInflater layoutInflater;
  
  public SingleItemRVAdapter(Context context,List 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 {
  private Context context;
  private List datas = new ArrayList();
  private LayoutInflater layoutInflater;
  
  public MultiItemRVAdapter(Context context, List 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 mViews = new SparseArray();
  
  public CommonViewHolder(View view) {
    super(view);
  }
  
  // 通过这个方法获取item子view
  public  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 {
  private Context context;
  private List datas = new ArrayList();
  private LayoutInflater layoutInflater;
  
  public MultiItemRVAdapter(Context context, List 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那些事儿)