目錄
作用
用於測量View的寬高,在執行 layout 的時候,根據測量的寬高去確定自身和子 View 的位置。
基礎知識
在 measure 過程中,設計到 LayoutParams 和 MeasureSpec 這兩個知識點。 這裡我們簡單說一下,如果還有不明白之處,Google it!
LayoutParams
簡單來說就是佈局引數,包含了 View 的寬高等資訊。每一個 ViewGroup 的子類都有相對應的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以看出 LayoutParams 是 ViewGroup 子類的內部類。
值 | 含義 |
---|---|
LayoutParams.MATCH_PARENT | 等同於在 xml 中設定 View 的屬性為 match_parent 和 fill_parent |
LayoutParams.WRAP_CONTENT | 等同於在 xml 中設定 View 的屬性為 wrap_content |
MeasureSpec
MeasureSpec 是 View 的測量規則。通常父控制元件要測量子控制元件的時候,會傳給子控制元件 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 型別的值。這個值裡面包含兩個資訊,SpecMode 和 SpecSize。一個 int 值怎麼會包含兩個資訊呢?我們知道 int 是一個4位元組32位的資料,在這兩個 int 型別的資料中,前面高2位是 SpecMode ,後面低30位代表了 SpecSize。
mode 有三種型別: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!
執行流程
注:以下涉及到原始碼的,都是版本27的。
我們知道,一個檢視的根 View 是 DecorView。在我們開啟一個 Activity 的時候,會將 DecorView 新增到 window 中,同時會建立一個 RootViewImpl物件,並將 RootViewImpl 物件和 DecorView 物件建立關聯。RootViewImplement 是連線 WindowManager 和 DecorView 的紐帶。具體 DecorView 詳解可以看 這篇文章
View的繪製流程就是從 RootViewImpl 開始的。在它的 performTraversals()
方法中執行了 performMeasure()
、performLayout
、performDraw
方法。而這三個方法又分別執行了view.measure()
、view.layout()
、view.draw()
方法,從而開始執行整個 View 樹的繪製流程
ViewGroup 中 measure 的執行流程
ViewGroup 本身是繼承 View 的,這是我們大家都知道的。在 ViewGroup 中並沒有找到 measure 方法,那麼就在它的父類 View 中找,具體原始碼如下:
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 的過程。具體總結如下:
以上就是 LinearLayout 測量子控制元件寬高的過程。從上述表格我們也可以看出,當我們在自定義控制元件繼承 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 的執行流程
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 就分析完了。
DecorView 的 measureSpec 計算邏輯
可能我們會有疑問,如果所有子控制元件的 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的工作原理