Android 开发者都知道,RecyclerView
是一个非常强大的组件,它的最基本作用是代替原来的 ListView
,但又由于其灵活性,RecyclerView
能够帮我们实现许多复杂的页面布局方案。
用一句话概括就是,ListView
能实现的 RecyclerView
也能实现,ListView
实现不了的,RecyclerView
也能实现。
相信很多 Android 初学者也跟我一样,是从郭霖的《第一行代码——Android》这本书开始的,不得不说这是一本非常适合入门的书籍,但在里面介绍 RecyclerView
的章节中,郭霖并没有介绍过 RecyclerView
分割线的实现方法,而在第二版第 14 章实战『CoolWeather』项目中则有如下描述:
之所以这次使用了
ListView
,是因为它会自动给每个子项之间添加一条分隔线,而如果使用RecyclerView
想实现同样的功能则会比较麻烦,这里我们总是选择最优的实现方案。
的确,在默认情况下,ListView
是有分割线的,而 RecyclerView
却没有:
郭霖在书中对于 RecyclerView
界面的调整,大多是用 CardView
来实现,以致于我在后面很长一段时间内以为 RecyclerView
设置分割线需要写一些比较复杂的代码,但随着不断的学习,我发现这个理论实际上是错误的,RecyclerView
其实也能够轻松地设置分割线。
RecyclerView
设置分割线是通过 addItemDecoration()
方法来添加的,该方法接收一个 ItemDecoration
对象,ItemDecoration
是一个抽象类,而 RecyclerView
中恰好有一个默认的实现,也就是 DividerItemDecoration
,所以我们可以直接使用:
recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
DividerItemDecoration
的构造函数接收两个参数,第一个是 Context
,用于访问资源,第二个 int
类型的参数表示分割线的方向,只能为 HORIZONTAL
或 VERTICAL
,即水平或垂直。
只需要在 setAdapter()
之前调用上方代码即可。效果如下:
看到没有,只需添加一行代码,即可在每个子项之间添加一条与 ListView
中几乎一摸一样的分割线,相比 RecyclerView
的其他代码改造,实在不麻烦。当然,也有可能是郭霖为了避免在书中花篇幅介绍一个非基础的知识点而故意不提了,毕竟他也曾说要尽量减少书的厚度。
关于 DividerItemDecoration
的实现并不复杂,毕竟也就一条直线的事,我们可以看看源码:
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
private int mOrientation;
private final Rect mBounds = new Rect();
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this " + "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}
@Nullable
public Drawable getDrawable() {
return mDivider;
}
@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;
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;
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(child.getTranslationX());
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 (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);
}
}
}
首先就是获取传入的方向,也就是水平还是垂直,然后在 onDraw()
中根据方向调用 drawVertical()
或 drawHorizontal()
实现对分割线的绘制。
绘制前先测量 RecyclerView
的 padding
,以得到与 item
不相邻两边的宽度。通过 getChildCount()
获取 item
的数量,然后遍历各个 item
,分别计算与 item
相邻两边的宽度,即可绘制出分割线的区域。
getItemOffsets()
方法用于设置 item
的 padding
属性,setDrawable()
则用于设置分割线的样式。
了解原理之后,我们就可以更灵活地为 RecyclerView
自定义分割线了。
举个例子,我们可以看到微信常用的一种分割线方式,是左侧有间隔的分割线:
那我们可以参照上方源码简单写一个:
public class WeChatDividerItemDecoration extends RecyclerView.ItemDecoration {
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
public static final int HORIZONTAL = LinearLayoutManager.HORIZONTAL;
public static final int VERTICAL = LinearLayoutManager.VERTICAL;
private static final String TAG = "DividerItem";
private Drawable mDivider;
private int mOrientation;
private int mMarginStart, mMarginEnd;
public WeChatDividerItemDecoration(Context context, int orientation, int marginStart, int marginEnd) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this " + "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
mMarginStart = marginStart;
mMarginEnd = marginEnd;
a.recycle();
setOrientation(orientation);
}
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull 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) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
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 top = child.getBottom() + params.bottomMargin;
final int bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left + mMarginStart, top, right - mMarginEnd, bottom);
mDivider.draw(canvas);
}
}
private void drawHorizontal(Canvas canvas, 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 + mMarginStart, right, bottom - mMarginEnd);
mDivider.draw(canvas);
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull 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);
}
}
}
效果如下:
只需通过构造函数将 marginStart
和 marginEnd
传入,通过在绘制时增减对应的边距,就可以自由的控制分割线两端的距离,同时还可以利用 drawVertical()
和 drawHorizontal()
分别适配两个方向。
可以看到,即使是自定义分割线,也并不困难,只要参考 DividerItemDecoration
中的代码,我们也可以依葫芦画瓢实现自己想要的效果。