1) GridLayoutManager:跨列(SpanSizeLookup)
场景:普通卡片占 1 列,Header/广告/底部加载占整行。
val spanCount = 2
val lm = GridLayoutManager(context, spanCount)
lm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when (adapter.getItemViewType(position)) {
VT_HEADER, VT_AD, VT_FOOTER -> spanCount // 整行
else -> 1 // 普通卡
}
}
}
// 可选:提升滚动稳定性
lm.isItemPrefetchEnabled = true
recyclerView.layoutManager = lm
要点
- getItemViewType(position) 要稳定,不要用 position 当类型。
- 多类型 + DiffUtil 组合:areItemsTheSame 同时比较 id 与类型。
- 网格间距建议用 ItemDecoration 计算(避免“双倍间距”),见 §5。
2) StaggeredGridLayoutManager:瀑布流(不规则高度)
基础用法
val sglm = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
recyclerView.layoutManager = sglm
Header/全宽模块:设置 fullSpan
override fun onBindViewHolder(holder: RVH, pos: Int) {
if (adapter.getItemViewType(pos) == VT_HEADER) {
(holder.itemView.layoutParams as? StaggeredGridLayoutManager.LayoutParams)
?.isFullSpan = true
}
holder.bind(...)
}
实战细节(避免抖动/错位)****
- 高度不稳定引发“跳动” :图片先占位(固定宽高比)再加载;或使用 ConstraintLayout + dimensionRatio。
- 间隙修复:默认 GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS 会在滚动中调换列以填空;若你需要“保持列稳定”,可设 GAP_HANDLING_NONE,但要自己处理空隙。
- 定位:findFirstVisibleItemPositions(IntArray) 返回多个列的第一个可见,取最小值即可。
- Header/FullSpan 不要在 onCreateViewHolder 强写,放在 onBind 或 onViewAttachedToWindow,以免复用导致普通卡也被全宽。
3) 吸附(SnapHelper)
3.1 PagerSnapHelper:翻页(一次吸附一个 item,效果类似 ViewPager)
recyclerView.layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
PagerSnapHelper().attachToRecyclerView(recyclerView)
适用:横向 Banner/视频流/卡片翻页。
提示:需要页指示器时,监听 onScrollStateChanged == SCROLL_STATE_IDLE 后用 findSnapView() 定位当前页。
3.2 LinearSnapHelper:居中对齐(允许一次滑过多项)
recyclerView.layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
LinearSnapHelper().attachToRecyclerView(recyclerView)
注意
- RecyclerView.setOnFlingListener 与 SnapHelper 互斥;attachToRecyclerView 会自动设置。
- 想限制 fling 速度可重写 RecyclerView.fling 或给 LayoutManager 做速度上限。
4) 拖拽排序 & 滑动删除(ItemTouchHelper)
4.1 最小可用版(长按拖拽 + 右滑删除)
class DragSwipeCallback(
private val onMoveItem: (from: Int, to: Int) -> Unit,
private val onSwipedItem: (pos: Int) -> Unit
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, // 允许上下拖拽
ItemTouchHelper.START or ItemTouchHelper.END // 允许左右滑动删除
) {
override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
onMoveItem(vh.bindingAdapterPosition, target.bindingAdapterPosition)
return true
}
override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) {
onSwipedItem(vh.bindingAdapterPosition)
}
override fun isLongPressDragEnabled() = true // 或者在 handle 上手动 startDrag
override fun isItemViewSwipeEnabled() = true
}
val helper = ItemTouchHelper(DragSwipeCallback(
onMoveItem = { from, to -> adapter.moveItem(from, to) },
onSwipedItem = { pos -> adapter.removeAt(pos) }
))
helper.attachToRecyclerView(recyclerView)
配合 DiffUtil / ListAdapter(推荐)
fun ArticleAdapter.moveItem(from: Int, to: Int) {
val cur = currentList.toMutableList()
val item = cur.removeAt(from)
cur.add(if (to > from) to - 1 else to, item)
submitList(cur) // 用差分派发移动动画
}
fun ArticleAdapter.removeAt(pos: Int) {
val cur = currentList.toMutableList().apply { removeAt(pos) }
submitList(cur) // 用差分派发删除动画
}
可定制
- 只允许对某些 viewType 拖拽/滑动:重写 getMovementFlags,不同类型返回 0 禁止。
- 自定义删除背景/图标:重写 onChildDraw() 绘制红色背景与 delete 图标。
- 手柄拖拽:isLongPressDragEnabled=false,在 onBind 的“拖拽手柄” onTouch 里 helper.startDrag(holder)。
5) 网格/瀑布的“均匀间距”ItemDecoration(可直接用)
class GridSpacingDecoration(
private val spanCount: Int,
private val spacingPx: Int,
private val includeEdge: Boolean = true
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, v: View, rv: RecyclerView, state: RecyclerView.State) {
val pos = rv.getChildAdapterPosition(v)
if (pos == RecyclerView.NO_POSITION) return
val lm = rv.layoutManager
val column = when (lm) {
is GridLayoutManager -> (v.layoutParams as GridLayoutManager.LayoutParams).spanIndex
is StaggeredGridLayoutManager -> (v.layoutParams as StaggeredGridLayoutManager.LayoutParams).spanIndex
else -> 0
}
if (includeEdge) {
outRect.left = spacingPx - column * spacingPx / spanCount
outRect.right = (column + 1) * spacingPx / spanCount
if (pos < spanCount) outRect.top = spacingPx
outRect.bottom = spacingPx
} else {
outRect.left = column * spacingPx / spanCount
outRect.right = spacingPx - (column + 1) * spacingPx / spanCount
if (pos >= spanCount) outRect.top = spacingPx
}
}
}
使用
recyclerView.addItemDecoration(GridSpacingDecoration(spanCount = 2, spacingPx = dp(8)))
6) 示例汇总
// 1) 网格 + Header/广告跨列
val grid = GridLayoutManager(ctx, 2).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(pos: Int) = when (adapter.getItemViewType(pos)) {
VT_HEADER, VT_AD -> 2 else -> 1
}
}
}
recyclerView.layoutManager = grid
recyclerView.addItemDecoration(GridSpacingDecoration(2, dp(8)))
// 2) 瀑布流 + 全宽 Header + 占位防抖
val sglm = StaggeredGridLayoutManager(2, VERTICAL).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
recyclerView.layoutManager = sglm
// 在 onBind:header 设置 isFullSpan = true;图片使用固定比例占位
// 3) 横向翻页吸附
recyclerView.layoutManager = LinearLayoutManager(ctx, HORIZONTAL, false)
PagerSnapHelper().attachToRecyclerView(recyclerView)
// 4) 拖拽 / 滑删(配合 ListAdapter.submitList 差分)
ItemTouchHelper(DragSwipeCallback(
onMoveItem = { from, to -> adapter.moveItem(from, to) },
onSwipedItem = { pos -> adapter.removeAt(pos) }
)).attachToRecyclerView(recyclerView)
7) 性能与避坑清单(与本章强相关)
- Grid 跨列频繁变化会触发布局抖动:尽量让跨列类型稳定,不要随滚动临时改变。
- Staggered 抖动:图片必须有固定高宽比占位;需要“列稳定”时用 GAP_HANDLING_NONE + 自控。
- SnapHelper 加在只有一个 LayoutManager 的 RV 上;切换 LM 前先 attach(null)。
- ItemTouchHelper 与 DiffUtil:先改数据再 submitList,不要手工 notify*。
- 多适配器(ConcatAdapter) :跨 Adapter 的拖拽/滑删需要把位置转换为 absoluteAdapterPosition。
- 稳定 ID:开启 setHasStableIds(true) + getItemId,动画更准、复用更稳。
一句话收束
Grid 用 SpanSizeLookup 精准控制跨列;Staggered 通过 fullSpan 与 gapStrategy 应对不规则高度;SnapHelper 让列表“对齐”或“翻页”;ItemTouchHelper 负责拖拽/滑删——与 DiffUtil/ListAdapter 配套,做到“数据驱动 + 高质量动画 + 流畅滚动”。