该工具类为 RecyclerView GridLayoutManager 专用网格间距装饰器
解决市面上绝大多数 GridDecoration 的通病:
- 存在跨列整行Item(标题/分组)时,position%spanCount 列数计算错乱
- 首尾Item左右边距不均、视觉错位
- 行间距叠加导致双倍高度
- 无法单独控制「网格第一行顶部间距」(标题和网格间距不协调)
- 边界越界崩溃问题
特性:完全兼容多类型网格、跨列标题、自动识别真实行列、均分空白、无叠加间距、可配置首行顶部边距
完整代码如下
package com.xxx.core.utils
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* 【终极适配版】RecyclerView 网格间距装饰器
* 适配:普通3列网格 + 跨列整行标题混合布局(工作台常用)
*
* 核心优势(解决所有旧版BUG)
* 1. 不使用 position % spanCount 计算列数,使用系统原生 [spanLookup.getSpanIndex] 获取真实列下标
* 彻底解决「存在跨列Item后列数错乱、间距不均」问题
* 2. 通过 spanGroupIndex 识别真实行,自动判断当前Item是否为「本行最后一个」
* 解决最后一列右边距残留问题
* 3. 行间距只使用 bottom 赋值,top默认0,杜绝上下叠加双倍间距
* 4. 单独配置网格首行 top 间距,完美适配【标题+网格】UI结构
* 5. 做了 position+1 越界保护,不会数组越界崩溃
* 6. 兼容 includeEdge 内外边距模式,新旧项目通用
*
* @param spanCount 网格总列数(例:3列传3)
* @param space 网格Item之间的间距(单位px,建议dp转px传入)
* @param includeEdge 是否包含屏幕左右边缘间距
* true:整列表左右留白,所有Item均分间隙
* false:每行第一个Item left=0、最后一个Item right=0,贴合边缘
* @param firstRowTopSpace 网格第一行顶部间距(仅普通网格Item生效,标题不生效)默认 0
* 用于解决「标题下方紧贴网格,没有间距」的UI问题
*/
class GridSpaceItemDecoration(
private val spanCount: Int,
private val space: Int,
private val includeEdge: Boolean,
private val firstRowTopSpace: Int = 0
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: 0
val manager = parent.layoutManager as GridLayoutManager
val spanLookup = manager.spanSizeLookup
// 当前item占用的列跨度
val itemSpan = spanLookup.getSpanSize(position)
// ====================== 跨列整行Item(分组标题/通栏布局)======================
// 整行占满所有列的View(标题)统一上下均分间距,不参与网格列均分逻辑
if (itemSpan == spanCount) {
outRect.set(0, space / 2, 0, space / 2)
return
}
// ====================== 普通网格Item逻辑 ======================
// 获取当前Item在本行中的真实列下标(系统原生API,不受标题跨列影响)
val realColumn = spanLookup.getSpanIndex(position, spanCount)
// 获取当前Item所在的行索引(网格真实行号)
val currentGroup = spanLookup.getSpanGroupIndex(position, spanCount)
if (includeEdge) {
// 模式1:包含边缘,屏幕左右留有空白,所有Item均匀分摊间距
outRect.left = space - realColumn * space / spanCount
outRect.right = (realColumn + 1) * space / spanCount
} else {
// 模式2:不包含边缘
// 本行第一个Item:左边距0
outRect.left = if (realColumn == 0) 0 else space / 2
// 判断当前Item是否是【本行最后一个】
// 原理:下一个Item行号 != 当前行号 = 当前是行尾
val isRowLast: Boolean = if (position + 1 < itemCount) {
val nextGroup = spanLookup.getSpanGroupIndex(position + 1, spanCount)
nextGroup != currentGroup
} else {
// 最后一条数据,直接判定行尾
true
}
// 本行最后一个Item:右边距0
outRect.right = if (isRowLast) 0 else space / 2
}
// 行间距:只留bottom,top默认0,杜绝双倍间距叠加
// 单独适配:网格第一行可自定义顶部间距(适配标题下方留白)
outRect.top = if (currentGroup == 0) firstRowTopSpace else 0
outRect.bottom = space
}
}
一、适配场景
- 纯网格列表(2列3列/4列均分)
- 标题通栏 + 网格Item混合布局(最常用工作台布局)
- 需要首行单独顶部留白的UI
- 禁止行间距双倍叠加的精致UI
二、调用示例(工作台标准用法)
// 3列网格、间距12dp、无边缘留白、首行顶部额外12dp间距
binding.rvWorkGrid.addItemDecoration(
GridSpaceItemDecoration(
spanCount = 3,
space = 12.dp2px(requireContext()),
includeEdge = false,
firstRowTopSpace = 12.dp2px(requireContext())
)
)
三、效果说明
includeEdge = false(推荐UI标准)
- 每行第一个Item:left=0
- 每行最后一个Item:right=0
- 中间Item:左右均分间隙
- Item之间间距统一、无重叠、无错位
- 标题通栏单独上下留白
- 网格第一行可独立设置 top 间距,完美分隔标题与内容