View的繪制流程(一)
每一個(gè)視圖的繪制過程都必須經(jīng)歷三個(gè)最主要的階段,即onMeasure()、onLayout()和onDraw()
Measure
知識(shí)點(diǎn)
1. MeasureSpec
- 我們通過Android系統(tǒng)的MeasureSpec類來測(cè)量view。
MeasureSpec是一個(gè)32位的int值,其中高2位為測(cè)量模式,低30位為測(cè)量的大小。
作用:一個(gè)MeasureSpec封裝了從父容器傳遞給子容器的布局要求。
含義:MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡(jiǎn)單的計(jì)算得出一個(gè)針對(duì)子View的測(cè)量要求,這個(gè)測(cè)量要求就是MeasureSpec。
- MeasureSpec的三種測(cè)量模式
UPSPECIFIED : 父容器對(duì)于子容器沒有任何限制,子容器想要多大就多大。
EXACTLY: 父容器已經(jīng)為子容器設(shè)置了尺寸,子容器應(yīng)當(dāng)服從這些邊界,不論子容器想要多大的空間:當(dāng)我們的控件layout_width(Height)屬性指定為具體的值或者是match_parent時(shí)。
AT_MOST:子容器可以是聲明大小內(nèi)的任意大?。寒?dāng)我們的控件layout_width(Height)屬性指定為wrap_content時(shí)。
2. ViewRootImpl
每個(gè)Activity都包含一個(gè)Window對(duì)象,在Android中Window對(duì)象通常由PhoneWindow來實(shí)現(xiàn)(是Activity和整個(gè)View系統(tǒng)交互的接口,每個(gè)Window都對(duì)應(yīng)著一個(gè)View和一個(gè)ViewRootImpl,Window(Window無法直接訪問,要通過WindowManager)和View通過ViewRootImpl來建立聯(lián)系,執(zhí)行添加,刪除,更新View等操作)。PhoneWindow將一個(gè)DecorView設(shè)置為整個(gè)應(yīng)用窗口的根View。DecorView將要顯示的具體內(nèi)容呈現(xiàn)在了PhoneWindow上。所以繪制的入口是由ViewRootImpl的performTraversals方法來發(fā)起Measure,Layout,Draw等流程的。
View的Measure過程
View測(cè)量前的準(zhǔn)備工作 ---> MeasureSpec
在ViewRoot的performTraversals()方法中,調(diào)用子View的measure()方法。measure()方法接收兩個(gè)參數(shù),widthMeasureSpec和heightMeasureSpec,這兩個(gè)值分別用于確定視圖的寬度和高度的規(guī)格和大小。雖然這兩個(gè)參數(shù)從父View傳遞過來,是父View的MeasureSpec和子View自己的LayoutParams共同決定的,而子View的LayoutParams其實(shí)就是我們?cè)趚ml寫的時(shí)候設(shè)置的layout_width和layout_height轉(zhuǎn)化而來的。(父View的Measure過程會(huì)等子View測(cè)量之后再對(duì)自己進(jìn)行測(cè)量)。
舉個(gè)例子:傳遞給子View的MeasureSpc的計(jì)算是通過調(diào)用measureChildWithMargins()來計(jì)算的
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
// 子View的LayoutParams,你在xml的layout_width和layout_height,
// layout_xxx的值最后都會(huì)封裝到這個(gè)個(gè)LayoutParams。
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//根據(jù)父View的測(cè)量規(guī)格和父View自己的Padding,
//還有子View的Margin和已經(jīng)用掉的空間大?。╳idthUsed),就能算出子View的MeasureSpec,具體計(jì)算
過程看getChildMeasureSpec方法。
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);
//通過父View的MeasureSpec和子View的自己LayoutParams的計(jì)算,算出子View的MeasureSpec,然后父
容器傳遞給子容器的
// 然后讓子View用這個(gè)MeasureSpec(一個(gè)測(cè)量要求,比如不能超過多大)去測(cè)量自己,如果子View是ViewGroup 那還會(huì)遞歸往下測(cè)量。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// spec參數(shù) 表示父View的MeasureSpec
// padding參數(shù) 父View的Padding+子View的Margin,父View的大小減去這些邊距,才能精確算出
// 子View的MeasureSpec的size
// childDimension參數(shù) 表示該子View內(nèi)部LayoutParams屬性的值(lp.width或者lp.height)
// 可以是wrap_content、match_parent、一個(gè)精確指(an exactly size),
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //獲得父View的mode
int specSize = MeasureSpec.getSize(spec); //獲得父View的大小
//父View的大小-自己的Padding+子View的Margin,得到值才是子View的大小。
int size = Math.max(0, specSize - padding);
int resultSize = 0; //初始化值,最后通過這個(gè)兩個(gè)值生成子View的MeasureSpec
int resultMode = 0; //初始化值,最后通過這個(gè)兩個(gè)值生成子View的MeasureSpec
switch (specMode) {
// Parent has imposed an exact size on us
//1、父View是EXACTLY的 !
case MeasureSpec.EXACTLY:
//1.1、子View的width或height是個(gè)精確值 (an exactly size)
if (childDimension >= 0) {
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//1.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//1.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST 。
}
break;
// Parent has imposed a maximum size on us
//2、父View是AT_MOST的 !
case MeasureSpec.AT_MOST:
//2.1、子View的width或height是個(gè)精確值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。
}
//2.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
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; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST
}
//2.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size; //size為父視圖大小
resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST
}
break;
// Parent asked to see how big we want to be
//3、父View是UNSPECIFIED的 !
case MeasureSpec.UNSPECIFIED:
//3.1、子View的width或height是個(gè)精確值 (an exactly size)
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; //size為精確值
resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY
}
//3.2、子View的width或height為 MATCH_PARENT/FILL_PARENT
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0; //size為0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED
}
//3.3、子View的width或height為 WRAP_CONTENT
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; //size為0! ,其值未定
resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED
}
break;
}
//根據(jù)上面邏輯條件獲取的mode和size構(gòu)建MeasureSpec對(duì)象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
DecorView的widthMeasureSpec和heightMeasureSpec是從ViewRootImpl中傳遞來的。也是類似的計(jì)算方法??梢詤⒖?a target="_blank" rel="nofollow">郭神的博客
MeasureSpec的計(jì)算流程大致就是:通過父View的MeasureSpec和子View的LayoutParams來計(jì)算出子View的計(jì)算標(biāo)準(zhǔn)MeasureSpec,并傳遞給子View的measure()方法讓子View測(cè)量自己和自己子View的尺寸。
下面介紹幾個(gè)常見的情況:
-
如果父View的MeasureSpec的模式是EXACTLY,那么父View的MeasureSpec.size就是確定大小的
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于match_parent(指定的值),那么子View的Size就是父View的size(在布局文件中給定的值)。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于wrap_content,那么子View的大小需要根據(jù)自己的content來確定,但是最終的大小不能超過父View的size。子View首先通過遍歷調(diào)用自身的子View并分別調(diào)用他們的measure()方法計(jì)算他們的尺寸,然后再計(jì)算自身的大小。而此時(shí)傳遞給自身子View的MeasureSpec參數(shù)中mode為AT_MOST,size暫定為父View的size。表示的意思就是自身的大小沒有不確切的值,其子View的大小最大為給定的MeasureSpec的大小,不能超過它(這就是AT_MOST 的意思)。
-
如果父View的MeasureSpec的模式是AT_MOST,說明父View的大小是不確定,最大的大小是MeasureSpec 的size值,不能超過這個(gè)值。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于match_parent,由于父View的大小無法確定,所以無法確子View的大小。此時(shí)返回的MeasureSpec的mode變成了AT_MOST,size是父View傳遞過來的MeasureSpec的size的值,代表最大不能超過這個(gè)值。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于wrap_content,由于父View的大小無法確定的同時(shí),在沒有計(jì)算出子View的content的大小是無法確定子View的大小。所以傳遞給子View的MeasureSpec的mode是AT_MOST,size就是父View中傳遞來的MeasureSpec中的size。代表大小暫定這個(gè)值,最大不能超過這個(gè)值。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于特定的值,那么傳遞給子View的MeasureSpec的mode是EXACTLY,size是指定的值。
-
如果父View的MeasureSpec的模式是UPSPECIFIED,表示沒有任何束縛和約束,不像AT_MOST表示最大只能多大,不也像EXACTLY表示父View確定的大小,子View可以得到任意想要的大小,不受約束。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于match_parent/wrap_content,由于父View的大小沒有任何約束,所以子View的大小也就沒有任何約束,此時(shí)傳遞給子View的MeasureSpec的mode是UPSPECIFIED,size是0。
- 如果此時(shí)子View的布局文件中屬性layout_width/height等于特定的值,那么傳遞給子View的MeasureSpec的mode是EXACTLY,size是指定的值。
View測(cè)量過程 ---> measure()方法
當(dāng)我們計(jì)算好子View的計(jì)算標(biāo)準(zhǔn)MeasureSpec時(shí),調(diào)用子View的measure()方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec,heightMeasureSpec);
.....
}
源碼中,View.measure()的方法是final,所以無法覆寫。View的測(cè)試工作在onMeasure()方法中完成。要想自定義View的測(cè)量,必須重寫onMeasure()方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure()方法中,調(diào)用了setMeasuredDimension()。這個(gè)方法可以簡(jiǎn)單理解就是給mMeasuredWidth和mMeasuredHeight設(shè)值,如果這兩個(gè)值一旦設(shè)置了,那么意味著對(duì)于這個(gè)View的測(cè)量結(jié)束了,這個(gè)View的寬高已經(jīng)有測(cè)量的結(jié)果出來了。
注意:setMeasuredDimension()方法一定要在onMeasure()方法中調(diào)用,否則會(huì)拋出異常。
//獲取的是android:minHeight屬性的值或者View背景圖片的大小值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//@param size參數(shù)一般表示設(shè)置了android:minHeight屬性或者該View背景圖片的大小值
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: //表示該View的大小父視圖未定,設(shè)置為默認(rèn)值
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getDefaultSize()的size參數(shù)由getSuggestedMinimumWidth()返回,這個(gè)值由布局文件中的android:minHeight/width和View的background決定尺寸共同決定。這個(gè)size就是該view的默認(rèn)大小。默認(rèn)的測(cè)試方法(getDefaultSize())中,當(dāng)MeasureSpec的mode不是UNSPECIFIED時(shí),都將MeasureSpec中的size返回。
相反在一些View的派生類中(TextView、Button、ImageView等),都是對(duì)onMeasure()方法重寫。如果該View(TextView)的MeasureSpec的mode是AT_MOST,會(huì)用其content進(jìn)行計(jì)算后的值和MeasureSpec中的size做比較,取小的一個(gè)作為子View(TextView)的尺寸。
ViewGroup測(cè)試過程--->onMeasure()
ViewGroup 類并沒有實(shí)現(xiàn)onMeasure。當(dāng)ViewGroup沒有確定的值時(shí)(wrap_content),需要對(duì)其子View進(jìn)行遍歷,并獲取所有子View的大小后,再來決定自己的大小。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
首先調(diào)用上述方法,遍歷所有的子View,然后調(diào)用下面的方法逐個(gè)進(jìn)行測(cè)量
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
只有當(dāng)所有子View測(cè)量完畢后,才去計(jì)算ViewGroup的尺寸(無論ViewGroup的layout_width/height屬性是什么)。
但是測(cè)量的工作實(shí)在onMeasure()中完成的,那么來看下FrameLayout是如何實(shí)現(xiàn)onMeasure()方法
//FrameLayout 的測(cè)量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 遍歷自己的子View,只要不是GONE的都會(huì)參與測(cè)量,measureChildWithMargins方法在最上面
// 的源碼已經(jīng)講過了,如果忘了回頭去看看,基本思想就是父View把自己的MeasureSpec
// 傳給子View結(jié)合子View自己的LayoutParams 算出子View 的MeasureSpec,然后繼續(xù)往下傳,
// 傳遞葉子節(jié)點(diǎn),葉子節(jié)點(diǎn)沒有子View,根據(jù)傳下來的這個(gè)MeasureSpec測(cè)量自己就好了。
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
....
....
}
}
.....
.....
//所有的孩子測(cè)量之后,經(jīng)過一系類的計(jì)算之后通過setMeasuredDimension設(shè)置自己的寬高,
//對(duì)于FrameLayout 可能用最大的字View的大小,對(duì)于LinearLayout,可能是高度的累加,
//具體測(cè)量的原理去看看源碼??偟膩碚f,父View是等所有的子View測(cè)量結(jié)束之后,再來測(cè)量自己。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
....
}
這里通過深度優(yōu)先遍歷來完成所有子View的測(cè)量,然后再對(duì)自身進(jìn)行測(cè)量。
注意:在setMeasuredDimension()方法調(diào)用之后,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測(cè)量出的寬高,以此之前調(diào)用這兩個(gè)方法得到的值都會(huì)是0。