RecyclerView是support v7中提供的一个控件,可以说是listView和GridView的增强版,提供了一种插拔式的体验。提供了三个设置方法来供我们快速做出一些我们想要的效果效果。
- LayoutManager:控制的是RecyclerView的显示方式。
- ItemDecoration:绘制Item之间的间隔,并在特定时间绘制一些我们想要的东西。
- ItemAnimator:控制Item增删的动画。
今天我们来学习一下RecyclerView的基本使用方法。我们想要使用它,首先我们要在app/build.gradle文件中添加依赖。
implementation 'com.android.support:recyclerview-v7:27.1.1'
添加好布局之后,和我们的listView一样,要创建Adapter和ViewHolder。和listView不同Recycler提供了内部抽象类为我们来继承,RecyclerView.Adapter
class MyAdapter extends RecyclerView.Adapter{
private List userList;
public MyAdapter(List userList) {
this.userList = userList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_main,parent,false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.tvName.setText("Name:"+userList.get(position).getName()); holder.tvAge.setText("Age:"+userList.get(position).getAge());
}
@Override
public int getItemCount() {
return userList.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
private TextView tvName ;
private TextView tvAge;
public ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tv_name);
tvAge = itemView.findViewById(R.id.tv_age);
}
}
}
我们可以看到 我们的MyAdapter继承自RecyclerView.Adapter,重写了三个方法。
- onCreateViewHolder():用来创建ViewHolder实例,我们在这个方法中将item布局加载进来,并创建一个ViewHolder实例,将我们的item布局传入到ViewHolder的构造方法中,并将ViewHolder返回。
- onBindViewHolder():通过ViewHolder拿到的item布局中的View进行赋值,写过listView的你对这一步一定不会陌生。
- getItemCount():;返回数据集的个数,也就是我们RecyclerView的item的个数。
ViewHolder的实现,我们继承RecyclerView.ViewHolder,重写了构造方法。
- 在构造方法中我们拿到Item的View。进行findViewById查找到我们索要用的控件。方便我们进行使用。
接下来我们在Activity中找到我们到RecyclerView进行使用。
//创建一个存储User数据的集合
List userList = new ArrayList<>();
//在布局中找到我们的RecyclerView
rclMain = findViewById(R.id.rcl_main);
//增加一些User假数据,放在我们的集合当中
for (int i=0 ;i<=30 ;i++){
userList.add(new User("张"+i,20+i));
}
//为我们的RecyclerView设置Adapter
rclMain.setAdapter(new MyAdapter(userList));
//设置我们的LayoutManager为线性布局竖直方向的
rclMain.setLayoutManager(new LinearLayoutManager(this));
//设置我们的Item之间的间隔为DividerItemDecoration为竖直方向。如需修改其默认样式,
//可以修改 中名字为android:listDivider的属性。
rclMain.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
以上的效果和listView完全相同,并且Item之间的间隔是一个白色的一条线。大家可以看一下1-1的图。
LayoutManager
RecyclerView 中设置LayoutManager方法的参数是RecyclerView.LayoutManager吧,这是一个抽象类,好在系统提供了3个实现类:
- LinearLayoutManager 现行管理器,支持横向、纵向。(默认)
- GridLayoutManager 网格布局管理器。
- StaggeredGridLayoutManager 瀑布就式布局管理器。
如果我们想做一个横向listView的效果也非常简单,稍微修改一下Item布局,将我们设置的LayoutManager设置成横向的LinearLayoutManager就可以了
rclMain.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
GridLayoutManager 实现纵向的网格布局。
//设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.VERTICAL,false));
GridLayoutManager 实现横向的网格布局。
//设置列数为3的纵向网格布局,注意 一下网格中间的间隔我这里设置的是item的margin实现的。我们上边Demo中使用的DividerItemDecoration,在这里是无法使用的,因为网格布局的间隔是纵向与横向都要的,而DividerItemDecoration只能设置纵向或者横向。无法同时设置
rclMain.setLayoutManager(new GridLayoutManager(this,3,GridLayoutManager.HORIZONTAL,false));
StaggeredGridLayoutManager实现瀑布流,已经看过上面的实现网格布局和线性list布局,我相信实现网格布局对你来说已经非常之简单了。如何随机设置item的高度保证item每个都不一样高。这里就不贴出来了。相信一定难不倒你的。
//设置数量和方向,我们设置的是3列纵向布局。
rclMain.setLayoutManager(new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL));
只要修改一行代码就能实现三种不同的布局方式,Recycler的强大你是不是已经体会到了。反正我早已经被他震撼到了。
ItemDecoration
我们可以使用addItemDecoration(Recycler.ItemDecoration)添加item之间要绘制的东西。RecyclerView.ItemDecoration是抽象类,Android只提供了DividerItemDecoration一个实现类用来给使用LinearLayoutManager的RecyclerView绘制Item之间的间隙。我们可以修改APPTheme 中的android:listDivider来修改我们想要的间隙样式
values/styles.xml
divider_bg.xml
而Android提供的这个不能满足我们所有的需求,比如我们想给我们的网格布局添加item之间的间隔就不能使用DividerItemDecoration,我们只能自己来实现了。继承RecyclerView.ItemDecoration。首先我们来了解一下RecyclerView.ItemDecoration中的几个方法的含义
- getItemOffests:可以通过outRect.set(l,t,r,b)设置指定itemview的paddingLeft,paddingTop, paddingRight, paddingBottom
- onDraw:可以通过一系列c.drawXXX()方法在绘制itemView之前绘制我们需要的内容。可以理解为在Item下层 像BackGround
- onDrawOver:与onDraw类似,只不过是在绘制itemView之后绘制,具体表现形式,就是绘制的内容在itemview上层。
我们只设置item间隔的话 是不需要进行onDraw和onDrawOver操作的,除非你想在你要设置的这个间隔中绘制一些想要的东西。而想要设置Item之间的间隔,只要通过计算我们就可以通过getItemOffests设置item的padding就能够实现我们的效果。首先我们来看一下我们具体的实现思路。
当orientation为vertical时,我们需要在getItemOffsets方法中计算每个Item的PaddingLeft,以及PaddingRight,保证每个Item的paddingLeft+paddingRight相等,这样才能达到均分的目的。
这里我们可以用数字的带入来计算我们每个item的padding规律,从而推导出我们要的公式。这里我也是查看的大神的博客,如果想知道具体规则可在本章底部找到答案。
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
RecyclerView.LayoutManager manager = parent.getLayoutManager();
int childPosition = parent.getChildAdapterPosition(view);
int itemCount = parent.getAdapter().getItemCount();
if (manager != null) {
if (manager instanceof GridLayoutManager) {
// manager为GridLayoutManager时
setGridOffset(((GridLayoutManager) manager).getOrientation(), ((GridLayoutManager) manager).getSpanCount(), outRect, childPosition, itemCount);
}
}
}
/**
* 设置GridLayoutManager 类型的 offest
*
* @param orientation 方向
* @param spanCount 个数
* @param outRect padding
* @param childPosition 在 list 中的 postion
* @param itemCount list size
*/
private void setGridOffset(int orientation, int spanCount, Rect outRect, int childPosition, int itemCount) {
float totalSpace = mSpace * (spanCount - 1) + mEdgeSpace * 2; // 总共的padding值
float eachSpace = totalSpace / spanCount; // 分配给每个item的padding值
int column = childPosition % spanCount; // 列数
int row = childPosition / spanCount;// 行数
float left;
float right;
float top;
float bottom;
if (orientation == GridLayoutManager.VERTICAL) {
top = 0; // 默认 top为0
bottom = mSpace; // 默认bottom为间距值
if (mEdgeSpace == 0) {
left = column * eachSpace / (spanCount - 1);
right = eachSpace - left;
// 无边距的话 只有最后一行bottom为0
if (itemCount / spanCount == row) {
bottom = 0;
}
} else {
if (childPosition < spanCount) {
// 有边距的话 第一行top为边距值
top = mEdgeSpace;
} else if (itemCount / spanCount == row) {
// 有边距的话 最后一行bottom为边距值
bottom = mEdgeSpace;
}
left = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
right = eachSpace - left;
}
} else {
// orientation == GridLayoutManager.HORIZONTAL 跟上面的大同小异, 将top,bottom替换为left,right即可
left = 0;
right = mSpace;
if (mEdgeSpace == 0) {
top = column * eachSpace / (spanCount - 1);
bottom = eachSpace - top;
if (itemCount / spanCount == row) {
right = 0;
}
} else {
if (childPosition < spanCount) {
left = mEdgeSpace;
} else if (itemCount / spanCount == row) {
right = mEdgeSpace;
}
top = column * (eachSpace - mEdgeSpace - mEdgeSpace) / (spanCount - 1) + mEdgeSpace;
bottom = eachSpace - top;
}
}
outRect.set((int) left, (int) top, (int) right, (int) bottom);
}
接下来是onDraw和onDrawOver的用法。这里我制作简单的介绍,如果你想要做出一些不一样的内容。文章末尾我会给出几个比较好的博客,供大家欣赏。
首先我们要知道onDraw和onDrawOver的执行时间,一下是RecyclerView绘制流程的源码
/**
* RecyclerView的draw方法
* @param c
*/
@Override
public void draw(Canvas c) {
// 调用父类也就是View的draw方法
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 执行ItemDecorations的onDrawOver方法
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
}
/**
* View的draw方法
* @param canvas
*/
@CallSuper
public void draw(Canvas canvas) {
....
// View会继续调用onDraw
if (!dirtyOpaque) onDraw(canvas);
....
}
/**
* RecyclerView的onDraw方法
* @param c
*/
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 执行ItemDecorations的onDraw方法
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
View先会调用draw方法,在draw中又会调用onDraw方法。 而在RecyclerView的draw方法中会先通过super.draw() 调用父类也就是View的draw方法,进而继续调用RecyclerView的OnDraw方法,ItemDecorations的onDraw方法就在此时会被调用,RecyclerView执行完super.draw()之后,ItemDecorations的onDrawOver方法也被调用,这也就解释了为什么说onDraw会绘制在itemview之前,表现形式是在最底层(抽象的说法,最底层应该是background),onDrawOver是在itemview绘制之后,表现形式在最上层。知道了onDraw和onDrawOver的具体执行时间与含义。我们这里再来看Android提供给我们的DividerItemDecoration实现我们listView的Divider效果是如何做到的
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
原理很简单,总结起来就是通过getItemOffsets方法来设置我们item的paddingBottom或paddingRight,然后在我们onDraw方法中遍历我们的item并在我们需要的区域内绘制我们想要的绘制的内容。
setItemAnimator
设置我们的Item动画效果。Android提供了一个默认的实现。DefaultItemAnimator,添加以下代码我们就可以了。
rclMain.setItemAnimator(new DefaultItemAnimator());
添加了这行代码我们再运行我们的代码发现,完全木有效果啊,什么鬼。
这里我忘记和大家说了。如果我们想要看到效果,就在我们的数据集合中添加删除数据。并执行notifyItemInserted(position)或notifyItemRemoved(position) ,就可以看到效果啦。当然这一种效果怎么可能满足的了我们产品的需求呢,幸好我们有大神。大神们已经开源了很多有关的代码。这就要大家自己去挖掘了。
点击事件
因为RecyclerView没有给我们提供onItemClickListener的方法。所以这个只能我们自己来实现,具体实现比较简单,和我们ListView,Item中设置点击按钮的实现方式是一样的。我们只需要自己在RecyclerView中写一个onclickListener监听器,并提供出去,在BindViewHolder的时候设置ItemView的点击事件,调用我们监听器的方法回调给我们的使用者就可以了。
class HomeAdapter extends RecyclerView.Adapter
{
//...
public interface OnItemClickLitener
{
void onItemClick(View view, int position);
void onItemLongClick(View view , int position);
}
private OnItemClickLitener mOnItemClickLitener;
public void setOnItemClickLitener(OnItemClickLitener mOnItemClickLitener)
{
this.mOnItemClickLitener = mOnItemClickLitener;
}
@Override
public void onBindViewHolder(final MyViewHolder holder, final int position)
{
holder.tv.setText(mDatas.get(position));
// 如果设置了回调,则设置点击事件
if (mOnItemClickLitener != null)
{
holder.itemView.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemClick(holder.itemView, pos);
}
});
holder.itemView.setOnLongClickListener(new OnLongClickListener()
{
@Override
public boolean onLongClick(View v)
{
int pos = holder.getLayoutPosition();
mOnItemClickLitener.onItemLongClick(holder.itemView, pos);
return false;
}
});
}
}
//...
}
知道了如何设置Item的点击事件,我相信设置item中单独View的点击事件也一定难不住你了。
不同类型的Item
我们可以根据RecyclerView.Adapter提供的getItemViewType();方法根据Position位置返回我们不同的Type类型。
在onCreateViewHolder中根据不同的type创建不同的布局和ViewHolder并返回。在onBindViewHolder中根据ViewHodler instanceof方法获取他数据哪个ViewHolder来进行赋值操作,例子如下:
lass HomeAdapter extends RecyclerView.Adapter {
private static final int TYPE_HEADER = 1;
private static final int TYPE_ITEM = 2;
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER) {
//...
return new HeaderViewHolder(v);
} else {
//...
return new MyViewHolder(v);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HeaderViewHolder) {
//...
} else if (holder instanceof MyViewHolder) {
//...
}
}
@Override
public int getItemCount() {
return mDatas.size() + 1; //增加头部ItemView
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return TYPE_HEADER;
} else {
return TYPE_ITEM;
}
}
class HeaderViewHolder extends RecyclerView.ViewHolder {
//...
}
class MyViewHolder extends RecyclerView.ViewHolder {
//...
}
}
参考文章:https://www.jianshu.com/p/bb09d3ddc64f
https://blog.csdn.net/lmj623565791/article/details/45059587
https://www.jianshu.com/p/e742df6f59e2
https://www.jianshu.com/p/6a093bcc6b83
https://www.jianshu.com/p/3221b5c8fc38