Android自定義View:MeasureSpec的真正意義與View大小控制

轉自:https://segmentfault.com/a/1190000007948959

父容器的限制與MeasureSpec

先假定,父容器是300dp*300dp的尺寸,如果子View的佈局參數是


<!--場景1-->
android:layout_width="match_parent"
android:layout_height="match_parent"
          

那麼按照我們的期望,希望子View的尺寸要是300dp*300dp,如果子View的佈局參數是

<!--場景2-->
android:layout_width="100dp"
android:layout_height="100dp"

按照我們的期望,希望子View的尺寸要是100dp*100dp,如果子View的佈局參數是

<!--場景3-->
android:layout_width="wrap_content"
android:layout_height="wrap_content"

按照我們的期望,希望子View的尺寸可以按照自己需求的尺寸來確定,但是最好不要超過300dp*300dp。

那麼父容器怎麼把這些要求告訴子View呢?MeasureSpec其實就是承擔這種作用:MeasureSpec是父控件提供給子View的一個參數,作爲設定自身大小參考,只是個參考,要多大,還是View自己說了算。先看下MeasureSpec的構成,MeasureSpec由size和mode組成,mode包括三種,UNSPECIFIED、EXACTLY、AT_MOST,size就是配合mode給出的參考尺寸,具體意義如下:

  • UNSPECIFIED(未指定),父控件對子控件不加任何束縛,子元素可以得到任意想要的大小,這種MeasureSpec一般是由父控件自身的特性決定的。比如ScrollView,它的子View可以隨意設置大小,無論多高,都能滾動顯示,這個時候,size一般就沒什麼意義。

  • EXACTLY(完全),父控件爲子View指定確切大小,希望子View完全按照自己給定尺寸來處理,跟上面的場景1跟2比較相似,這時的MeasureSpec一般是父控件根據自身的MeasureSpec跟子View的佈局參數來確定的。一般這種情況下size>0,有個確定值。

  • AT_MOST(至多),父控件爲子元素指定最大參考尺寸,希望子View的尺寸不要超過這個尺寸,跟上面場景3比較相似。這種模式也是父控件根據自身的MeasureSpec跟子View的佈局參數來確定的,一般是子View的佈局參數採用wrap_content的時候。

先來看一下ViewGroup源碼中measureChild怎麼爲子View構造MeasureSpec的:

 protected void measureChild(View child, int parentWidthMeasureSpec,
         int parentHeightMeasureSpec) {
     final LayoutParams lp = child.getLayoutParams();

     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
             mPaddingLeft + mPaddingRight, lp.width);
     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
             mPaddingTop + mPaddingBottom, lp.height);

     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
 }
 

由於任何View都是支持Padding參數的,在爲子View設置參考尺寸的時候,需要先把自己的Padding給去除,這同時也是爲了Layout做鋪墊。接着看如何getChildMeasureSpec獲取傳遞給子View的MeasureSpec的:

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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

可以看到父控件會參考自己的MeasureSpec跟子View的佈局參數,爲子View構建合適的MeasureSpec,盜用網上的一張圖來描述就是


當子View接收到父控件傳遞的MeasureSpec的時候,就可以知道父控件希望自己如何顯示,這個點對於開發者而言就是onMeasure函數,先來看下View.java中onMeasure函數的實現:

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

其中getSuggestedMinimumWidth是根據設置的背景跟最小尺寸得到一個備用的參考尺寸,接着看getDefaultSize,如下:


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

可以看到,如果自定義View沒有重寫onMeasure函數,MeasureSpec.AT_MOST跟MeasureSpec.AT_MOST的表現是一樣的,也就是對於場景2跟3的表現其實是一樣的,也就是wrap_content就跟match_parent一個效果,現在我們知道MeasureSpec的主要作用:父控件傳遞給子View的參考,那麼子View拿到後該如何用呢?

自定義View尺寸的確定

接收到父控件傳遞的MeasureSpec後,View應該如何用來處理自己的尺寸呢?onMeasure是View測量尺寸最合理的時機,如果View不是ViewGroup相對就比較簡單,只需要參照MeasureSpec,並跟自身需求來設定尺寸即可,默認onMeasure的就是完全按照父控件傳遞MeasureSpec設定自己的尺寸的。這裏重點講一下ViewGroup,爲了獲得合理的寬高尺寸,ViewGroup在計算自己尺寸的時候,必須預先知道所有子View的尺寸,舉個例子,用一個常用的流式佈局FlowLayout來講解一下如何合理的設定自己的尺寸。

先分析一下FLowLayout流式佈局(從左到右)的特點:FLowLayout將所有子View從左往右依次放置,如果當前行,放不開的就換行。從流失佈局的特點來看,在確定FLowLayout尺寸的時候,我們需要知道下列信息,

  • 父容器傳遞給FlowLayout的MeasureSpec推薦的大小(超出了,顯示不出來,又沒意義)

  • FlowLayout中所有子View的寬度與寬度:計算寬度跟高度的時候需要用的到。

  • 綜合MeasureSpec跟自身需求,得出合理的尺寸

首先看父容器傳遞給FlowLayout的MeasureSpec,對開發者而言,它可見於onMeasure函數,是通過onMeasure的參數傳遞進來的,它的意義上面的已經說過了,現在來看,怎麼用比較合理?其實ViewGroup.java源碼中也提供了比較簡潔的方法,有兩個比較常用的measureChildren跟resolveSize,在之前的分析中我們知道measureChildren會調用getChildMeasureSpec爲子View創建MeasureSpec,並通過measureChild測量每個子View的尺寸。那麼resolveSize呢,看下面源碼,resolveSize(int size, int measureSpec)的兩個輸入參數,第一個參數:size,是View自身希望獲取的尺寸,第二參數:measureSpec,其實父控件傳遞給View,推薦View獲取的尺寸,resolveSize就是綜合考量兩個參數,最後給一個建議的尺寸:

 public static int resolveSize(int size, int measureSpec) {
        return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
    }

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
   case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

可以看到:

  • 如果父控件傳遞給的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就說明,父控件對自己沒有任何限制,那麼尺寸就選擇自己需要的尺寸size

  • 如果父控件傳遞給的MeasureSpec的mode是MeasureSpec.EXACTLY,就說明父控件有明確的要求,希望自己能用measureSpec中的尺寸,這時就推薦使用MeasureSpec.getSize(measureSpec)

  • 如果父控件傳遞給的MeasureSpec的mode是MeasureSpec.AT_MOST,就說明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就選擇MeasureSpec.getSize(measureSpec),否則用自己想要的尺寸就行了

對於FlowLayout,可以假設每個子View都可以充滿FlowLayout,因此,可以直接用measureChildren測量所有的子View的尺寸:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();
    int paddingTop = getPaddingTop();
    int count = getChildCount();
    int maxWidth = 0;
    int totalHeight = 0;
    int lineWidth = 0;
    int lineHeight = 0;
    int extraWidth = widthSize - paddingLeft - paddingRight;
    
    <!--直接用measureChildren測量所有的子View的高度-->
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    
    <!--現在可以獲得所有子View的尺寸-->
    
    for (int i = 0; i < count; i++) {
        View view = getChildAt(i);
        if (view != null && view.getVisibility() != GONE) {
            if (lineWidth + view.getMeasuredWidth() > extraWidth) {
                totalHeight += lineHeight ;
                lineWidth = view.getMeasuredWidth();
                lineHeight = view.getMeasuredHeight();
                maxWidth = widthSize;
            } else {
                lineWidth += view.getMeasuredWidth();
            }
            <!--獲取每行的最高View尺寸-->
            lineHeight = Math.max(lineHeight, view.getMeasuredHeight());
        }
    }
    totalHeight = Math.max(totalHeight + lineHeight, lineHeight);
    maxWidth = Math.max(lineWidth, maxWidth);
    
    <!--totalHeight 跟 maxWidth都是FlowLayout渴望得到的尺寸-->
    <!--至於合不合適,通過resolveSize再來判斷一遍,當然,如果你非要按照自己的尺寸來,也可以設定,但是不太合理-->
    totalHeight = resolveSize(totalHeight + paddingBottom + paddingTop, heightMeasureSpec);
    lineWidth = resolveSize(maxWidth + paddingLeft + paddingRight, widthMeasureSpec);
    setMeasuredDimension(lineWidth, totalHeight);
}

可以看到,設定自定義ViewGroup的尺寸其實只需要三部:

  • 測量所有子View,獲取所有子View的尺寸

  • 根據自身特點計算所需要的尺寸

  • 綜合考量需要的尺寸跟父控件傳遞的MeasureSpec,得出一個合理的尺寸

頂層View的MeasureSpec是誰指定

傳遞給子View的MeasureSpec是父容器根據自己的MeasureSpec及子View的佈局參數所確定的,那麼根MeasureSpec是誰創建的呢?我們用最常用的兩種Window來解釋一下,Activity與Dialog,DecorView是Activity的根佈局,傳遞給DecorView的MeasureSpec是系統根據Activity或者Dialog的Theme來確定的,也就是說,最初的MeasureSpec是直接根據Window的屬性構建的,一般對於Activity來說,根MeasureSpec是EXACTLY+屏幕尺寸,對於Dialog來說,如果不做特殊設定會採用AT_MOST+屏幕尺寸。這裏牽扯到WindowManagerService跟ActivityManagerService,感興趣的可以跟蹤一下WindowManager.LayoutParams ,後面也會專門分析一下,比如,實現最簡單試的全屏的Dialog就跟這些知識相關。


發佈了12 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章