14、ItemDecoration:给列表项添加分割线、间距、背景
说实话,RecyclerView 刚出来那会儿,我第一反应是:「这玩意儿怎么连个分割线都没有?」
ListView 自带一个 divider 属性,多省事。但 RecyclerView 的设计哲学不一样——它把「装饰」这件事完全交给你自己控制。这就是 ItemDecoration 存在的意义。
我个人习惯把 ItemDecoration 理解为「列表项的化妆师」。它不改变数据,不改变布局,只在绘制阶段给每个 item 加上分割线、间距、背景,甚至还能画一些花哨的装饰效果。
14.1 ItemDecoration 的核心原理
ItemDecoration 有三个回调方法,但大部分场景我们只用两个:
getItemOffsets()—— 控制 item 之间的间距onDraw()—— 在 item 内容之下绘制装饰onDrawOver()—— 在 item 内容之上绘制装饰(比如粘性头部)
你想想看,RecyclerView 绘制一个列表项的顺序是:先画背景 → 调用 onDraw() → 画 item 内容 → 调用 onDrawOver()。所以分割线通常放在 onDraw() 里,因为它在 item 内容下面,不会遮挡文字。
关键点:getItemOffsets() 决定了「留多少空白」,onDraw() 决定了「在空白里画什么」。两者配合才能做出好看的分割线。
14.2 最常用的分割线实现
我在项目中写过不下十种分割线方案,但最常用的还是这个——一个简单的水平分割线,带左右边距:
class SimpleDividerItemDecoration(
private val dividerHeight: Int = 1,
private val dividerColor: Int = Color.LTGRAY,
private val leftMargin: Int = 0,
private val rightMargin: Int = 0
) : RecyclerView.ItemDecoration() {
private val paint = Paint().apply {
color = dividerColor
strokeWidth = dividerHeight.toFloat()
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
// 最后一项不添加间距
if (parent.getChildAdapterPosition(view) != parent.adapter?.itemCount?.minus(1)) {
outRect.bottom = dividerHeight
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + leftMargin
val right = parent.width - parent.paddingRight - rightMargin
for (i in 0 until parent.childCount - 1) {
val child = parent.getChildAt(i)
val top = child.bottom
val bottom = top + dividerHeight
c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint)
}
}
}
使用起来也很简单:
recyclerView.addItemDecoration(
SimpleDividerItemDecoration(
dividerHeight = 2,
dividerColor = Color.parseColor("#E0E0E0"),
leftMargin = 16,
rightMargin = 16
)
)
小技巧:如果你用的是 Kotlin,可以给 RecyclerView 写一个扩展函数,把常用的分割线封装起来。我一般会在项目里放一个 DividerDecorationFactory 单例,统一管理所有分割线样式。
14.3 给列表项添加间距
有时候你不需要分割线,只需要 item 之间有点呼吸感。比如卡片式列表,每个卡片之间留 8dp 间距就够了。
这种情况,我建议直接在 getItemOffsets() 里设置间距,不用画任何东西:
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
outRect.left = spacing
outRect.right = spacing
outRect.top = spacing
outRect.bottom = spacing
}
}
嗯,这里要注意:如果每个 item 都加了左右间距,第一个 item 左边会有多余空白。你可以判断一下 position,如果是第一列就只加右边距,最后一列只加左边距。不过 GridLayoutManager 的情况更复杂,我后面会讲。
14.4 给列表项添加背景
背景其实不一定要用 ItemDecoration。你完全可以在 item 布局里设置 android:background。但有些场景下,你需要动态改变背景,或者给不同位置的 item 设置不同背景,这时候 ItemDecoration 就派上用场了。
我曾经做过一个聊天列表,需要给每条消息交替显示浅灰和白色背景。用 ItemDecoration 实现非常优雅:
class AlternatingBackgroundDecoration(
private val evenColor: Int = Color.WHITE,
private val oddColor: Int = Color.parseColor("#F5F5F5")
) : RecyclerView.ItemDecoration() {
private val paint = Paint()
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val position = parent.getChildAdapterPosition(child)
paint.color = if (position % 2 == 0) evenColor else oddColor
c.drawRect(
child.left.toFloat(),
child.top.toFloat(),
child.right.toFloat(),
child.bottom.toFloat(),
paint
)
}
}
}
注意:如果你在 item 布局里也设置了背景,ItemDecoration 绘制的背景会被覆盖。解决办法是让 item 布局的背景透明,或者把 ItemDecoration 的绘制放在 onDrawOver() 里。
14.5 GridLayoutManager 下的特殊处理
网格布局的分割线比线性布局复杂得多。你需要考虑每一行最后一列不要右边距,每一列最后一行不要下边距。
我封装了一个通用的网格分割线工具,核心逻辑是这样的:
class GridDividerDecoration(
private val spanCount: Int,
private val spacing: Int,
private val includeEdge: Boolean = false
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val column = position % spanCount
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing
} else {
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}
这段代码我改过好几版。最开始我直接用 outRect.left = spacing / 2 这种写法,结果发现第一列和最后一列的间距不对称。后来参考了 Material Design 的规范,才用了这种按比例分配的方式。
14.6 知识体系总览
下面这张图总结了 ItemDecoration 的核心知识点和常见用法:
14.7 性能注意事项
ItemDecoration 的 onDraw() 和 onDrawOver() 会在每次列表滚动时被频繁调用。所以:
- 不要在 onDraw 里创建对象——Paint、Rect 这些应该提前初始化好
- 避免复杂的计算——比如每次都重新算一遍 item 位置,可以缓存起来
- 如果分割线很简单——直接用
DividerItemDecoration这个官方类就够了,别自己造轮子
我的经验:曾经在一个聊天列表里用了复杂的 ItemDecoration 画气泡背景,结果列表快速滑动时明显卡顿。后来把背景改到 item 布局里用 ShapeDrawable 实现,流畅度提升了一大截。所以,能用布局解决的,就别用 ItemDecoration。
14.8 实战建议
最后给你几个我在项目中总结出来的建议:
- 分割线颜色别用纯黑——用
#E0E0E0或者更浅的颜色,视觉上更舒服 - 间距单位用 dp——别写死 px,不同屏幕密度下效果差很多
- 考虑 RTL 布局——如果你的 App 支持阿拉伯语,分割线的左右边距要反过来
- 调试时把分割线颜色设成红色——一眼就能看出间距有没有生效
ItemDecoration 这东西,说白了就是「画图」。你掌握了 Canvas 的基本操作,就能玩出各种花样。但别忘了,它的本质是装饰,别喧宾夺主。
公众号:蓝海资料掘金营,微信deep3321