RecyclerView总结与多功能便捷Adapter的封装
Comment虽然RecyclerView在5.0时代就已经出来了,不过因为各方面原因,以及ListView以前用的很多,各种特殊处理也更熟悉,所以当出现非表格形式的列表级控件需求的时候第一时间想到的是listview而不是RecyclerView,所以造成了RecyclerView虽然出了不少时间了,但是和ListView的熟练度相比还是差很多,这次主要就是总结记录一下RecyclerView的基础用法并对其Adapter进行封装,方便更简单的进行各种功能的实现.
封装的项目在Git的RecyclerViewUtils,项目具体内容可以参照文章下半部分或项目中的README
ps:虽然RecyclerView在5.0才正式推出,不过在4.4的launcher3的代码中就已经出现了RecyclerView的身影,说明Google很早就开始编写了,只是因为没有完善或其他什么原因后续才推出.
RecyclerView的一般使用
1. 设置LayoutManager
recyclerView.setLayoutManager(LayoutManager manager); |
RecyclerView支持三种LayoutManager:
LinearLayoutManager 和ListView类似,不过可以通过传入不同的参数控制方向:
LinearLayoutManager.VERTICAL 纵向 |
GridLayoutManager 和GridView类似,也可以通过传入不同的参数控制方向:
GridLayoutManager.VERTICAL 纵向 |
StaggeredGridLayoutManager 会自动根据内容大小对高度或宽度进行适应的GridLayout,可以简单的完成瀑布流效果,也可以通过不同的参数控制方向:
StaggeredGridLayoutManager.HORIZONTAL 纵向 |
1.虽然上面三个LayoutManager控制方向的参数都看起来名称不相同,不过其实都是相同的值:
OrientationHelper.HORIZONTAL |
2.如果使用最简单的构造方法,默认方向为VERTICAL
3.如果传入了方向,创建LinearLayoutManager和GridLayoutManager的时候需要还有一个boolean变量reverseLayout需要传入,表示是否从最后一个开始布局,一般这个值是传入false的.
实际效果是传入adapter集合的数据会从列表的最后面开始往前布局,列表初始显示的时候也会显示在最后一个.
StaggeredGridLayoutManager并没有提供该方式,应该是由于StaggeredGridLayoutManager是另外实现的.
LinearLayoutManager继承于LayoutManager,GridLayoutManager继承于LinearLayoutManager
GridLayoutManager相当于是对LinearLayoutManager的扩展
但StaggeredGridLayoutManager是重新继承于LayoutManager,不是对GridLayoutManager进行拓展.
4.创建GridLayoutManager和StaggeredGridLayoutManager的对象的时候,需要传入一个spanCount的参数,如果传入的方向是横向就代表多少横行,传入的方向是竖向,代表多少竖列.
2. 添加分割线
recyclerView.addItemDecoration(RecyclerView.ItemDecoration decor) |
此处需要传入一个分割线ItemDecoration
抽象类的实现类的对象.
v7包中有一个简单的实现类DividerItemDecoration
不过这个实现类只支持传入横向或竖向的的分割线不能同时横竖都显示.
DividerItemDecoration的分割线样式是使用主题中android:listDivider属性指定的样式,如果想更改样式可以在styles.xml中对使用的主题重写该属性:
<item name="android:listDivider">@drawable/divider_bg</item> |
使用自己drawable下的文件作为分割线的样式,可以使用shape自己绘制对应的分割线,具体的查看shape的用法,下面是两个简单的例子:
使用对应颜色值和宽高作为分割线:
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
使用渐变的颜色值作为分割线:
<shape xmlns:android="http://schemas.android.com/apk/res/android" |
如果需要GridLayoutManager横竖都显示分割线,需要自己编写一个类继承ItemDecoration,在onDraw()方法中进行实现.
在onDraw使用参数中RecyclerView的对象获取LayoutManager,然后判断是哪种布局的哪个方向:
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); |
确定方向后即可使用方法中的Canvas绘制分割线,中间可以使用recyclerView的对象调用getChildCount()获取子布局数量,遍历所有子布局.通过获取子布局的坐标确定分割线的坐标
/** |
对ItemDecoration的简单实现包含在RecyclerViewUtils/SimpleDecoration中
给item设置状态选择器后分割线消失的情况
如果需要给item设置状态选择器,这时就只能给item的对象设置background为状态选择器的drawable了,但是上面的分割线实际上是绘制在RecyclerView上的,并不是绘制在item上,所以item的背景肯定会挡住分割线.
只是普通的状态选择器时,可以将默认的颜色设置给recyclerView,然后item的状态选择器的默认颜色设置为透明.
这种方式对5.0以上的水波纹效果的状态选择器不适用,当水波纹状态选择器里面的颜色有透明时就没有水波纹效果了,只能给item的布局中的控件设置margin做出类似分割线的效果或在item中添加view作为分割线.
总的来说就是分割线绘制在recyclerView不在item的view上,item默认为透明的,当设置颜色之后就会挡住分割线,采用上面一些方式避免挡住分割线或在item上显示分割线等多种方法都可以解决.
3. 添加Item添加和移除的动画
recyclerView.setItemAnimator(ItemAnimator animator); |
这里需要传入RecyclerView.ItemAnimator的实现类,需要自己实现可以继承其简单实现类SimpleItemAnimator对其中的方法进行编写.
v7包中提供了简单的实现DefaultItemAnimator.
recyclerView.setItemAnimator(new DefaultItemAnimator()); |
另外对于移除和添加时刷新界面不再是像ListView调用notifyDataSetChanged()
即可,RecyclerView添加时需要调用notifyItemInserted(int position)
,移除item的时候调用notifyItemRemoved(int position)
,另外除了添加和移除,还有item改变的刷新方法:notifyItemChanged(int position)
对于动画的实现并没有深究,更多的内容可以在Git上面找到,比如RecyclerViewItemAnimators,不过运行了之后发现动画效果并不是很明显.
4. 实现RecyclerView.Adapter和RecyclerView.ViewHolder
RecyclerView的adapter和viewHolder已经对item的复用进行了实现,只需要进行继承,在对应的方法中进行对应的操作即可.
1. 实现ViewHolder
继承RecyclerViewHolder后,必须要重写其有参构造,其参数View itemView就是item的View对象,这里需要从其中找到所有需要的对象,并将找到的对象提升为成员变量方便直接使用.
class MyViewHolder extends RecyclerView.ViewHolder { |
2. 实现Adapter
继承RecyclerView.Adapter时,需要传入一个泛型,这个泛型就是实现的ViewHolder,需要重写3个方法:
getItemCount()
需要返回item数量,一般就是传入集合是size
VH onCreateViewHolder(ViewGroup parent, int viewType)
创建ViewHolder的对象并返回
onBindViewHolder(final MyViewHolder holder, final int position)
绑定数据
具体实现例如:
class MyAdapter extends RecyclerView.Adapter<MyViewHolder> { |
实现ViewHolder和Adapter之后设置给RecyclerView就可以显示数据
recyclerView.setAdapter(myAdapter);
5. RecyclerView并没有提供设置Item的点击事件或长按事件,需要自己在Adapter中onBindViewHolder()方法中进行设置.
可以在onBindViewHolder(VH holder, int position);从holder获取整个item的对象,对其设置点击或长按事件.
也可以在Adapter中添加两个接口
public interface OnItemClickListener { |
然后在onBindViewHolder中,设置item的view的点击和长按事件为当onItemClickListener和onItemLongClickListener不为null时,调用接口中的方法
可以参考RecyclerViewUtils/BaseAdapter中的BaseAdapter的部分代码.
6. RecyclerView的下拉刷新和加载更多
RecyclerView的下拉刷新有很多实现方式,在网上可以找到很多实现的方式,Google也提供了官方的android.support.v4.widget.SwipeRefreshLayout,在xml文件中对ListView或RecyclerView进行包裹后即可获取在列表顶部时下拉事件,显示刷新状态的控件.
SwipeRefreshLayout提供了一些方法:
设置刷新控件的背景颜色:
refreshLayout.setProgressBackgroundColorSchemeResource(int colorRes); |
设置刷新进度条的颜色,可以传入多个颜色值,会依次显示:
refreshLayout.setColorSchemeResources(); |
设置当前是否显示刷新控件:
setRefreshing(boolean refreshing); |
RecyclerView加载更多网上也有很多实现方式,可以自行寻找需求需要的样式,在这里提供一个Git上对SwipeRefreshLayout进行修改,不仅支持下拉刷新也支持上拉加载更多.SwipyRefreshLayout
为了和条目拖动兼容和方便使用进行了少量修改,复制到了RecyclerViewUtils/SwipyRefreshLayout中.
可以在xml中使用属性app:refresh_mode="top/bottom/both"
表示支持哪些模式
或者在代码中调用
setRefreshMode(SwipyRefreshLayout.TOP|SwipyRefreshLayout.BOTTOM |SwipyRefreshLayout.BOTH); |
其他使用方式与SwipeRefreshLayout基本一样.
7. RecyclerView的条目拖动和侧拉删除
网上也有很多其他实现方式,这里提供一个Google提供的ItemTouchHelper的用法,7.0系统设置中语音设置那里的item的上下拖动就是使用该类实现的.
创建ItemTouchHelper对象需要传入一个ItemTouchHelper.Callback子类对象,所以首先需要编写一个ItemTouchHelper.Callback的继承类
1. 在getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)方法中指定可以支持的拖放和滑动的方向,调用makeMovementFlags(int dragFlags, int swipeFlags)来返回
在是表格布局的时候只支持上下左右的条目拖动不支持侧拉删除,在LinearLayoutManager的情况下左右滑动时为侧拉删除和上下的时候为条目拖动
/** |
2. 在移动条目的时候会调用onMove方法,在侧拉时会调用onSwiped方法,所以在此方法中对数据进行操作.
如果是内部类的形式可以直接调用外部的adapter的对象,不是内部类的时候需要将adapter通过构造方法传入.
在onMove方法中可以通过参数获取到起始位置的position和目标位置的position
获取初始位置 viewHolder.getAdapterPosition()
获取目标位置 target.getAdapterPosition()
拿到position后交换集合中的数据并刷新界面:
Collections.swap(list, fromPosition, toPosition); |
在onSwiped中也是同样获取到侧拉条目的position
viewHolder.getAdapterPosition() |
拿到position后移除集合中的数据并刷新界面:
list.remove(position); |
另外也可以采用回调的形式,只需要把索引传入对应的方法即可
具体可以查看RecyclerViewUtils中的ItemTouchDataCallBack和SimpleItemTouchCallBack以及BaseAdapter
另外在ItemTouchHelper.Callback继承类中还有一些方法:
决定是否支持长按拖动 isLongPressDragEnabled(); |
上面的方法都可以进行重写后添加逻辑或进行更改,重写后如果不是完全对逻辑进行了修改需要保留super语句.
如果想通过触摸某个控件触发条目的拖动
只需要对对应的控件设置setOnTouchListener(),当事件为点击时调用ItemTouchHelper的startDrag(ViewHolder viewHolder)开启拖动:
view.setOnTouchListener(new View.OnTouchListener() { |
创建完ItemTouchHelper后最后需要与recyclerView关联绑定:
touchHelper.attachToRecyclerView(recyclerView); |
8. 多类型的item的显示
在RecyclerView.Adapter中getItemViewType(int position)方法返回的是表示item的类型的type.
对item进行复用时会根据getItemViewType(int position)返回的判断是否是对应的item才进行复用.
当要实现多类型item的时候需要自行对type进行管理,使用集合中的数据判断是否是对应的type然后决定使用哪种对应的item的布局,
在onCreateViewHolder(ViewGroup parent, int viewType)方法中,也可以看到传来的参数有item的type类型.
1. 使用接口IType表示每种item的Type的实现,在接口中提供下面的方法:
获取对应的item的layout的id的方法: int getLayoutId(); |
2. 将所有的type在adapter中通过集合来管理,提供添加type的方法. 然后重写getItemViewType()方法
对集合进行遍历,根据索引获取集合中对应的数据,使用数据判断是否是此type类型的item
private SparseArrayCompat<IType<T>> types = new SparseArrayCompat<>(); |
3. 在创建viewHolder的方法onCreateViewHolder(ViewGroup parent, int viewType)中根据viewType获取集合中对应的IType对象,获取对应的layout的id创建ViewHolder对象.
/** |
4. 在绑定数据的方法中遍历type的集合,根据position获取集合中对应的数据,根据数据判断是否是对应的type,是则调用IType中的设置数据的方式进行数据赋值.
/** |
这样就完成了多类型item的adapter,会自动根据传入adapter的集合中每条数据的区别显示对应的item.
具体内容可以查看IType, MViewHolder; BaseAdapter
对上述内容的综合封装
封装的逻辑基本与上述相同,进行了少量的修改.比如:
- SwipeRefreshLayout是在顶部或底部时捕捉下拉和上拉事件打到目的,所以在最顶部和最底部的时候拖动条目会产生冲突,为了兼容条目拖动进行了少量修改.
- 将ItemTouchHelper封装到Adapter中,将通过指定控件设置触发拖动的方法封装到ViewHolder部分.
- 对于单类型item将多type类型进行了一个简单的实现,更方便单类型item的实现等
更多具体的内容有兴趣的话可以查看RecyclerViewUtils/library中的代码.
使用案例可以查看:RecyclerViewUtils/sample中的代码.
这里主要对library的用法进行说明:
单类型item:
创建SimpleAdapter对象,设置给RecyclerView即可:
/** |
比如:
SimpleAdapter simpleAdapter = new SimpleAdapter<String>(this, R.layout.item_simple, list) { |
多类型item
1. 如果没有其他特殊需求可以直接new出BaseAdapter的对象.如果有其他需求可以继重写部分方法.
可以重写其中的onViewHolderCreated()
方法在创建完ViewHolder后做一些操作;
可以重写其中的isClickEnabled()
设置哪些type不可点击.
2. 向adapter中添加IType的实现类
在getLayoutId()
返回对应itemType的布局的id
在isThisTypeItem()
提供判断是否是对应itemType的方法.
在setData()
对item中对应id的控件设置对应数据.可以使用MViewHolder中提供的一系列方法进行链式调用
比如:集合中为String类型,分别有数字和其他字符类型,将数字类型显示为一种item,其他字符显示为一种item.
adapter = new BaseAdapter<String>(this, list) { |
如果需要设置分割线,可以使用SimpleDecoration.
/** |
如果需要开启条目拖动和条目侧拉删除,调用adapter中的openItemTouch方法
/** |
比如:
simpleAdapter.openItemTouch(recyclerview, true, true, swipyrefreshlayout); |
如果要设置指定控件触发条目拖动
在SimpleAdapter的setItemData()
方法或IType实现类的setData()
方法中,使用holder调用setDragListener(int id)
即可,需要adapter调用上面的openItemTouch()
方法,否则不会生效.
比如:holder.setDragListener(R.id.iv_hand);
如果需要设置item的点击和长按事件
点击和长按事件封装在BaseAdapter中,如果同时设置了长按拖动和长按点击事件,会同时触发.
adapter.setOnItemLongClickListener(this); |
如果要添加下拉刷新或上拉加载更多
在布局文件中使用org.nesscurie.recyclerviewutils.SwipyRefreshLayout
节点包裹RecyclerView,在xml中设置对应的refresh_mode
或在代码中使用setRefreshMode(int)
设置.
<org.nesscurie.recyclerviewutils.SwipyRefreshLayout |
可以使用setColorSchemeResources()设置刷新控件中间的进度条的颜色,支持多个颜色,会依次出现
swipyrefreshlayout.setColorSchemeResources(android.R.color.holo_blue_bright, |
可以使用setProgressBackgroundColor()设置刷新控件的背景.
swipyrefreshlayout.setProgressBackgroundColor(android.R.color.darker_gray); |
设置刷新监听的回调,根据传来的参数判断是下拉刷新还是上拉加载更多
swipyrefreshlayout.setOnRefreshListener(this); |
刷新完毕后调用swipyrefreshlayout.setRefreshing(false);
隐藏刷新控件
这次对RecyclerView的总结和Adapter的封装主要目的是加深对RecyclerView各需求的熟悉程度,另外锻炼一下封装的思维.RecyclerView出来的时间也挺久了,网络上对其的各种需求都有比较详细的讲解.有其他需要可以在网络上找到很多文章.