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 类型的参数表示分割线的方向,只能为 HORIZONTALVERTICAL,即水平或垂直。

只需要在 setAdapter() 之前调用上方代码即可。效果如下:

RecyclerView 设置分割线

看到没有,只需添加一行代码,即可在每个子项之间添加一条与 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() 实现对分割线的绘制。

绘制前先测量 RecyclerViewpadding,以得到与 item 不相邻两边的宽度。通过 getChildCount() 获取 item 的数量,然后遍历各个 item,分别计算与 item 相邻两边的宽度,即可绘制出分割线的区域。

getItemOffsets() 方法用于设置 itempadding 属性,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);
        }
    }
}

效果如下:

自定义分割线

只需通过构造函数将 marginStartmarginEnd 传入,通过在绘制时增减对应的边距,就可以自由的控制分割线两端的距离,同时还可以利用 drawVertical()drawHorizontal() 分别适配两个方向。

可以看到,即使是自定义分割线,也并不困难,只要参考 DividerItemDecoration 中的代码,我们也可以依葫芦画瓢实现自己想要的效果。