Android RecyclerView 自定义分割线及最后一项去除分割线全解析

在 Android 开发中,RecyclerView 作为高效的列表展示控件,被广泛应用于各种场景。为了提升列表的可读性和美观度,我们通常需要为列表项之间添加分割线(Divider)。然而,系统默认的分割线样式单一,且无法灵活控制(例如去除最后一项的分割线)。本文将详细介绍如何为 RecyclerView 实现自定义分割线,并解决“最后一项去除分割线”这一常见需求,同时涵盖最佳实践和示例代码,帮助开发者快速掌握相关技巧。

目录#

  1. RecyclerView 分割线基础:ItemDecoration 简介
  2. 创建基础自定义分割线
    • 2.1 自定义 ItemDecoration 类
    • 2.2 定义分割线样式(颜色、高度、边距)
    • 2.3 在 RecyclerView 中应用分割线
  3. 核心需求:最后一项去除分割线
    • 3.1 判断“最后一项”的逻辑
    • 3.2 修改 getItemOffsets 方法
    • 3.3 修改 onDraw 方法
  4. 高级场景:适配不同布局管理器
    • 4.1 线性布局(LinearLayoutManager)
    • 4.2 网格布局(GridLayoutManager)
  5. 最佳实践与注意事项
    • 5.1 分割线复用性设计
    • 5.2 性能优化
    • 5.3 处理数据动态变化
  6. 完整示例代码
  7. 总结
  8. 参考资料

1. RecyclerView 分割线基础:ItemDecoration 简介#

RecyclerView 本身不直接支持分割线,而是通过 ItemDecoration 类实现。ItemDecoration 是一个抽象类,允许开发者在列表项之间或周围绘制额外的装饰(如分割线、间距等)。其核心方法包括:

方法名作用
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)为每个列表项设置偏移量(即分割线的“占位空间”)
onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)在列表项下方绘制装饰(如分割线)
onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)在列表项上方绘制装饰(如悬浮标题)

关键逻辑:通过 getItemOffsets 为分割线预留空间,再通过 onDraw 在预留空间中绘制分割线。

2. 创建基础自定义分割线#

2.1 自定义 ItemDecoration 类#

首先,创建一个继承 RecyclerView.ItemDecoration 的自定义类(如 CustomDividerDecoration),并重写 getItemOffsetsonDraw 方法。

2.2 定义分割线样式(颜色、高度、边距)#

分割线的样式(颜色、高度、左右边距)可通过构造方法传入,增强灵活性。例如:

  • 分割线高度:通过 dividerHeight 定义(单位:px 或 dp,建议使用 dp 并转换为 px)。
  • 分割线颜色:通过 dividerColor 定义。
  • 左右边距:通过 leftMarginrightMargin 定义(避免分割线与屏幕边缘对齐)。

代码示例

public class CustomDividerDecoration extends RecyclerView.ItemDecoration {
    private final Paint mPaint;
    private final int dividerHeight; // 分割线高度(px)
    private final int leftMargin;    // 左间距(px)
    private final int rightMargin;   // 右间距(px)
 
    // 构造方法:传入分割线高度(dp)、颜色、左右边距(dp)
    public CustomDividerDecoration(int dividerHeightDp, int color, int leftMarginDp, int rightMarginDp) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.FILL);
 
        // 将 dp 转换为 px(需传入 Context)
        this.dividerHeight = dp2px(dividerHeightDp);
        this.leftMargin = dp2px(leftMarginDp);
        this.rightMargin = dp2px(rightMarginDp);
    }
 
    // dp 转 px 工具方法
    private int dp2px(int dp) {
        return (int) (dp * Resources.getSystem().getDisplayMetrics().density + 0.5f);
    }
}

2.3 在 RecyclerView 中应用分割线#

在 Activity 或 Fragment 中,通过 recyclerView.addItemDecoration() 方法添加自定义分割线:

// 初始化 RecyclerView
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
 
// 添加自定义分割线:高度 1dp,灰色,左右边距 16dp
CustomDividerDecoration divider = new CustomDividerDecoration(
    1, 0xFFE0E0E0, 16, 16
);
recyclerView.addItemDecoration(divider);

3. 核心需求:最后一项去除分割线#

默认情况下,分割线会应用于所有列表项之间,包括最后一项的下方,这显然不合理。我们需要通过逻辑判断,对最后一项“隐藏”分割线。

3.1 判断“最后一项”的逻辑#

通过 RecyclerView 的适配器(Adapter)获取列表总项数 itemCount,当前项的位置 position 可通过 parent.getChildAdapterPosition(view) 获取。若 position == itemCount - 1,则为最后一项。

注意:需处理 adapter 为空的情况,避免空指针异常(NPE)。

3.2 修改 getItemOffsets 方法#

getItemOffsets 用于为分割线预留空间。若当前项是最后一项,应将下方偏移量设为 0,即不预留分割线空间:

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    RecyclerView.Adapter adapter = parent.getAdapter();
    if (adapter == null) return;
 
    int itemCount = adapter.getItemCount();
    int position = parent.getChildAdapterPosition(view);
 
    // 最后一项不预留分割线空间
    if (position != itemCount - 1) {
        outRect.bottom = dividerHeight; // 非最后一项,底部预留分割线高度
    } else {
        outRect.bottom = 0; // 最后一项,底部偏移为 0
    }
}

3.3 修改 onDraw 方法#

onDraw 用于绘制分割线。即使 getItemOffsets 已预留空间,仍需确保最后一项不绘制分割线:

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    super.onDraw(c, parent, state);
    RecyclerView.Adapter adapter = parent.getAdapter();
    if (adapter == null) return;
 
    int itemCount = adapter.getItemCount();
    int childCount = parent.getChildCount();
 
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        int position = parent.getChildAdapterPosition(child);
 
        // 最后一项不绘制分割线
        if (position == itemCount - 1) {
            continue;
        }
 
        // 计算分割线的位置:位于当前 item 的底部
        int left = child.getLeft() + leftMargin;
        int right = child.getRight() - rightMargin;
        int top = child.getBottom();
        int bottom = top + dividerHeight;
 
        // 绘制分割线
        c.drawRect(left, top, right, bottom, mPaint);
    }
}

4. 高级场景:适配不同布局管理器#

上述逻辑默认适用于 垂直方向的 LinearLayoutManager。若使用水平方向或 GridLayoutManager,需调整判断逻辑。

4.1 水平方向 LinearLayoutManager#

水平方向时,分割线为垂直方向(位于 item 右侧),最后一项的右侧不应有分割线。此时需修改 getItemOffsetsonDraw

  • getItemOffsets:设置 outRect.right 而非 outRect.bottom
  • onDraw:计算分割线的 left(item 右侧)、right(left + 分割线宽度)。

关键代码修改

// 在构造方法中增加方向参数
private final int orientation;
 
public CustomDividerDecoration(..., int orientation) {
    this.orientation = orientation;
}
 
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    // ... 省略 adapter 和 position 判断逻辑 ...
    if (orientation == LinearLayoutManager.VERTICAL) {
        outRect.bottom = (position != itemCount - 1) ? dividerHeight : 0;
    } else { // 水平方向
        outRect.right = (position != itemCount - 1) ? dividerWidth : 0;
    }
}

4.2 网格布局(GridLayoutManager)#

网格布局中,“最后一项”的定义更复杂:需判断当前 item 是否在最后一行。例如,2 列网格中,若总项数为 5,则最后一行只有 1 项(第 5 项),需去除其底部分割线;若总项数为 6,则最后一行 2 项均需去除底部分割线。

判断逻辑

  1. 获取 GridLayoutManager 的列数 spanCount
  2. 计算最后一行的起始位置:lastRowStartPos = itemCount - (itemCount % spanCount == 0 ? spanCount : itemCount % spanCount)
  3. position >= lastRowStartPos,则为最后一行,不绘制分割线。

代码示例

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    RecyclerView.Adapter adapter = parent.getAdapter();
    if (adapter == null) return;
 
    int itemCount = adapter.getItemCount();
    int position = parent.getChildAdapterPosition(view);
    if (position == RecyclerView.NO_POSITION) return;
 
    // 判断是否为 GridLayoutManager
    if (parent.getLayoutManager() instanceof GridLayoutManager) {
        GridLayoutManager gridLayoutManager = (GridLayoutManager) parent.getLayoutManager();
        int spanCount = gridLayoutManager.getSpanCount();
        int lastRowStartPos = itemCount - (itemCount % spanCount == 0 ? spanCount : itemCount % spanCount);
 
        // 最后一行不预留分割线空间
        if (position >= lastRowStartPos) {
            outRect.bottom = 0;
        } else {
            outRect.bottom = dividerHeight;
        }
    } else {
        // 线性布局逻辑(省略)
    }
}

5. 最佳实践与注意事项#

5.1 分割线复用性设计#

  • 参数化构造:通过构造方法传入分割线高度、颜色、边距、方向等参数,避免硬编码。
  • 支持 Drawable:除了纯色分割线,可支持通过 Drawable 定义复杂样式(如虚线、渐变),示例:
    public CustomDividerDecoration(Drawable divider, int leftMargin, int rightMargin) {
        this.divider = divider;
        this.leftMargin = leftMargin;
        this.rightMargin = rightMargin;
    }

5.2 性能优化#

  • 避免在 onDraw 中创建对象onDraw 会频繁调用,若在其中创建 RectPaint 等对象,会导致内存抖动。应将对象声明为成员变量。
  • 减少绘制区域:通过 leftMarginrightMargin 限制分割线宽度,避免绘制全屏宽度。

5.3 处理数据动态变化#

当列表数据更新(如 notifyDataSetChanged)时,需确保分割线状态同步更新。RecyclerView 会自动触发 ItemDecoration 的重绘,无需额外处理,但需确保 getItemOffsetsonDraw 中的逻辑依赖最新的 itemCountposition

6. 完整示例代码#

自定义分割线类(支持垂直 LinearLayoutManager)#

public class CustomDividerDecoration extends RecyclerView.ItemDecoration {
    private final Paint mPaint;
    private final int dividerHeight; // 分割线高度(px)
    private final int leftMargin;    // 左间距(px)
    private final int rightMargin;   // 右间距(px)
 
    public CustomDividerDecoration(int dividerHeightDp, int color, int leftMarginDp, int rightMarginDp) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(color);
        mPaint.setStyle(Paint.Style.FILL);
 
        this.dividerHeight = dp2px(dividerHeightDp);
        this.leftMargin = dp2px(leftMarginDp);
        this.rightMargin = dp2px(rightMarginDp);
    }
 
    private int dp2px(int dp) {
        return (int) (dp * Resources.getSystem().getDisplayMetrics().density + 0.5f);
    }
 
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        RecyclerView.Adapter adapter = parent.getAdapter();
        if (adapter == null) return;
 
        int itemCount = adapter.getItemCount();
        int position = parent.getChildAdapterPosition(view);
        if (position == RecyclerView.NO_POSITION) return;
 
        // 最后一项不预留分割线空间
        outRect.bottom = (position != itemCount - 1) ? dividerHeight : 0;
    }
 
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        RecyclerView.Adapter adapter = parent.getAdapter();
        if (adapter == null) return;
 
        int itemCount = adapter.getItemCount();
        int childCount = parent.getChildCount();
 
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(child);
            if (position == RecyclerView.NO_POSITION) continue;
 
            // 最后一项不绘制分割线
            if (position == itemCount - 1) continue;
 
            // 计算分割线位置
            int left = child.getLeft() + leftMargin;
            int right = child.getRight() - rightMargin;
            int top = child.getBottom();
            int bottom = top + dividerHeight;
 
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }
}

Activity 中应用#

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(new MyAdapter(getData()));
 
        // 添加自定义分割线:1dp 高,灰色(#E0E0E0),左右边距 16dp
        CustomDividerDecoration divider = new CustomDividerDecoration(
            1, 0xFFE0E0E0, 16, 16
        );
        recyclerView.addItemDecoration(divider);
    }
 
    private List<String> getData() {
        List<String> data = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            data.add("列表项 " + (i + 1));
        }
        return data;
    }
 
    // 简单的 Adapter
    static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
        private final List<String> mData;
 
        MyAdapter(List<String> data) {
            mData = data;
        }
 
        @NonNull
        @Override
        public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext())
                    .inflate(android.R.layout.simple_list_item_1, parent, false);
            return new MyViewHolder(view);
        }
 
        @Override
        public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
            holder.textView.setText(mData.get(position));
        }
 
        @Override
        public int getItemCount() {
            return mData.size();
        }
 
        static class MyViewHolder extends RecyclerView.ViewHolder {
            TextView textView;
 
            MyViewHolder(@NonNull View itemView) {
                super(itemView);
                textView = itemView.findViewById(android.R.id.text1);
            }
        }
    }
}

7. 总结#

本文详细介绍了 RecyclerView 自定义分割线的实现方法,核心步骤包括:

  1. 通过 ItemDecoration 类的 getItemOffsets 预留分割线空间;
  2. 通过 onDraw 绘制分割线样式;
  3. 通过判断 position == itemCount - 1 去除最后一项的分割线。

同时,针对不同布局管理器(线性、网格)和方向(垂直、水平)提供了适配方案,并总结了复用性设计、性能优化等最佳实践。掌握这些技巧后,开发者可灵活实现各种分割线效果,提升列表的视觉体验。

8. 参考资料#