You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过构造函数实现水平RecyclerView按指定行列左到右排列

解决方案

你的核心问题是GridLayoutManager水平模式下默认采用「列优先」填充顺序(先填满一列的所有行,再切换到下一列),和你需要的「行优先」(先填满一行的所有列,再切换到下一行)完全相反。直接继承GridLayoutManager很难修改这个核心逻辑,因此需要自定义LayoutManager来实现需求。

自定义LayoutManager实现

下面是完整的CustomHorizontalGridLayoutManager实现,支持指定行列数、水平滚动、反转滚动,并且严格按照「行优先」顺序填充:

internal class CustomHorizontalGridLayoutManager(
    private val rows: Int,
    private val columns: Int,
    private val isReversed: Boolean = false
) : RecyclerView.LayoutManager() {

    private var itemWidth = 0
    private var itemHeight = 0
    private var totalGroups = 0
    private var totalItemCount = 0

    override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
        return RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
        )
    }

    override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        if (state.itemCount == 0) {
            detachAndScrapAttachedViews(recycler)
            return
        }

        totalItemCount = state.itemCount
        // 计算item高度:RecyclerView高度平均分配给指定行数
        itemHeight = height / rows
        // 这里设置item为正方形,可根据需求修改(比如固定宽度、比例宽度等)
        itemWidth = itemHeight

        // 计算总共有多少个"块"(每个块包含 rows*columns 个item)
        totalGroups = (totalItemCount + rows * columns - 1) / (rows * columns)

        detachAndScrapAttachedViews(recycler)

        val visibleLeft = paddingLeft
        val visibleRight = width - paddingRight

        // 布局当前可见范围内的块
        val firstVisibleGroup = (visibleLeft / (columns * itemWidth)).coerceAtLeast(0)
        val lastVisibleGroup = ((visibleRight + columns * itemWidth) / (columns * itemWidth)).coerceAtMost(totalGroups - 1)

        for (group in firstVisibleGroup..lastVisibleGroup) {
            val startPos = group * rows * columns
            val endPos = minOf(startPos + rows * columns, totalItemCount) - 1

            for (position in startPos..endPos) {
                val view = recycler.getViewForPosition(position)
                addView(view)

                val groupInnerPos = position % (rows * columns)
                val rowIndex = groupInnerPos / columns
                val colIndex = groupInnerPos % columns

                var left = group * columns * itemWidth + colIndex * itemWidth
                val top = paddingTop + rowIndex * itemHeight
                val right = left + itemWidth
                val bottom = top + itemHeight

                // 处理反转滚动逻辑
                if (isReversed) {
                    val totalContentWidth = totalGroups * columns * itemWidth
                    left = totalContentWidth - right
                }

                layoutDecoratedWithMargins(view, left, top, right, bottom)
            }
        }
    }

    override fun canScrollHorizontally(): Boolean = true

    override fun scrollHorizontallyBy(
        dx: Int,
        recycler: RecyclerView.Recycler,
        state: RecyclerView.State
    ): Int {
        if (totalItemCount == 0) return 0

        val scrollDelta = if (isReversed) -dx else dx
        var consumed = scrollDelta

        // 计算滚动后的偏移量,限制在合法范围内
        val currentScroll = computeHorizontalScrollOffset(state)
        val maxScroll = (totalGroups * columns * itemWidth - width).coerceAtLeast(0)
        val clampedScroll = (currentScroll + consumed).coerceIn(0, maxScroll)
        consumed = clampedScroll - currentScroll

        // 偏移子view实现滚动
        offsetChildrenHorizontal(-consumed)

        // 回收不可见view,布局新进入视野的view
        recycleAndLayoutVisibleViews(recycler, state)

        return if (isReversed) -consumed else consumed
    }

    private fun recycleAndLayoutVisibleViews(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
        val visibleLeft = paddingLeft + computeHorizontalScrollOffset(state)
        val visibleRight = visibleLeft + width

        // 回收不在可见区域的view
        val scrapViews = ArrayList<RecyclerView.ViewHolder>()
        for (i in 0 until childCount) {
            val view = getChildAt(i) ?: continue
            val viewLeft = getDecoratedLeft(view)
            val viewRight = getDecoratedRight(view)

            if (viewRight < visibleLeft || viewLeft > visibleRight) {
                scrapViews.add(getChildViewHolder(view))
            }
        }
        scrapViews.forEach {
            removeView(it.itemView)
            recycler.recycleView(it.itemView)
        }

        // 布局新进入视野的view
        val firstVisibleGroup = (visibleLeft / (columns * itemWidth)).coerceAtLeast(0)
        val lastVisibleGroup = ((visibleRight + columns * itemWidth) / (columns * itemWidth)).coerceAtMost(totalGroups - 1)

        for (group in firstVisibleGroup..lastVisibleGroup) {
            val startPos = group * rows * columns
            val endPos = minOf(startPos + rows * columns, totalItemCount) - 1

            for (position in startPos..endPos) {
                if (findViewByPosition(position) == null) {
                    val view = recycler.getViewForPosition(position)
                    addView(view)

                    val groupInnerPos = position % (rows * columns)
                    val rowIndex = groupInnerPos / columns
                    val colIndex = groupInnerPos % columns

                    var left = group * columns * itemWidth + colIndex * itemWidth
                    val top = paddingTop + rowIndex * itemHeight
                    val right = left + itemWidth
                    val bottom = top + itemHeight

                    if (isReversed) {
                        val totalContentWidth = totalGroups * columns * itemWidth
                        left = totalContentWidth - right
                    }

                    // 调整滚动偏移后的位置
                    left -= computeHorizontalScrollOffset(state)

                    layoutDecoratedWithMargins(view, left, top, right, bottom)
                }
            }
        }
    }

    override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {
        return totalGroups * columns * itemWidth
    }

    override fun computeHorizontalScrollOffset(state: RecyclerView.State): Int {
        return if (isReversed) {
            (totalGroups * columns * itemWidth - width) - computeHorizontalScrollExtent(state)
        } else {
            -getChildAt(0)?.let { getDecoratedLeft(it) - paddingLeft } ?: 0
        }
    }

    override fun computeHorizontalScrollExtent(state: RecyclerView.State): Int {
        return width
    }
}

代码说明

  1. 填充逻辑:

    • 将所有item按rows×columns划分为多个块,每个块内先填满一行(从左到右),再填下一行
    • 通过计算每个item的组索引、行索引、列索引,确定其在布局中的位置
  2. 尺寸计算:

    • item高度自动平分RecyclerView的高度(按指定行数)
    • 默认设置item为正方形,可根据需求修改itemWidth的计算逻辑(比如固定宽度、基于屏幕比例等)
  3. 滚动处理:

    • 支持水平滚动,自动回收不可见view,避免内存占用过高
    • 支持反转滚动(isReversed=true时从右往左滚动)

使用方式

直接在代码中替换原有的LayoutManager即可:

_binding?.horizontalRecyclerView?.layoutManager =
    CustomHorizontalGridLayoutManager(rows = 2, columns = 5, isReversed = false)

内容的提问来源于stack exchange,提问作者jasiu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 22:05:03