前言
我们都知道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进行逻辑处理。
思考
onCreateViewHolder
和onBindViewHolder
方法中的条件判断我们是否可以通过某种方式把它们去掉,再观察一下这两个方法中的逻辑,我们发现这些条件判断语句都是跟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了,何必再占用其他资源做缓存复用呢。