RecyclerView GridLayoutManger平分间距问题
背景问题
在RecyclerView的网格布局中,我们经常会遇到要给每个Item设置间距的情况,并使用GridLayoutManger,如下图:
A(0) ~ A(3)是网格中的一行,要个每个Item设置间距SpaceH,两边分别设置边距为edgeH,要实现这种情况,我们一般会使用ItemDecoration,重写它的getItemOffsets方法计算每个Item的左右边距,很容易误写成一下方式: (gridSize为一行有几列)
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
// 获取第几列
val column = position % gridSize
outRect.left = if (column == 0) edgeH else spaceH / 2
outRect.right = if (column < gridSize - 1) spaceH / 2 else edgeH
}
写成这样的原因主要是认为只要给每个Item合适的左右间距就好了,然而运行以后会发现每个Item的宽度不相等,这还要从GridLayoutManager平分原理说起,每个Item的宽度是这样计算的
- 平分reyclerView的宽度,得到每个网格的宽度grideWidth = parentWidth / gridSize
- 减去每个item左右间距,childWidth = gridWidth - outRect.left - outRect.right
有了以上计算公式,可以很容易发现item的宽度会出现不一定相等的情况,例如
- A(0) = grideWidth - edgeH - spaceH / 2
- A(1) = grideWidth - spaceH
可以发现A(0) 和A(1)的宽度只有在edgeH = spaceH / 2 时才相等,其他时候都是不等的。
推导过程
那究竟怎么算呢?根据childWidth = gridWidth - outRect.left - outRect.right,我们可以知道,要求每个Item都相等,只需要每个Item对应的outRect.left + outRect.right都相等即可。
我们将第n个item左边的边距 定为 L(n), 右边的边距定为R(n), 将他们的和定为p,p目前是未知的,得到第一个算式
① L(n) + R(n) = p
另外,我们设置网格时都会设置两个Item之间的间距,我们定为spaceH
,那么第n个和n+1个之间的间距由R(n) + L(n+1)组成,可以得到第二个算式
② R(n) + L(n+1) =
spaceH
得到这两个算式后就是纯粹的数学问题了
- 首先第一个算式,我们可以把所有情况枚举出来,下面gridSize为网格的列数,它肯定是已知的
L(0) + R(0) = p
L(1) + R(1) = p
....
L(gridSize-1) + R(gridSize-1) = p
将这些式子全部相加可以发现,R(0) + L(1) , R(1) + L(2)这些,都是第②个算式,总共有gridSize-1个,所有就有一下算式
L(0) + (gridSize - 1) * h + R(gridSize -1 ) = gridSize * p
又由于网格两边都为edgeH,即L(0)和R(gridSize -1 )为edgeH,可以算出p的值为
p = (2 * edgeH + (gridSize - 1) * spaceH) / gridSize
- 再仔细发现算式①和②左边都有R(n),我们通过减法将他消除掉消除掉,即②-①,就剩下:
L(n+1) - L(n) = spaceH - p
这个式子明显是一个等差数列,等差数列是有公式的,可以直接得出一下结论
L(n) = L(0) + n * (spaceH - p)
注L(0)为edgeH,且因为我们的下标是从0开始算的,所以后面是乘以n
- 由于p在第一步已经算出来了,所以L(n)的值就是已知的了
L(n) = edgeH + n * (spaceH - p)
那么R(n)格局算式①和②都可以算出来,
R(n) = p - L(n)
ItemDecoration实现
最终,我们可以得到这样的结果
class GridSpaceDecoration(
private val gridSize: Int,
private val spaceH: Int = 0,
private val spaceV: Int = 0,
private val edgeH: Int = 0 // 网格两边的间距
): RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
// 获取第几列
val column = position % gridSize
// 第几行
val row: Int = position / gridSize
if (row != 0) { // 设置top
outRect.top = spaceV
}
// p为每个Item都需要减去的间距
val p = (2 * edgeH + (gridSize - 1) * spaceH) * 1f / gridSize
val left = edgeH + column * (spaceH - p)
val right = p - left
outRect.left = Math.round(left)
outRect.right = Math.round(right)
}
}
- 也许有人会说,两边的间距可以通过recyclerView的paddingLeft和paddingRight计算得来,这样的确可以,但关键问题在于,很多时候我们需要通过GridLayoutManger实现不同类型的Item,不同Item之间可能就需要通过ItemDecoration来设置了,至于多类型的怎么写这里就不做赘述了。
- 网上很多文章的算式很多都没有考虑左右的边距,而且没有推导过程,都是找规律的,这里主要是用数学方式做推导,记录下推导过程
- 细心一下可以发现,如果edgeH大于spaceH,那么得到的item左右边距有些是负数,不过并不影响最终效果,这个也是同事通过测试后发现的,自己本能的以为edgeH是不能大于spaceH的。。。