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

效果图:

相关推荐
东风西巷19 分钟前
BLURRR剪辑软件免费版:创意剪辑,轻松上手,打造个性视频
android·智能手机·音视频·生活·软件需求
JhonKI23 分钟前
【MySQL】行结构详解:InnoDb支持格式、如何存储、头信息区域、Null列表、变长字段以及与其他格式的对比
android·数据库·mysql
ab_dg_dp2 小时前
Android 位掩码操作(&和~和|的二进制运算)
android
潜龙952713 小时前
第3.2.3节 Android动态调用链路的获取
android·调用链路
追随远方14 小时前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频
撰卢15 小时前
MySQL 1366 - Incorrect string value:错误
android·数据库·mysql
恋猫de小郭16 小时前
Flutter 合并 ‘dot-shorthands‘ 语法糖,Dart 开始支持交叉编译
android·flutter·ios
牛马程序小猿猴16 小时前
15.thinkphp的上传功能
android
林家凌宇16 小时前
Flutter 3.29.3 花屏问题记录
android·flutter·skia
时丶光17 小时前
Android 查看 Logcat (可纯手机方式 无需电脑)
android·logcat