一点点有助于巧用RecyclerView的小技巧
在RecyclerView问世之前,ListView可能是我们使用频率最高的系统控件之一了。而随着Android的发展,虽然ListView依旧重要,但确实越来越多的时候大家都开始选择使用RecyclerView了。当然这也是事物发展的必然,个人觉得最重要的原因就是RecyclerView相对来说,确实灵活性更高。
但是显然并不能说RecyclerView就优于ListView,二者各有优劣,我们应该根据不同的需求选择最合适的进行使用。这里的重点是:当我们已经用习惯了ListView,刚开始转向RecyclerView的时候,还是容易在很多小地方出现水土不服的。故此,在这里记录几个关于RecyclerView比较实用的小技巧。
添加Header/Footer
我们知道想要为ListView添加上一个Header或者Footer是非常容易的,因为ListView本身已经提供了相关的方法接口,我们只负责调用就可以了。
而在RecyclerView里我们是找不到类似于setHeaderView这样的方法的,但是这样的功能确实又还是比较常用的。所以这时应该如何做呢?
其实关于RecyclerView有一个非常有用的东西叫做viewType,而它究竟能起到什么作用呢?我们具体来看一看。假设我们先写一个最基本的Adapter类:
public class SimpleRecyclerAdapter extends RecyclerView.Adapter<SimpleRecyclerAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return null;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public int getItemCount() {
return 0;
}
public class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
}
}
}
以上就是一个最最基本的RecyclerView的Adapter类,我们可以看到一个命名非常能够说明其作用的方法叫做onCreateViewHolder。
如果对ListView的使用已经有了了解,我们就知道ViewHolder实际上就是用来复用ItemView,从而大大提高效率的。所以onCreateViewHolder顾名思义就是在为RecyclerView的itemView创建ViewHolder时所调用的,我们在此需要注意到的是该方法有一个参数叫做viewType。
实际上,从其命名我们就很容易联想到:它多半是与创建ViewHolder时,itemView的布局类型有关系的。那么,其作用究竟如何?其实我们可以到RecyclerView的源码去简单找找答案,简单来说,其逻辑可以归纳如下:
在RecyclerView开始初始化需要显示的item数据的时候,会通过方法getViewForPosition(int position)来获取对应的itemView。
而这个获取的过程,其实是含有一个缓存机制的。这里源码很长,我们没有那么多精力也没有必要去全部读的明明白白,就捡关键的几行代码看:
- final int type = mAdapter.getItemViewType(offsetPosition);
- holder = getRecycledViewPool().getRecycledView(type);
- holder = mAdapter.createViewHolder(RecyclerView.this, type);
其实,分析一下以上我们提炼出的这几行代码。我们可以得知:
- RecyclerView在获取itemView的时候,会首先通过getItemViewType方法去获取该position位置的viewType。
- 当获取到了type就会根据它的值去RecycledViewPool这个缓存池中查找对应类型的ViewHolder来进行复用。
- 但是,如果当前缓存池中还没有可以进行复用的ViewHolder怎么办呢?当然就是通过createViewHolder来创建全新的ViewHolder了。
- 在createViewHolder方法中adapter里的onCreateViewHolder方法就被回调了,所以这也是为什么自定义的Adapter类必须覆写这个方法的原因。
- 而创建出的ViewHolder在合适的时机就会被加入到缓存池,以便其他的item进行复用。
以上谈到的这个过程只要对于ListView使用ViewHolder的原理有所了解,相信就不难理解。
当然,除了getViewForPosition(int position)之外,还有另一个方法也很关键,即:bindViewToPosition(View view, int position)。
这个方法的实现逻辑相对来说更简单一点,我们只需要明白这个方法的核心作用就是:将给定的视图绑定到指定位置(position)。其大致逻辑是:
- 首先,会通过ViewHolder holder = getChildViewHolderInt(view)去获取ViewHolder。
- 之后只要获取到的该itemView的ViewHolder不为空,那么就会通过mAdapter.bindViewHolder(holder, offsetPosition)进行视图的数据绑定。
- 最后同理的,在该方法内Adapter的onBindViewHolder方法就会被回调,这当然也就是会什么我们必须覆写onBindViewHolder的原因了。
OK,那么有了以上的分析作为基础,我们对RecyclerView的工作流程会有一个大概的了解。如果想要更加深入,我们可以自己再继续到源码中去进行研究。这里至少记住一个关键点,那就是:RecyclerView在获取itemView的时候,其布局是与ViewType相关的。现在我们回到之前分析中的一行代码:
- final int type = mAdapter.getItemViewType(offsetPosition);
也就是说,我们发现我们提到的ViewType这个东西,在源码中会通过Adapter类的getItemViewType方法来进行获取。但是回顾一下我们自定义的Adapter类,似乎并没有覆写这个方法。由此我们很容易可以推测出,在源码中这个方法肯定是有默认实现的:
public int getItemViewType(int position) {
return 0;
}
由此我们知道,源码中该方法的实现很简单,就是固定的返回0。这意味着:只要我们不自己覆写该方法,那么itemView就永远只有固定的一种type。
但与此同时也代表着,我们可以自己覆写该方法添加额外的ViewType。那么,所谓的添加Header这种操作,不就很容易实现了吗?
public class SimpleRecyclerAdapter extends RecyclerView.Adapter<SimpleRecyclerAdapter.ViewHolder> {
private List<String> data;
private static final int TYPE_HEADER = 0;
private static final int TYPE_CONTENT = 1;
private View mHeaderView;
public void setHeaderView(View headerView) {
mHeaderView = headerView;
notifyItemInserted(0);
}
public SimpleRecyclerAdapter(List<String> data) {
this.data = data;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder holder = null;
if (viewType == TYPE_HEADER) {
holder = new ViewHolder(mHeaderView);
} else if (viewType == TYPE_CONTENT) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
holder = new ViewHolder(view);
}
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_HEADER)
return;
holder.tvContent.setText(data.get(getRealPosition(holder)));
}
private int getRealPosition(ViewHolder holder) {
return mHeaderView == null ? holder.getLayoutPosition() : holder.getLayoutPosition() - 1;
}
@Override
public int getItemCount() {
return mHeaderView == null ? data.size() : data.size() + 1;
}
@Override
public int getItemViewType(int position) {
if (mHeaderView == null)
return TYPE_CONTENT;
if (position == 0) {
return TYPE_HEADER;
} else {
return TYPE_CONTENT;
}
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tvContent;
public ViewHolder(View itemView) {
super(itemView);
tvContent = (TextView) itemView.findViewById(R.id.tv_content);
}
}
}
以上就是我们实现的一个最基本的可以设置Header的Adapter。我们分析一下会发现逻辑其实非常简单,关键其实就在于:
- 在设置Header的方法setHeaderView当中,我们通过notifyItemInserted(0)告诉RecyclerView在最前方插入了一个item。
- 覆写getItemViewType方法,在这里判断该postion位置的itemView其viewType究竟是TYPE_HEADER还是TYPE_CONTENT。
- 而添加了额外的ViewType之后,自然就需要在onCreateViewHolder中根据不同的ViewType创建不同类型的ViewHolder。
- 那么,同样的道理,在onBindViewHolder我们自然也应该根据ViewType的不同做对应逻辑的数据绑定操作。
- 最后,因为插入了一个新的item作为Header,但显然这是不计算进data的数量的。所以还需要对getItemCount和getPosition做额外的计算。
好了,现在运行一下程序,我们得到如下的效果:
这样我们就已经为RecyclerView成功的添加了一个Header了,当然这只是一个最最基本的例子,重在掌握其原理就行。实际上重中之重应该是掌握RecyclerView创建itemView的工作原理和viewType这个东西,因为灵活的运用viewType可以很方便的完成很多需求,比如我们接着要看的。
不同类型的item布局
对于实际开发来说,RecyclerView、ListView这类控件的使用肯定不会像我们学习Demo时那样简单规律。比如,很多时候一个列表中不同的item之间它们的布局样式也是不同的。举例来说,最近没事的时候自己在做一个练手的小项目,其中有一个界面是这样的:
要实现这种效果肯定有很多方法,但我们想做的是在一个RecyclerView中直接搞定它。与此同时,再比如说非常常见的聊天界面,聊天列表的布局也是会分为接受的消息和发出的消息两种样式。那么,我们又该怎么简单的实现它呢?有了之前的基础,我们其实很容易举一反三。ViewType这个东西用在这里实在是合适到不能再合适了。首先让我们分别定义好接受和发出的消息两种布局文件:
接着,当然就是根据我们这里的需求来定义这个RecyclerView的Adapter类了:
public class ChatRecyclerAdapter extends RecyclerView.Adapter<ChatRecyclerAdapter.ViewHolder> {
public static final int TYPE_MSG_FROM = 0;
public static final int TYPE_MSG_TO = 1;
private List<ChatMessage> data;
public ChatRecyclerAdapter(List<ChatMessage> data) {
this.data = data;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewHolder holder = null;
if (viewType == TYPE_MSG_FROM) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_from, parent, false);
holder = new ViewHolder(view);
} else if (viewType == TYPE_MSG_TO) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat_to, parent, false);
holder = new ViewHolder(view);
}
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.tvContent.setText(data.get(position).getMessageContent());
}
@Override
public int getItemCount() {
return data.size();
}
@Override
public int getItemViewType(int position) {
return data.get(position).getMsgType();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView tvContent;
public ViewHolder(View itemView) {
super(itemView);
tvContent = (TextView) itemView.findViewById(R.id.tv_message_content);
}
}
}
瞄一眼代码,这种实现方式是不是还是挺优雅的呢?接下来简单的写下调用测试,然后看看效果吧:
setOnItemClickListener
要说RecyclerView最让人郁闷的就是居然没有setOnItemClickListener这样的东西,第一次用的时候我是懵逼的?哈哈,之所以这么说是因为实际使用中,可能说基本上百分之九十的列表都是要实现item的点击事件的。那么,既然RecyclerView自身没有提供的话,关于这种需求我们又要作何实现呢?这里记录一种自己比较喜欢的方式。
我们分析一下,其实所谓的setOnItemClickListener其实本质就是:给列表中的每个Item添加点击事件,所以显然我们应该在itemView上找切入点。幸运的是,有了之前的基础,我们能够记得在ViewHolder的构造器中,实际上是能够拿到itemView的。那么就很容易实现添加点击事件这种需求了。
首先,让我们自定义一个监听接口:
public interface RecyclerItemOnClickListener {
void onItemClick(View view, int position);
}
接着,就可以开始编写Adapter类了:
public class ClickableRecyclerAdapter extends RecyclerView.Adapter<ClickableRecyclerAdapter.ViewHolder> {
private RecyclerItemOnClickListener mListener;
private List<String> data;
public ClickableRecyclerAdapter(List<String> data) {
this.data = data;
}
public void setOnItemClickListener(RecyclerItemOnClickListener listener) {
this.mListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.tvContent.setText(data.get(position));
}
@Override
public int getItemCount() {
return data.size();
}
public class ViewHolder extends RecyclerView.ViewHolder{
TextView tvContent;
public ViewHolder(View itemView) {
super(itemView);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mListener == null)
return;
mListener.onItemClick(v,getAdapterPosition());
}
});
tvContent = (TextView) itemView.findViewById(R.id.tv_content);
}
}
}
最后,当然依旧是编写测试代码来看看效果:
好了,就总结到这里了,都是一些简单但也比较使用的小技巧。其实在积累使用经验的过程中,可以发现关于RecyclerView的使用技巧其实还是挺多的;但也可以发现大多数优雅的技巧都是建立在RecyclerView自身的工作原理上的。所以总的来说依旧是那样,越了解RecyclerView自身的机制,才能越得心应手的进行拓展。