RecyclerView 详解(二)—— 布局、绘制与滑动
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. LayoutManager 工作原理
1.1 职责
LayoutManager 负责:
- 决定 item 的摆放位置(线性、网格、瀑布流)
- 决定何时回收不可见的 item
- 处理滚动
- 支持预取(Prefetch)
1.2 核心方法
java
public abstract static class LayoutManager {
// 必须实现:生成默认的 LayoutParams
public abstract LayoutParams generateDefaultLayoutParams();
// 布局子 View(核心方法)
public void onLayoutChildren(Recycler recycler, State state) {}
// 是否支持水平/垂直滚动
public boolean canScrollHorizontally() { return false; }
public boolean canScrollVertically() { return false; }
// 处理滚动
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { return 0; }
public int scrollVerticallyBy(int dy, Recycler recycler, State state) { return 0; }
}1.3 LinearLayoutManager 的 fill 过程
java
// LinearLayoutManager.fill() 简化
int fill(Recycler recycler, LayoutState layoutState, State state) {
int remainingSpace = layoutState.mAvailable;
while (remainingSpace > 0 && layoutState.hasMore(state)) {
// 1. 从 Recycler 获取 ViewHolder
View view = layoutState.next(recycler); // 内部调用 recycler.getViewForPosition()
// 2. 添加到 RecyclerView
if (layoutState.mScrapList == null) {
addView(view); // 正常布局
} else {
addDisappearingView(view); // 动画中消失的 View
}
// 3. 测量
measureChildWithMargins(view, 0, 0);
// 4. 布局(确定位置)
layoutDecoratedWithMargins(view, left, top, right, bottom);
// 5. 更新剩余空间
remainingSpace -= view.getDecoratedMeasuredHeight();
}
return consumed;
}1.4 三种内置 LayoutManager
LinearLayoutManager:
- 线性排列(垂直/水平)
- 支持 reverseLayout(反向排列)
- 支持 stackFromEnd(从底部开始填充,如聊天列表)
GridLayoutManager:
- 继承自 LinearLayoutManager
- 支持 spanCount(列数)和 SpanSizeLookup(动态列宽)
kotlin
val layoutManager = GridLayoutManager(context, 3)
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
// header 占满一行,普通 item 占 1 列
return if (adapter.getItemViewType(position) == TYPE_HEADER) 3 else 1
}
}StaggeredGridLayoutManager:
- 瀑布流布局
- 每个 item 可以有不同高度
- 支持全宽 item(
setFullSpan(true))
1.5 自定义 LayoutManager
kotlin
class CustomLayoutManager : RecyclerView.LayoutManager() {
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
// 1. 回收所有现有 View
detachAndScrapAttachedViews(recycler)
// 2. 填充可见区域
var offsetY = 0
for (i in 0 until itemCount) {
if (offsetY > height) break // 超出可见区域,停止
val view = recycler.getViewForPosition(i)
addView(view)
measureChildWithMargins(view, 0, 0)
val width = getDecoratedMeasuredWidth(view)
val height = getDecoratedMeasuredHeight(view)
layoutDecorated(view, 0, offsetY, width, offsetY + height)
offsetY += height
}
}
override fun canScrollVertically() = true
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: RecyclerView.State): Int {
// 1. 移动所有子 View
offsetChildrenVertical(-dy)
// 2. 回收不可见的 View
recycleInvisibleViews(recycler)
// 3. 填充新出现的 View
fillVisibleViews(recycler)
return dy
}
}2. 滑动机制
2.1 滑动流程
手指触摸 ACTION_DOWN
→ RecyclerView.onInterceptTouchEvent
→ 记录初始位置
手指移动 ACTION_MOVE
→ 判断滑动距离是否超过 touchSlop
→ 超过:拦截事件,进入滑动模式
→ scrollByInternal(dx, dy)
→ LayoutManager.scrollVerticallyBy(dy)
→ offsetChildrenVertical(-dy) // 移动子 View
→ fill() // 填充新 item
→ recycleByLayoutState() // 回收旧 item
手指抬起 ACTION_UP
→ 计算 fling 速度
→ 如果速度足够大
→ ViewFlinger.fling(velocityX, velocityY)
→ OverScroller 计算每帧滚动距离
→ postOnAnimation → 每帧回调 run()
→ scrollBy() → 同上面的滑动逻辑2.2 Fling 惯性滑动
java
// ViewFlinger 是 RecyclerView 的内部类
class ViewFlinger implements Runnable {
OverScroller mOverScroller;
void fling(int velocityX, int velocityY) {
mOverScroller.fling(0, 0, velocityX, velocityY, ...);
postOnAnimation(); // 注册到 Choreographer
}
@Override
public void run() {
if (mOverScroller.computeScrollOffset()) {
int dx = mOverScroller.getCurrX() - mLastX;
int dy = mOverScroller.getCurrY() - mLastY;
// 执行滚动
scrollByInternal(dx, dy);
// 还没停,继续下一帧
postOnAnimation();
}
}
}2.3 SnapHelper
SnapHelper 让滑动停止时自动对齐到某个 item:
kotlin
// LinearSnapHelper:对齐到最近的 item 中心
LinearSnapHelper().attachToRecyclerView(recyclerView)
// PagerSnapHelper:一次只滑一页(类似 ViewPager)
PagerSnapHelper().attachToRecyclerView(recyclerView)原理:SnapHelper 监听滑动状态,在 IDLE 时计算当前位置与目标对齐位置的偏移量,调用 smoothScrollBy 修正。
java
// SnapHelper 核心逻辑
private final RecyclerView.OnScrollListener mScrollListener = new OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
snapToTargetExistingView(); // 对齐
}
}
};
void snapToTargetExistingView() {
View snapView = findSnapView(layoutManager); // 找到要对齐的 View
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView); // 计算偏移
recyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]); // 平滑滚动对齐
}3. ItemDecoration
3.1 原理
ItemDecoration 在 item 绘制的前后插入自定义绘制,并可以为 item 添加偏移量(间距):
java
public abstract static class ItemDecoration {
// 在 item 之前绘制(item 会覆盖在上面)
public void onDraw(Canvas c, RecyclerView parent, State state) {}
// 在 item 之后绘制(覆盖在 item 上面)
public void onDrawOver(Canvas c, RecyclerView parent, State state) {}
// 为 item 设置偏移量(间距)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {}
}绘制顺序:ItemDecoration.onDraw → item 自身绘制 → ItemDecoration.onDrawOver
3.2 分割线实现
kotlin
class DividerDecoration(
private val height: Int = 1.dp,
private val color: Int = Color.LTGRAY
) : RecyclerView.ItemDecoration() {
private val paint = Paint().apply { this.color = color }
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
// 除了最后一个 item,每个 item 底部留出分割线高度
val position = parent.getChildAdapterPosition(view)
if (position < state.itemCount - 1) {
outRect.bottom = height
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: State) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin
c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), (top + height).toFloat(), paint)
}
}
}3.3 吸顶效果(Sticky Header)
kotlin
class StickyHeaderDecoration(
private val adapter: StickyHeaderAdapter
) : RecyclerView.ItemDecoration() {
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: State) {
val topChild = parent.getChildAt(0) ?: return
val topPosition = parent.getChildAdapterPosition(topChild)
val headerView = adapter.getHeaderView(parent, topPosition)
// 测量和布局 header
headerView.measure(
View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
// 检查是否需要被下一个 header 推上去
val nextHeaderPos = findNextHeaderPosition(topPosition)
val nextHeaderView = parent.findViewHolderForAdapterPosition(nextHeaderPos)?.itemView
val offset = if (nextHeaderView != null && nextHeaderView.top < headerView.measuredHeight) {
nextHeaderView.top - headerView.measuredHeight
} else 0
c.save()
c.translate(0f, offset.toFloat())
headerView.draw(c)
c.restore()
}
}4. ItemAnimator
4.1 动画类型
DefaultItemAnimator 支持四种动画:
- add:新 item 淡入
- remove:旧 item 淡出
- move:item 位置变化时平移
- change:item 内容变化时交叉淡入淡出
4.2 动画触发条件
notifyItemInserted(pos) → add 动画
notifyItemRemoved(pos) → remove 动画
notifyItemMoved(from,to) → move 动画
notifyItemChanged(pos) → change 动画
notifyDataSetChanged() → 无动画(所有 ViewHolder 失效)4.3 Pre-layout 和 Post-layout
RecyclerView 的动画依赖两次 layout:
Step 1: Pre-layout(dispatchLayoutStep1)
→ 记录当前所有 item 的位置(动画起始状态)
→ 对于 remove 的 item,仍然参与布局
→ 对于 insert 的 item,还不存在
Step 2: Post-layout(dispatchLayoutStep2)
→ 按新数据重新布局(动画结束状态)
→ remove 的 item 不再参与
→ insert 的 item 出现
Step 3: 执行动画(dispatchLayoutStep3)
→ 对比 pre 和 post 的位置差异
→ 生成对应的动画(add/remove/move/change)
→ 执行动画这就是为什么 notifyItemChanged 会用到 mChangedScrap:pre-layout 需要旧的 ViewHolder 记录起始位置,post-layout 用新的 ViewHolder 显示新数据,两者之间做交叉淡入淡出动画。
4.4 自定义 ItemAnimator
kotlin
class SlideInAnimator : DefaultItemAnimator() {
override fun animateAdd(holder: RecyclerView.ViewHolder): Boolean {
holder.itemView.translationX = holder.itemView.width.toFloat()
holder.itemView.alpha = 0f
ViewCompat.animate(holder.itemView)
.translationX(0f)
.alpha(1f)
.setDuration(addDuration)
.setListener(object : ViewPropertyAnimatorListener {
override fun onAnimationEnd(view: View) {
dispatchAddFinished(holder)
}
override fun onAnimationStart(view: View) { dispatchAddStarting(holder) }
override fun onAnimationCancel(view: View) {}
})
.start()
return true
}
}