Android | 利用ItemDecoration绘制RecyclerView分割线

RecyclerView.ItemDecoration介绍

RecyclerView.ItemDecoration 是 Android 提供的一种扩展机制,用于为 RecyclerView 的每个子项(Item)添加装饰(Decoration)。它通常用于绘制分割线、边距、背景等,目的是增强 RecyclerView 的显示效果。

  • RecyclerView.ItemDecoration 是一个抽象类,通过重写其方法,可以实现对 RecyclerView 中的每个子项进行额外的绘制或者布局调整。
  • 通常用来绘制分割线、设置间距、添加背景等。
  • ItemDecoration 的功能是独立的,它不会影响 Adapter 和 LayoutManager 的工作逻辑。

RecyclerView.ItemDecoration 提供了以下三个方法供重写:

1、getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state): 用于设置每个子项的偏移量(即子项之间的间距)。outRect 是一个矩形,定义了子项的上下左右的间距。

2、onDraw(Canvas c, RecyclerView parent, RecyclerView.State state):在子项绘制之前调用,用于绘制装饰内容(如背景、边框等),绘制内容会被子项覆盖。

3、onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state):在子项绘制之后调用,用于绘制装饰内容(如浮动效果)。绘制内容会覆盖在子项之上。

所有的ItemDecorations绘制都是顺序执行,即:onDraw() < Item View < onDrawOver(), onDraw() 可以用来绘制divider,但在此之前必须在getItemOffsets设置了padding范围,否则onDraw()的绘制是在ItemView的下面导致不可见;onDrawOver()是绘制在最上层,所以可以用来绘制悬浮框等,下面来看各个方法:

1、getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) 内部调用outRect.set(int left, int top, int right, int bottom)来改变ItemView的边界,类似于给ItemView设置Padding,默认getItemOffsets不会影响ItemView的边界,即默认内部调用的是outRect.set(0, 0, 0, 0),如果想得到当前正在修饰的ItemView的位置,可以通过parent.getChildAdapterPosition(view)来获取。

2、onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) 在画布canvas上进行绘制,onDraw()方法是在ItemView被绘制之前执行的,因此onDraw()的绘制是在ItemView下方。

3、onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) 在画布canvas上进行绘制,onDrawOver()方法是在ItemView被绘制之后执行的,因此onDrawOver()的绘制是在ItemView上方。

DividerItemDecoration

在RecyclerView库中,官方已经帮我们实现了一个DividerItemDecoration如下:

kotlin 复制代码
//androidx.recyclerview.widget.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;

    /**
     * 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);
        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);
    }

    /**
     * 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;
    }

    /**
     * @return the {@link Drawable} for this divider.
     */
    @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;
        //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();
        }

        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);
        }
    }
}

这个类 DividerItemDecoration 是一个自定义的 RecyclerView.ItemDecoration,用于为 RecyclerView 的列表项之间绘制分隔线,主要功能如下:

  • 支持垂直和水平分隔线绘制:垂直方向(VERTICAL)和水平方向(HORIZONTAL),可根据 RecyclerView 的布局方向决定分隔线的绘制方式。
  • 自定义分隔线 Drawable:通过 setDrawable() 方法可以设置自定义的分隔线样式,支持任意的 Drawable。
  • 计算分隔线的偏移量:通过 getItemOffsets() 方法为每个列表项预留分隔线的空间,偏移量根据分隔线的宽度或高度(取决于方向)动态调整。
  • 绘制分隔线:onDraw() 方法负责在每个列表项之间绘制分隔线,支持根据 RecyclerView 的 clipToPadding 属性处理分隔线的裁剪,确保在有内边距的情况下分隔线显示正常。在使用该类时,可以直接将 DividerItemDecoration 添加到 RecyclerView 中。例如:
kotlin 复制代码
RecyclerView recyclerView = findViewById(R.id.recycler_view);
DividerItemDecoration decoration = new DividerItemDecoration(context, DividerItemDecoration.VERTICAL);
recyclerView.addItemDecoration(decoration);

如果需要自定义分隔线样式:

kotlin 复制代码
Drawable customDivider = ContextCompat.getDrawable(context, R.drawable.custom_divider);
decoration.setDrawable(customDivider);
recyclerView.addItemDecoration(decoration);

示例

对上面代码简单封装一下,写成一个扩展函数:

kotlin 复制代码
/**
 * @param context 上下文,用于转换 dp 到 px
 * @param heightDp 分割线的高度(单位:dp)
 * @param color 分割线的颜色
 * @param drawable 传入的drawable
 * @param orientation 布局方向[DividerItemDecoration.VERTICAL]、[DividerItemDecoration.HORIZONTAL]
 */
fun RecyclerView.createDivider(
    context: Context,
    dividerDp: Float = 0.5f,
    color: Int = Color.TRANSPARENT,
    drawable: Drawable? = null,
    orientation: Int = DividerItemDecoration.VERTICAL,
) {
    //优先使用传入的drawable,没有传入的话创建动态分割线Drawable
    val shapeDrawable = drawable ?: ShapeDrawable(RectShape()).apply {
        // 设置分割线的高度
        val dividerPx = (dividerDp * context.resources.displayMetrics.density + 0.5f).toInt()
        if (orientation == DividerItemDecoration.HORIZONTAL) {
            intrinsicWidth = dividerPx
        } else {
            intrinsicHeight = dividerPx
        }
        // 设置分割线的颜色
        paint.color = color
    }
    //将动态分割线添加到 RecyclerView
    val dividerItemDecoration = DividerItemDecoration(context, orientation).apply {
        setDrawable(shapeDrawable)
    }
    this.addItemDecoration(dividerItemDecoration)
}

使用它:

kotlin 复制代码
/**
 * 图片处理
 */
class DividerFragment : Fragment() {

    private val recyclerView: RecyclerView by id(R.id.rv_view)

    override fun getLayoutId(): Int {
        return R.layout.layout_rv
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // 准备数据
        val dataList = mutableListOf<String>().apply {
            for (i in 0..19) {
                add("Item $i")
            }
        }
        // 设置适配器
        val adapter = MyAdapter(dataList)
        recyclerView.run {
            layoutManager = LinearLayoutManager(context)
            context?.let {
                //设置背景色
                createDivider(it, dividerDp = 1f, color = Color.GRAY)
            }
            recyclerView.adapter = adapter
        }
    }

    class MyAdapter(
        private val dataList: List<String>,
    ) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_textview, parent, false)
            return MyViewHolder(view)
        }

        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            holder.textView.text = dataList[position]
        }

        override fun getItemCount(): Int = dataList.size

        class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            val textView: TextView = itemView.findViewById(R.id.text_view)
        }
    }
}

执行结果:

改一下Drawable,将上面的createDivider()扩展方法稍微改一下,改成传一个图片进去:

kotlin 复制代码
//设置drawable
createDivider(it, drawable = it.resources.getDrawable(R.drawable.icon_divider))

效果图:

相关推荐
恋猫de小郭3 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker8 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴8 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭19 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab20 小时前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
鹏程十八少1 天前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker1 天前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 天前
Android 协程时代,Handler 应该退休了吗?
android