RecyclerView GridLayoutManger平分间距问题

3,371 阅读3分钟

背景问题

在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的。。。