視圖的寬高可以靈活變化
android對于的視圖布局的定義提供了一種很靈活的實現(xiàn)晶姊,就是當(dāng)給視圖的寬高屬性設(shè)置wrap_content/match_parent削葱,視圖可以根據(jù)所處布局的情況動態(tài)的改變寬高。
wrap_content:根據(jù)自視圖的內(nèi)容決定視圖大小
match_parent:占滿父視圖空間
wrap_content/match_parent有什么奧秘能夠使視圖的寬高靈活變化呢开财?
measureSpec影響寬高的計算
一個視圖的繪制過程主要有三個部分:測量(measure)眶拉,布局(layout)、繪制(draw)鸯旁。其中測量過程就是計算一個視圖寬高的過程噪矛,主要通過onMeasure方法實現(xiàn)量蕊。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
onMeasure方法接收來至父視圖對子視圖寬高的約束measureSpec,具體的視圖會根據(jù)measureSpec來計算視圖的寬高艇挨。
measureSpec的一些概念:
measureSpec是一個32位的值残炮,其前兩位表示測量的模式,后30位表示測量的大小缩滨。
測量模式有三種:
EXACTLY:父控件給子控件決定特定的大小势就。
AT_MOST:子控件至多達到指定大小的值。
UNSPECIFIED:父控件沒有對子控件施加任何約束脉漏,子控件可以得到任意想要的大小苞冯。
android提供了相應(yīng)的方法來處理measureSpec
// 獲取測量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 獲取測量大小
int specSize = MeasureSpec.getSize(measureSpec);
// 通過大小和模式生成measureSpec
MeasureSpec.makeMeasureSpec(resultSize, resultMode);
例如 TextView的onMeasure實現(xiàn):
// onMeasure的關(guān)鍵實現(xiàn)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
...
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
...
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
...
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
...
setMeasuredDimension(width, height);
}
首先:
從父視圖的約束measureSpec獲得約束的模式specMode和約束的大小specSize。
其次:
如果模式為EXACTLY(父控件給子控件決定特定的大胁嗑蕖)舅锄,則TextView的寬高直接為父視圖約束的寬高;
如果模式是AT_MOST(子控件至多達到指定大小的值)司忱,則取TextView自身大小與父視圖約束大小的最小值皇忿;
如果為UNSPECIFIED(父控件沒有對子控件施加任何約束,子控件可以得到任意想要的大刑谷浴)鳍烁,TextView沒有額外處理就是取TextView自身的大小。
因此繁扎,視圖寬高的計算是由父視圖傳遞的約束measureSpec決定的幔荒。
那么measureSpec又是怎么來的呢?
wrap_content/match_parent影響measureSpec的計算
父視圖在自己測量時會調(diào)用子視圖的onMeasure方法,測量子視圖
例如下面的一個布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!" />
</LinearLayout>
其測量的時序圖:
在measureChildWithMargins方法中調(diào)用了子視圖的measure方法铺峭,最終執(zhí)行了子視圖的onMeasure方法墓怀。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
而子視圖onMeasure方法所需要的measureSpec就是由getChildMeasureSpec方法得到的。
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
在getChildMeasureSpec方法中終于見到了久違的wrap_content\match_parent卫键,可以看出子視圖的measureSpec由兩部分決定傀履,父視圖的measureSpec和子視圖的LayoutParams。
如果子視圖的LayoutParams寬高設(shè)置的是具體的大小
...
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
...
那么子視圖meauserSpec約束模式就是EXACTLY莉炉,約束大小就是LayoutParams中定義的大小钓账,根據(jù)前面介紹的TextView的onMeasure方法邏輯,計算出的大小就是約束的大小絮宁。
如果父視圖約束的模式是EXACTLY(通常為父視圖的寬高定義為具體大邪鹉骸),子視圖LayoutParams中定義的是match_parent(占滿父視圖)
...
case MeasureSpec.EXACTLY:
if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
...
得到的子視圖的measureSpec約束模式還是EXACTLY绍昂,約束大小為父視圖約束的大小啦粹,TextView計算的寬高則與父視圖一樣大,實現(xiàn)占滿父視圖的效果窘游;
如果子視圖定義為wrap_content
...
case MeasureSpec.EXACTLY:
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;
}
...
得到的子視圖的measureSpec約束模式為AT_MOST唠椭,約束大小為父視圖約束的大小,TextView計算的寬高則是自身大小與父視圖約束大小的最小值一樣大忍饰,實現(xiàn)由自身大小決定寬高贪嫂,但不會超過父視圖約束大小的效果。
還有很多種組合方式艾蓝,就不一一列舉力崇,可以看到我們平常記憶理解的wrap_content\match_parent靈活變化視圖寬高的作用效果,正是主要通過這段代碼邏輯實現(xiàn)的赢织。
理解視圖測量時的一些疑問:
頂層視圖的measureSpec的由來
從前面的討論可以知道亮靴,測量方法的參數(shù)measureSpec都是由父視圖傳遞進來的,那么最頂層的視圖的measureSpec是從哪來的于置?
我們經(jīng)常給activity設(shè)置的layout布局的根視圖并非是最頂層視圖茧吊。
先看一張圖:
content部分為給activity設(shè)置的布局,其外層還有一個視圖DecorView俱两,這個才是最頂層視圖饱狂。
而DecorView的測量過程是在ViewRootImpl的performTraversals方法中
private void performTraversals() {
...
//獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高宪彩,lp.widthhe和lp.height表示 DecorView根布局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
//執(zhí)行測量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//執(zhí)行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//執(zhí)行繪制操作
performDraw();
...
}
通過getRootMeasureSpec方法創(chuàng)建了DecorView的測量規(guī)格休讳,在performMeasure方法中執(zhí)行了DecorView的測量,遍歷整個子視圖尿孔。
并且視圖的布局(layout)和繪制(draw)也是從這里發(fā)起的俊柔,通過先后執(zhí)行performMeasure筹麸、performLayout、performDraw方法雏婶,完成整個頁面的繪制過程
ListView對子視圖測量的區(qū)別
ListView的子視圖布局高度無論是設(shè)置wrap_content還是match_parent物赶,其高度都為子視圖自身的大小,這又是為什么呢留晚?
ListView的中對子視圖的測量調(diào)用的是其自己定義的方法measureScrapChild
private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
LayoutParams p = (LayoutParams) child.getLayoutParams();
...
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
...
}
當(dāng)子視圖的高度設(shè)置為wrap_content或match_parent時酵紫,生成的子視圖測量模式都是UNSPECIFIED,因此最終計算出的高度還是子視圖自身的高度错维。