本篇作為自定義View的前置章節(jié)
View的繪制流程
View的繪制一般分為三步,分別為Measure,Layout和Draw,即測量,布局和繪制。
View的事件傳遞從父空間的Measure開始,傳遞過程如 圖1 所示。

PS:事實上,View的繪制流程是從ViewRoot的performTraversals方法開始的,Measure,Layout和Draw方法之前還分別有名為performMeasure,performLayout和performDraw的方法,這里從簡,不影響邏輯。
MeasureSpec
說到View的繪制流程,就不能不提到 MeasureSpec 這個概念,MeasureSpec的字面意義是測量規(guī)范,這是View繪制流程中非常重要的一個變量,它影響著View的測量過程。
MeasureSpec的內(nèi)容為一個32位的int值,高兩位代表SpecMode,測量模式,低30位代表SpecSize,為某種測量模式下的規(guī)格大小。SpecMode與SpecSize均可通過對MeasureSpec進行解包獲得。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
...
}
其中,SpecMode有三類,每一種都有其特殊的含義。
- UNSPECIFIED,父容器不對View做任何限制,用于系統(tǒng)內(nèi)部,自己寫控件的時候用不到。
- EXACTLY,View所需大小為一個精確值,這種情況下,View的最終大小就是其SpecSize的值。對應于布局文件中,match_parent和具體數(shù)值這兩種情況。
- AT_MOST,View的大小被限定在一個范圍之內(nèi),其最大值為父容器所能提供的最大值,但其具體大小要看View的具體實現(xiàn)。對應于布局文件中,wrap_content這種情況。
View的MeasureSpec是在父容器的getChildMeasureSpec()方法中生成,之后才會調(diào)用View的Measure方法,將其傳遞給View。
getChildMeasureSpec()方法作用主要是根據(jù)父容器的MeasureSpec,同時結合View本身的LayoutParams來確定View的MeasureSpec,這其中的生成規(guī)律如圖2,有條件的同學推薦去看看源碼。

UNSPECIFIED模式用不到,不做討論。
簡單說明下這個表格,橫軸標題為父容器測量模式,縱軸標題為子View的layoutparams參數(shù),六種不同情況下,他們會為子View生成不同的MeasureSpec。
對于這個表格的理解,我們以布局文件中,layout_width屬性為例說明。layout_height屬性與其類似。
先看表頭:
EXACTLY對應于LayoutParams中的match_parent或者是一個準確的大小。那么我們可以認為,在布局文件中,父容器的寬屬性為100dp。
AT_MOST對用于LayoutParams中的wrap_content,我們認為,此時父容器的寬屬性為wrap_content。
那么這個表格可以這么理解:
- 當子View的寬是一個精確的數(shù)值(dp/px),那么不論父容器是那種測量模式,子View的測量模式都會是精確數(shù)值(EXACTLY)模式,子View的規(guī)格大小都是自身布局文件中所輸入的大小(childSize)。因為它自身的數(shù)值在其布局文件中已經(jīng)確定了。
- 當子View的寬的大小為填充父容器,子View的寬的大小一定會跟父容器相同(parentSize),但是測量模式為會分兩種情況:
- 當子View父容器的寬是精確數(shù)值(EXACTLY)模式時,測量模式為精確數(shù)值(EXACTLY)模式。由于父容器為一個精確數(shù)值(上文的假設100dp),那么子View填充父容器,子View的寬也就是確定的,為父容器的寬的數(shù)值(100dp),那么它的測量模式自然是精確模式。
- 當子控件父容器的寬為限制最大值(AT_MOST)模式時,測量模式同樣為限制最大值(AT_MOST)模式。由于子View的寬填充父容器,但父容器的寬還未確定, 只有一個限制最大值的范圍,所以子控件的寬也無法確定,也只是有一個最大范圍,這個范圍與父容器的范圍相同。
- 這種情況會比較特殊,先要重新明確一個概念,MeasureSpec中,低30位為控件的規(guī)格大小,而非實際大小。當子View的寬為wrap_content時,它的大小是不確定的,所以測量模式為限制最大值(AT_MOST)模式,但是此時,它的規(guī)格大小為父容器的大?。╬arentSize)而非是子容器自身的大小,因為此時子容器的測量過程還未開始,無法計算出這個View實際的大小,而在理論上,這個控件寬度最大可以達到父容器的寬度,所以規(guī)格大小為父容器的大?。╬arentSize)。至于其自身到底有多大,會在子View的onMeasure中進行計算。
PS:上文中,若父容器有內(nèi)邊距,子控件填充時,所得的寬的數(shù)值要將其減去。
View的工作流程
measure過程
View的measure過程
View的measure過程由measure方法調(diào)用onMeasure方法來完成,我們來說說View的measure過程的大致流程。View的各個子類與其流程大致相同。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension這個方法的作用是設置View寬高的測量值,這個不重要,我們需要關心的是getDefaultSize這個方法。
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;
}
簡單來說,getDefaultSize這個方法,返回的就是measureSpec中的specSize即View的測量大小,UNSPECIFIED的情況不需要在意,我們需要關心的是剩余的兩種情況。
從源碼上可以得知,自定義控件直接繼承View時,需要重寫onMeasure方法,并規(guī)定wrap_content時自身的大小。否則在布局中使用wrap_content與使用match_parent是一個效果。這一點結合圖2以及以上源碼很好理解。
在實踐過程中,我們需要給自定義控件指定一個最小寬高值,當布局文件中使用wrap_content屬性時,設置控件為最小寬高,其他情況使用遵循系統(tǒng)測量值即可。如TextView,EdittextView以及其他所有系統(tǒng)控件,他們對wrap_content都做了特殊處理。
自定義控件時候,是一定要處理wrap_content。
ViewGroup的measure過程
對于ViewGroup來說,除了需要measure自身以外,還需要遍歷所有子控件的measure方法。
ViewGroup這個類是一個抽象類,他沒有對onMeasure做具體實現(xiàn)(onMeasure的實現(xiàn)交由子類來完成,如Linearlayout)。但重要的是,他有一個叫做measureChildren的方法,會遍歷每一個子元素,通過子控件的Layoutparams計算出每一個子控件的的measureSpec,再調(diào)用子控件的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];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
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);
}
PS:measureSpec生成規(guī)則見圖2.
上文提到,ViewGroup是一個抽象類,他沒有定義measure的具體過程,這個過程交由他的子類來實現(xiàn),因為ViewGroup的子類有多種多樣如LinearLayout,RelativeLayout,他們的測量過程完全不同,由此無法做統(tǒng)一實現(xiàn)。
例如,LinearLayout的onMeasure會先對所有子控件進行遍歷,得到所有子控件的寬高信息之后,LinearLayout自身的寬高會被測量為所有子控件寬/高的累計的和。RelativeLayout,F(xiàn)rameLayout等均有自己的測量規(guī)則。
View的measure是View三個流程中,最為復雜的一個,measure完成后,就可以通過getMeasureWidth,getMeasureHeigth方法正確的獲得View的測量寬高??傄氖?,只有當measure完全完成后,獲取到的View的寬高才是準確的,而在某些情況中,onMeasure會被執(zhí)行多次,因此不要在onMeasure獲取控件寬高,應在onLayout過程中獲取。
layout過程
layout的作用是ViewGroup用來確定子控件的位置。
當ViewGroup的位置被確定以后,它會便利所有子元素并且調(diào)用其layout方法,在layout中,onLayout方法又會被調(diào)用。
layout方法確定View自身的位置,onLayout方法確定當前View所有子元素的位置。
layout方法大致的流程如下:假設有三個控件控件A,控件B,控件C。A為B的父控件,B為C的父控件。首先會初始化當前控件(B控件)的mLeft、mRight、mTop、mBottom這四個頂點的位置,將這四個值交給當前控件的父控件(A控件),來確定當前控件(B控件)在其父控件(A控件)中的位置。接著,當前控件(B控件)會調(diào)用自身的onLayout方法,來確定當前控件(B控件)的子控件(C控件)的在當前控件(B控件)中的位置。
因為View的子類(如TextView)沒有子控件,所有onLayout方法在View以及他的子類中是一個空函數(shù),并且View的子類大部分情況下,不會對該方法進行實現(xiàn)。同樣的,ViewGroup沒有對onLayout進行實現(xiàn),因為不同的布局實現(xiàn)方式完全不同。
以LinearLayout的Vertical模式為例,onLayout時會遍歷所有子控件,先看代碼
void layoutVertical(int left, int top, int right, int bottom) {
...
final int count = getVirtualChildCount();
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
可以看到,此方法會遍歷所有子控件,并為所有控件指定對應位置,其中childTop會逐漸增大,這樣的話接下來的子控件就會處于比較靠下的位置,即Vertical模式下,LinearLayout的樣子??梢钥吹?,LinearLayout的onLayout方法在確定自身位置后,會通過setChildFrame方法調(diào)用自身子控件的layout方法,來確定子控件的位置,通過這樣一層層傳遞,就完成了整個View樹的layout過程。
PS:網(wǎng)上流傳一句話,自定義ViewGroup只需重寫onMeasure,onLayout。自定義View只需重寫onMeasure,onDraw。我認為是不準確的。雖然自定義View沒有子控件,不需要調(diào)用onLayout來確定子控件的位置,但自定義View若想獲取自身測量寬高并且做出一些處理的話,可以通過重寫onLayout來完成。
draw過程
draw是三個過程中,最簡單的一個步驟了,他的作用是將View繪制在屏幕上。
View的繪制過程是通dispatchDraw方法來實現(xiàn)的,dispatchDraw方法會遍歷所有子控件的draw方法,將事件傳遞下去。
需要在意的是,View有一個特殊方法setWillNotDraw,若一個View沒有需要繪制的內(nèi)容,那么將這個編輯設置為true之后,系統(tǒng)會對其做相應的優(yōu)化,View默認不啟用,但是ViewGroup會默認將其啟用。當我們知道,一個ViewGroup需要通過onDraw來繪制內(nèi)容的時候,需要主動關閉WILL_NOT_DRAW這個標志位。
一些細節(jié)
在Activity中,得到View已經(jīng)被測量完畢的回掉
假設我們需要在Activity啟動的時候,獲取View的狂傲信息,但因為View的測繪流程與Activity的生命周期是一個異步的過程,所以我們無法通過Activity的生命周期來獲取一個View的繪制狀態(tài),因此在Activity的任何一個生命周期狀態(tài)中,我們都不能保證,一定能獲取到View的寬高信息。
所以,我們推薦以下幾種方法。
- onWindowFocusChanged
這個方法在View初始化完成之后會被調(diào)用,但是這個方法不止會被調(diào)用一次,View每次獲取焦點,這個方法都會被調(diào)用。若頻繁的進行onResume,這個方法也會被頻繁調(diào)用。經(jīng)典寫法如下。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
- view.post(runnable)
通過post方法,將一個runnable放在消息隊列的尾部,當Looper調(diào)用次runnable的時候,View肯定已經(jīng)初始化好了。為防止執(zhí)行post的時候,view已經(jīng)加在完畢,所以在onStart方法中添加。
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- ViewTreeObserver
使用ViewTreeObserver的OnGlobalLayoutListener接口便能很好的解決這個問題,當View樹發(fā)生改變時,onGlobalLayout會被回調(diào)。但是伴隨View樹的改變,onGlobalLayout會被回調(diào)多次。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
- view.measure(int widthMeasureSpec, int hightMeasureSpec)
這個方法太復雜,不推薦使用,此處不表。
getWight/getHight與getMeasureWight/getMeasureHight的區(qū)別
我們以其中一對的對比來說明。
在View的默認實現(xiàn)中,getWight與getMeasureWight是相等的,只不過MeasureWight形成于View的measure過程中,而Wight形成于View的layout過程,即,兩者的賦值機制不同。在開發(fā)過程中,我們盡可以認為兩者為相同的。
當然我們可以在layout函數(shù)中強行改變getWight的數(shù)值,使其兩者不相同,但是這么做對于開發(fā)沒有意義。
getX/getY與getRawX/getRawY
getRawX()、getRawY()返回的是觸摸點相對于屏幕左上角的坐標
而getX()、getY()返回的則是觸摸點相對于View左上角的坐標
附:
我自己看源碼的一個網(wǎng)站:
http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/
不用翻墻,雖然代碼老了點,但是用來學習源碼沒什么問題。
參考資料:《Android開發(fā)藝術探索》
個人理解,難免有錯誤紕漏,歡迎指正。轉載請注明出處。