RecyclerView 源码分析(二):布局

前言

本文将主要分析 RecyclerView 的布局过程(以第一次布局为例),由于 RecyclerView 的 measure 也与 layout 关系比较密切,所以接下来主要看 onMeasure 和 onLayout,先看 onMeasure:

onMeasure

   @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        // mLayout 是 LayoutManager,通过 setLayoutManager 方法设置,没有设置则为空
        if (mLayout == null) {
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        // 是否开启自动测量(RV 提供的几种 LM 都开启了自动测量)
        if (mLayout.isAutoMeasureEnabled()) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            
            // 实际上就是调用 RV 的 defaultOnMeasure 方法
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

            final boolean measureSpecModeIsExactly =
                    widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
            // 如果测量模式是 EXACTLY,退出
            if (measureSpecModeIsExactly || mAdapter == null) {
                return;
            }

            // 布局状态为 STEP_START 时,进行 step1
            if (mState.mLayoutStep == State.STEP_START) {
                dispatchLayoutStep1();
            }

            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            // 进行 step2
            dispatchLayoutStep2();

            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            // 判断是否要测量两次
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
            // ...
        }
    }

RecyclerView 重写了 onMeasure 方法。该方法中,先判断是否有设置 LayoutManager,没有设置就执行 defaultOnMeasure。

设置了 LayoutManager 的话,就要判断 LayoutManager 是否开启了自动测量,开启的话就会使用默认的测量机制,否则就需要通过 LayoutManager 的 onMeasure 方法来完成测量工作。系统提供的几个 LayoutManager 都开启了自动测量。

自动测量时,涉及到一个重要的类:RecyclerView.State,这个类封装了当前 RecyclerView 的状态信息。其 mLayoutStep 变量表示当前 RecyclerView 的布局状态,状态有三种:

  • STEP_START
  • STEP_LAYOUT
  • STEP_ANIMATIONS

一开始的状态为 STEP_START,调用完 dispatchLayoutStep1 方法后,状态变为 STEP_LAYOUT,表示接下来要进行布局,调用完 dispatchLayoutStep2 方法后,状态变为 State.STEP_ANIMATIONS,等待之后在 layout 时执行 dispatchLayoutStep3

这三个 step 负责不同的工作,step1 负责更新和记录状态,step2 真正进行布局,step3 执行动画并进行清理工作。

可以看到,在开启自动测量时,RecyclerView 如果是 WRAP_CONTENT 状态,就要根据子 View 所占空间大小动态调整自己的大小,这时它就将子 View 的 measure 和 layout 提前到 onMeasure 中,因为它需要确定子 View 的大小和位置后,再来设置自己的大小。所以就会在 onMeasure 中执行 step1 和 step2。

接下来看一下 RecyclerView 的 layout 过程:

onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
        dispatchLayout();
        TraceCompat.endSection();
        mFirstLayoutComplete = true;
    }

调用 dispatchLayout:

    void dispatchLayout() {
        // ...
        
        mState.mIsMeasuring = false;
        // 如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1
        // 至于 step2,如果发现尺寸发生了改变,将会再执行一次
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

可以看到,如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1,至于 step2,如果发现尺寸发生了改变,将会再执行一次,否则也不会执行。最后执行 step3。

下面分别看下这 3 个 step,首先看 step1

RecyclerView#dispatchLayoutStep1

    private void dispatchLayoutStep1() {
        // ...
        
        // (1)
        processAdapterUpdatesAndSetAnimationFlags();

        // (2)
        if (mState.mRunSimpleAnimations) {
            // ...
        }
        if (mState.mRunPredictiveAnimations) {
            // ...
        }

        // ...
        mState.mLayoutStep = State.STEP_LAYOUT;
    }

先看注释(2),这里会根据 mRunSimpleAnimations 和 mRunPredictiveAnimations 的值来决定是否运行简单动画和预动画。这两个值是在哪里设置的呢?答案是在注释(1)的 processAdapterUpdatesAndSetAnimationFlags 方法处:

RecyclerView#processAdapterUpdatesAndSetAnimationFlags

    private void processAdapterUpdatesAndSetAnimationFlags() {
        // ...
        
        // mItemsAddedOrRemoved:当有 item 添加或删除的时候设置为 ture
        // mItemsChanged:当有 item 的数据更新时设置为 true
        boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
        
        // 1. mFirstLayoutComplete:第一次 layout 完成后,设置为 true
        // 2. mItemAnimator:默认为 DefaultItemAnimator,可通过 RecyclerView 的 setItemAnimator 方法设置
        // 3. mDataSetHasChangedAfterLayout:调用 setAdapter、swapAdapter 或 notifyDateSetChanged 
        // 后设置为 true,在 layout 过程的 step3 中设置为 false
        // 4. mLayout.mRequestedSimpleAnimations:默认为 false,
        // 可以通过调用 LayoutManager 的 requestSimpleAnimationsInNextLayout 方法将该值设置为 true
        // 5. mAdapter.hasStableIds:默认为 false,可通过 Adapter 的 setHasStableIds 方法设置
        mState.mRunSimpleAnimations = mFirstLayoutComplete
                && mItemAnimator != null
                && (mDataSetHasChangedAfterLayout
                || animationTypeSupported
                || mLayout.mRequestedSimpleAnimations)
                && (!mDataSetHasChangedAfterLayout
                || mAdapter.hasStableIds());
                
        // predictiveItemAnimationsEnabled:LinearLayoutManager 默认支持预动画,返回 true
        mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
                && animationTypeSupported
                && !mDataSetHasChangedAfterLayout
                && predictiveItemAnimationsEnabled();
    }

里面的一些属性在注释中已经有说明。这里以第一次 layout 为例,此时由于第一次 layout 过程还未完成,mFirstLayoutComplete 为 false,mRunSimpleAnimations 也就为 false,进而 mRunPredictiveAnimations 也为 false。

所以在第一次 layout 中,并不会进行简单动画和预动画。这里就先不分析了,详细过程在分析动画的时候再说。

下面重点看一下 step2:

RecyclerView#dispatchLayoutStep2

    private void dispatchLayoutStep2() {
        // ...

        mLayout.onLayoutChildren(mRecycler, mState);

        // ...
    }

step2 进行真正的布局,布局任务交由 LayoutManager 负责,调用其 onLayoutChildren 方法为所有子 View 布局。该方法交由具体的 LayoutManager 实现,这里以 LinearLayoutManager 为例,看一下它的 onLayoutChildren 实现:

LinearLayoutManager#onLayoutChildren

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // ...

        // AnchorInfo 的 mValid 属性默认为 false 
        if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
                || mPendingSavedState != null) {
            mAnchorInfo.reset();
            // mShouldReverseLayout 和 mStackFromEnd 默认都为 false
            // 异或操作后结果仍为 false
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // 找到锚点的位置,保存到 AnchorInfo 的 mPosition 中
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        }

        // ...
        
        // (1)
        detachAndScrapAttachedViews(recycler);
        
        if (mAnchorInfo.mLayoutFromEnd) {
            // ...
        } else {
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            // ...

            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);

            // ...
        }
        
        // ...
    }

注释(1)处调用了 detachAndScrapAttachedViews 方法,该方法会将子 View 移除并根据情况添加到相应缓存中。所以如果不是第一次 layout,RecyclerView 已经存在子 View 的话,在重新填充布局前,会将旧的子 View 添加到缓存中,这样之后填充布局时就可以直接从缓存中拿,不用再次创建子 View。

下面看下布局过程,主要分两步:

1. 找到锚点(auchor 点)

该过程通过 updateAnchorInfoForLayout 方法实现:

updateAnchorInfoForLayout

    private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
            AnchorInfo anchorInfo) {
        // 一般这里都是返回 false
        if (updateAnchorFromPendingData(state, anchorInfo)) {
            return;
        }
        
        // 首先从子 View 中获取锚点
        if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
            return;
        }

        // 没有从子 View 得到锚点,就将头或尾设置为锚点(默认将头设置为锚点)
        anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
    }

继续看下 updateAnchorFromChildren 方法,该方法从子 View 中获取锚点

updateAnchorFromChildren

    private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
            RecyclerView.State state, AnchorInfo anchorInfo) {
        if (getChildCount() == 0) {
            return false;
        }
        // 将被 focus 的子 View 作为锚点
        final View focused = getFocusedChild();
        if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
            anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
            return true;
        }
        
        if (mLastStackFromEnd != mStackFromEnd) {
            return false;
        }
        
        // 根据 layout 的方向决定锚点,默认从上往下,所以锚点在头部
        View referenceChild = anchorInfo.mLayoutFromEnd
                ? findReferenceChildClosestToEnd(recycler, state)
                : findReferenceChildClosestToStart(recycler, state);
        if (referenceChild != null) {
            anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
            // ...
            return true;
        }
        return false;
    }

可以看到,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认从上往下布局,所以锚点选取头部。

如果想要从下往上布局,可以这样设置:

    linearLayoutManager.setStackFromEnd(true);

这样的话,锚点会在尾部,数据加载完后首先显示的是底部的数据。

2. 填充布局

根据布局方向,先后填充满锚点上方和下方的所有区域

填充的过程调用 fill 方法:

fill

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // ...
        
        // 进行 layout 时,layoutState.mScrollingOffset 的值等于 
        // LayoutState.SCROLLING_OFFSET_NaN,不会进入此 if 块,这里先不分析
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // ...
            // 进行回收工作
            recycleByLayoutState(recycler, layoutState);
        }
        
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // ...
            
            // (1)
            layoutChunk(recycler, state, layoutState, layoutChunkResult);

            // ...
        }
        // ...
    }

看注释(1)处,在 while 循环里有一个 layoutChunk 方法,只要还有剩余空间,就不会不断执行该方法:

layoutChunk

    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        // (1)
        View view = layoutState.next(recycler);

        // ...
        
        // 默认情况下,layoutState.mScrapList 等于 null
        if (layoutState.mScrapList == null) {
            // mShouldReverseLayout 默认为 false,可通过 LLM 的 setReverseLayout 方法设置
            // 从上往下填充布局时,layoutState.mLayoutDirection 为 LayoutState.LAYOUT_END
            // 默认情况下,从上往下布局时进入 if 块
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                // (2)
                addView(view);
            } else {
                addView(view, 0);
            }
        } else {
            // ...
        }

        // ...
        
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        layoutDecoratedWithMargins(view, left, top, right, bottom);

    }

先看注释(1)处,这里返回下一个要填充的 View,来看下具体过程:

    View next(RecyclerView.Recycler recycler) {
        // ...
        
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }

可以看到,获取 View 的工作也是交给了 Recycler,通过 Recycler 的 getViewForPosition 来获取一个指定位置的子 View,该方法在 Recycler 已经分析过了。

继续看注释(2)处的 addView 方法:

    private void addViewInt(View child, int index, boolean disappearing) {
        final ViewHolder holder = getChildViewHolderInt(child);
        
        // ...
        
        // 该 ViewHolder 从 ChangedScrap、AttachedScrap、HiddenViews 中得到
        // 或者该 ViewHolder 曾经通过 scrapView 方法缓存到 Scrap 缓存中 
        if (holder.wasReturnedFromScrap() || holder.isScrap()) {
            // 做些清理工作:删除 Scrap 缓存、清除标记等
            if (holder.isScrap()) {
                holder.unScrap();
            } else {
                holder.clearReturnedFromScrapFlag();
            }
            // 子 View 重新 attach 到 RecyclerView 中
            mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
            // DISPATCH_TEMP_DETACH:该值默认为 false,且没看到有地方将其设置为 true
            if (DISPATCH_TEMP_DETACH) {
                ViewCompat.dispatchFinishTemporaryDetach(child);
            }
        } 
        // 该子 View 一直是有效的,只是可能要移动下位置(对应滑动时没有滑出屏幕的子 View)
        else if (child.getParent() == mRecyclerView) {
            int currentIndex = mChildHelper.indexOfChild(child);
            if (index == -1) {
                index = mChildHelper.getChildCount();
            }
            // 将该子 View 移动到正确位置
            if (currentIndex != index) {
                mRecyclerView.mLayout.moveView(currentIndex, index);
            }
        } 
        // 其他情况,例如从 CahcedView 或 RecycledViewPool 得到的缓存 View,或者是新创建的 View
        else {
            mChildHelper.addView(child, index, false);
            lp.mInsetsDirty = true;
            if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
                mSmoothScroller.onChildAttachedToWindow(child);
            }
        }
        
        // ...
    }

该方法通过判断 View 的来源,利用不同的方式将子 View 添加到 RecyclerView 中,填充完布局。

最后看一下 step3:

dispatchLayoutStep3

    private void dispatchLayoutStep3() {
        // ...
        
        // 将 layout 状态重置回 State.STEP_START
        mState.mLayoutStep = State.STEP_START;
        
        // 执行动画
        if (mState.mRunSimpleAnimations) {
            // ...
        }

        // 清空 attachedScrap
        mLayout.removeAndRecycleScrapInt(mRecycler);
        // 重置一系列的变量
        mState.mPreviousLayoutItemCount = mState.mItemCount;
        mDataSetHasChangedAfterLayout = false;
        mDispatchItemsChangedEvent = false;
        mState.mRunSimpleAnimations = false;

        mState.mRunPredictiveAnimations = false;
        mLayout.mRequestedSimpleAnimations = false;
        // 清空 changedScrao
        if (mRecycler.mChangedScrap != null) {
            mRecycler.mChangedScrap.clear();
        }

        // 其它清理工作
    }

step3 主要是执行动画和进行一系列的清理工作,例如重置 layout 状态,清理 Scrap 缓存等等。由于在第一次布局时,mState.mRunSimpleAnimations 为 false,不会执行动画,动画部分就先不分析了。

小结

前面说了这么多,这里小结一下 onLayout 的过程:

  1. layout 过程分为 3 个 step,step1 负责更新和记录状态,step2 真正进行布局,step 执行动画并进行清理工作。如果 RecyclerView 的宽高为 WRAP_CONTENT 模式,那么需要在 measure 过程提前进行 step1 和 step2,先获得子 View 的大小,才能确定自己的大小。而 step3 肯定是在 layout 过程执行。
  2. step2 真正进行布局,布局任务由 LayoutManager 负责,通过它的 onLayoutChildren 方法对子 View 进行布局。布局过程分两步:
    1. 找到锚点,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认头部为锚点。
    2. 根据布局方向,先后填充满锚点上方和下方的区域,填充所需的 View 交由 Recycler 提供。

写在最后

本文主要以第一次布局,分析了 RecyclerView 的 measure 和 layout 过程。当然,主要分析的还是 layout 过程。至于第二次 layout 或者是更新列表时的 layout,会在动画和缓存上有所不同,但主要流程还是一样的,并且缓存相关的在第一篇有更详细的说明,而动画的话可能会在后面另开一篇来讲。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容