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

效果图:

相关推荐
betazhou2 分钟前
MySQL相关性能查询语句
android·数据库·mysql
一起养小猫8 分钟前
Flutter for OpenHarmony 进阶:Timer组件与倒计时系统深度解析
android·网络·笔记·flutter·json·harmonyos
符哥200825 分钟前
Fastjson2.X 使用详解
android·java
月明泉清31 分钟前
Android中对于点击事件的深度梳理(三)
android
电饭叔39 分钟前
DataFrame和 Series 索引
android·python
lexiangqicheng1 小时前
【全网最全】React Native 安卓原生工程结构与构建机制深度解析
android·react native·react.js
数据蜂巢1 小时前
MySQL 8.0 生产环境备份脚本 (Percona XtraBackup 8.0+)
android·mysql·adb
jingling5551 小时前
uniapp | 基于高德地图实现位置选择功能(安卓端)
android·前端·javascript·uni-app
fatiaozhang95272 小时前
晶晨S905L/S905LB-通刷-slimbox 9.19-Mod ATV-安卓9-线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
爱怪笑的小杰杰2 小时前
UniApp 桌面应用实现 Android 开机自启动(无原生插件版)
android·java·uni-app