如何通过构造函数实现水平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 } }
代码说明
填充逻辑:
- 将所有item按
rows×columns划分为多个块,每个块内先填满一行(从左到右),再填下一行 - 通过计算每个item的组索引、行索引、列索引,确定其在布局中的位置
- 将所有item按
尺寸计算:
- item高度自动平分RecyclerView的高度(按指定行数)
- 默认设置item为正方形,可根据需求修改
itemWidth的计算逻辑(比如固定宽度、基于屏幕比例等)
滚动处理:
- 支持水平滚动,自动回收不可见view,避免内存占用过高
- 支持反转滚动(
isReversed=true时从右往左滚动)
使用方式
直接在代码中替换原有的LayoutManager即可:
_binding?.horizontalRecyclerView?.layoutManager = CustomHorizontalGridLayoutManager(rows = 2, columns = 5, isReversed = false)
内容的提问来源于stack exchange,提问作者jasiu
相关产品推荐
相关产品推荐

