Android中mesure過程詳解 (結合Android 4.0.4 最新源碼)

如何遍歷並繪制View樹?之前的文章Android中invalidate() 函數詳解(結合Android 4.0.4 最新源碼)中提到invalidate()最後會發起一個View樹遍歷的請求,並通過執行performTraersal()來響應該請求,performTraersal()正是對View樹進行遍歷和繪制的核心函數,內部的主體邏輯是判斷是否需要重新測量視圖大小(measure),是否需要重新佈局(layout),是否重新需要繪制(draw)。measure過程是遍歷的前提,隻有measure後才能進行佈局(layout)和繪制(draw),因為在layout的過程中需要用到measure過程中計算得到的每個View的測量大小,而draw過程需要layout確定每個view的位置才能進行繪制。下面我們主要來探討一下measure的主要過程,相對與layout和draw,measure過程理解起來比較困難。

      我們在編寫layout的xml文件時會碰到layout_width和layout_height兩個屬性,對於這兩個屬性我們有三種選擇:賦值成具體的數值,match_parent或者wrap_content,而measure過程就是用來處理match_parent或者wrap_content,假如layout中規定所有View的layout_width和layout_height必須賦值成具體的數值,那麼measure其實是沒有必要的,但是google在設計Android的時候考慮加入match_parent或者wrap_content肯定是有原因的,它們會使得佈局更加靈活。

      首先我們來看幾個關鍵的函數和參數:

      1、public final void measue(int widthMeasureSpec, int heightMeasureSpec);

      2、protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

      3、protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)

      4、protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)

      5、protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed)

     接著我們來看View類中measure和onMeasure函數的源碼:

[java] 
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || 
                widthMeasureSpec != mOldWidthMeasureSpec || 
                heightMeasureSpec != mOldHeightMeasureSpec) { 
 
            // first clears the measured dimension flag 
            mPrivateFlags &= ~MEASURED_DIMENSION_SET; 
 
            if (ViewDebug.TRACE_HIERARCHY) { 
                ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); 
            } 
 
            // measure ourselves, this should set the measured dimension flag back 
            onMeasure(widthMeasureSpec, heightMeasureSpec); 
 
            // flag not set, setMeasuredDimension() was not invoked, we raise 
            // an exception to warn the developer 
            if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { 
                throw new IllegalStateException("onMeasure() did not set the" 
                        + " measured dimension by calling" 
                        + " setMeasuredDimension()"); 
            } 
 
            mPrivateFlags |= LAYOUT_REQUIRED; 
        } 
 
        mOldWidthMeasureSpec = widthMeasureSpec; 
        mOldHeightMeasureSpec = heightMeasureSpec; 
    } 
      由於函數原型中有final字段,那麼measure根本沒打算被子類繼承,也就是說measure的過程是固定的,而measure中調用瞭onMeasure函數,因此真正有變數的是onMeasure函數,onMeasure的默認實現很簡單,源碼如下:

[java] 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
    } 
      onMeasure默認的實現僅僅調用瞭setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,而measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,一旦這兩個變量被賦值,則意味著該View的測量工作結束。

[java] 
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 
        mMeasuredWidth = measuredWidth; 
        mMeasuredHeight = measuredHeight; 
 
        mPrivateFlags |= MEASURED_DIMENSION_SET; 
    } 
      對於非ViewGroup的View而言,通過調用上面默認的measure——>onMeasure,即可完成View的測量,當然你也可以重載onMeasure,並調用setMeasuredDimension來設置任意大小的佈局,但一般不這麼做,因為這種做法太“專政”,至於為何“專政”,讀完本文就會明白。

      對於ViewGroup的子類而言,往往會重載onMeasure函數負責其children的measure工作,重載時不要忘記調用setMeasuredDimension來設置自身的mMeasuredWidth和mMeasuredHeight。如果我們在layout的時候不需要依賴子視圖的大小,那麼不重載onMeasure也可以,但是必須重載onLayout來安排子視圖的位置,這在下一篇博客中會介紹。 

      再來看下measue(int widthMeasureSpec, int heightMeasureSpec)中的兩個參數, 這兩個參數分別是父視圖提供的測量規格,當父視圖調用子視圖的measure函數對子視圖進行測量時,會傳入這兩個參數,通過這兩個參數以及子視圖本身的LayoutParams來共同決定子視圖的測量規格,在ViewGroup的measureChildWithMargins函數中體現瞭這個過程,稍後會介紹。

     MeasureSpec參數的值為int型,分為高32位和低16為,高32位保存的是specMode,低16位表示specSize,specMode分三種:

      1、MeasureSpec.UNSPECIFIED,父視圖不對子視圖施加任何限制,子視圖可以得到任意想要的大小;

      2、MeasureSpec.EXACTLY,父視圖希望子視圖的大小是specSize中指定的大小;

      3、MeasureSpec.AT_MOST,子視圖的大小最多是specSize中的大小。

      以上施加的限制隻是父視圖“希望”子視圖的大小按MeasureSpec中描述的那樣,但是子視圖的具體大小取決於多方面的。

      ViewGroup中定義瞭measureChildren, measureChild,  measureChildWithMargins來對子視圖進行測量,measureChildren內部隻是循環調用measureChild,measureChild和measureChildWithMargins的區別就是是否把margin和padding也作為子視圖的大小,我們主要分析measureChildWithMargins的執行過程:

[java]
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); 

      總的來看該函數就是對父視圖提供的measureSpec參數進行瞭調整(結合自身的LayoutParams參數),然後再來調用child.measure()函數,具體通過函數getChildMeasureSpec來進行參數調整,過程如下:

[java] 
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 = 0; 
                resultMode = MeasureSpec.UNSPECIFIED; 
            } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
                // Child wants to determine its own size…. find out how 
                // big it should be 
                resultSize = 0; 
                resultMode = MeasureSpec.UNSPECIFIED; 
            } 
            break;  www.aiwalls.com
        } 
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
    } 
      getChildMeasureSpec的總體思路就是通過其父視圖提供的MeasureSpec參數得到specMode和specSize,並根據計算出來的specMode以及子視圖的childDimension(layout_width和layout_height中定義的)來計算自身的measureSpec,如果其本身包含子視圖,則計算出來的measureSpec將作為調用其子視圖measure函數的參數,同時也作為自身調用setMeasuredDimension的參數,如果其不包含子視圖則默認情況下最終會調用onMeasure的默認實現,並最終調用到setMeasuredDimension,而該函數的參數正是這裡計算出來的。
      總結:從上面的描述看出,決定權最大的就是View的設計者,因為設計者可以通過調用setMeasuredDimension決定視圖的最終大小,例如調用setMeasuredDimension(100, 100)將視圖的mMeasuredWidth和mMeasuredHeight設置為100,100,那麼父視圖提供的大小以及程序員在xml中設置的layout_width和layout_height將完全不起作用,當然良好的設計一般會根據子視圖的measureSpec來設置mMeasuredWidth和mMeasuredHeight的大小,已尊重程序員的意圖。

作者:zjmdp

發佈留言