Android开发小技巧:GridLayoutManager如何正确的增加条目之间的间隙

518 阅读2分钟

GridLayoutManager增加间隙很奇怪,假如有三列,第一列左边距0dp,第三列右边距0dp,列之间的间隙为12dp,如果简单的通过设置第一列left=0dp,right=6dp,第二列left=6dp,right=6dp,第三列left=6dp,right=0dp,实现的效果并不能满足实际需求。这是因为6dp 是固定值,但 RecyclerView 的可用宽度(屏幕宽度 - padding)是动态的。如果总列宽和间隙的累加值与屏幕宽度不匹配,会导致:

  • 间隙过大​:列宽被挤压,内容显示不全。
  • 间隙过小​:右侧出现空白,无法对齐边缘。

所以通过 ​比例分配​ 或 ​公式化动态计算,让总间隙和列宽自动适配屏幕宽度:

import android.annotation.SuppressLint
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import xxx.common.kts.dp2pxInt
import kotlin.math.roundToInt


/**
 * GridLayoutManager 间距 ItemDecoration(dp 入参,无跨列)
 *
 * - 左右边缘恒定为 0:最左 item 的 left=0,最右 item 的 right=0
 *   (如果需要边缘留白,请在 RecyclerView 的 paddingStart/paddingEnd 或父布局中控制)
 *
 * - 条目之间横向间距恒定为 itemHSpaceDp(经典算法 includeEdge=false)
 * - 每个条目都设置 top/bottom(不做首行/末行特殊处理)
 * @param itemHSpaceDp 传dp值
 * @param topDp 传dp值
 * @param bottomDp 传dp值
 */
class CommonGridItemDecoration(
    private val itemHSpaceDp: Float,   //条目之间横向间距(dp)
    private val topDp: Float,          //每个条目顶部间距(dp)
    private val bottomDp: Float,       //每个条目底部间距(dp)
) : RecyclerView.ItemDecoration() {

    private var itemHPx = Int.MIN_VALUE
    private var topPx = Int.MIN_VALUE
    private var bottomPx = Int.MIN_VALUE

    @SuppressLint("WrongConstant")
    override fun getItemOffsets(
            outRect: Rect,
            view: View,
            parent: RecyclerView,
            state: RecyclerView.State
    ) {

        val lm = parent.layoutManager as? GridLayoutManager ?: return
        val position = parent.getChildAdapterPosition(view)
        if (position == RecyclerView.NO_POSITION) return

        // dp -> px:只算一次并缓存
        if (itemHPx == Int.MIN_VALUE) {
            itemHPx = itemHSpaceDp.dp2pxInt
            topPx = topDp.dp2pxInt
            bottomPx = bottomDp.dp2pxInt
        }

        val spanCount = lm.spanCount.coerceAtLeast(1)
        val column = position % spanCount

        outRect.top = topPx
        outRect.bottom = bottomPx

        // 经典算法(includeEdge = false):
        // left  = column * S / spanCount
        // right = S - (column + 1) * S / spanCount
        // 性质:
        // - column=0      => left=0
        // - column=last   => right=0
        // - 相邻两列 gap:right(c) + left(c+1) = S
        val s = itemHPx.toFloat()
        val n = spanCount.toFloat()

        outRect.left = (column * s / n).roundToInt()
        outRect.right = (s - (column + 1) * s / n).roundToInt()
    }

}

也可以封装最左/右侧带参数的,但是那样会让整个算法比较复杂,不利于维护,所以这里没有写出来,如果需要可以让AI帮忙封装。