RecyclerView预布局

RecyclerView应该是我们使用非常频繁的一个组件 我们也有必要学习分析一下RecyclerView#onLayout工作流程 对我们日后分析优化RecyclerView也会有帮助
美其名曰 知其然也知所以然

预布局是什么?

这个问题其实我们可以先阅读完下面的代码 然后再来回想这个问题 所以我先给出结论
首先预布局就是先布局一次(🤣凑个字数) 然后会形成一个快照(pre-layout) 如item1234 然后再布局一次(post-layout) 形成另外一张快照 item134 这样我们其实就知道了整个动画轨迹 就可以生成动画
上面看不懂 没关系 看下图👇

pre-layout.png

下面分别是三种状态

  • 初始状态
  • pre-layout(预布局阶段 生成一个快照 详细代码我们会在下面分析)
  • post-layout(布局阶段 生成另一个快照)

然后我们就知道了item3的初始位置和终止位置 就可以生成动画并执行

源码调试手段

因为我们分析源码的过程中 经常有很多个分支 不知道如何是好😑 所以就需要断点调试手段了
首先我们启动一个AVD模拟器 然后AVD的版本号一定要和compileSdkVersion版本号是一样的 不然会出现代码行数不一致导致调试不了问题
然后我们就可以愉快的找一个调试点开始调试啦

RecyclerView#onLayout流程

先从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() {
        if (mAdapter == null) {
            Log.e(TAG, "No adapter attached; skipping layout");
            // leave the state in START
            return;
        }
        if (mLayout == null) {
            Log.e(TAG, "No layout manager attached; skipping layout");
            // leave the state in START
            return;
        }
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            // First 2 steps are done in onMeasure but looks like we have to run again due to
            // changed size.
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

我们发现会按照调用顺序
dispatchLayoutStep1()-> dispatchLayoutStep2()-> dispatchLayoutStep3()

我们挨个分析一下各方法功能 然后最后会做一个总结 朋友们也可以先点击✈️先看一眼结论

开始分析吧 任重而道远

 private void dispatchLayoutStep1() {
         ......
         //设置mInPreLayout 预布局标志位
        mState.mInPreLayout = mState.mRunPredictiveAnimations;
               mState.mLayoutStep = State.STEP_LAYOUT;
        ......
        //调用LayoutManager.onLayoutChildren方法
        mLayout.onLayoutChildren(mRecycler, mState);
        ......
    }

我们看到这里设置了mInPreLayout 我们草率的先认定它是预布局的标志位 当然结果也是对的🤡
全局搜索一下mInPreLayout的赋值 发现只有这里两处对mInPreLayout赋值
一处是onMeasure()

 if (mState.mRunPredictiveAnimations) {
     mState.mInPreLayout = true;
 } 

那么mState.mRunPredictiveAnimations一定是true 不然就没办法判断了

image.png

我们断点验证一下 发现mState.mRunPredictiveAnimations确实是true

LinearLayoutManager#onLayoutChildren()

我们这里以LinearLayoutManager来分析 其他几个LayoutManger其实也差不太多
根据注释 我们看到onLayoutChildren主要做了四件事情

  • 检查children 找到第一个需要变化(删除或者添加)的position
  • 从底向上填充布局
  • 从上向下填充布局
  • 滚动布局来填充
@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        // layout algorithm:
        // 1) by checking children and other variables, find an anchor coordinate and an anchor
        //  item position.
        // 2) fill towards start, stacking from bottom
        // 3) fill towards end, stacking from top
        // 4) scroll to fulfill requirements like stack from bottom.
        // create layout state
        .......
        //将所有view先回收
        detachAndScrapAttachedViews(recycler);
        final int firstLayoutDirection;
        //从下往上
        if (mAnchorInfo.mLayoutFromEnd) {
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForStart;
            //填充布局
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
            final int firstElement = mLayoutState.mCurrentPosition;
            if (mLayoutState.mAvailable > 0) {
                extraForEnd += mLayoutState.mAvailable;
            }
            // fill towards end
            updateLayoutStateToFillEnd(mAnchorInfo);
            mLayoutState.mExtraFillSpace = extraForEnd;
            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;

            if (mLayoutState.mAvailable > 0) {
                // end could not consume all. add more items towards start
                extraForStart = mLayoutState.mAvailable;
                updateLayoutStateToFillStart(firstElement, startOffset);
                mLayoutState.mExtraFillSpace = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            //从上往下 和上面一样的流程
            ......
        }

        ......
    }

下面就到了我们熟悉的fill()方法 我们可能在很多文章都看到过这个方法 会依次调用layoutChunk来对布局进行填充 这边我们需要重点关注一下预布局相关的不同点🤣 毕竟我们这篇文章的主题是分析预布局的啊

我们看一下fill()方法 这边我们会重点关注一下remainSpace 我们之前一直在提的 预布局会形成一张快照 postLayout也会形成另一张快照 fill()方法中会对两次布局做不同的处理

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        ......
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;//可用剩余布局空间
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //只要remainingSpace>0 并且position<itemsCount 
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            layoutChunkResult.resetInternal();
            //layout item
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            if (layoutChunkResult.mFinished) {
                break;
            }
            layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            //这里我们重点看一下 只有Post-Layut 或者mIgnoreConsumed为false remainingSpace才会减少 ①
            //否则remainingSpace不会减少
            if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                    || !state.isPreLayout()) {
                layoutState.mAvailable -= layoutChunkResult.mConsumed;
                // we keep a separate remaining space because mAvailable is important for recycling
                remainingSpace -= layoutChunkResult.mConsumed;
            }

            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                recycleByLayoutState(recycler, layoutState);
            }
            if (stopOnFocusable && layoutChunkResult.mFocusable) {
                break;
            }
        }
        return start - layoutState.mAvailable;
    }

我们将上面的代码精简了一下 并且写了一些注释 我们发现
他是一个while循环 直到remainingSpace < 0 或者 position > itemsCount 才会布局

我们上面提了好几次的 Pre-Layout 会形成一张1234的快照 这张快照包含即将要删除的item2 以及删除item2之后填充的item4
这里就会产生一个疑问 他是如何计算Pre-Layout的高度呢?

我们看到最开始代码int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
👏哈哈哈哈哈哈哈哈 结果出来了 就是layoutState.mExtraFillSpace 然而现实给我好好的上了一课🙅 调试发现上面的变量为0

答案:其实是上面注释①的地方 只有Pre-Layout或者mIgnoreConsumedfalse remainingSpace才会减少

我搜了一下mIgnoreConsumed什么时候会为false

 // Consume the available space if the view is not removed OR changed
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }

只有当需要remvoe 或者需要需要change 才会ignore

所以 朋友们 👏结果很明显了啊 预布局的时候 remainingSpace会忽略需要remove的item 所以会形成一张1234的快照

dispatchLayoutStep1()终于分析完了 分析源码的过程总是枯燥又带着一点收获啊

dispatchLayoutStep1()小总结

dispatchLayoutStep1()也就是我们今天需要分析的Pre-Layout的流程 生成了一张即将消失的内容+即将显示内容的快照

然后我们接着分析一下dispatchLayoustStep2() 也就是Post-Layout

分析一下 Post-Layout💪

我们先看一眼dispatchLayoutStep2的源码

private void dispatchLayoutStep2() {
        // Step 2: Run layout
        //将mInPreLayout置为false
        //这里表示预布局阶段已经正式结束了
        mState.mInPreLayout = false;
        //又到了熟悉的onLayoutChildren()环节
        mLayout.onLayoutChildren(mRecycler, mState);

        mState.mStructureChanged = false;
        mPendingSavedState = null;

        // onLayoutChildren may have caused client code to disable item animations; re-check
        mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
        mState.mLayoutStep = State.STEP_ANIMATIONS;
        onExitLayoutOrScroll();
        stopInterceptRequestLayout(false);
    }

上面的源码比较简短 我们发现dispatchLayoutStep2()方法中 首先mState.mInPreLayout = false; 表示Pre-Layout已经结束了 然后进入了我们熟悉的onLayoutChildren环节
我们上面刚刚分析过onLayoutChildren 因为state.isPreLayout()一定为false 所以remainingSpace一定会扣除所有空间
所以 结论出现了! dispatchLayoutStep2会生成一张item134的快照

等等! 我写到这里的时候 突然引发了我一个另一个疑惑 上面所说的并不能证明dispatchLayoutStep2会生成一张item134的快照?只能说明会生成一张item1234?
咋回事啊? 然后我又去彻底翻了一遍源码 终于找到了根源所在 :-(

我们之前没有分析layoutChunk方法 这个方法在fill()中 会从recycler中获取合适的ViewHolderView 然后添加到RecyclerView

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
     //根据当前position 获取View
    View view = layoutState.next(recycler);
    ......
} 

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    //根据position获取View
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

这里会根据mCurrentPosition获取对应的ViewHolder 然后将View添加到RecyclerView 但是我们还是没办法得到 为什么Post-Layout会生成item134呢?

Post-LayoutPre-Layout 还有两个地方有区别

public int getItemCount() {
    return mInPreLayout
            ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout)
            : mItemCount;
}

我们发现预布局和后布局的ItemCount是不一样的
另外就是我们在fill()之前 会先回收所有的ViewHolder 想看scrapView代码可以直接点击跳转看一下
scrapView 会将所有的ViewHolder放入mAttachedScrap
mAttachedScrap中存在position分别为0、0、1、2的四个表项 如下图(被Remove的ViewHolderposition会置为0)

image.png

根据上面两个条件 我们会发现 在fill()的过程中 只会生成item134快照
终于分析出来为啥是item134了 凌晨一点还在码字 脑子转的太累了😶

如果你还想问 那什么时候把删除的position置为0的? 我只能猜测是(不能保证)processAdapterUpdatesAndSetAnimationFlags(); 然后就只能靠你自己分析了 我已经分析不动了😷

<span id = "dispatch方法">小总结</span>

1. onLayoutChildren()

  • dispatchLayoutStep1()
    预布局阶段 生成布局快照(remainingSpace忽略即将remove的item)
  • dispatchLayoutStep2()
    post-layout阶段 生成布局快照(不带即将remove的item)
  • dispatchLayoutStep3()
    执行动画 因为上面两个步骤已经生成了开始状态和结束状态的快照 所以我们可以拿到动画的起始位置和终止位置

思考另外一个问题,RecyclerView删除动画是如何执行的?

我们上面已经分析了dispatchLayoutStep1()dispatchLayoutStep2 已经得到了两次快照所有item的位置信息 接下来就可以开始执行动画了 执行动画的方法是dispatchLayoutStep3() 代码流程等我睡醒再分析一下吧😪 太困了

<span id = "scrap"> detachAndScrapAttachedViews ()</span>

我们看到onLayoutChildren的时候 会先调用detachAndScrapAttachedViews 将所有View detach 然后再fill() 我们分析一下detachAndScrapAttachedViews方法做了哪些事情

 public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
 }
 
 private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        //如果isInvalid() 就调用removeViewAt
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
         //detachView
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

这里又不知道会走哪个分支了😠 不过 Debug是我们的好伙伴啊🆙 我们断点发现 是走下面分支的 我们继续往下分析😂(分析源码就是这样 点到为止)

上面的detachViewAt(index); 主要是将view从children中双向删除 会调用到ViewGroupremoveFromArray 不是我们这里的重点呐 我们先跳过 我们分析一下recycler.scrapView(view);

然后会将所有的View回收到mAttachedScrap中

void scrapView(View view) {
    final ViewHolder holder = getChildViewHolderInt(view);
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        //如果holder没有失效 没有被移除 则放入mAttachedScrap中
        if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
            throw new IllegalArgumentException("Called scrap view with an invalid view."
                    + " Invalid views cannot be reused from scrap, they should rebound from"
                    + " recycler pool." + exceptionLabel());
        }
        holder.setScrapContainer(this, false);
        mAttachedScrap.add(holder);
    } else {
         //只有当holder需要改变才会放入mChangedScrap中
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        holder.setScrapContainer(this, true);
        mChangedScrap.add(holder);
    }
}

总结

能坚持看完的都是勇士 分析一篇源码也真的是很累啊 不过还是受益良多 分析完预布局源码 现在对RecyclerView的布局流程认知更加清晰了 对以后优化和分析ReyclerView也会很有帮助 如果有对RecyclerView复用机制感兴趣的朋友 可以阅读我另外一篇文章啊 链接贴在下面啦

RecyclerView缓存回收源码分析

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

推荐阅读更多精彩内容