在Android體系中View作為視覺上的呈現(xiàn),扮演著非常重要的角色。盡管Android提供了一套包含很多控件的GUI庫。但是在大多數情況下,因為交互或展現(xiàn)的定制化要求,我們不是不能直接拿來使用的。怎么解決這一問題呢?
為了防止Android應用界面的同類化嚴重,我們需要通過自定義View來實現(xiàn)更友好的用戶體驗和更美觀的呈現(xiàn)效果。
前言
為了更好的自定義View,我們應該掌握View的基本流程,如:View的測量流程、布局流程及繪制流程;
除了三大流程之外,View常見的回調方法也是需要熟練掌握的。
- 構造方法:
基本屬性和自定義屬性的初始化和賦值; - onAttach
- onVisibilityChanged
- onDetach
View的工作原理
ViewRoot對應于ViewRootImpl類,它是連接WindowManger和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。
在ActivityThread中,當Activity對象創(chuàng)建完畢后,會將DecorView添加到Window中,同時創(chuàng)建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立關聯(lián)。參看下面源碼(位于android-25/android/app/ActivityThread.java
):
//該代碼位于handleLaunchActivity->handleResumeActivity方法中
ActivityClientRecord r = mActivities.get(token);
...
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
View的繪制流程是從ViewRoot的performTraversals方法開始的,經過measure、layout和draw三個過程才最終將一個View繪制出來。其中:
- measure:用來測量View的寬和高;
- layout:用來View在父容器放置的位置;
- draw:負責將View繪制在屏幕上;

如圖所示,performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程;其中performMeasure中會調用measure方法,measure方法又會調用onMeasure方法,在onMeasure方法則會對所有的子View進行measure過程,這時measure流程就從父容器傳遞到子元素中去。然后子元素重復父容器的measure過程,如此反復就完成了整個View樹的遍歷,這樣就完成了依次measure過程。另外兩個過程是類似的,不贅述。
Measure過程
measure過程決定了View的寬高,measure完成以后可以通過getMeasureWidth和getMeasureHeight方法來獲取View測量后的寬高,幾乎所有情況下這兩個值都等同于View的最終寬高值,特殊情況下除外(onLayout時強制指定寬高)。
Layout過程
layout過程決定了View的四個頂點的坐標和實際的View寬高。方法完成后,可以通過getTop、getBottom、getLeft、getRight來拿到View四個頂點的位置,并可以通過getWidth、getHeight方法來拿到View最終寬高。
Draw過程
draw過程決定了View的顯示內容,只有draw方法完成之后View的內容才能顯示在屏幕上。
DecorView層級組成
DecorView作為頂級的View,一般情況下它內部會包含一個豎直方向的LinearLayout,這個LinearLayout里面有上下兩部分(具體情況和系統(tǒng)版本及主題有關),上面是標題欄,下面是內容區(qū)。
Activity通過setContentView所設置的布局其實是添加到DecorView的內容區(qū)里面了。而內容區(qū)的id是content,因此可以理解制定布局的方式不叫setView而叫setContentView;確切的說,設置的布局是加在id為content的FrameLayout的布局中。
如圖:

理解MeasureSpec
MeasureSpec通過將高2位的SpecMode和低30位的SpecSize打包成一個32位的int里面來避免過多的內存分配。為了方便操作,其同時提供了打包和解包方法。
SpecMode
UNSPECIFIED(未指明模式)
父容器不對View做任何限制,要多大給多大,這種情況下一般用于系統(tǒng)內部,表示一種測量的狀態(tài)。EXACTLY(準確模式)
父容器已經檢測出View所需要的精確大小,這時View的最終大小就是低30位的SpecSize指定的值。這種情況對應于LayoutParams中的match_parent和具體的數值這兩種模式。AT_MOST(最大模式)
父容器指定了一個可用大小即就是低30位的SpecSize的值,最終View的值不能大于這個值。它對應于LayoutParmas中的wrap_content。
MeasureSpec和LayoutParams的對應關系
Android系統(tǒng)內部是通過MeasureSpec來進行View測量的,但正常情況下我們使用View指定MeasureSpec;同時我們也可以給View設置LayoutParams。在View的測量時,系統(tǒng)會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然后在根據MeasureSpec來確定View測量后的寬高。
- MeasureSpec不是僅由LayoutParams決定的,其需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬高。
- 頂級View(DecorView)的MeasureSepc由窗口的尺寸和其自身的LayoutParams來共同決定。
- 對于普通View,它的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams來共同決定。
- MeasureSpec一旦確定onMeasure中就可以確定View的測量寬、高。
DecorView中對應關系
- LayoutParams.MATCH_PARENT:精確模式大小就是窗口的大小。
- LayoutParams.WRAP_CONTENT:最大模式,大小不確定,但不能超過窗口的大小
- 固定模式(如,100dp):精確模式,大小為LayoutParams指定的大小。
普通View中的對應關系
- 當View采用固定模式寬高的時候,不管父容器的MeasureSpec是什么,View的MeasureSpec是精確模式且其大小為當前View要求的大小。
- 當View寬高采用match_parent時,那么View的MeasureSpec是精確模式且其大小為父容器剩余的空間大??;如果父容器為最大模式,那么View的MeasureSpec是最大模式且其大小不超過父容器剩余的空間大小。
- 當View的寬高采用wrap_content時,不論父容器的模式是精確模式還是最大模式,View的MeasureSpec是最大模式且其大小不超過父容器剩余的空間大小。

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);
}
View的工作流程
View的工作流程主要是指measure(測量)、layout(布局)、draw(繪制)這三大流程。
其中measure確定了View的測量寬高,layout確定了View的最終寬高和四個頂點的位置,draw則將View繪制到屏幕上。
measure過程
measure過程要分兩種情況來分析。如果是原始的View,那么通過measure方法就完成了View的測量過程;如果是一個ViewGroup除了完成自己的測量過程外,還要遍歷調用所有子元素的measure方法,各個子元素再遞歸的執(zhí)行這個流程。
原始View的measure過程
View的measure過程使用measure方法完成的,measure方法是一個final修飾的方法,意味著子類不能重寫此方法,在measure方法中會調用其onMeasure方法。onMeasure方法源碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
onMeasure方法很簡潔,setMeasuredDimension方法會設置View的寬高的測量值。我們同時需要關注getDefaultSize這個方法,這個方法邏輯很簡單在AT_MOST和EXACTLY這兩種情形下,getDefaultSize大小就是MeasureSpec的specSize的值。
而在UNSPECIFIED(位指明模式)的情形下,一般用于系統(tǒng)內部的測量過程,getSuggestedMinimumWidth、getSuggestedMinimumHeight這兩個方法的返回值。源碼如下:
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
這種情形下,可以得出如下結論:
如果View沒有設置背景,那么返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回minWidth和背景的最小寬度、高度之間的最大值。
結論
直接繼承View的自定義空間需要重寫onMeasure方法,并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用了match_parent。
View在布局中使用wrap_content,那么他的specMode為AT_MOST,這種模式下寬高等于specSize,根據之前的表格可以得知,specSize就是parentSize即父容器當前剩余的空間大小;很顯然這種效果等同于布局中使用match_parent。
如何解決這個問題?很簡單,只需要給View指定一個默認的內部寬高,并在wrap_content是設置此寬高即可。代碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_ MOST && heightSpecMode == MeasureSpec.AT_ MOST){
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_ MOST){
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_ MOST){
setMeasuredDimension(widthSpecSize, mHeight);
}
}
查看TextView、ImageView等的源碼可以知道,針對wrap_content的情形,在onMeasure方法中都做了特殊處理,可以自行閱讀查看。
ViewGroup的measure過程
和View不同的是ViewGroup是一個抽象類,因此它沒有重寫onMeasure方法,而是提供了一個叫做measureChildren的方法,如下所示。
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);
}
上述代碼可以看出ViewGroup在measure時,會對每一個子元素進行measure,measureChild方法的實現(xiàn)就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來獲取子元素的MeasureSpec,接著講MeasureSpec傳遞給依次調用子元素的measure方法進行測量。
總結
ViewGroup作為一個抽象類并沒有定義其測量的具體過程,代表測量過程的onMeasure方法需要各個子類進行具體的實現(xiàn),比如LinearLayout、RelativeLayout等。ViewGroup的實現(xiàn)類有不同的布局特性,導致系統(tǒng)無法統(tǒng)一實現(xiàn),因此才決定交給擴展類區(qū)實現(xiàn)。
View的測量過程是三大流程中最復雜的一個,measure完成以后,通過getMeasureWidth/Height方法就可以正確獲取到View的測量寬、高。需要注意的是,極端的情況下,系統(tǒng)可能需要多次調用measure才能最終確定測量寬、高,這種情形下onMeasure拿到的測量寬高可能并不準確。一個比較好的習慣是在onLayout方法中獲取View的測量寬高,或者最終寬高。