相信絕大多數Android開發者都有自定義View來滿足各種各樣需求的經歷,也知道一個View的繪製展示要經過measure、layout、draw三大流程,三者中measure的過程相比是稍微複雜一點點的。這篇文章作為一個Android基礎的分享,分享一下view/viewGroup measure的過程,view/viewGroup是如何通過measure來確定自己的寬高,最後通過自定義一個流式佈局來實踐一下。
layout的過程本質上就是計算設定自己的座標或者自己child的座標,而draw需要畫布和畫筆以及提供的豐富的api來繪製你想要的效果。
本文主要分為以下三部分:
- 什麼是MeasureSpec,它的作用是什麼
- measure過程是什麼樣的,它是如何確定一個View/ViewGroup的寬高
- 重寫onMeasure()方法來自定義一個流式佈局
1. 什麼是MeasureSpec,它的作用是什麼
MeasureSpec的作用
我們知道View是通過onMeasure()來確定自己的寬高的(ViewGroup是個抽象類繼承自View,它並沒有重寫onMeasure(),所以如果自定義ViewGroup的時候沒有重寫onMeasure(),它最終調的還是View的onMeasure()方法),那麼假定我們沒有重寫onMeasure()方法,那麼View的寬高是怎樣的呢? View.onMeasure:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
顯然是通過setMeasureDimension()方法來確定寬高的,來看一下這個方法:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
```
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
複製程式碼
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//賦值給寬高成員變數,寬高確定,此時外界就可以通過getMeasuredWidth()、getMeasuredHeight()來獲取View的寬高了
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
複製程式碼
通過以上程式碼我們可以知道,View的onMeasure()裡通過setMeasuredDimension(int measuredWidth, int measuredHeight)方法就可以設定View的寬高,那麼裡面的兩個寬高引數是怎麼來的呢?接著看getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec):
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//取出MeasureSpec中的specMode和specSize
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:
//將specSize賦值給result
result = specSize;
break;
}
return result;
}
複製程式碼
終於我們的主角MeasureSpec登場了,從程式碼可以看出,最終設定的寬高也就是從MeasureSpec中取出的specSize。所以綜上所訴,在沒有重寫onMeasure()方法的情況下,MeasureSpec就決定了View/ViewGroup的寬高。
MeasureSpec基礎知識
接下來介紹一些MeasureSpec的基礎知識,讓我們更加了解什麼是MeasureSpec:
從getDefaultSize方法中也可以看出,MeasureSpec中包含兩部分,一個是SpecMode(測量模式),一個是SpecSize(某種測量模式下的規格大小)。它是用一個32位的int值來表示的,高2位程式碼SpecMode,低30位代表SpecSize。
specMode主要分為三種:
模式 | 說明 |
---|---|
EXACTLY | 設定了精確的寬高。如width、height設定了具體值或者設定為 match_parent,都屬於這種模式 |
AT_MOST | width、height設定為wrap_content則屬於這種模式 |
UNSPECIFIED | 以上兩種模式是我們佈局裡常見的,最大也不會大過父佈局,而這種模式一般用於系統, 父容器不對View有任何限制 |
MeasureSpec是如何生成的
通過上面我們知道了在不重寫onMeasure()的情況下,一個View/ViewGroup的MeasureSpec就決定了這個View/ViewGroup的寬高,顯然這個MeasureSpec是這個View/ViewGroup的父容器在呼叫子View的measure()方法時傳進來的,也就是說一個View/ViewGroup的MeasureSpec是由其父容器生成的,那麼是怎麼生成的呢?裡面的SpecSize和SpecMode是由什麼決定的呢?
這裡由於程式碼比較多就不貼了,父容器通過調ViewGroup中的getChildMeasureSpec()來生成子View的MeasureSpec。getChildMeasureSpec()中主要是通過父容器的MeasureSpec以及子Views設定的寬高來共同決定子View的MeasureSpec中的SpecMode和SpecSize。
getChildMeasureSpec()程式碼裡的生成規則:
1.當子View的寬高設定的是具體數值時,顯然我們可以直接拿到子View的寬高,則子View寬高就確定了,不用再去考慮父容器的SpecMode了,此時子View的SpecMode為EXACTLY,SpecSize就是設定的寬高。
2.當子View的寬高設定的是match_parent, 則不管父容器的SpecMode是什麼模式,子View的SpecSize就等於父容器的寬高,而子View的SpecMode隨父容器的SpecMode。(這裡沒有考慮UNSPECIFIED模式,如果父容器是UNSPECIFIED模式,則子View SpecSize為0,SpecMode為UNSPECIFIED)
3.當子View的寬高設定的是wrap_content,因為這種情況父容器實在不知道子View應該多寬多高,所以子View的SpecSize給的是父容器的寬高,也就是說只是給子View限制了一個最大寬高,而子View的SpecMode是AT_MOST模式。(這裡沒有考慮UNSPECIFIED模式,如果父容器是UNSPECIFIED模式,則子View SpecSize為0,SpecMode為UNSPECIFIED)。
通過上面的解析我們可以知道,當你給一個View/ViewGroup設定寬高為具體數值或者match_parent,它都能正確的顯示,但是如果你設定的是wrap_content,則預設顯示出來是其父容器的大小,如果你想要它正常的顯示為wrap_content,則你就要自己重寫onMeasure()來自己計算它的寬高度並設定。所以我們平常自定義View/ViewGroup的時候之所以要重寫onMeasure(),就是為了能讓wrap_content達到效果。
2. measure過程是什麼樣的,它是如何確定一個View/ViewGroup的寬高
我們知道,整個繪製流程是從ViewRootImpl類中performTraversals()開始的,這裡面分別執行performMeasure、performLayout、performDraw來完成整個繪製的三大流程。而三大流程都是至頂向下,今天這裡只說measure的過程。
這裡以DecorView(根View)面放著一個ViewGroup(ViewGroupA)ViewGroup裡面放著一個View(ViewB)為例來說明整個測量的流程:
1. ViewRootImpl.performTraversals()->performMeasure():
這裡面會調getRootMeasureSpec()根據手機螢幕的寬高和DecorView的LayoutParams生成DecorView的MeasureSpec,然後呼叫DecorView的measure()開始DecorView的測量
2.DecorView.measure()->onMeasure():
DecorView繼承自FrameLayout,所以會走到FrameLayout的onMeasure(),onMeasure()裡調measureChild()來根據上面說的規則為ViewGroupA生成MeasureSpec,並通過ViewGroupA.measure()開始ViewGroupA的測量
3.ViewGroupA.measure()->onMeasure():
這是我們自定義的一個ViewGroup(繼承自ViewGroup) 假如我們沒有重寫onMeasure()的話,則預設調的是View.onMeasure(),則不會發起對子View的measure,它裡面的子View也就不會被測量(0),而這個ViewGroup如果沒有設定具體寬高的話,(wrap_content)則ViewGroup展示的就是父容器的寬高(根據上面說的MeasureSpec生成規則)。
所以如果我們繼承自ViewGroup來自定義一個ViewGroup的話,是肯定要重寫onMeasure()的,裡面要呼叫measureChild()來為子View生成MeasureSpec並調child.measure()開始對child的測量,這樣子View才能被測量顯示。而如果我們要使設定的wrap_content生效,還要根據子View測量結果進行計算從而得到自己的寬高,最後通過調setMeasuredDimension(int measuredWidth, int measuredHeight)來設定自己的寬高,從而達到wrap_content的效果。
4. ViewB.measure()->onMeasure():
View的測量相對於ViewGroup要簡單點,因為不用去Measure child,但是一樣的,如果要使wrap_conten生效需自己重寫onMeasure()計算。
3.重寫onMeasure()方法來自定義一個流式佈局
關於流式佈局,也叫自動換行佈局,一行放不下時會自動折行,效果如下:
具體實現地址:github.com/zhengcx/Lin…
通過上面的measure流程分析,這個效果應該還是比較好實現的,寬度測量上其實不需要我們太關心,直接拿MeasureSpec中的SpecSize就行了,所以主要是高度上的測量,我們要做的其實也是上面說的幾點:
1.根據規則為子View生成MeasureSpec,並迴圈測量子View (ViewGroup.measureChild()裡做的事)
2.拿到測量好的子View的寬高,來計算自己wrap_content時應該呈現的寬高。像這個例子主要是根據子View的寬度來判斷是否需要折行,如果折行,則ViewGroup的高度也就隨之變大,總之需要去計算ViewGroup應該展示的高度。
3.通過調setMeasuredDimension(int measuredWidth, int measuredHeight)來設定自己的寬高。
具體實現見github:github.com/zhengcx/Lin…