RecyclerView ItemDecoration 完全解析
Posted 星火燎原2016
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecyclerView ItemDecoration 完全解析相关的知识,希望对你有一定的参考价值。
我们都知道,使用 RecyclerView 时 ,我们不能像 ListView 那样通过 setDivider() 的方式来设置分割线,好在 android 为我们提供了定制性更强的 ItemDecoration 来为 RecyclerView 设置分割线。
什么是 ItemDecoration ?
顾名思义 ItemDecoration 就是 Item 的装饰,我们可以在 Item 的上下左右添加自定义的装饰,比如 横线,图案。同时系统已经为我们提供了一个 DividerItemDecoration, 如果这个 DividerItemDecoration 不满足我们的需求,我们就可以通过自定义 ItemDecoration 来实现了。
下面我们看下系统的 DividerItemDecoration :
DividerItemDecoration(系统提供)
DividerItemDecoration 的使用非常简单,只需添加下面代码即可:
DividerItemDecoration decoration = new DividerItemDecoration(this,DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(decoration);
效果:
如果想要修改 DividerItemDecoration 的颜色和高度,可以调用它的 setDrawable(drawable) 设置一个 Drawable 对象
// MainActivity.java
DividerItemDecoration decoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL);
Drawable dividerDrawable = getResources().getDrawable(R.drawable.drawable_divider);
decoration.setDrawable(dividerDrawable);
recyclerView.addItemDecoration(decoration);
// res/drawable/drawable_divider.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
android:shape="rectangle">
<!-- 渐变色 -->
<gradient
android:angle="135"
android:centerColor="#4CAF50"
android:endColor="#2E7D32"
android:startColor="#81C784"
android:type="linear" />
<size android:height="1dp" />
</shape>
效果:
自定义 ItemDecoration
如果上述 DividerItemDecoration 不能满足我们的需求,那么我们就可以自定义 ItemDecoration ,ItemDecoration 的自定义主要需要重写以下三个方法:
/**
* 自定义 ItemDecoration
*/
public class LinearItemDecoration extends RecyclerView.ItemDecoration
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.getItemOffsets(outRect, view, parent, state);
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(c, parent, state);
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDrawOver(c, parent, state);
1. getItemOffsets()
getItemOffsets() 主要作用是在 item 的四周留下边距,效果和 margin 类似,item 的四周留下边距后,我们就可以通过 onDraw() 在这个边距上绘制了。
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
(1) 参数 Rect outRect : 表示 item 的上下左右所留下的边距。其中 outRect 的 left, top,right,bottom 即为 item 四周留下的边距的距离,默认都为 0 ;示意图如下:
(2) 参数 View view : 指当前 item 的 View 对象;
(3) 参数 RecyclerView parent : 指 RecyclerView 本身;
(4) RecyclerView.State state : 指 RecyclerView 当前的状态;
1.1 getItemOffsets() 应用例子:
既然 getItemOffsets(Rect outRect) 方法可以设置 item 四周的边距大小,那就可以设置 recyclerview 背景色和 item 四周的边距,使得 item 四周的边距透出 recyclerview 背景色来达到分割线的目的。
当然 item 的背景色需要和 recyclerview 的背景色不一致才有效果;
首先将 recyclerview 的背景色设置为 colorAccent 红色, 将 item 的背景色设置为 白色:
<!--- activity_main.xml ----->
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
</android.support.constraint.ConstraintLayout>
<!--- item_recycler.xml ----->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#fff"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical" />
</LinearLayout>
然后继承 ItemDecoration 类,重写 getOffsets() 方法,将 outRect 的上边距 top 设置为 10px;
/**
* 自定义 ItemDecoration
*/
public class LinearItemDecoration extends RecyclerView.ItemDecoration
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.getItemOffsets(outRect, view, parent, state);
outRect.top = 10; // 10px
最后将这个 LinearItemDecoration 添加到 RecyclerView 中:
LinearItemDecoration decoration = new LinearItemDecoration();
recyclerView.addItemDecoration(decoration);
效果如下(下方的红色是 因为RecyclerView 高度为 match_parent ,但 item 数据只有 5 条):
从效果图可以看出:每一个 item 的 顶部都有一个红色的背景线,包括第一个 item 顶部也有(怎么解决呢?见 2.1 节详解),
同理,我们可以设置为 底部 10px,左侧 20px ,右侧 40px;
public class LinearItemDecoration extends RecyclerView.ItemDecoration
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.getItemOffsets(outRect, view, parent, state);
outRect.bottom = 10;
outRect.left = 20;
outRect.right = 40;
效果:
可以看到:每个 item 的左测,底部,右侧都有了间距,露出了 RecyclerView 的背景色了。
2. onDraw()
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(canvas, parent, state);
onDraw() 函数中的 parent , state 参数和 getItemOffsets() 方法中的参数含义是一样的,canvas 参数是 getItemOffsets() 函数所留下的左右上下的空白区域对应的 Canvas 画布对象。我们可以在这个区域中利用 Paint 画笔绘制任何图形。
比如在 item 左侧绘制一个 空心圆。
/**
* 自定义 ItemDecoration
*/
public class LinearItemDecoration extends RecyclerView.ItemDecoration
private static final String TAG = "LinearItemDecoration";
private Paint paint;
public LinearItemDecoration()
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.getItemOffsets(outRect, view, parent, state);
Log.e(TAG, "getItemOffsets: " );
outRect.bottom = 10;
outRect.left = 100;
outRect.right = 40;
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(canvas, parent, state);
Log.e(TAG, "onDraw: ");
for (int i = 0; i < parent.getChildCount(); i++)
View childView = parent.getChildAt(i);
canvas.drawCircle(50, childView.getTop() + childView.getHeight() / 2, 20, paint);
效果:
我们在 getItemOffsets() 和 onDraw() 方法中都添加了日志,日志打印如下:
从日志中可以看出: getItemOffsets() 方法执行了 5 遍,和 数据源个数是一样的,但 onDraw() 方法只执行了一遍,由此我们知道, getItemOffsets() 是针对每个 item 都会执行一次,也就是说 每个 item 的 outRect 可以设置为不同值,但是 onDraw(),onDrawOver() 是针对 ItemDecoration 的,不是针对 item 的,只执行一次。所以我们在 onDraw() ,onDrawOver() 中绘制的时候,需要遍历每个 item 进行绘制。
优雅获取 outRect 中的值
在上面例子中,我们在 onDraw 中获取 outRect 中的值都是写成计算好的固定值,显然这种硬编码的方式不利于扩展,其实,我们可以通过 LayoutManager 来获取 getItemOffsets() 中设置的 outRect 的值。
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(canvas, parent, state);
Log.e(TAG, "onDraw: ");
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
for (int i = 0; i < parent.getChildCount(); i++)
View childView = parent.getChildAt(i);
int leftDecorationWidth = layoutManager.getLeftDecorationWidth(childView);
int topDecorationHeight = layoutManager.getTopDecorationHeight(childView);
int rightDecorationWidth = layoutManager.getRightDecorationWidth(childView);
int bottomDecorationHeight = layoutManager.getBottomDecorationHeight(childView);
上面硬编码可以改成:
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(canvas, parent, state);
Log.e(TAG, "onDraw: ");
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
for (int i = 0; i < parent.getChildCount(); i++)
View childView = parent.getChildAt(i);
int leftDecorationWidth = layoutManager.getLeftDecorationWidth(childView);
int left = leftDecorationWidth / 2;
canvas.drawCircle(left, childView.getTop() + childView.getHeight() / 2, 20, paint);
2.1 扩展1 – 减少背景设置,避免过度绘制
为了减少过度绘制,我们将 activity_main 中 RecyclerView , item_recycler 中的背景全部去掉,不设置任何背景,然后在 LinearItemDecoration 中进行纯绘制分割线,代码如下:
public class LinearItemDecoration extends RecyclerView.ItemDecoration
private static final String TAG = "LinearItemDecoration";
private Paint paint;
private Paint dividerPaint;
public LinearItemDecoration()
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
dividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
dividerPaint.setColor(Color.parseColor("#e6e6e6"));
dividerPaint.setStyle(Paint.Style.FILL);
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.getItemOffsets(outRect, view, parent, state);
Log.e("ItemOffsets", "getItemOffsets: ");
outRect.bottom = 5;
outRect.left = 100;
@Override
public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDraw(canvas, parent, state);
Log.e(TAG, "onDraw: ");
RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
for (int i = 0; i < parent.getChildCount(); i++)
View childView = parent.getChildAt(i);
int leftDecorationWidth = layoutManager.getLeftDecorationWidth(childView);
int topDecorationHeight = layoutManager.getTopDecorationHeight(childView);
int rightDecorationWidth = layoutManager.getRightDecorationWidth(childView);
int bottomDecorationHeight = layoutManager.getBottomDecorationHeight(childView);
int left = leftDecorationWidth / 2;
canvas.drawCircle(left, childView.getTop() + childView.getHeight() / 2, 20, paint);
// getItemOffsets()中的设置的是 bottom = 5px;所以在 drawRect 时,top 为 childView.getBottom,bottom为top+bottomDecorationHeight
canvas.drawRect(new Rect(
leftDecorationWidth,
childView.getBottom(),
childView.getWidth() + leftDecorationWidth,
childView.getBottom() + bottomDecorationHeight
), dividerPaint);
@Override
public void onDrawOver(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)
super.onDrawOver(canvas, parent, state);
效果:
注意: 上面 getItemOffsets() 中设置的是 bottom = 5px; 所以在 onDraw() 方法的 drawRect 时,top值为 childView.getBottom, bottom值为 top+bottomDecorationHeight。
同理:getItemOffsets() 中设置是 top = 5px, 那么在 onDraw() 方法 drawRect 时,bottom 值为 childView.getTop() , top 值为 bottom - topDecorationHeight
2.2 扩展2 – 实现竖直进度分割线
在 getItemOffsets() 方法中左侧留下空白区域,然后在 onDraw() 方法中绘制 圆和 竖线。代码如下:
<!--- item_recycler.xml ---->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:gravity="center_vertical"
android:orientation="vertical"
android:padding="10dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#332"
android:textSize="16sp"
tools:text="我是 title" />
<TextView
android:id="@+id/tv_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_vertical"
android:textColor="#666"
android:textSize="14sp"
以上是关于RecyclerView ItemDecoration 完全解析的主要内容,如果未能解决你的问题,请参考以下文章
RecyclerView系列:RecyclerView嵌套RecyclerView(BaseRecyclerViewAdapterHelper实现)