View 繪製體系知識梳理(3) 繪製流程之 Measure 詳解

澤毛發表於2017-12-21

一、測量過程的信使 - MeasureSpec

因為測量是一個從上到下的過程,而在這個過程當中,父容器有必要告訴子View它的一些繪製要求,那麼這時候就需要依賴一個信使,來傳遞這個要求,它就是MeasureSpec. MeasureSpec是一個32位的int型別,我們把它分為高2位和低30位。 其中高2位表示mode,它的取值為:

  • UNSPECIFIED(0) : The parent has not imposed any constraint on the child. It can be whatever size it wants.
  • EXACTLY(1) : The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.
  • AT_MOST(2) : The child can be as large as it wants up to the specified size.

30位表示具體的size

MeasureSpec是父容器傳遞給View的寬高要求,並不是說它傳遞的size是多大,子View最終就是多大,它是根據**父容器的MeasureSpec和子ViewLayoutParams**共同計算出來的。

為了更好的理解上面這段話,我們需要藉助ViewGroup中的兩個函式:

  • measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
  • getChildMeasureSpec(int spec, int padding, int childDimension)
    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);
    }

    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) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製程式碼

可以看到,在呼叫getChildMeasureSpec之前,需要考慮parentchild之間的間距,這包括parentpaddingchildmargin,因此,參與傳遞給childMeasureSpec的引數要考慮這麼幾方面:

  • 父容器的measureSpecpadding
  • Viewheightwidht以及margin

下面我們來分析getChildMeasureSpec的具體流程,它對寬高的處理邏輯都是相同的,根據父容器measureSpecmode,分成以下幾種情況:

1.1 父容器的modeEXACTLY

這種情況下說明父容器的大小已經確定了,就是固定的值。

  • View指定了大小 那麼子Viewmode就是EXACTLYsize就是佈局裡面的值,這裡就有疑問了,View所指定的寬高大於父容器的寬高怎麼辦呢?,我們先留著這個疑問。
  • ViewMATCH_PARENTView希望和父容器一樣大,因為父容器的大小是確定的,所以子View的大小也是確定的,size就是父容器measureSpecsize - 父容器的padding - 子View``margin
  • ViewWRAP_CONTENT 子容器只要求能夠包裹自己的內容,但是這時候它又不知道它所包裹的內容到底是多大,那麼這時候它就指定自己的大小就不能超過父容器的大小,所以modeAT_MOSTsize和上面類似。

1.2 父容器的modeAT_MOST

在這種情況下,父容器說明了自己最多不能超過多大,數值在measureSpecsize當中:

  • View指定大小 同上分析。
  • ViewMATCH_PARENTView希望和父容器一樣大,而此時父容器只知道自己不能超過多大,因此子View也就只能知道自己不能超過多大,所以它的modeAT_MOSTsize就是父容器measureSpecsize - 父容器的padding - 子View``margin
  • ViewWRAP_CONTENT 子容器只要求能夠包裹自己的內容,但是這時候它又不知道它所包裹的內容到底是多大,這時候雖然父容器沒有指定大小,但是它指定了最多不能超過多少,這時候子View也不能超過這個值,所以modeAT_MOSTsize的計算和上面類似。

1.3 父容器的modeUNSPECIFIED

  • View指定大小 同上分析。
  • ViewMATCH_PARENTView希望和父容器一樣大,但是這時候父容器並沒有約束,所以子View也是沒有約束的,所以它的mode也為UNSPECIFIEDsize的計算和之前一致。
  • ViewWRAP_CONTENTView不知道它包裹的內容多大,並且父容器是沒有約束的,那麼也只能為UNSPECIFIED了,size的計算和之前一致。

二、測量過程的起點 - performTraversals()

介紹完了基礎的知識,我們來從起點來整個看一下從View樹的根節點到葉節點的整個測量的過程。 我們先直接說明結論,整個測量的起點是在ViewRootImplperformTraversals()當中

private void performTraversals() {
        ......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        //...
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼

上面的mView是通過setView(View view, WindowManager.LayoutParams attrs, View panelParentView)傳進來的,那麼這個view是什麼時候傳遞進來的呢? 現在回憶一下,在ActivityThreadhandleResumeActivity中,我們呼叫了ViewManager.add(mDecorView, xxx),而這個方法最終會呼叫到WindowManagerGlobal的下面這個方法:

    public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
            root = new ViewRootImpl(view.getContext(), display);
        }
        //....
        root.setView(view, wparams, panelParentView);
    }
複製程式碼

也就是說,上面的**mView也就是我們在setContentView當中渲染出來的mDecorView**,也就是說它是整個View樹的根節點,因為mDecorView是一個FrameLayout,所以它呼叫的是FrameLayoutmeasure方法。 那麼這整個從根節點遍歷完整個View樹的過程是怎麼實現的呢? 它其實就是依賴於measureonMeasure

  • 對於Viewmeasure是在它裡面定義的,而且它是一個final方法,因此它的所有子類都沒有辦法重寫該方法,在該方法當中,會呼叫onMeasure來設定最終測量的結果,對於View來說,它只是簡單的取出父容器傳進來的要求來設定,並沒有複雜的邏輯。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -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;
            }

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
複製程式碼
  • 對於ViewGroup,由於它是View的子類,因此它不可能重寫measure方法,並且它也沒有重寫onMeasure方法。
  • 對於繼承於View的控制元件,例如TextView,它會重寫onMeasure,與View#onMeasure不同的是,它會考慮更多的情況來決定最終的測量結果。
  • 對於繼承於ViewGroup的控制元件,例如FrameLayout,它同樣會重寫onMeasure方法,與繼承於View的控制元件不同的是,由於ViewGroup可能會有子View,因此它在設定自己最終的測量結果之前,還有一個重要的任務:呼叫子Viewmeasure方法,來對子View進行測量,並根據子View的結果來決定自己的大小

因此,整個從上到下的測量,其實就是一個View樹節點的遍歷過程,每個節點的onMeasure返回時,就標誌它的測量結束了,而這整個的過程是以Viewmeasure方法為紐帶的:

  • 整個過程的起點是mDecorView這個根節點的measure方法,也就是performTraversals中的那句話。
  • 如果節點有子節點,也就是說它是繼承於ViewGroup的控制元件,那麼在它的onMeasure方法中,它並不會直接呼叫子節點的onMeasure方法,而是通過呼叫子節點measure方法,由於子節點不可能重寫View#measure方法,因此它最終是通過View#measure來呼叫子節點重寫的onMeasure來進行測量,子節點再在其中進行響應的邏輯處理。
  • 如果節點沒有子節點,那麼當它的onMeausre方法被呼叫時,它需要設定好自己的測量結果就行了。

對於measureonMeasure的區別,我們可以用一句簡單的話來總結一下:measure負責進行測量的傳遞,onMeasure負責測量的具體實現

三、測量過程的終點 - onMeasure當中的setMeasuredDimension

上面我們講到設定的測量結果,其實測量過程的最終目的是:通過呼叫setMeasuredDimension方法來給mMeasureHeightmMeasureWidth賦值。 只要上面這個過程完成了,那麼該ViewGroup/View/及其實現類的測量也就結束了,而**setMeasuredDimension必須在onMeasure當中呼叫,否則會丟擲異常**,所以我們觀察所有繼承於ViewGroup/View的控制元件,都會發現它們最後都是呼叫上面說的那個方法。 前面我們已經分析過,measure只是傳遞的紐帶,因此它的邏輯是固定的,我們直接看各個類的onMeasure方法就好。

3.1 ViewonMeasure

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

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }

    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
複製程式碼

這裡,我們會根據前面所說的,父容器傳遞進來measureSpec中的mode來給這兩個變數賦值:

  • 如果modeUNSPECIFIED,那麼說明父容器並不指望多個,因此子View根據自己的背景或者minHeight/minWidth屬性來給自己賦值。
  • 如果是AT_MOST或者EXACTLY,那麼就把它設定為父容器指定的size

3.2 ViewGrouponMeasure

由於ViewGroup的目的是為了容納各子View,但是它並不確定子View應當如何排列,也就不知道該如何測量自己,因此它的onMeasure是沒有任何意義的,所以並沒有重寫,而是應當由繼承於它的控制元件來重寫該方法。

3.3 繼承於ViewGroup控制元件的onMeasure

為了方面,我們以DecorView為例,經過前面的分析,我們知道當我們在performTraversals中呼叫它的measure方法時,最終會回撥到它對應的控制元件型別,也就是FrameLayoutonMeasure方法:

@Override
    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));

        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }
複製程式碼

我們可以看到,整個的onMeasure其實分為三步:

  • 遍歷所有子View,呼叫measureChildWithMargins進行第一次子View的測量,在第一節中,我們也分析了這個方法,它最終也是呼叫子Viewmeasure方法。
  • 根據第一步的結果,呼叫setMeasuredDimension來設定自己的測量結果。
  • 遍歷所有子View,根據第二步的結果,呼叫child.measure進行第二次的測量。

這也驗證了第二節中的結論:父容器和子View的關聯是通過measure進行關聯的。 同時我們也可以有一個新的結論,對於View樹的某個節點,它的測量結果有可能並不是一次決定的,這是由於父容器可能需要依賴於子View的測量結果,而父容器的結果又可能會影響子View,但是,我們需要保證這個過程不是無限呼叫的。

相關文章