RecyclerView源碼分析一:測(cè)量 布局 繪制

注意:本文基于25.4.0源碼

RecyclerView的源碼非常復(fù)雜玉凯,僅僅RecyclerView.java一個(gè)文件就有一萬(wàn)多行势腮,閱讀起來(lái)十分困難。不過(guò)RecyclerView作為一個(gè)View漫仆,再?gòu)?fù)雜也得遵循View的基本法:三大流程捎拯。所以我們從View繪制的三大流程入手就會(huì)輕松許多。

Measure

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        //LayoutManager為空
        if (mLayout == null) {
            //設(shè)置默認(rèn)寬高
            defaultOnMeasure(widthSpec, heightSpec);
            return;
        }
        //默認(rèn)自動(dòng)測(cè)量
        if (mLayout.mAutoMeasure) {
            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            //通過(guò)LayoutManger計(jì)算寬高
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || mAdapter == null) {
                return;
            }
            if (mState.mLayoutStep == State.STEP_START) {
                /**
                 * 處理adpter更新
                 * 決定是否要執(zhí)行動(dòng)畫(huà)
                 * 保存動(dòng)畫(huà)信息
                 * 如果有必要的話盲厌,進(jìn)行預(yù)布局
                 */
                dispatchLayoutStep1();
            }
            // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
            // consistency
            mLayout.setMeasureSpecs(widthSpec, heightSpec);
            mState.mIsMeasuring = true;
            //進(jìn)行真正的測(cè)量和布局
            dispatchLayoutStep2();

            // now we can get the width and height from the children.
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

            // if RecyclerView has non-exact width and height and if there is at least one child
            // which also has non-exact width & height, we have to re-measure.
            if (mLayout.shouldMeasureTwice()) {
                mLayout.setMeasureSpecs(
                        MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
                mState.mIsMeasuring = true;
                dispatchLayoutStep2();
                // now we can get the width and height from the children.
                mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
            }
        } else {
              //非自動(dòng)測(cè)量
        }
    }

先進(jìn)行整體描述一下measure流程

  • 如果未設(shè)置LayoutManger署照,設(shè)置默認(rèn)寬高祸泪,結(jié)束,否則繼續(xù)向下
  • 分為自動(dòng)測(cè)量和非自動(dòng)測(cè)量?jī)煞N情況建芙,一般情況都為自動(dòng)測(cè)量没隘,我們這里也只分析自動(dòng)測(cè)量情況
  • 通過(guò)LayoutManger初步計(jì)算寬高(一般使用默認(rèn)寬高計(jì)算方式),如果RecyclerView的寬高都是EXACTLY的禁荸,則測(cè)量結(jié)束右蒲,否則繼續(xù)測(cè)量
  • dispatchLayoutStep1處理adpter更新,決定是否要執(zhí)行動(dòng)畫(huà)赶熟,保存動(dòng)畫(huà)信息瑰妄,處理預(yù)布局
  • dispatchLayoutStep2進(jìn)行真正測(cè)量布局,對(duì)子view(itemView)進(jìn)行measurelayout钧大,確定子view的寬高和位置
  • 如果RecyclerView仍然有非精確的寬和高翰撑,或者這里還有至少一個(gè)Child還有非精確的寬和高,再進(jìn)行一次測(cè)量

下面進(jìn)行關(guān)鍵點(diǎn)梳理

設(shè)置默認(rèn)寬高

mLayout就是recyclerView.setLayoutManager(layoutManager)中設(shè)置的layoutManager啊央。當(dāng)mLayoutnull的時(shí)候眶诈,使用默認(rèn)測(cè)量方法,這個(gè)時(shí)候RecyclerView空白什么都不會(huì)顯示

    void defaultOnMeasure(int widthSpec, int heightSpec) {
        // calling LayoutManager here is not pretty but that API is already public and it is better
        // than creating another method since this is internal.
        final int width = LayoutManager.chooseSize(widthSpec,
                getPaddingLeft() + getPaddingRight(),
                ViewCompat.getMinimumWidth(this));
        final int height = LayoutManager.chooseSize(heightSpec,
                getPaddingTop() + getPaddingBottom(),
                ViewCompat.getMinimumHeight(this));

        setMeasuredDimension(width, height);
    }

默認(rèn)測(cè)量時(shí)瓜饥,我們可以看到會(huì)使用LayoutManager.chooseSize()方法獲取寬高

        public static int chooseSize(int spec, int desired, int min) {
            final int mode = View.MeasureSpec.getMode(spec);
            final int size = View.MeasureSpec.getSize(spec);
            switch (mode) {
                case View.MeasureSpec.EXACTLY:
                    return size;
                case View.MeasureSpec.AT_MOST:
                    return Math.min(size, Math.max(desired, min));
                case View.MeasureSpec.UNSPECIFIED:
                default:
                    return Math.max(desired, min);
            }
        }

很簡(jiǎn)單逝撬,這里不做過(guò)多介紹

自動(dòng)測(cè)量

當(dāng)mLayout不為null的時(shí)候,會(huì)進(jìn)行判斷是否進(jìn)行自動(dòng)測(cè)量乓土。mLayout.mAutoMeasure默認(rèn)為true宪潮,表示自動(dòng)測(cè)量,例如LinearLayoutManager趣苏,除非你自定義LayoutManager或者調(diào)用setAutoMeasureEnabled(false)狡相。

    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
        setOrientation(orientation);
        setReverseLayout(reverseLayout);
        setAutoMeasureEnabled(true);
    }
初步測(cè)量寬高

自動(dòng)測(cè)量時(shí),先調(diào)用mLayout.onMeasure食磕,委托給mLayout進(jìn)行測(cè)量尽棕。

            final int widthMode = MeasureSpec.getMode(widthSpec);
            final int heightMode = MeasureSpec.getMode(heightSpec);
            final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
                    && heightMode == MeasureSpec.EXACTLY;
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            if (skipMeasure || mAdapter == null) {
                return;
            }
        public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
            mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
        }

onMeasure方法默認(rèn)使用RecyclerView的默認(rèn)測(cè)量,和上面一樣彬伦。

如果RecyclerView的寬高都是固定值或者adapter為空滔悉,此時(shí)測(cè)量結(jié)束。否則調(diào)用dispatchLayoutStep1dispatchLayoutStep2繼續(xù)進(jìn)行測(cè)量单绑。

下面繼續(xù)看dispatchLayoutStep1dispatchLayoutStep2回官,其實(shí)onLayout中還有一個(gè)dispatchLayoutStep3,這三個(gè)方法共同組成了RecyclerView的繪制布局過(guò)程搂橙。

  • dispatchLayoutStep1 處理adpter更新歉提,決定是否要執(zhí)行動(dòng)畫(huà),保存動(dòng)畫(huà)信息,如果有必要的話唯袄,進(jìn)行預(yù)布局弯屈。方法結(jié)束狀態(tài)置為State.STEP_LAYOUT
  • dispatchLayoutStep1 進(jìn)行真正的測(cè)量和布局操作。方法結(jié)束狀態(tài)置為State.STEP_ANIMATIONS
  • dispatchLayoutStep1 觸發(fā)動(dòng)畫(huà)并進(jìn)行任何必要的清理恋拷。方法結(jié)束狀態(tài)重置為State.STEP_START
dispatchLayoutStep1

dispatchLayoutStep1方法主要和動(dòng)畫(huà)和預(yù)布局相關(guān),這里暫時(shí)先略過(guò)厅缺,直接看dispatchLayoutStep2蔬顾。

dispatchLayoutStep2
    private void dispatchLayoutStep2() {
        ...
        // Step 2: Run layout
        mState.mInPreLayout = false;
        mLayout.onLayoutChildren(mRecycler, mState);
        ...
    }

我們可以看到View的測(cè)量和布局委托給mLayout進(jìn)行處理,從這里可以看出RecyclerView的靈活性湘捎,只要替換不同的LayoutManger就能夠?qū)崿F(xiàn)不同的布局诀豁,相當(dāng)靈活。onLayoutChildren方法默認(rèn)為空窥妇,需要各個(gè)實(shí)現(xiàn)類(lèi)去實(shí)現(xiàn)舷胜。
onLayoutChildren主要用來(lái)對(duì)RecyclerView的ItemView進(jìn)行measurelayout,后面再進(jìn)行詳細(xì)介紹活翩。

根據(jù)子View的寬高計(jì)算自身的寬高

dispatchLayoutStep2成功之后烹骨,我們已經(jīng)完成對(duì)RecyclerView的子View的測(cè)量和布局,下面就可以根據(jù)子view的寬高來(lái)計(jì)算自己的寬高了材泄。這里比較簡(jiǎn)單就不做具體介紹了沮焕,主要需要注意的是DecoratedBounds,即recyclerView.addItemDecoration(itemDecoration)中的itemDecoration所需占的空間拉宗。

二次測(cè)量

是否需要二次測(cè)量和具體LayoutManger有關(guān)峦树,由LayoutManger來(lái)具體實(shí)現(xiàn),以LinearLayoutManager舉例

    @Override
    boolean shouldMeasureTwice() {
        return getHeightMode() != View.MeasureSpec.EXACTLY
                && getWidthMode() != View.MeasureSpec.EXACTLY
                && hasFlexibleChildInBothOrientations();
    }

果RecyclerView仍然有非精確的寬和高旦事,或者這里還有至少一個(gè)Child還有非精確的寬和高魁巩,我們就需要再次測(cè)量。

LinearLayoutManager的onLayoutChildren

下面我們具體介紹一下LinearLayoutManager的onLayoutChildren實(shí)現(xiàn)姐浮,來(lái)看一下LinearLayoutManager是怎么對(duì)子View進(jìn)行布局的谷遂。

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

        if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
                mPendingSavedState != null) {
            mAnchorInfo.reset();
            //Item布局方向
            mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
            // calculate anchor position and coordinate
            //查找錨點(diǎn),錨點(diǎn)可以看做是布局的一個(gè)起始點(diǎn)单料,以這個(gè)點(diǎn)為基點(diǎn)埋凯,分別向上和向下進(jìn)行測(cè)量布局
            updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
            mAnchorInfo.mValid = true;
        }

        ...
        
        //將屏幕上顯示的Item移除,并將對(duì)應(yīng)viewholder暫存起來(lái)
        detachAndScrapAttachedViews(recycler);
        mLayoutState.mInfinite = resolveIsInfinite();
        mLayoutState.mIsPreLayout = state.isPreLayout();
        if (mAnchorInfo.mLayoutFromEnd) {
            //表示RecyclerView是從下往上位置為0,1,2...順序
            // fill towards start
            updateLayoutStateToFillStart(mAnchorInfo);
            mLayoutState.mExtra = 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.mExtra = 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.mExtra = extraForStart;
                fill(recycler, mLayoutState, state, false);
                startOffset = mLayoutState.mOffset;
            }
        } else {
            //與上面布局方法類(lèi)似扫尖,只是方向相反
            ...
        }

        ...
    }
  • 首先確定布局方向白对,updateAnchorInfoForLayout查找錨點(diǎn)。布局方向用來(lái)確定Item是從上往下順序顯示還是從下往上順序顯示换怖;錨點(diǎn)用來(lái)確認(rèn)布局起始點(diǎn)
  • detachAndScrapAttachedViews如果屏幕上有Item顯示甩恼,則將它們?nèi)恳瞥⑶視捍嫫饋?lái)
  • 以錨點(diǎn)坐標(biāo)為起始點(diǎn),從錨點(diǎn)處分別向上和向下布局Item条摸。fill()方法用來(lái)做具體添加child操作悦污,并對(duì)child進(jìn)行測(cè)量和布局

Layout

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

Layout非常簡(jiǎn)單,主要通過(guò)dispatchLayout實(shí)現(xiàn)钉蒲。

    void dispatchLayout() {
        ...
        mState.mIsMeasuring = false;
        if (mState.mLayoutStep == State.STEP_START) {
            //measure階段未對(duì)children布局
            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.
            //執(zhí)行過(guò)布局但size有改變
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            //執(zhí)行過(guò)布局且布局未發(fā)生變化
            mLayout.setExactMeasureSpecsFrom(this);
        }
        dispatchLayoutStep3();
    }

這個(gè)方法很簡(jiǎn)單切端,主要保證RecyclerView必須經(jīng)歷三個(gè)過(guò)程--dispatchLayoutStep1、dispatchLayoutStep2顷啼、dispatchLayoutStep3踏枣。如果開(kāi)啟自動(dòng)測(cè)量就會(huì)在measure階段對(duì)children進(jìn)行布局,如果未開(kāi)啟自動(dòng)測(cè)量layout階段就會(huì)對(duì)children進(jìn)行布局钙蒙。

  private void dispatchLayoutStep3() {
        mState.mLayoutStep = State.STEP_START;
        if (mState.mRunSimpleAnimations) {
            執(zhí)行動(dòng)畫(huà)
            ...
        }
        清除狀態(tài)和無(wú)用信息
        ...
    }

Draw

Draw流程主要處理的就是ItemDecoration的一些繪制操作茵瀑,類(lèi)似分割線、懸浮title之類(lèi)躬厌。

   @Override
    public void draw(Canvas c) {
        super.draw(c);
        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ...
    }

是不是很熟悉马昨,這里就是ItemDecorationonDrawOver方法,children的繪制在super.draw(c)中扛施,可以看出onDrawOver的繪制是在最上層鸿捧。

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

這里繪制的是ItemDecorationonDraw方法。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末煮嫌,一起剝皮案震驚了整個(gè)濱河市笛谦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昌阿,老刑警劉巖饥脑,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異懦冰,居然都是意外死亡灶轰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)刷钢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)笋颤,“玉大人,你說(shuō)我怎么就攤上這事内地“槌危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵阱缓,是天一觀的道長(zhǎng)非凌。 經(jīng)常有香客問(wèn)我,道長(zhǎng)荆针,這世上最難降的妖魔是什么敞嗡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任颁糟,我火速辦了婚禮,結(jié)果婚禮上喉悴,老公的妹妹穿的比我還像新娘棱貌。我一直安慰自己,他們只是感情好箕肃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布婚脱。 她就那樣靜靜地躺著,像睡著了一般勺像。 火紅的嫁衣襯著肌膚如雪起惕。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天咏删,我揣著相機(jī)與錄音,去河邊找鬼问词。 笑死督函,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的激挪。 我是一名探鬼主播辰狡,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼垄分!你這毒婦竟也來(lái)了宛篇?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤薄湿,失蹤者是張志新(化名)和其女友劉穎叫倍,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體豺瘤,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吆倦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坐求。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚕泽。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖桥嗤,靈堂內(nèi)的尸體忽然破棺而出须妻,到底是詐尸還是另有隱情,我是刑警寧澤泛领,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布荒吏,位于F島的核電站,受9級(jí)特大地震影響师逸,放射性物質(zhì)發(fā)生泄漏司倚。R本人自食惡果不足惜豆混,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望动知。 院中可真熱鬧皿伺,春花似錦、人聲如沸盒粮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)丹皱。三九已至妒穴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間摊崭,已是汗流浹背讼油。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呢簸,地道東北人矮台。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像根时,于是被迫代替她去往敵國(guó)和親瘦赫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容