1、什么是RecyclerView
RecyclerView是Android 5.0 materials design中的组件之一,相应的还有CardView、Palette等。看名字我们就能看出一点端倪,没错,它主要的特点就是复用。我们知道,Listview中的Adapter中可以实现ViewHolder的复用。
- RecyclerView 和 ListView 布局效果的对比
- RecyclerView 和 ListView 一些常用的功能 和 API 的对比
布局效果对比
ListView 大家对效果已经很熟悉了,这里直接跳过,而作为 RecyclerView,它能带给效果要比 ListView 强大得多
Android 默认提供的 RecyclerView 就能支持 线性布局、网格布局、瀑布流布局 三种(这里我们暂且不提代码细节,后文再说),而且同时还能够控制横向还是纵向滚动。怎样,从效果上足以碾压 ListView 有木有
API 使用对比
当然,一个控件我们不能完全只看效果,关键还是要看实用性,看看有没有方便我们调用的 API提高我们的开发效率。所以,接下来我们就从各个方面来看看 RecyclerView 和 ListView 在提供的API调用上的一些实践比较。
基础使用
ListView 的基础使用大家再熟悉不过,其使用的关键点主要如下:
- 继承重写 BaseAdapter 类
- 自定义 ViewHolder 和 convertView 一起完成复用优化工作
由于 ListView 已经老生常谈,所以此处就不去写示例代码了。 RecyclerView 基础使用关键点同样有两点:
- 继承重写 RecyclerView.Adapter 和 RecyclerView.ViewHolder
- 设置布局管理器,控制布局效果
第一步:继承重写 RecyclerView.Adapter 和 RecyclerView.ViewHolder
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.RecyclerViewHolder> {
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
...
return viewHolder;
}
@Override
public void onBindViewHolder(final RecyclerViewHolder holder, int position) {
}
@Override
public int getItemCount() {
if (data == null) {
return 0;
}
return data.size();
}
class RecyclerViewHolder extends RecyclerView.ViewHolder {
public RecyclerViewHolder(View itemView) {
super(itemView);
}
}
第二步:设置布局管理器,控制布局效果
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(mRecyclerAdapter);
从基础使用上看,我们明显可以看出,RecyclerView 相比 ListView 在基础使用上的区别主要有如下几点:
- ViewHolder 的编写规范化了
- RecyclerView 复用 Item 的工作 Google 全帮你搞定,不再需要像 ListView 那样自己调用 setTag
- RecyclerView 需要多出一步 LayoutManager 的设置工作
布局效果
在最开始就提到,RecyclerView 能够支持各种各样的布局效果,这是 ListView 所不具有的功能,那么这个功能如何实现的呢?其核心关键在于 RecyclerView.LayoutManager 类中。从前面的基础使用可以看到,RecyclerView 在使用过程中要比 ListView 多一个 setLayoutManager 步骤,这个 LayoutManager 就是用于控制我们 RecyclerView 最终的展示效果的。
而 LayoutManager 只是一个抽象类而已,系统已经为我们提供了三个相关的实现类 LinearLayoutManager(线性布局效果)、GridLayoutManager(网格布局效果)、StaggeredGridLayoutManager(瀑布流布局效果)。
关于 LayoutManager 的使用有下面一些常见的 API(有些在 LayoutManager 实现的子类中)
canScrollHorizontally();
//能否横向滚动
canScrollVertically();
//能否纵向滚动
scrollToPosition(int position);
//滚动到指定位置
setOrientation(int orientation);
//设置滚动的方向
getOrientation();
//获取滚动方向
findViewByPosition(int position);
//获取指定位置的Item View
findFirstCompletelyVisibleItemPosition();
//获取第一个完全可见的Item位置
findFirstVisibleItemPosition();
//获取第一个可见Item的位置
findLastCompletelyVisibleItemPosition();
//获取最后一个完全可见的Item位置
findLastVisibleItemPosition();
//获取最后一个可见Item的位置
局部刷新
在 ListView 中,说到刷新很多童鞋会记得 notifyDataSetChanged() ,但是说到局部刷新估计有很多童鞋就知道得比较少了。我们知道在更新了 ListView 的数据源后,需要通过 Adapter 的 notifyDataSetChanged 来通知视图更新变化,这样做比较的好处就是调用简单,坏处就是它会重绘每个 Item,但实际上并不是每个 Item 都需要重绘。最常见的,例如:朋友圈点赞,点赞只是更新当前点赞的Item,并不需要每个 Item 都更新。然而 ListView 并没有提供局部刷新某个 Item 的 API 给我们,只能自己自足实现刷新单个 Item 的效果,套路大致如下方的 updateItemView:
/**
* 更新Item视图,减少不必要的重绘
* @param listView
* @param position
*/
public void updateItemView(ListView listView, int position) {
//换算成 Item View 在 ViewGroup 中的 index
int index = position - listView.getFirstVisiblePosition();
if (index >= 0 && index < listView.getChildCount()) {
//更新数据
AuthorInfo authorInfo = mAuthorInfoList.get(position);
authorInfo.setNickName("Google Android");
authorInfo.setMotto("My name is Android .");
authorInfo.setPortrait(R.mipmap.ic_launcher);
//更新单个Item
View itemView = listView.getChildAt(index);
getView(position, itemView, listView);
}
}
RecyclerView.Adapter 则我们提供了 notifyItemChanged 用于更新单个 Item View 的刷新,我们可以省去自己写局部更新的工作。
谈到动画效果,还有一个很关键的类不得不提,那就是 ItemTouchHelper 。
ItemTouchHelper 是系统为我们提供的一个用于滑动和删除 RecyclerView 条目的工具类,用起来也是非常简单的,大致两步:
- 创建 ItemTouchHelper 实例,同时实现 ItemTouchHelper.Callback 中的抽象方法,用于初始化 ItemTouchHelper
- 调用 ItemTouchHelper 的 attachToRecyclerView 方法关联上 RecyclerView 即可
示例代码大致如下:
//ItemTouchHelper 用于实现 RecyclerView Item 拖曳效果的类
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// actionState : action状态类型,有三类 ACTION_STATE_DRAG (拖曳),ACTION_STATE_SWIPE(滑动),ACTION_STATE_IDLE(静止)
int dragFlags = makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
ItemTouchHelper.UP | ItemTouchHelper.DOWN // 支持上下的拖曳
| ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);// 支持左右的拖曳
int swipeFlags = makeMovementFlags(ItemTouchHelper.ACTION_STATE_SWIPE,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);//表示支持左右的滑动
// 直接返回0表示不支持拖曳和滑动
return makeMovementFlags(dragFlags, swipeFlags);
}
/**
* @param recyclerView attach的RecyclerView
* @param viewHolder 拖动的Item
* @param target 放置Item的目标位置
* @return
*/
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 要拖曳的位置
int fromPosition = viewHolder.getAdapterPosition();
// 要放置的目标位置
int toPosition = target.getAdapterPosition();
mRecyclerAdapter.moveItem(fromPosition, toPosition);
return true;
}
/**
* @param viewHolder 滑动移除的Item
* @param direction
*/
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 获取要滑动删除的Item位置
int position = viewHolder.getAdapterPosition();
mRecyclerAdapter.removeItem(position);
}
});
itemTouchHelper.attachToRecyclerView(recyclerView);
虽然代码中有注释,但还是稍稍解释一下,主要重写的是 getMovementFlags 、 onMove 、 onSwiped 三个抽象方法,getMovementFlags 用于告诉系统,我们的 RecyclerView 到底是支持滑动还是拖曳。如上面的示例代码,就是表示着同时支持上下左右四个方向的拖曳和左右两个方向的滑动效果。如果是滑动,则 onSwiped 会被回调,如果是拖曳 onMove 会被回调。
RecyclerView点击的实现方式
在adapter中定义接口实现点击
public interface OnItemClickLitener {
void onItemClick(View view, int position);
void onItemLongClick(View view, int position);
}
B、通过 addOnItemTouchListener 和势判断结合实现的系统提供的 GestureDetector 手势
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
});
其中第三个方法是处理触摸事件冲突的,跟我们没关系不用管它,前两个方法是不是很熟悉呢,这不就是View的事件分发机制里面的事件拦截和事件处理的两个方法吗,参数里为我们提供了触摸事件的数据MotionEvent,我们要做的就是去解析坐标点和触摸规律来识别触摸手势,然后获取触摸的是哪一个item,再执行我们的回调,听起来很复杂,但是我前面已经说过了,sdk已经为我们实现了手势的识别:
GestureDetectorCompat 就是处理手势的类:手势探测器,它比GestureDetector能更好兼容低版本的api,但使用方法是一致的,我们实例化一个手势探测器:
mGestureDetector = new GestureDetectorCompat(context,new GestureListener(){...});
我们实例化手势探测器的时候需要提供一个手势监听器:OnGestureListener,探测器识别出手势后就会回调手势监听器中对应的方法,我们就可以在回调方法中做我们想做的事情了。
sdk为我们提供了两个手势监听器:OnGestureListener,OnDoubleTapListener
OnGestureListener的回调接口如下:
//用户按下屏幕就会触发
public boolean onDown(MotionEvent e);
//如果是按下的时间超过瞬间,而且在按下的时候没有松开或者是拖动的,那么onShowPress就会执行
public void onShowPress(MotionEvent e);
//一次单独的轻击抬起操作,也就是轻击一下屏幕,就是普通点击事件
public boolean onSingleTapUp(MotionEvent e);
//在屏幕上拖动事件
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
//长按触摸屏,超过一定时长,就会触发这个事件
public void onLongPress(MotionEvent e);
//滑屏,用户按下触摸屏、快速移动后松开
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
OnDoubleTapListener的回调接口如下:
//单击事件。用来判定该次点击是SingleTap而不是DoubleTap,
//如果连续点击两次就是DoubleTap手势,如果只点击一次,
//系统等待一段时间后没有收到第二次点击则判定该次点击为SingleTap而不是DoubleTap,
//然后触发SingleTapConfirmed事件
public boolean onSingleTapConfirmed(MotionEvent e);
//双击事件
public boolean onDoubleTap(MotionEvent e);
//双击间隔中发生的动作。指触发onDoubleTap以后,在双击之间发生的其它动作
public boolean onDoubleTapEvent(MotionEvent e);
可以看出OnGestureListener主要回调各种单击事件,而OnDoubleTapListener回调各种双击事件。而我们需要处理的点击事件其实就是上面的:onSingleTapUp()
值得一提的是sdk 还提供了一个外部类SimpleOnGestureListener,这个类实现了上面两个接口的所有方法,但全都是空实现,函数体里什么也没写,其中就是把上面两个接口合并一下,给出默认的空实现,这样继承SimpleOnGestureListener的时候就不用实现每一个方法了,既然如此,那么我们可以定义一个类去继承它。
定义一个ItemTouchHelperGestureListener 继承自SimpleOnGestureListener ,实现onSingleTapUp方法:
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
}
}
到这里,已经获取到了RecyclerView的点击事件和触摸事件数据MotionEvent ,那么我们怎么知道点击的是哪一个item呢?RecyclerView已经为我们提供了这样的方法:findChildViewUnder(),我们可以通过这个方法获得点击的item,同时我们调用RecyclerView的另一个方法getChildViewHolder(),可以获得该item的ViewHolder,最后再回调我们定义的虚方法onItemClick()就ok了,这样我们就可以在外部实现该方法来获得item的点击事件了:
@Override
public boolean onSingleTapUp(MotionEvent e) {
View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (child!=null) {
RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(child);
onItemClick(vh);
}
return true;
}
完整的代码:
recyclerView.addOnItemTouchListener(new OnRecyclerItemClickListener(recyclerView) {
@Override
public void onItemClick(RecyclerView.ViewHolder vh) {
//item点击事件
}
});
public abstract class OnRecyclerItemClickListener implements RecyclerView.OnItemTouchListener{
private GestureDetectorCompat mGestureDetector;
private RecyclerView recyclerView;
// 实例化手势探测器
public OnRecyclerItemClickListener(RecyclerView recyclerView){
this.recyclerView = recyclerView;
mGestureDetector = new GestureDetectorCompat(recyclerView.getContext(),new ItemTouchHelperGestureListener());
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
mGestureDetector.onTouchEvent(e);
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
mGestureDetector.onTouchEvent(e);
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
/**
*定义一个手势监听器
*/
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvent e) {
View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
if (child!=null) {
RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(child);
onItemClick(vh);
}
return true;
}
//长点击事件,本例不需要不处理
//@Override
//public void onLongPress(MotionEvent e) {
// View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
// if (child!=null) {
// RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(child);
// onItemLongClick(vh);
// }
//}
public abstract void onItemClick(RecyclerView.ViewHolder vh);
//public abstract void onItemLongClick(RecyclerView.ViewHolder vh);
}
3、RecyclerView优点和缺点
优点:
RecyclerView本身它是不关心视图相关的问题的,由于ListView的紧耦合的问题,google的改进就是RecyclerView本身不参与任何视图相关的问题。它不关心如何将子View放在合适的位置,也不关心如何分割这些子View,更不关心每个子View各自的外观。更进一步来说就是RecyclerView它只负责回收和重用的工作,这也是它名字的由来。
所有关于布局、绘制和其他相关的问题,也就是跟数据展示相关的所有问题,都被委派给了一些”插件化”的类来处理。这使得RecyclerView的API变得非常灵活。你需要一个新的布局么?接入另一个LayoutManager就可以了!你想要不同的动画么?接入一个新的ItemAnimator就可以了,诸如此类等等。
独立的LayoutManager,可以灵活的控制RecyclerView中items的布局:LinearLayoutManager(垂直布局、水平布局)、GridLayoutManager(网格布局)、StaggeredGridLayoutManager(瀑布流布局)。
缺点:
1、不能简单的设置子item的点击事件。
在RecyclerView中,没有一个onItemClickListener方法。所以目前在适配器中处理这样的事件比较好。如果想要从适配器上添加或移除条目,需要明确通知适配器。这与先前的notifyDataSetChanged()方法稍微有些不同。具体操作在适配器代码中就可以体现。
2、分割线,多选,单选的属性没有提供;需要我们自己想办法。
ListView中Google为我们提供了SetDivider(Drawable divider)这样的方法来设置分隔线,那么在RecyclerView中提供了:addItemDecoration(RecyclerView.ItemDecoration decor)这个方法了设置分隔线
3、不能简单的加头和尾 。
RecyclerView中,没有addHeaderView和addFooderView方法,通过控制Adapter的itemType来设置的,思路就是根据不同的itemType去加载不同的布局。
整体总结它的几点如下:
Adapter:包装数据集合并且为每个条目创建视图。
ViewHolder:保存用于显示每个数据条目的子View。
LayoutManager:将每个条目的视图放置于适当的位置。
ItemDecoration:在每个条目的视图的周围或上面绘制一些装饰视图。
ItemAnimator:在条目被添加、移除或者重排序时添加动画效果。