注册

RecyclerView GridLayoutManger平分间距问题

背景问题


在RecyclerView的网格布局中,我们经常会遇到要给每个Item设置间距的情况,并使用GridLayoutManger,如下图:


image.png


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的宽度是这样计算的



  1. 平分reyclerView的宽度,得到每个网格的宽度grideWidth = parentWidth / gridSize
  2. 减去每个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



得到这两个算式后就是纯粹的数学问题了



  1. 首先第一个算式,我们可以把所有情况枚举出来,下面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




  1. 再仔细发现算式①和②左边都有R(n),我们通过减法将他消除掉消除掉,即②-①,就剩下:

L(n+1) - L(n) = spaceH - p

这个式子明显是一个等差数列,等差数列是有公式的,可以直接得出一下结论


L(n) = L(0) + n * (spaceH - p)



注L(0)为edgeH,且因为我们的下标是从0开始算的,所以后面是乘以n




  1. 由于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)
}

}



  1. 也许有人会说,两边的间距可以通过recyclerView的paddingLeft和paddingRight计算得来,这样的确可以,但关键问题在于,很多时候我们需要通过GridLayoutManger实现不同类型的Item,不同Item之间可能就需要通过ItemDecoration来设置了,至于多类型的怎么写这里就不做赘述了。
  2. 网上很多文章的算式很多都没有考虑左右的边距,而且没有推导过程,都是找规律的,这里主要是用数学方式做推导,记录下推导过程
  3. 细心一下可以发现,如果edgeH大于spaceH,那么得到的item左右边距有些是负数,不过并不影响最终效果,这个也是同事通过测试后发现的,自己本能的以为edgeH是不能大于spaceH的。。。

0 个评论

要回复文章请先登录注册