
ViewRootImpl的performTraversals()
private void performTraversals() {
// cache mView since it is used so much below...
//mView就是DecorView根布局
final View host = mView;
//在Step3 成員變量mAdded賦值為true,因此條件不成立
if (host == null || !mAdded)
return;
//是否正在遍歷
mIsInTraversal = true;
//是否馬上繪制View
mWillDrawSoon = true;
...
//頂層視圖DecorView所需要窗口的寬度和高度
int desiredWindowWidth;
int desiredWindowHeight;
...
//在構(gòu)造方法中mFirst已經(jīng)設(shè)置為true,表示是否是第一次繪制DecorView
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
//如果窗口的類型是有狀態(tài)欄的,那么頂層視圖DecorView所需要窗口的寬度和高度就是除了狀態(tài)欄
if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
|| lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {//否則頂層視圖DecorView所需要窗口的寬度和高度就是整個屏幕的寬高
DisplayMetrics packageMetrics =
mView.getContext().getResources().getDisplayMetrics();
desiredWindowWidth = packageMetrics.widthPixels;
desiredWindowHeight = packageMetrics.heightPixels;
}
}
...
//獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高,lp.widthhe和lp.height表示DecorView根布局寬和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
//執(zhí)行測量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//執(zhí)行布局操作
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//執(zhí)行繪制操作
performDraw();
}
啟動Activity后,視圖添加,繪制。是在ViewRootImpl.setView(),這方法中會丟一個Runnable給Choreographer,在下次Vsync信號來的時候會調(diào)用performTraversals(),然后會調(diào)用relaywindow(),請求WMS關(guān)聯(lián)Java層Surface和native層Surface,然后執(zhí)行View的繪制流程performMeasure(),performLayout(),performDraw()
Measure
MeasureSpec
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
//根據(jù)原有寬高計算獲取不同模式下的具體寬高值
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
...
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
//在該方法中子控件完成具體的測量
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
...
}
MeasureSpec是View的內(nèi)部類,內(nèi)部封裝了View的規(guī)格尺寸,以及View的寬高信息。在Measure的流程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器是施加的規(guī)則轉(zhuǎn)換為MeasureSpec,然后在onMeasure()方法中具體確定控件的寬高信息
MeasureSpec的常量中指定了兩種內(nèi)容,一種為尺寸模式,一種為具體的寬高信息。其中高2位表示尺寸測量模式,低30位表示具體的寬高信息。
①UNSPECIFIED:未指定模式,父容器沒有對當(dāng)前View有任何限制,當(dāng)前View可以任意取尺寸。一般用于系統(tǒng)內(nèi)部的測量
②AT_MOST:最大模式,當(dāng)前尺寸是當(dāng)前View能取的最大尺寸,對應(yīng)于在xml文件中指定控件大小為wrap_content屬性,子View的最終大小是父View指定的大小值,并且子View的大小不能大于這個值
③EXACTLY :精確模式,當(dāng)前的尺寸就是當(dāng)前View應(yīng)該取的尺寸,對應(yīng)于在xml文件中指定控件為match_parent屬性或者是具體的數(shù)值,父容器測量出View所需的具體大小
match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View給我們提供的所有剩余空間,而父View剩余空間是確定的,也就是這個測量模式的整數(shù)里面存放的尺寸。
wrap_content--->AT_MOST。怎么理解:就是我們想要將大小設(shè)置為包裹我們的view內(nèi)容,那么尺寸大小就是父View給我們作為參考的尺寸,只要不超過這個尺寸就可以啦,具體尺寸就根據(jù)我們的需求去設(shè)定。
固定尺寸(如100dp)--->EXACTLY。用戶自己指定了尺寸大小,我們就不用再去干涉了,當(dāng)然是以指定的大小為主啦。
對于每一個View,都持有一個MeasureSpec,MeasureSpec保存了該View的尺寸測量模式以及具體的寬高信息,MeasureSpec受自身的LayoutParams和父容器的MeasureSpec共同影響。

View的Measure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
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;
}
通過上文對MeasureSpec的分析,在這里我們就能明確,getDefaultSize實質(zhì)上就是根據(jù)測繪模式確定子View的具體大小,而對于自定義View而言,子View的寬高信息不僅由自身決定,如果它被包裹在ViewGroup中就需要具體測量得到其精確值。
View的Measure過程中遇到的問題以及解決方案
View 的measure過程和Activity的生命周期方法不是同步執(zhí)行的,因此無法保證Activity執(zhí)行了onCreate、onStart、onResume時某個View已經(jīng)測量完畢了。如果View還沒有測量完畢,那么獲得的寬和高都是0。下面是3種解決該問題的方法:
①Activity/View的onWindowsChanged()方法
onWindowFocusChanged()方法表示 View 已經(jīng)初始化完畢了,寬高已經(jīng)準(zhǔn)備好了,這個時候去獲取是沒問題的。這個方法會被調(diào)用多次,當(dāng)Activity繼續(xù)執(zhí)行或者暫停執(zhí)行的時候,這個方法都會被調(diào)用,代碼如下:
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus){
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
}
②View.post(runnable)方法
通過post將一個 Runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候View也已經(jīng)初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
③ViewTreeObsever
使用 ViewTreeObserver 的眾多回調(diào)方法可以完成這個功能,比如使用onGlobalLayoutListener 接口,當(dāng) View樹的狀態(tài)發(fā)生改變或者View樹內(nèi)部的View的可見性發(fā)生改變時,onGlobalLayout 方法將被回調(diào)。伴隨著View樹的變化,這個方法也會被多次調(diào)用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width=view.getMeasuredWidth();
int height=view.getMeasuredHeight();
}
});
}
Layout

layout 的作用是ViewGroup來確定子元素的位置,當(dāng) ViewGroup 的位置被確定后,在layout中會調(diào)用onLayout ,在onLayout中會遍歷所有的子元素并調(diào)用子元素的 layout 方法。
在代碼中設(shè)置View的成員變量 mLeft,mTop,mRight,mBottom 的值,這幾個值是在屏幕上構(gòu)成矩形區(qū)域的四個坐標(biāo)點,就是該View顯示的位置,不過這里的具體位置都是相對與父視圖的位置而言,而 onLayout 方法則會確定所有子元素位置,ViewGroup在onLayout函數(shù)中通過調(diào)用其children的layout函數(shù)來設(shè)置子視圖相對與父視圖中的位置,具體位置由函數(shù) layout 的參數(shù)決定。下面我們先看View的layout 方法(只展示關(guān)鍵性代碼)如下:
/*
*@param l view 左邊緣相對于父布局左邊緣距離
*@param t view 上邊緣相對于父布局上邊緣位置
*@param r view 右邊緣相對于父布局左邊緣距離
*@param b view 下邊緣相對于父布局上邊緣距離
*/
public void layout(int l, int t, int r, int b) {
...
//記錄 view 原始位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//調(diào)用 setFrame 方法 設(shè)置新的 mLeft、mTop、mBottom、mRight 值,
//設(shè)置 View 本身四個頂點位置
//并返回 changed 用于判斷 view 布局是否改變
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//第二步,如果 view 位置改變那么調(diào)用 onLayout 方法設(shè)置子 view 位置
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調(diào)用 onLayout
onLayout(changed, l, t, r, b);
...
}
...
}
Draw

在View的draw()方法的注釋中,說明了繪制流程中具體每一步的作用,源碼中對于draw()方法的注釋如下,我們在這里重點分析注釋中除第2、第5步外的其他步驟。
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background(繪制背景)
* 2. If necessary, save the canvas' layers to prepare for fading(如果需要的話,保存畫布背景以展示漸變效果)
* 3. Draw view's content(繪制View的內(nèi)容)
* 4. Draw children(繪制子View)
* 5. If necessary, draw the fading edges and restore layers(如果需要的話,繪制漸變邊緣并恢復(fù)畫布圖層。)
* 6. Draw decorations (scrollbars for instance)(繪制裝飾(例如滾動條scrollbar))
*/
視圖重繪
1、requestLayout重新繪制視圖
子View調(diào)用requestLayout方法,會標(biāo)記當(dāng)前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調(diào)用三大流程,從measure開始,對于每一個含有標(biāo)記位的view及其子View都會進行測量、布局、繪制。
2、invalidate在UI線程中重新繪制視圖
當(dāng)子View調(diào)用了invalidate方法后,會為該View添加一個標(biāo)記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區(qū)域,直到傳遞到ViewRootImpl中,最終觸發(fā)performTraversals方法,進行開始View樹重繪流程(只繪制需要重繪的視圖)。
3、postInvalidate在非UI線程中重新繪制視圖
這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI線程中調(diào)用,invalidate則是在UI線程中調(diào)用。
一般來說,如果View確定自身不再適合當(dāng)前區(qū)域,比如說它的LayoutParams發(fā)生了改變,需要父布局對其進行重新測量、擺放、繪制這三個流程,往往使用requestLayout。而invalidate則是刷新當(dāng)前View,使當(dāng)前View進行重繪,不會進行測量、布局流程,因此如果View只需要重繪而不需要測量,布局的時候,使用invalidate方法往往比requestLayout方法更高效。
參考
自定義View心法——View工作流程
自定義View,有這一篇就夠了
Android自定義View全解
教你步步為營掌握自定義View
Android自定義控件,你們是如何系統(tǒng)學(xué)習(xí)的?
安卓自定義View教程目錄