轉載請註明出處:http://blog.csdn.net/hjf_huangjinfu/article/details/51147636
概述
Android中View框架的工作機制中,主要有三個過程:
1、View樹的測量(measure)Android View框架的measure機制
2、View樹的佈局(layout) Android View框架的layout機制
3、View樹的繪製(draw)Android View框架的draw機制
View框架的工作流程爲:測量每個View大小(measure)-->把每個View放置到相應的位置(layout)-->繪製每個View(draw)。
本文主要講述三大流程中的measure過程。
帶着問題來思考整個measure過程。
1、系統爲什麼要有measure過程?
開發人員在繪製UI的時候,基本都是通過XML佈局文件的方式來配置UI,而每個View必須要設置的兩個羣屬性就是layout_width和layout_height,這兩個屬性代表着當前View的尺寸。
官方文檔截圖:
所以這兩個屬性的值是必須要指定的,這兩個屬性的取值只能爲三種類型:
1、固定的大小,比如100dp。
2、剛好包裹其中的內容,wrap_content。
3、想要和父佈局一樣大,match_parent / fill_parent。
由於Android希望提供一個更優雅的GUI框架,所以提供了自適應的尺寸,也就是 wrap_content 和 match_parent 。
試想一下,那如果這些屬性只允許設置固定的大小,那麼每個View的尺寸在繪製的時候就已經確定了,所以可能都不需要measure過程。但是由於需要滿足自適應尺寸的機制,所以需要一個measure過程。
2、measure過程都幹了點什麼事?
由於上面提到的自適應尺寸的機制,所以在用自適應尺寸來定義View大小的時候,View的真實尺寸還不能確定。但是View尺寸最終需要映射到屏幕上的像素大小,所以measure過程就是幹這件事,把各種尺寸值,經過計算,得到具體的像素值。measure過程會遍歷整棵View樹,然後依次測量每個View真實的尺寸。具體是每個ViewGroup會向它內部的每個子View發送measure命令,然後由具體子View的onMeasure()來測量自己的尺寸。最後測量的結果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的數據單位是像素。
3、對於自適應的尺寸機制,如何合理的測量一顆View樹?
系統在遍歷完佈局文件後,針對佈局文件,在內存中生成對應的View樹結構,這個時候,整棵View樹種的所有View對象,都還沒有具體的尺寸,因爲measure過程最終是要確定每個View打的準確尺寸,也就是準確的像素值。但是剛開始的時候,View中layout_width和layout_height兩個屬性的值,都只是自適應的尺寸,也就是match_parent和wrap_content,這兩個值在系統中爲負數,所以系統不會把它們當成具體的尺寸值。所以當一個View需要把它內部的match_parent或者wrap_content轉換成具體的像素值的時候,他需要知道兩個信息。
1、針對於match_parent,父佈局當前具體像素值是多少,因爲match_parent就是子View想要和父佈局一樣大。
2、針對wrap_content,子View需要根據當前自己內部的content,算出一個合理的能包裹所有內容的最小值。但是如果這個最小值比當前父佈局還大,那不行,父佈局會告訴你,我只有這麼大,你也不應該超過這個尺寸。
由於樹這種數據結構的特殊性,我們在研究measure的過程時,可以只研究一個ViewGroup和2個View的簡單場景。大概示意圖如下:
也就是說,在measure過程中,ViewGroup會根據自己當前的狀況,結合子View的尺寸數據,進行一個綜合評定,然後把相關信息告訴子View,然後子View在onMeasure自己的時候,一邊需要考慮到自己的content大小,一邊還要考慮的父佈局的限制信息,然後綜合評定,測量出一個最優的結果。
4、那麼ViewGroup是如何向子View傳遞限制信息的?
談到傳遞限制信息,那就是MeasureSpec類了,該類貫穿於整個measure過程,用來傳遞父佈局對子View尺寸測量的約束信息。簡單來說,該類就保存兩類數據。
1、子View當前所在父佈局的具體尺寸。
2、父佈局對子View的限制類型。
那麼限制類型又分爲三種類型:
1、UNSPECIFIED,不限定。意思就是,子View想要多大,我就可以給你多大,你放心大膽的measure吧,不用管其他的。也不用管我傳遞給你的尺寸值。(其實Android高版本中推薦,只要是這個模式,尺寸設置爲0)
2、EXACTLY,精確的。意思就是,根據我當前的狀況,結合你指定的尺寸參數來考慮,你就應該是這個尺寸,具體大小在MeasureSpec的尺寸屬性中,自己去查看吧,你也不要管你的content有多大了,就用這個尺寸吧。
3、AT_MOST,最多的。意思就是,根據我當前的情況,結合你指定的尺寸參數來考慮,在不超過我給你限定的尺寸的前提下,你測量一個恰好能包裹你內容的尺寸就可以了。
源代碼分析
在View的源代碼中,提取到了下面一些關於measure過程的信息。
我們知道,整棵View樹的根節點是DecorView,它是一個FrameLayout,所以它是一個ViewGroup,所以整棵View樹的測量是從一個ViewGroup對象的measure方法開始的。
View:
1、measure
/** 開始測量一個View有多大,parent會在參數中提供約束信息,實際的測量工作是在onMeasure()中進行的,該方法會調用onMeasure()方法,所以只有onMeasure能被也必須要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);
父佈局會在自己的onMeasure方法中,調用child.measure ,這就把measure過程轉移到了子View中。
2、onMeasure
/** 具體測量過程,測量view和它的內容,來決定測量的寬高(mMeasuredWidth mMeasuredHeight )。該方法中必須要調用setMeasuredDimension(int, int)來保存該view測量的寬高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
子View會在該方法中,根據父佈局給出的限制信息,和自己的content大小,來合理的測量自己的尺寸。
3、setMeasuredDimension
/** 保存測量結果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);
當View測量結束後,把測量結果保存起來,具體保存在mMeasuredWidth和mMeasuredHeight中。
ViewGroup:
1、measureChildren
/** 讓所有子view測量自己的尺寸,需要考慮當前ViewGroup的MeasureSpec和Padding。跳過狀態爲gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
測量所有的子View尺寸,把measure過程交到子View內部。
2、measureChild
/** 測量單個View,需要考慮當前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
對每一個具體的子View進行測量。
3、measureChildWithMargins
/** 測量單個View,需要考慮當前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();
對每一個具體的子View進行測量。但是需要考慮到margin等信息。
4、getChildMeasureSpec
/** measureChildren過程中最困難的一部分,爲child計算MeasureSpec。該方法爲每個child的每個維度(寬、高)計算正確的MeasureSpec。目標就是把當前viewgroup的MeasureSpec和child的LayoutParams結合起來,生成最合理的結果。
比如,當前ViewGroup知道自己的準確大小,因爲MeasureSpec的mode爲EXACTLY,而child希望能夠match_parent,這時就會爲child生成一個mode爲EXACTLY,大小爲ViewGroup大小的MeasureSpec。
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension);
根據當前自身的狀況,以及特定子View的尺寸參數,爲特定子View計算一個合理的限制信息。
源代碼:
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);
}
僞代碼:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
獲取限制信息中的尺寸和模式。
switch (限制信息中的模式) {
case 當前容器的父容器,給當前容器設置了一個精確的尺寸:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
你就用父容器的尺寸值就行了;
} else if (子View希望包裹內容) {
你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
}
break;
case 當前容器的父容器,給當前容器設置了一個最大尺寸:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
} else if (子View希望包裹內容) {
你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
}
break;
case 當前容器的父容器,對當前容器的尺寸不限制:
if (子View申請固定的尺寸) {
你就用你自己申請的尺寸值就行了;
} else if (子View希望和父容器一樣大) {
父容器對子View尺寸不做限制。
} else if (子View希望包裹內容) {
父容器對子View尺寸不做限制。
}
break;
} return 對子View尺寸的限制信息;
}
當自定義View的時候,也需要處理measure過程,主要有兩種情況。
1、繼承自View的子類。
需要覆寫onMeasure來正確測量自己。最後都需要調用setMeasuredDimension來保存測量結果
一般來說,自定義View的measure過程僞代碼爲:
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int viewSize = 0;
swith (mode) {
case MeasureSpec.EXACTLY:
viewSize = size; //當前View尺寸設置爲父佈局尺寸
break;
case MeasureSpec.AT_MOST:
viewSize = Math.min(size, getContentSize()); //當前View尺寸爲內容尺寸和父佈局尺寸當中的最小值
break;
case MeasureSpec.UNSPECIFIED:
viewSize = getContentSize(); //內容有多大,就設置尺寸爲多大
break;
default:
break;
}
setMeasuredDimension(viewSize);
2、繼承自ViewGroup的子類。
不但需要覆寫onMeasure來正確測量自己,可能還要覆寫一系列measureChild方法,來正確的測量子view,比如ScrollView。或者乾脆放棄父類實現的measureChild規則,自己重新實現一套測量子view的規則,比如RelativeLayout。最後都需要調用setMeasuredDimension來保存測量結果。
一般來說,自定義ViewGroup的measure過程的僞代碼爲:
//ViewGroup開始測量自己的尺寸
viewGroup.onMeasure();
//ViewGroup爲每個child計算測量限制信息(MeasureSpec)
viewGroup.getChildMeasureSpec();
//把上一步生成的限制信息,傳遞給每個子View,然後子View開始measure自己的尺寸
child.measure();
//子View測量完成後,ViewGroup就可以獲取每個子View測量後的尺寸
child.getChildMeasuredSize();
//ViewGroup根據自己自身狀況,比如Padding等,計算自己的尺寸
viewGroup.calculateSelfSize();
//ViewGroup保存自己的尺寸
viewGroupsetMeasuredDimension();
案例分析
很多開發人員都遇到過這種需求,就是ScrollView內部嵌套ListView,而該ListView數據條數是不確定的,所以需要設置爲包裹內容,然後就會發現ListView就會顯示第一行出來。然後就會百度到一條解決方案,繼承ListView,覆寫onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
問題是解決了,但是很多開發人員並不知道爲什麼。
下面會從ScrollView和ListView的measure過程來分析一下。
1、爲什麼會出現上述問題?
備註:截取部分問題相關代碼,並不是完整代碼。
看看ListView的onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View child = obtainView(0, mIsScrap);
childHeight = child.getMeasuredHeight();
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
}
當MeasureSpec mode爲UNSPECIFIED的時候,只測量第一個item打的高度,跟問題描述相符,所以我們猜測可能是因爲ScrollView傳遞了一個UNSPECIFIED限制給ListView。
再來看ScrollView的onMeasure代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
調用了父類的onMeasure:
看看FrameLayout的onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
}
調用了measureChildWithMargins,但是因爲ScrollView覆寫了該方法,所以看看ScrollView的measureChildWithMargins方法:
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final int childHeightMeasureSpec =
MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
果然,它向ListView的onMeasure傳遞了一個UNSPECIFIED的限制。
爲什麼呢,想想,因爲ScrollView,本來就是可以在豎直方向滾動的佈局,所以,它對它所有的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,因爲我本來就是需要豎直滑動的,它的本意就是如此,所以它對子View高度不做任何限制。
2、爲什麼這種解決方法可以解決這個問題?
看看ListView的onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View child = obtainView(0, mIsScrap);
childHeight = child.getMeasuredHeight();
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
}
只要讓heightMode == MeasureSpec.AT_MOST,它就會測量它的完整高度,所以第一個數據,限制mode的值就確定下來了。第二個數據就是尺寸上限,如果給個200,那麼當ListView數據過多的時候,該ListView最大高度就是200了,還是不能完全顯示內容,怎麼辦?那麼就給個最大值吧,最大值是多少呢,Integer.MAX_VALUE?
先看一下MeasureSpec的代碼說明:
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
他用最高兩位存儲mode,用其他剩餘未存儲size。所以Integer.MAX_VALUE >> 2,就是限制信息所能攜帶的最大尺寸數據。所以最後就需要用這兩個值做成一個限制信息,傳遞給ListView的height維度。
也就是如下代碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
自己動手
下面我們自己寫一個自定義的ViewGroup,讓它內部的每一個子View都垂直排布,並且讓每一個子View的左邊界都距離上一個子View的左邊界一定的距離。並且支持wrap_content。大概看起來如下圖所示:
實際運行效果如下圖所示:
代碼如下:
public class VerticalOffsetLayout extends ViewGroup {
private static final int OFFSET = 100;
public VerticalOffsetLayout(Context context) {
super(context);
}
public VerticalOffsetLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
child.measure(childWidthSpec, childHeightSpec);
}
switch (widthMode) {
case MeasureSpec.EXACTLY:
width = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
int widthAddOffset = i * OFFSET + child.getMeasuredWidth();
width = Math.max(width, widthAddOffset);
}
break;
default:
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
height = height + child.getMeasuredHeight();
}
break;
default:
break;
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = 0;
int right = 0;
int top = 0;
int bottom = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
left = i * OFFSET;
right = left + child.getMeasuredWidth();
bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
top += child.getMeasuredHeight();
}
}
}