源碼分析UI繪制三部曲之measure

眾所周知,UI繪制三部曲是measure、layout、draw

本篇我們分析View#measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                // 看這里
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            ...
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

可以看到measure方法傳入了兩個參數(shù)widthMeasureSpec和heightMeasureSpec,分別是寬和高的測量規(guī)格讹躯, MeasureSpec是一個32位的int值,前2位是SpecMode缠劝,后30位是SpecSize潮梯。我們找到調(diào)用measure方法的地方,來看看這兩個測量規(guī)格是如何獲取的惨恭,通過查看源碼秉馏,調(diào)用measure的地方在ViewRootImpl#performMeasure

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

繼續(xù)找performMeasure的調(diào)用地方在ViewRootImpl#performTraversals()

 private void performTraversals() {
        ...
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

                    if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed!  mWidth="
                            + mWidth + " measuredWidth=" + host.getMeasuredWidth()
                            + " mHeight=" + mHeight
                            + " measuredHeight=" + host.getMeasuredHeight()
                            + " coveredInsetsChanged=" + contentInsetsChanged);

                     // Ask host how big it wants to be
                     // 看這里
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

                    // Implementation of weights from WindowManager.LayoutParams
                    // We just grow the dimensions as needed and re-measure if
                    // needs be
                    int width = host.getMeasuredWidth();
                    int height = host.getMeasuredHeight();
                    boolean measureAgain = false;
        
        ...
 }

可以看到childWidthMeasureSpec是通過getRootMeasureSpec(mWidth, lp.width)獲取的,childHeightMeasureSpec是通過getRootMeasureSpec(mHeight, lp.height)獲取的脱羡,mWidth表示窗口的寬萝究,lp.width表示的是頂層View-DecorView布局屬性的寬,mHeight同理表示的是窗口的高度锉罐,lp.height表示的是頂層View-DecorView布局屬性的高帆竹,我們先來看下ViewRootImpl#getRootMeasureSpec

    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

MeasureSpec.makeMeasureSpec()是用來打包SpecSize和SpecMode,可以看到這段代碼根據(jù)DecorView的寬和窗口的寬來確定DecorView的measureSpec并返回,到這里我們總結(jié)下:DecorView的MeasureSpec由窗口大小和自身LayoutParams決定并遵循如下規(guī)則:

  • LayoutParams的寬高為LayoutParams.MATCH_PARENT:SepcMode為精確模式脓规,SepcSize為窗口大小
  • LayoutParams的寬高為LayoutParams.WRAP_CONTENT:SepcMode為最大模式栽连,SepcSize最大為窗口大小
  • LayoutParams的寬高為固定大小:SepcMode為精確模式,大小為LayoutParams的大小

我們繼續(xù)回到performMeasure方法中秒紧,起哄調(diào)用了View#measure绢陌,這里主要是對測量的一些優(yōu)化工作,我們重點看onMeasure方法

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

其中調(diào)用了setMeasuredDimension熔恢,繼續(xù)跟進(jìn)

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

其中又調(diào)用了setMeasuredDimensionRaw方法脐湾,繼續(xù)跟進(jìn)

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

可以看到這里主要做的工作就是保存控件最終的寬高到mMeasuredWidth,mMeasuredHeight 叙淌,但DecorView是繼承FragmentLayout秤掌,所以調(diào)用DecorView的onMeasure方法其實是調(diào)用FragmentLayout的onMeasure方法,跟進(jìn)FragmentLayout#onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

       ...
}

onMeasure方法里面?zhèn)鬟^來的兩個MeasureSpec就是當(dāng)前容器自己MeasureSpec鹰霍,可以看到其中通過一個for循環(huán)遍歷子View 闻鉴,并調(diào)用measureChildWithMargins,傳入了child和自己的寬高測量規(guī)格衅谷,跟進(jìn)ViewGroup#measureChildWithMargins

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

這段代碼通過getChildMeasureSpec方法獲取child的MeasureSpec椒拗,然后調(diào)用child的measure方法并且傳入child的MeasureSpec來讓child測量自己似将, 看下ViewGroup#getChildMeasureSpec

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

第一個參數(shù)spec是父容器的MeasureSpec获黔,第二個參數(shù)padding表示父容器已經(jīng)使用的空間,第三個參數(shù)childDimension就是child的布局參數(shù)里面對應(yīng)的寬高尺寸在验,首先通過spec獲取父容器的specMode和specSize玷氏,再根據(jù)這兩個值來確定child自己的specMode和specSize,并調(diào)用makeMeasureSpec將specMode和specSize打包成MeasureSpec并返回腋舌,我們來總結(jié)下:子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams決定盏触,根據(jù)代碼可知如下規(guī)則

image.png

我們繼續(xù)回到FragmentLayout#onMeasure

   public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
   
        ...
   
        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

       ...
}

在測量完子View之后,會得到自己的最大的寬和高块饺,接著調(diào)用setMeasuredDimension方法來決定自身的寬高赞辩,至此ViewGroup的測量源碼分析完畢。

最后我們來總結(jié)下:

View 測量代碼調(diào)用順序

measure --> onMeasure --> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的寬高)

ViewGroup 測量代碼調(diào)用順序

measure --> onMeasure(測量子控件的寬高)--> setMeasuredDimension -->setMeasuredDimensionRaw(保存自己的寬高)

DecorView的MeasureSpec由窗口大小和自身的LayoutParams決定
子View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams決定
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末授艰,一起剝皮案震驚了整個濱河市辨嗽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌淮腾,老刑警劉巖糟需,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谷朝,居然都是意外死亡洲押,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門圆凰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杈帐,“玉大人,你說我怎么就攤上這事专钉∧锏矗” “怎么了干旁?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炮沐。 經(jīng)常有香客問我争群,道長,這世上最難降的妖魔是什么大年? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任换薄,我火速辦了婚禮,結(jié)果婚禮上翔试,老公的妹妹穿的比我還像新娘轻要。我一直安慰自己,他們只是感情好垦缅,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布冲泥。 她就那樣靜靜地躺著,像睡著了一般壁涎。 火紅的嫁衣襯著肌膚如雪凡恍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天怔球,我揣著相機(jī)與錄音嚼酝,去河邊找鬼。 笑死竟坛,一個胖子當(dāng)著我的面吹牛闽巩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播担汤,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼涎跨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了崭歧?” 一聲冷哼從身側(cè)響起隅很,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驾荣,沒想到半個月后外构,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡播掷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年审编,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歧匈。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡垒酬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勘究,我是刑警寧澤矮湘,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站口糕,受9級特大地震影響缅阳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜景描,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一十办、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧超棺,春花似錦向族、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至氧苍,卻和暖如春夜矗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背候引。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工侯养, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留敦跌,地道東北人澄干。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像柠傍,于是被迫代替她去往敵國和親麸俘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

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