這是AndroidUI繪制流程分析的第二篇文章,主要分析界面中View是如何繪制到界面上的具體過(guò)程。
1、ViewRoot和DecorView
ViewRoot對(duì)應(yīng)于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過(guò)ViewRoot來(lái)完成的。在ActivityThread中,當(dāng)Activity對(duì)象被創(chuàng)建完畢后,會(huì)將DecorView添加到Window中,同時(shí)會(huì)創(chuàng)建ViewRootImpl對(duì)象,并將ViewRootImpl對(duì)象和DecorView建立關(guān)聯(lián)。
View的繪制流程是從ViewRoot的performTraversals方法開(kāi)始的,它經(jīng)過(guò)measure、layout和draw三個(gè)過(guò)程才最終將一個(gè)View繪制出來(lái)。
measure過(guò)程決定了View的寬/高,Measure完成以后,可以通過(guò)getMeasuredWidth和getMeasuredHeight方法來(lái)獲取View測(cè)量后的寬/高,在幾乎所有的情況下,它等同于View的最終的寬/高,但是特殊情況除外。Layout過(guò)程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的寬/高,完成以后,可以通過(guò)getTop、getBottom、getLeft和getRight來(lái)拿到View的四個(gè)頂點(diǎn)的位置,可以通過(guò)getWidth和getHeight方法拿到View的最終寬/高。Draw過(guò)程決定了View的顯示,只有draw方法完成后View的內(nèi)容才能呈現(xiàn)在屏幕上。
DecorView作為頂級(jí)View,一般情況下,它內(nèi)部會(huì)包含一個(gè)豎直方向的LinearLayout,在這個(gè)LinearLayout里面有上下兩個(gè)部分,上面是標(biāo)題欄,下面是內(nèi)容欄。在Activity中,我們通過(guò)setContentView所設(shè)置的布局文件其實(shí)就是被加到內(nèi)容欄中的,而內(nèi)容欄id為content??梢酝ㄟ^(guò)下面方法得到content:ViewGroup content = findViewById(R.android.id.content)。通過(guò)content.getChildAt(0)可以得到設(shè)置的view。DecorView其實(shí)是一個(gè)FrameLayout,View層的事件都先經(jīng)過(guò)DecorView,然后才傳遞給我們的View。

MeasureSpec
MeasureSpec代表一個(gè)32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測(cè)量模式,而SpecSize是指在某種測(cè)量模式下的規(guī)格大小。
SpecMode有三類,如下所示:
UNSPECIFIED
父容器不對(duì)View有任何限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部。
EXACTLY
父容器已經(jīng)檢測(cè)出View所需要的精確大小,這個(gè)時(shí)候View的最終大小就是SpecSize所指定的值,對(duì)應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種模式。
AT_MOST
父容器指定一個(gè)可用大小即
SpecSize,View的大小不能大于這個(gè)值,對(duì)應(yīng)于LayoutParams中的wrap_content。
LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進(jìn)一步?jīng)Q定View的寬/高。
對(duì)于頂級(jí)View,即DecorView和普通View來(lái)說(shuō),MeasureSpec的轉(zhuǎn)換過(guò)程略有不同。對(duì)于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定;

對(duì)于普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;

MeasureSpec一旦確定,onMeasure就可以確定View的測(cè)量寬/高。
小結(jié)一下
當(dāng)子
View采用具體的寬/高時(shí),不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式+子View的LayoutParams中指定的具體的大小。
當(dāng)子
View的寬/高采用match_parent時(shí),這時(shí)候父容器的MeasureSpec會(huì)發(fā)揮作用,子View的模式總是跟父容器的模式一樣:
- 如果父容器的模式是精確模式(
EXACTLY),那么子View的MeasureSpec就是精確模式+父容器的剩余空間(或者說(shuō)父容器的可用空間);- 如果父容器的模式是最大模式
(AT_MOST),那么子View的MeasureSpec就是最大模式+父容器的剩余空間(或者說(shuō)父容器的可用空間);- 如果父容器的模式是未指定模式(
UNSPECIFIED),那么子View的MeasureSpec就是未指定模式+父容器的剩余空間或者0
當(dāng)子 View 的寬高采用
wrap_content時(shí),這時(shí)候父容器的 MeasureSpec 同樣會(huì)發(fā)揮作用:
- 如果父容器的模式是精確模式(
EXACTLY),那么子View的MeasureSpec就是最大模式+父容器的剩余空間(或者說(shuō)父容器的可用空間);- 如果父容器的模式是最大模式(
AT_MOST),那么子View的MeasureSpec就是最大模式+父容器的剩余空間(或者說(shuō)父容器的可用空間);- 如果父容器的模式是未指定模式(
UNSPECIFIED),那么子View的MeasureSpec就是未指定模式+父容器的剩余空間或者0。
當(dāng)子 View 的寬高采用 wrap_content 時(shí),不管父容器的模式是精確模式還是最大模式,子 View的模式總是最大模式+父容器的剩余空間。
View的工作流程
View的工作流程主要是指measure、layout、draw三大流程,即測(cè)量、布局、繪制。其中measure確定View的測(cè)量寬/高,layout確定view的最終寬/高和四個(gè)頂點(diǎn)的位置,而draw則將View繪制在屏幕上。

measure過(guò)程
measure過(guò)程要分情況,如果只是一個(gè)原始的view,則通過(guò)measure方法就完成了其測(cè)量過(guò)程,如果是一個(gè)ViewGroup,除了完成自己的測(cè)量過(guò)程外,還會(huì)遍歷調(diào)用所有子元素的measure方法,各個(gè)子元素再遞歸去執(zhí)行這個(gè)流程。
View的measure過(guò)程
如果是一個(gè)原始的 View,那么通過(guò) measure 方法就完成了測(cè)量過(guò)程,在 measure 方法中會(huì)去調(diào)用 View 的 onMeasure 方法,View 類里面定義了 onMeasure 方法的默認(rèn)實(shí)現(xiàn):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 先獲取建議的最小寬度和高度
int suggestedMinimumWidth = getSuggestedMinimumWidth();
int suggestedMinimumHeight = getSuggestedMinimumHeight();
// 再通過(guò) getDefaultSize 方法獲取寬度和高度的測(cè)量值
int measuredWidth = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec);
int measuredHeight = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec);
// 最后調(diào)用 setMeasuredDimension 方法設(shè)置 View 寬度和高度的測(cè)量值。
setMeasuredDimension(measuredWidth, measuredHeight);
}
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源碼:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
我們只看
getSuggestedMinimumWidth方法:如果View的背景mBackground == null成立,即View沒(méi)有設(shè)置背景,那么View的建議最小寬度就是mMinWidth(mMinWidth對(duì)應(yīng)于android:minWidth這個(gè)屬性所指定的值);如果View設(shè)置了背景,那么 View 的建議最小寬度為mMinWidth和mBackground.getMinimumWidth()中較大的那個(gè)。mBackground是一個(gè) Drawable 對(duì)象,所以去看Drawable 類下的getMinimumWidth方法:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看到,getMinimumWidth 方法獲取的是 Drawable 的原始寬度。如果存在原始寬度(即滿足 intrinsicWidth > 0),那么直接返回原始寬度即可;如果不存在原始寬度(即不滿足intrinsicWidth > 0),那么就返回 0。
ShapeDrawable沒(méi)有原始寬度和高度,而BitmapDrawable有原始寬度和高度。
接著看最重要的 getDefaultSize 方法:
// 這是一個(gè) static 修飾的方法,所以是一個(gè)工具方法
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
如果specMode為 MeasureSpec.UNSPECIFIED即未指定模式,那么返回由方法參數(shù)傳遞過(guò)來(lái)的尺寸作為 View 的測(cè)量寬度和高度;
如果specMode不是MeasureSpec.UNSPECIFIED 即是最大模式或者精確模式,那么返回從 measureSpec 中取出的specSize作為 View 測(cè)量后的寬度和高度。
看一下剛才的表格:
當(dāng) specMode 為 EXACTLY 或者 AT_MOST 時(shí),View 的布局參數(shù)為 wrap_content 或者 match_parent 時(shí),給 View 的 specSize 都是 parentSize。這會(huì)比建議的最小寬高要大。這是不符合我們的預(yù)期的。因?yàn)槲覀兘oView設(shè)置wrap_content是希望View的大小剛好可以包裹它的內(nèi)容。
因此:
直接繼承 View 的自定義控件需要重寫(xiě) onMeasure 方法并設(shè)置 wrap_content 時(shí)的自身大?。ńo View 指定一個(gè)默認(rèn)的內(nèi)部寬高)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpecSize);
}else if(heightSpecSize == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
ViewGroup的measure過(guò)程
如果是一個(gè) ViewGroup,除了完成自己的 measure 過(guò)程以外,還會(huì)遍歷去調(diào)用所有子元素的 measure 方法,各個(gè)子元素再遞歸去執(zhí)行 measure 過(guò)程。
ViewGroup 并沒(méi)有重寫(xiě) View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 這幾個(gè)方法專門用于測(cè)量子元素。
// 遍歷所有的子元素,并使用 measureChild 方法來(lái)對(duì)每一個(gè)子元素進(jìn)行 measure。
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];
// 只有當(dāng)子元素的可見(jiàn)性不是 GONE 時(shí),才對(duì)它進(jìn)行測(cè)量。這一點(diǎn)是個(gè)細(xì)節(jié)。
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
// 測(cè)量每一個(gè)子元素:最重要的邏輯是通過(guò) getChildMeasureSpec 方法來(lái)獲取測(cè)量子元素所需要的寬度 MeasureSpec 和 高度 MeasureSpec。
// getChildMeasureSpec(int spec, int padding, int childDimension) 方法需要的參數(shù)必須搞清楚:
// spec 是 ViewGroup 從 onMeasure 方法接收到的 MeasureSpec,
// padding 是 ViewGroup 已使用的空間,childDimension 是子元素的布局參數(shù)的寬和高。
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);
}
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);
}
為什么ViewGroup 類沒(méi)有像 View 一樣對(duì)其 onMeasure 方法做統(tǒng)一的實(shí)現(xiàn),實(shí)際上 ViewGroup 繼承了 View 類對(duì) onMeasure 方法的實(shí)現(xiàn)?這是因?yàn)椴煌?ViewGroup 子類有不同的布局特性,這導(dǎo)致它們的測(cè)量細(xì)節(jié)各不相同,比如 LinearLayout 和 RelativeLayout 這兩者的布局特性顯然不同,因此 ViewGroup 無(wú)法統(tǒng)一實(shí)現(xiàn)。
為什么說(shuō)在 onLayout 方法中去獲取 View 的測(cè)量寬/高是一個(gè)好的習(xí)慣?
正常情況下,View 的 measure 過(guò)程完成以后,通過(guò) getMeasuredWidth/getMeasuredHeight 方法就可以正確地獲取到 View 的測(cè)量寬/高;但是,在某些極端情況下,系統(tǒng)可能需要多次 measure 才能確定最終的測(cè)量寬/高,在這種情形下,在 onMeasure 方法中拿到的測(cè)量寬/高很可能是不準(zhǔn)確的。
在 Activity 啟動(dòng)時(shí),如何獲取某個(gè) View 的寬/高?
| 方式 | 解釋 |
|---|---|
Activity/View.onWindowFocusChanged |
在 onWindowFocusChanged 回調(diào)時(shí),表示 View 已經(jīng)初始化完畢了,寬/高已經(jīng)準(zhǔn)備好了,所以這時(shí)去獲取寬/高是沒(méi)有問(wèn)題的。但是,onWindowFocusChanged會(huì)被多次調(diào)用,當(dāng) Activity 的窗口得到焦點(diǎn)和失去焦點(diǎn)時(shí)均會(huì)被調(diào)用一次。 |
View.post(Runnable runnable) |
通過(guò) post 可以將一個(gè) Runnable 對(duì)象放到消息隊(duì)列的尾部,然后等到 Looper 調(diào)用此 Runnable的時(shí)候,View 也已經(jīng)初始化好了。注意:是 View.post方法而不是 Handler.post 方法 |
ViewTreeObserver 的多個(gè)回調(diào),如 OnGlobalLayoutListener 接口。 |
OnGlobalLayoutListener 接口:當(dāng) View 樹(shù)的狀態(tài)發(fā)生改變或者 View 樹(shù)內(nèi)部的 View 的可見(jiàn)性發(fā)生改變時(shí),它的 onGlobalLayout 方法將被回調(diào)。需要注意的是,伴隨著 View 樹(shù)的狀態(tài)改變等,onGlobalLayout 會(huì)被調(diào)用多次。使用完接口后,記得移除。 |
使用 View.measure(int widthMeasureSpec, int heightMeasureSpec)方法進(jìn)行手動(dòng)測(cè)量 |
這種方法比較復(fù)雜。當(dāng) View 的 LayoutParams 為 match_parent 時(shí),這種方式不能測(cè)量出具體的寬/高;當(dāng) View 的 LayoutParams 為具體的數(shù)值(如 100 dp)時(shí),這種方式可以測(cè)量出具體的寬/高;當(dāng) View 的 LayoutParams 為 wrap_content 時(shí), 這種方式不能測(cè)量出具體的寬/高。 |
Layout
如果是 View 的話,那么在它的 layout 方法中就確定了自身的位置(具體來(lái)說(shuō)是通過(guò) setFrame 方法來(lái)設(shè)定 View 的四個(gè)頂點(diǎn)的位置,即初始化 mLeft,mRight,mTop,mBottom 這四個(gè)值),layout 過(guò)程就結(jié)束了。
如果是ViewGroup 的話,那么在它的layout 方法中只是確定了 ViewGroup 自身的位置,要確定子元素的位置,就需要重寫(xiě) onLayout方法;在 onLayout 方法中,會(huì)調(diào)用子元素的 layout 方法,子元素在它的layout 方法中確定自己的位置,這樣一層一層地傳遞下去完成整個(gè) View 樹(shù)的 layout 過(guò)程。
layout 方法和 onLayout 方法的區(qū)別是什么?
layout 方法的作用是確定View本身的位置,即設(shè)定View 的四個(gè)頂點(diǎn)的位置,這樣就確定了 View 在父容器中的位置;
onLayout 方法的作用是父容器確定子元素的位置,這個(gè)方法在 View 中是空實(shí)現(xiàn),因?yàn)?View 沒(méi)有子元素了,在 ViewGroup 中則進(jìn)行抽象化,它的子類必須實(shí)現(xiàn)這個(gè)方法。
View 的 getMeasuredWidth 和 getWidth 這兩個(gè)方法有什么區(qū)別?
getMeasuredWidth獲取的是測(cè)量寬度,定義了一個(gè)View想要在父View里的尺寸,getWidth獲取的是寬度,有時(shí)也被稱為繪制寬度,定義了繪制時(shí)或者布局后屏幕上的View的實(shí)際尺寸。兩者的賦值時(shí)機(jī)不同,測(cè)量寬/高的賦值時(shí)機(jī)要早于最終寬/高。具體來(lái)說(shuō),
View的測(cè)量寬/高形成于View的measure過(guò)程,而View的最終寬/高形成于View的layout過(guò)程。兩者的值多數(shù)情況下是相等的,但在某些特殊情況下會(huì)不一致。例如有兩種特殊情況:
- 重寫(xiě) View 的 layout 方法:手動(dòng)修改傳入 layout 方法的參數(shù)值;
- View 需要多次 measure 才能確定自己的測(cè)量寬/高,在前幾次的測(cè)量過(guò)程中,其得出的測(cè)量寬/高有可能和最終寬/高不一致,但最終來(lái)看,測(cè)量寬/高還是會(huì)和最終寬/高相同。
Draw
1.繪制背景(background.draw(canvas););
2.繪制自己(onDraw);
3.繪制 children(dispatchDraw(canvas));
4.繪制裝飾(onDrawScrollBars)。
dispatchDraw 方法的調(diào)用是在 onDraw 方法之后,也就是說(shuō),總是先繪制自己再繪制子 View。
對(duì)于 View 類來(lái)說(shuō),dispatchDraw 方法是空實(shí)現(xiàn)的,對(duì)于 ViewGroup 類來(lái)說(shuō),dispatchDraw 方法是有具體實(shí)現(xiàn)的。
ViewGroup 的 draw 過(guò)程是如何傳遞的
通過(guò) dispatchDraw來(lái)傳遞的。dispatchDraw 會(huì)遍歷調(diào)用子元素的 draw 方法,如此 draw 事件就一層一層傳遞了下去。dispatchDraw 在 View 類中是空實(shí)現(xiàn)的,在 ViewGroup 類中是真正實(shí)現(xiàn)的。
View 類中的 setWillNotDraw 方法的含義及其開(kāi)發(fā)意義是什么?
如果一個(gè) View 不需要繪制任何內(nèi)容,那么就設(shè)置這個(gè)標(biāo)記為 true,系統(tǒng)會(huì)進(jìn)行進(jìn)一步的優(yōu)化。
當(dāng)創(chuàng)建的自定義控件繼承于 ViewGroup 并且不具備繪制功能時(shí),就可以開(kāi)啟這個(gè)標(biāo)記,便于系統(tǒng)進(jìn)行后續(xù)的優(yōu)化;當(dāng)明確知道一個(gè) ViewGroup 需要通過(guò) onDraw 繪制內(nèi)容時(shí),需要關(guān)閉這個(gè)標(biāo)記。
查看 LinearLayout 對(duì)這個(gè)方法的調(diào)用:setWillNotDraw(divider == null);,在有 divider 時(shí)才會(huì)關(guān)閉這個(gè)標(biāo)記,否則是打開(kāi)的;ScrollView 直接使用 setWillNotDraw(false);;ViewStub 類則使用 setWillNotDraw(true);。
自定義View
自定義View的分類
| 分類 | 用途 | 特點(diǎn) |
|---|---|---|
| 1.繼承 View 重寫(xiě) onDraw 方法 | 用于實(shí)現(xiàn)一些不規(guī)則的效果,不方便通過(guò)布局的組合方式來(lái)達(dá)到 | 需要通過(guò)繪制的方式來(lái)實(shí)現(xiàn),即重寫(xiě) onDraw 方法;需要自己支持 wrap_content,處理 padding |
| 2.繼承 ViewGroup 派生特殊的 Layout | 用于實(shí)現(xiàn)自定義的布局 | 稍微復(fù)雜一些,需要合適地處理 ViewGroup 的測(cè)量、布局這兩個(gè)過(guò)程,并同時(shí)處理好子元素的測(cè)量和布局過(guò)程 |
| 3.繼承特定的 View | 用于擴(kuò)展已有的 View 的功能 | 不需要自己支持 wrap_content 和 padding 等 |
| 4.繼承特定的 ViewGroup | 用于實(shí)現(xiàn)幾種 View 組合在一起的效果 | 不要自己處理 ViewGroup 的測(cè)量和布局這兩個(gè)過(guò)程,方法2能實(shí)現(xiàn)的效果一般方法4也可以實(shí)現(xiàn),但方法2更接近 View 底層 |
自定義 View 的注意事項(xiàng)
- 對(duì)于直接繼承
View或者ViewGroup的控件,要在onMeasure方法中對(duì)wrap_content做特殊處理; - 必要時(shí),讓自定義
View支持padding; - 盡量不要在自定義
View中使用Handler,用View自己的post方法就行; - 及時(shí)關(guān)閉自定義
View中的線程或者動(dòng)畫(huà),比如在onDetachedFromWindow方法中; - 自定義
View帶有嵌套滑動(dòng)時(shí),要處理好滑動(dòng)沖突; - 有自定義布局屬性的,在構(gòu)造方法中取得屬性后應(yīng)及時(shí)調(diào)用
recycle方法回收資源; - 在
onDraw和onTouchEvent方法中都應(yīng)避免創(chuàng)建對(duì)象,過(guò)多操作會(huì)造成卡頓; - 自定義
ViewGroup要重載關(guān)于LayoutParams的幾個(gè)方法; - 必要時(shí)添加對(duì)
View的狀態(tài)存儲(chǔ)與恢復(fù)的支持; - 對(duì)于自定義
ViewGroup,需要繪制內(nèi)容但是又沒(méi)有在布局中設(shè)置background的話,會(huì)畫(huà)不出來(lái),這時(shí)候需要調(diào)用setWillNotDraw方法,并設(shè)置為false。
參考:《Android開(kāi)發(fā)藝術(shù)探索》