Skip to content

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
    }
}