0%

RecyclerView添加分割线

RecyclerView 并没有 divider 属性,但是我们可以通过 RecyclerView 的 addItemDecoration() 来添加分割线,该方法参数为 RecyclerView.ItemDecoration。

介绍

当 RecyclerView 添加 ItemDecoration 后,RecyclerView 在绘制每个 item 的时候,会去绘制 decorator,也就是会调用 ItemDecoration 的 onDraw() 和 onDrawOver() 方法。

RecyclerView.ItemDecoration 是抽象类,主要提供三个方法:

  • onDraw(Canvas c, RecyclerView parent, State state): 在绘制item(drawChild) 前调用
  • onDrawOver(Canvas c, RecyclerView parent, State state): 在绘制item(drawChild) 后调用
  • getItemOffsets(Rect outRect, View view, RecyclerView parent, State state):outRect设置 item 的偏移量,用于绘制 decorator(也就是divider)

关于 getItemOffsets 函数:
RecyclerView 添加分割线,实际上就是 RecyclerView 的 item 之间添加了用作分割线的View,自然而然后续的 item 就会有偏移量,所以用 getItemOffsets 中的 outRect 来保存 item 的偏移量,从而便于绘制 decorator。

实现

实际上在当前版本的 RecyclerView (25.3.1) 中已经有 ItemDecoration 关于分割线的默认实现类 DividerItemDecoration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package android.support.v7.widget;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.view.View;
import android.widget.LinearLayout;

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;

// 如果不设置,则默认的分割线为 android.R.attr.listDivider 指定的 drawable
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };

private Drawable mDivider;

/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;

private final Rect mBounds = new Rect();

/**
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
* {@link LinearLayoutManager}.
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
setOrientation(orientation);
}

/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}

/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}

// 绘制 RecyclerView 为垂直布局时的分割线,此时分割线为水平分割线
@SuppressLint("NewApi")
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
// 需要考虑clipToPadding的boolean值
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(ViewCompat.getTranslationY(child));
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}

// 绘制 RecyclerView 为水平布局时的分割线,此时分割线为垂直分割线
@SuppressLint("NewApi")
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
// 需要考虑clipToPadding的boolean值
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();
}

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(ViewCompat.getTranslationX(child));
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mOrientation == VERTICAL) { // 垂直方向的RecyclerView, item 的 bottom 偏移量 = 分割线高度
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else { // 水平方向的RecyclerView, item 的 right 偏移量 = 分割线宽度
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}

在代码中添加:

1
recyclerView.addItemDecoration(new DividerItemDecoration(recyclerView.getContext(), mLayoutManager.getOrientation()));

就有了分割线。 默认的分割线效果是系统自带的 listDivider 的效果,我们也可以在主题配置文件中自定义全局的分割线,或者调用 setDivider 为每个 RecyclerView 设置单独的分割线。

网络流行代码存在的问题

目前好多博客中关于 DividerItemDecorationdrawVertical()drawHorizontal() 方法与官方的方法其实是有出入的:

public void drawVertical(Canvas c, RecyclerView parent) {
    final int left = parent.getPaddingLeft();
    final int right = parent.getWidth() - parent.getPaddingRight();
    // 网上的方法最主要的问题是没有考虑 clipToPadding 这个参数,所以说这里缺少相应代码片段

    final int childCount = parent.getChildCount();
    // 下面这块没什么问题,和官方方案殊途同归
    // 官方的getDecoratedBoundsWithMargins实际上也是通过 LayoutParams 来获取分割线边界的
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int top = child.getBottom() + params.bottomMargin;
        final int bottom = top + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

public void drawHorizontal(Canvas c, RecyclerView parent) {
    final int top = parent.getPaddingTop();
    final int bottom = parent.getHeight() - parent.getPaddingBottom();

    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                .getLayoutParams();
        final int left = child.getRight() + params.rightMargin;
        final int right = left + mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(c);
    }
}

注释中说的比较明确了,网上好多方法主要的问题在于 没有考虑 clipToPadding 属性值clipToPadding 表示控件的绘制区域是否在 padding 区域外面,其默认值为 true。比如,垂直方向的 RecyclerView,当 clipToPadding=false 时,其初始绘制区域与 padding 值有关,但向上滑动时,RecylerView 的 item 会滑到 padding 区域里面。

下面用示意图来进行解释,RecyclerView 的 paddingTop = 40dp, clipToPadding = false, 下图中白色区域为 paddingTop 区域:

初始状态下 向上滑动

小结

总的来说,目前添加分割线只需要使用 recyclerview-v7 包下的 DividerItemDecoration 类即可,分割线可以通过 setDivider 来个性化指定,也可以通过配置主题中的 android:listDivider 来全局指定。

参考

Android RecyclerView 使用完全解析 体验艺术般的控件
RecyclerView系列之二:添加分隔线
android:clipToPadding和android:clipChildren