用於測量View的寬高,在執行 layout 的時候,根據測量的寬高去肯定自身和子 View 的位置。java
在 measure 過程當中,設計到 LayoutParams 和 MeasureSpec 這兩個知識點。 這裏咱們簡單說一下,若是還有不明白之處,Google it!android
簡單來講就是佈局參數,包含了 View 的寬高等信息。每個 ViewGroup 的子類都有相對應的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。能夠看出 LayoutParams 是 ViewGroup 子類的內部類。ide
值 | 含義 |
---|---|
LayoutParams.MATCH_PARENT | 等同於在 xml 中設置 View 的屬性爲 match_parent 和 fill_parent |
LayoutParams.WRAP_CONTENT | 等同於在 xml 中設置 View 的屬性爲 wrap_content |
MeasureSpec 是 View 的測量規則。一般父控件要測量子控件的時候,會傳給子控件 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的值。這個值裏面包含兩個信息,SpecMode 和 SpecSize。一個 int 值怎麼會包含兩個信息呢?咱們知道 int 是一個4字節32位的數據,在這兩個 int 類型的數據中,前面高2位是 SpecMode ,後面低30位表明了 SpecSize。 源碼分析
UNSPECIFIED
,
EXACTLY
,
AT_MOST
測量模式 | 應用 |
---|---|
EXACTLY | 精準模式,當 width 或 height 爲固定 xxdp 或者爲 MACH_PARENT 的時候,是這種測量模式 |
AT_MOST | 當 width 或 height 設置爲 warp_content 的時候,是這種測量模式 |
UNSPECIFIED | 父容器對當前 View 沒有任何顯示,子 View 能夠取任意大小。通常用在系統內部,好比:Scrollview、ListView。 |
咱們怎麼從一個 int 值裏面取出兩個信息呢?別擔憂,在 View 內部有一個 MeasureSpec 類。這個類已經給咱們封裝好了各類方法:佈局
//將 Size 和 mode 組合成一個 int 值
int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);
//獲取 size 大小
int size = MeasureSpec.getSize(measureSpec);
//獲取 mode 類型
int mode = MeasureSpec.getMode(measureSpec);
複製代碼
具體實現細節,能夠查看源碼,or Google it!this
注:如下涉及到源碼的,都是版本27的。spa
咱們知道,一個視圖的根 View 是 DecorView。在咱們開啓一個 Activity 的時候,會將 DecorView 添加到 window 中,同時會建立一個 RootViewImpl對象,並將 RootViewImpl 對象和 DecorView 對象創建關聯。RootViewImpl 是鏈接 WindowManager 和 DecorView 的紐帶。具體 DecorView 詳解能夠看 這篇文章設計
View的繪製流程就是從 RootViewImpl 開始的。在它的 performTraversals()
方法中執行了 performMeasure()
、performLayout
、performDraw
方法。而這三個方法又分別執行了view.measure()
、view.layout()
、view.draw()
方法,從而開始執行整個 View 樹的繪製流程 3d
ViewGroup 自己是繼承 View 的,這是咱們你們都知道的。在 ViewGroup 中並無找到 measure 方法,那麼就在它的父類 View 中找,具體源碼以下:code
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
/*....省略代碼....*/
if (forceLayout || needsLayout) {
/*....省略代碼....*/
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
//執行 onMeasure 方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/*....省略代碼....*/
}
/*....省略代碼....*/
}
複製代碼
咱們能夠看出,measure 方法是被 final 修飾了,子類不能重寫。measure 方法中調用了 onMeasure 方法。
而後咱們繼續尋找 onMeasure 方法,會發如今 ViewGroup 中並無實現 onMeasure 方法,只有在 View 中發現了 onMeasure 方法。WTF?難道 ViewGroup 的 onMeasure 也會走 View 中的方法?並非的,ViewGroup 自己是一個抽象類,在 Android SDK 中有不少它的子類,如:LinearLayout、RelativeLayout、FrameLayout等等,這些控件的特性都是不同的,測量規則天然也都不同。它們都各自實現了 onMeasure 方法,而後去根據本身的特定測量規則進行控件的測量。(PS:若是咱們的自定義控件繼承 ViewGroup 的時候,必定要重寫 onMeasure 方法的,根據需求來制定測量規則)
這裏咱們以 LinearLayout 爲例,來進行源碼分析:
//LinearLayout 類
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
//若是方向是垂直方向,就進行垂直方向的測量
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
//進行水平方向的測量
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
複製代碼
measureVertical 和 measureHorizontal 過程相似,咱們對 measureVertical 進行分析。(如下源碼有所刪減)
//LinearLayout 類
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
float totalWeight = 0;
final int count = getVirtualChildCount();
//獲取 LinearLayout 的寬高模式 SpecMode
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean skippedMeasure = false;
// See how tall everyone is. Also remember max width.
//遍歷子 View ,查看每個子類有多高,而且記住最大的寬度。
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
//measureNullChild() 恆返回 0,
mTotalLength += measureNullChild (i);
continue;
}
//若是子控件時 GONE 狀態,就跳過,不進行測量。
//也能夠看出,若是子 View 是 INVISIBLE 也是要測量大小的。
if (child.getVisibility() == View.GONE) {
//getChildrenSkipCount 也是恆返回爲 0 的。
i += getChildrenSkipCount(child, i);
continue;
}
//獲取子控件的參數信息。
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//子控件是否設置了權重 weight
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
//若是設置了權重,就將 skippedMeasure 標記爲 true。
//後面會根據 skippedMeasure 的值和其餘條件來決定是否進行從新繪製。
//因此說,在 LinearLayout 中使用了 weight 權重,會致使測量兩次,比較耗時。
//能夠考慮使用 RelativeLayout 或者 ConstraintLayout
skippedMeasure = true;
} else {
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
//計算已經使用過的高度
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
/*這句代碼是關鍵,從字面意思就能夠理解出,該方法是在 layout 以前進行子 View 的測量。*/
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
}
}
}
複製代碼
那麼咱們在查看 measureChildBeforeLayout 方法:
//LinearLayout 類
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
複製代碼
再查看 measureChildWithMargins 方法,最終來到了 ViewGroup 類:
//ViewGroup 類
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
/*獲取子 View 的佈局參數 MarginLayoutParams 能夠獲取子 View 設置的 margin 屬性。*/
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//獲取子 View 寬度的 MeasureSpec 值。
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//獲取子 View 高度的 MeasureSpec 值。
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
在 ViewGroup 中還有一個方法爲
measureChild(int widthMeasureSpec, int heightMeasureSpec)
。這個方法和measureChildWithMargins
做用一致,都是生成子 View 的 measureSpec。只是傳參不一樣。
裏面在獲取子 View 寬高屬性的時候,都是經過 getChildMeasureSpec 方法來獲取的。這個方法是 ViewGroup 具體實現根據自身的 measureSpec 和子 View 的 LayoutParams 來設置子 View 的 measureSpec 的主要過程。
//ViewGroup 類
/** * @param spec 父類的 measureSpec * @param padding 父類的 padding + 子類的 margin * @param childDimension 子 View 的 LayoutParams.width/LayoutParams.height 屬性 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父控件的測量模式 specMode
int specMode = MeasureSpec.getMode(spec);
//獲取父控件的測量大小 SpecSize
int specSize = MeasureSpec.getSize(spec);
//獲取父控件剩餘的寬度/高度大小
int size = Math.max(0, specSize - padding);
//子 View 的測量大小
int resultSize = 0;
//子 View 的測量模式
int resultMode = 0;
switch (specMode) {
// 父控件的寬高模式是精準模式 EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//若是子 View 的寬/高是具體的值(具體的 xxdp/px)
//模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是具體設置的大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//若是子 View 的寬/高是 MATCH_PARENT
//模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是父控件剩餘的空間
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//若是子 View 的寬/高是 WRAP_CONTENT
/*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
//父控件測量模式爲 AT_MOST,會給子 View 一個最大的值
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//若是子 View 的寬/高是具體的值(具體的 xxdp/px)
//模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是具體設置的大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//若是子 View 的寬/高是 MATCH_PARENT
/*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//若是子 View 的寬/高是 MATCH_PARENT
/*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
//父控件不限制子 View 的寬高,通常用於 ListView、Scrollview
//平時基本不用,暫不分析
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;
}
//生成子 View 的 measSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
以上就是 ViewGroup 根據自身 measureSpec 和 子 View 的 LayoutParams 生成子 View 的 measureSpec 的過程。具體總結以下:
從上述表格咱們也能夠看出,當咱們在自定義控件繼承 View 的時候,仍是要重寫 View 的 onMeasure 方法來處理 wrap_content 的狀況,若是不處理 wrap_content 的狀況,wrap_content 的效果是和 match_parent 同樣的,都是填充滿父控件。能夠在 xml 佈局中直接添加一個
<View android:layout_width="match_parent" android:layout_height="wrap_content"/>
控件自行感覺一下。
LinearLayout 測量完子控件後,根據子控件的寬高來設置自身的寬高:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// Add in our padding
//添加自身的 padding 值
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
//從 最小建議高度 和 heightSize 中取最大值,getSuggestedMinimumHeight 在後面有分析
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
/*....省略代碼....*/
//遍歷完子控件後,來設置自身的寬高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
複製代碼
//若是 LinearLayout 高爲具體值,heightSizeAndState 就是具體的值
//不然是 子控件 的高度之和,可是也不能超過它的父容器的剩餘空間。
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);
}
複製代碼
至此,咱們能夠得知,當 ViewGroup 生成子 View 寬/高的 measureSpec 後,開始調用子 View 進行測量。若是子 View 繼承了 ViewGroup 就重複執行上述流程(各個不一樣的 ViewGroup 子類執行各自的 onMeasure 方法);若是是具體的 View,就開始執行具體 View 的 measure 過程。最後根據子控件的寬高和其餘條件來決定自身的寬高。
View 的 measure 具體源碼在 ViewGroup 中已經分析過,這裏主要分析 View 的 onMeasure 過程。
//View 類
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//經過 getDefaultSize 獲取寬高大小,設置爲測量值。
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
getDefaultSize 具體源碼
//View 類
/** * @param size 經過 getSuggestedMinimumWidth 獲取的建議最小寬度 * @param measureSpec 經過父控件生成的 measureSpec */
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:
//若是是 UNSPECIFIED 就設置爲建議最小值
result = size;
break;
/*不然就都設置爲經過父控件生成的值(若是子控件爲具體的 xxdp/px值,就是具體的值,若是不是就是父控件的剩餘空間。具體能夠查看上面的分析)*/
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製代碼
//建議最小的值
//View 類
protected int getSuggestedMinimumWidth() {
//判斷是否有設置背景 Background 若是沒有,建議最小值就是設置的 minWidth;
//若是有,就取 mMinWidth 和 背景最小值 二者的最大值。
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
背景最小值是多少呢?點擊查看源碼,就來到了 Drawable 類。
//Drawable 類
public int getMinimumWidth() {
//首先獲取 Drawable 的原始寬度
final int intrinsicWidth = getIntrinsicWidth();
//若是有原始寬度,就返回原始寬度;若是沒有,就返回 0
//注: 好比 ShapeDrawable 就沒有原始寬度,BitmapDrawable 有原始寬高(圖片尺寸)
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
複製代碼
至此,View的 measure 就分析完了。
可能咱們會有疑問,若是全部子控件的 measureSpec 都是父控件結合自身的 measureSpec 和子 View 的 LayoutParams 來生成的。那麼做爲視圖的頂級父類 DecorView 怎麼獲取本身的 measureSpec 呢?下面咱們來分析源碼:(如下源碼有所刪減)
//ViewRootImpl 類
private void performTraversals() {
//獲取 DecorView 寬度的 measureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
//獲取 DecorView 高度的 measureSpec
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
//開始執行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
//ViewRootImpl 類
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
複製代碼
windowSize 是 widow 的寬高大小,因此咱們能夠看出 DecorView 的 measureSpec 是根據 window 的寬高大小和自身的 LayoutParams 來生成的。
《Android開發藝術探索》第四章-View的工做原理