12、RecyclerView的LayoutManager:LinearLayoutManager与GridLayoutManager的源码分析,自定义LayoutManager。

聊到 RecyclerView,绕不开的就是 LayoutManager。很多人觉得它就是个布局管理器,负责把 item 摆上去。嗯,这么说也没错,但太浅了。我做了这么多年系统 UI,可以负责任地告诉你:LayoutManager 是 RecyclerView 的灵魂。你想想看,RecyclerView 本身不关心 item 怎么摆、怎么滚动、怎么回收,这些脏活累活全扔给了 LayoutManager。

今天我们就来扒一扒 LinearLayoutManager 和 GridLayoutManager 的源码,看看它们到底是怎么工作的。最后我会带你手写一个自定义 LayoutManager,让你彻底搞懂这套机制。

12.1 LinearLayoutManager:最基础的线性布局

LinearLayoutManager 是我们用得最多的一个。它负责把 item 排成一行或一列。说白了,就是纵向或横向的线性排列。

它的核心逻辑其实就两个:布局子 View回收子 View。我当年刚接触 RecyclerView 时,以为布局就是简单的 for 循环往里 addView。后来看了源码才发现,事情没那么简单。

12.1.1 布局流程:onLayoutChildren

入口方法是 onLayoutChildren。这个方法会在 RecyclerView 初次布局、数据变化、或者滚动后需要重新布局时被调用。

// LinearLayoutManager 简化版 onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (mPendingSavedState != null) {
        // 恢复保存的状态
    }
    // 1. 分离所有 View 到 Scrap 中
    detachAndScrapAttachedViews(recycler);
    // 2. 计算锚点位置
    updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
    // 3. 从锚点开始填充
    fill(recycler, mLayoutState, state, false);
}

这里有个关键点:detachAndScrapAttachedViews。它会把当前屏幕上所有的 View 先「分离」出来,放到一个临时缓存里。为什么要这么做?因为布局要重新计算,旧的 View 位置可能已经不对了。分离后,这些 View 可以被复用,而不是销毁重建。

核心思想:RecyclerView 的布局不是「清空重画」,而是「分离复用」。这是它性能好的根本原因。

12.1.2 填充逻辑:fill 方法

fill 方法是 LinearLayoutManager 最核心的循环。它负责从某个起始位置开始,不断从 Recycler 中获取 View,然后测量、布局,直到填满可见区域。

// 简化版 fill 方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
    int remainingSpace = layoutState.mAvailable; // 剩余可用空间
    int laidOut = 0;
    
    while (remainingSpace > 0 && layoutState.hasMore(state)) {
        // 1. 从 Recycler 获取 View
        View view = layoutState.next(recycler);
        // 2. 测量 View
        measureChildWithMargins(view, 0, 0);
        // 3. 计算消耗的空间
        int consumed = getDecoratedMeasuredWidth(view);
        // 4. 布局 View
        layoutChunk(view, layoutState);
        // 5. 更新剩余空间
        remainingSpace -= consumed;
        laidOut++;
    }
    return laidOut;
}

你看,这个循环很简洁。但实际源码里要考虑的事情多得多:边距、分割线、ItemAnimator 的动画状态、预取(Prefetch)等等。我曾在项目中遇到过一个问题:快速滑动时 item 闪烁。后来发现是 fill 方法里没有正确处理预取逻辑,导致 View 被提前回收了。

避坑指南:我曾经在自定义 LayoutManager 时,直接复用了 fill 方法,但忘了处理 layoutState.mAvailable 的负值情况。结果在快速滑动时,item 布局错乱。记住:剩余空间可能是负数,表示已经滚过头了。

12.2 GridLayoutManager:网格布局的巧妙实现

GridLayoutManager 继承自 LinearLayoutManager。很多人以为它是完全独立的实现,其实不是。它复用了 LinearLayoutManager 的滚动和回收逻辑,只是在布局时做了「分栏」处理。

它的核心是 SpanSizeLookup。这个类决定了每个 item 占几列。默认是 1 列,你可以让某个 item 占满整行(比如广告位)。

12.2.1 分栏逻辑:如何计算每行的 item 数

GridLayoutManager 在布局时,会先计算当前行已经占了多少列。如果加上当前 item 的 span 会超过总列数,就换行。

// GridLayoutManager 的布局核心
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState) {
    // 1. 获取当前行的 span 信息
    SparseIntArray spanIndexCache = mSpanIndexCache;
    // 2. 计算当前行能放多少个 item
    int spanCount = mSpanCount;
    int currentSpanIndex = 0;
    
    // 3. 循环获取 item,直到填满一行
    while (currentSpanIndex < spanCount && layoutState.hasMore(state)) {
        View view = layoutState.next(recycler);
        int spanSize = mSpanSizeLookup.getSpanSize(view);
        // 如果当前行放不下,就换行
        if (currentSpanIndex + spanSize > spanCount) {
            break;
        }
        // 测量并布局
        measureChild(view);
        layoutDecoratedWithMargins(view, ...);
        currentSpanIndex += spanSize;
    }
}

这里有个细节:GridLayoutManager 的 item 宽度是均分的。总宽度减去分割线后,除以总列数,就是每列的宽度。如果某个 item 占 2 列,它的宽度就是 2 倍列宽加上中间的分割线。

注意:GridLayoutManager 不支持不等高的 item(除非你用 StaggeredGridLayoutManager)。如果你需要瀑布流效果,请使用 StaggeredGridLayoutManager,而不是在 GridLayoutManager 里硬改高度。

12.3 自定义 LayoutManager:从零开始

好了,理论说完了。我们来点实际的。我会带你写一个最简单的自定义 LayoutManager,实现「从右到左」的横向布局。为什么选这个?因为系统自带的 LinearLayoutManager 只支持从左到右,不支持 RTL 的横向布局(虽然它支持 RTL 属性,但实现上有些坑)。

12.3.1 继承 RecyclerView.LayoutManager

自定义 LayoutManager 需要继承 RecyclerView.LayoutManager,并实现几个关键方法。

public class RightToLeftLayoutManager extends RecyclerView.LayoutManager {
    
    private int mTotalWidth = 0; // 所有 item 的总宽度
    
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(
            RecyclerView.LayoutParams.WRAP_CONTENT,
            RecyclerView.LayoutParams.WRAP_CONTENT
        );
    }
    
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (state.getItemCount() == 0) {
            detachAndScrapAttachedViews(recycler);
            return;
        }
        // 分离旧 View
        detachAndScrapAttachedViews(recycler);
        
        // 从右向左布局
        int parentRight = getWidth() - getPaddingRight();
        int parentTop = getPaddingTop();
        
        mTotalWidth = 0;
        
        for (int i = 0; i < state.getItemCount(); i++) {
            View view = recycler.getViewForPosition(i);
            addView(view);
            measureChildWithMargins(view, 0, 0);
            
            int childWidth = getDecoratedMeasuredWidth(view);
            int childHeight = getDecoratedMeasuredHeight(view);
            
            // 从右向左布局:item 的右边界对齐父容器右边界
            int left = parentRight - childWidth - mTotalWidth;
            int top = parentTop;
            int right = left + childWidth;
            int bottom = top + childHeight;
            
            layoutDecoratedWithMargins(view, left, top, right, bottom);
            
            mTotalWidth += childWidth;
        }
    }
    
    @Override
    public boolean canScrollHorizontally() {
        return true; // 支持横向滚动
    }
    
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        // 处理横向滚动
        int travel = dx;
        // 限制滚动范围
        if (mTotalWidth < getWidth()) {
            travel = 0; // 内容不足一屏,不滚动
        } else {
            if (dx < 0) {
                // 向右滚动(内容向左移动)
                travel = Math.max(dx, -(mTotalWidth - getWidth()));
            } else {
                // 向左滚动(内容向右移动)
                travel = Math.min(dx, 0);
            }
        }
        
        // 移动所有子 View
        offsetChildrenHorizontal(-travel);
        return travel;
    }
}

这个实现很简单,但已经能工作了。它把 item 从右向左排列,并且支持横向滚动。你可能会问:为什么不用系统的 LinearLayoutManager 设置布局方向?嗯,系统的实现里,RTL 布局在滚动边界处理上有些 bug,我遇到过几次,干脆自己写了。

12.3.2 回收与复用:必须处理的关键点

上面的代码有个大问题:没有回收屏幕外的 View。如果你有 1000 个 item,它会一次性创建 1000 个 View,内存直接爆炸。

正确的做法是在 scrollHorizontallyBy 里,回收移出屏幕的 View,并填充新进入屏幕的 View。

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 1. 计算实际滚动距离
    int travel = dx;
    // ... 边界限制代码同上 ...
    
    // 2. 移动 View
    offsetChildrenHorizontal(-travel);
    
    // 3. 回收移出屏幕的 View
    recycleViewsOutOfBounds(recycler);
    
    // 4. 填充新进入屏幕的 View
    fillGap(recycler, state);
    
    return travel;
}

private void recycleViewsOutOfBounds(RecyclerView.Recycler recycler) {
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        if (getDecoratedRight(child) < 0 || getDecoratedLeft(child) > getWidth()) {
            removeAndRecycleView(child, recycler);
        }
    }
}

这里我用了 removeAndRecycleView,它会将 View 回收到 Recycler 的缓存池中,而不是销毁。下次需要时,直接从缓存取,避免了频繁的 inflate 和 bind。

经验之谈:自定义 LayoutManager 时,回收逻辑是最容易出 bug 的地方。我建议你遵循「先回收,再填充」的顺序。如果先填充再回收,可能会导致刚填充的 View 又被回收掉,造成闪烁。

12.4 知识体系总览

为了让你更直观地理解 LayoutManager 的整体架构,我画了一张图:

LayoutManager 知识体系 RecyclerView LayoutManager(抽象基类) LinearLayoutManager GridLayoutManager 继承 StaggeredGridLayoutManager 核心方法 onLayoutChildren() | scrollHorizontallyBy() | scrollVerticallyBy() | canScroll()

从这张图可以看出,LinearLayoutManager 是基础,GridLayoutManager 继承自它,StaggeredGridLayoutManager 则是独立的实现。自定义 LayoutManager 时,你可以继承任意一个,也可以直接从 LayoutManager 基类开始。

12.5 总结

今天的内容不少,我们来捋一捋重点:

  • LinearLayoutManager 的核心是 fill 循环,它决定了 item 如何被填充到屏幕上。
  • GridLayoutManager 复用了 LinearLayoutManager 的滚动逻辑,通过 SpanSizeLookup 实现分栏。
  • 自定义 LayoutManager 需要实现 onLayoutChildren、滚动方法,以及最重要的回收复用逻辑。

说实话,自定义 LayoutManager 的门槛不低。我当年第一次写的时候,光是回收逻辑就调了三天。但一旦你掌握了这套机制,RecyclerView 对你来说就没有秘密了。你可以实现任何你想要的布局效果——环形布局、扇形布局、甚至 3D 旋转布局。

嗯,今天就到这里。下一章我们会深入 RecyclerView 的缓存机制,看看 Recycler 到底是怎么管理 View 的。到时候你会发现,LayoutManager 和 Recycler 的配合,才是 RecyclerView 性能的终极秘密。

练习建议:试着基于今天的代码,实现一个「从下到上」的纵向布局。你会发现,只需要改几个坐标计算,就能得到完全不同的效果。动手试试吧。

公众号:蓝海资料掘金营,微信deep3321