自定義View是Android開發中最普通的需求,靈活控制View的尺寸是開發者面臨的第一個問題,好比,爲何明明使用的是WRAP_CONTENT卻跟MATCH_PARENT表現相同。在處理View尺寸的時候,咱們都知道最好在onMeasure中設定好自定義View尺寸,那麼究竟如何合理的選擇這個尺寸呢。直觀來講,可能有如下問題須要考慮:java
自定的View最好不要超過父控件的大小,這樣才能保證本身能在父控件中完整顯示android
自定的View(若是是ViewGroup)的子控件最好不要超過本身的大小,這樣才能保證子控件顯示完整ide
若是明確爲View指定了尺寸,最好按照指定的尺寸設置函數
以上三個問題多是自定義ViewGroup最須要考慮的問題,首先先解決第一個問題。佈局
先假定,父容器是300dp*300dp的尺寸,若是子View的佈局參數是spa
<!--場景1--> android:layout_width="match_parent" android:layout_height="match_parent"
那麼按照咱們的指望,但願子View的尺寸要是300dp*300dp,若是子View的佈局參數是code
<!--場景2--> android:layout_width="100dp" android:layout_height="100dp"
按照咱們的指望,但願子View的尺寸要是100dp*100dp,若是子View的佈局參數是ci
<!--場景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給出的參考尺寸,具體意義以下:rem
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拿到後該如何用呢?
接收到父控件傳遞的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是父容器根據本身的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就跟這些知識相關。