在從Android 6.0源碼的角度剖析Activity的啓動過程和從Android 6.0源碼的角度剖析Window內部機制原理的文章中,我們分別詳細地闡述了一個界面(Activity)從啓動到顯示的整個流程和View是如何添加到Activity的Window中的。本文就在上述兩篇文章基礎上,從源碼的角度剖析View的繪製過程,同時分析在源碼中View的繪製入口在哪裏。
1. View繪製入口分析
在剖析Window內部機制原理中我們曾談到,當調用WindowManager的addView()方法向Window中添加視圖佈局(比如DecorView)時,實際上調用的是WindowManagerGlobal的addView()
方法,該方法首先會創建一個與View綁定ViewRootImpl對象,然後再調用ViewRootImpl的setView()
方法進入執行View繪製流程,但此時並沒有真正開始View的繪製。ViewRootImpl.setView()方法會繼續調用ViewRootImpl的requestLayout()
方法,該方法實現也比較簡單,它首先會通過ViewRootImpl的CheckThread()
方法檢查當前線程是否爲主線程,從而限定了更新View(界面)只能在主線程,子線程更新View會直接報Only the original thread that created a view hierarchy can touch its views.
異常;然後,再將mLayoutRequested
標誌設置爲true並調用ViewRootIpml的scheduleTraversals()
方法,從該方法名中我們可以推測出,此方法將會執行一個Traversals(遍歷)任務。ViewRootIpml.scheduleTraversals()方法源碼如下:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//mTraversalRunnable執行繪製任務
// 最終調用performTraversals
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
果不其然,在ViewRootIpml.scheduleTraversals()方法的源碼中,我們看到它會去調用 mChoreographer.postCallback的方法,並傳入一個mTraversalRunnable
對象。通過跟蹤postCallback方法可知,這個方法最終調用mHandler的sendMessageAtTime方法執行一個異步任務,即mTraversalRunnable,它會去執行渲染View任務。
//ViewRootImpl.TraversalRunnable
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
// ViewRootImpl.doTraversal()方法
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 真正的View繪製入口
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
從mTraversalRunnable對象可知,在它的run()
方法中會調用ViewRootImpl的doTraversal()
方法,這個方法最終調用ViewRootImpl的performTraversals()
方法,從該方法名可推出,它的作用應該是執行遍歷。從之前的 schedule traversals到perform traversals,也就是說,很可能performTraversals()方法就是View繪製的真正入口
。接下來,就來看下這個方法的源碼,看下我們的推測是否正確。
//ViewRootImpl.performTraversals()方法
private void performTraversals() {
// cache mView since it is used so much below...
// 緩存mView
final View host = mView;
//...
if (!mStopped || mReportNextDraw) {
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// 獲取子View的測量規範
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
// view的測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
//...
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
//View的佈局
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
}
//...
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
//View的繪製
performDraw();
}
}
}
從上述源碼可知,performTraversals確實是View繪製的入口,且它會依次去調用performMeasure()
、performLayout()
和performDraw()
方法,這些方法分別會去調用View的measure()
、layout()
和draw()
方法,從而實現View的測量、佈局以及繪製過程。另外,由於Android的界面是層級式的,即由多個View疊起來呈現的,類似於數據結構中的樹結構,對於這種視圖結構我們稱之爲視圖樹。因此,對於一個界面的繪製,肯定是遍歷去繪製視圖樹中的View,這也正解釋了入口方法爲什麼稱爲“執行遍歷(performTraversals)”!
ViewRootImpl是View中的最高層級,屬於所有View的根,但ViewRootImpl不是View只是實現了ViewParent接口,它實現了View和WindowManager之間的通信協議,實現的具體細節在WindowManagerGlobal這個類當中。View的繪製流程就是ViewRootImpl發起的。
Activity啓動過程流程圖:
2. View繪製過程分析
從上一節的講解我們知道,View的繪製過程主要經歷三個階段,即測量(Measure
)、佈局(Layout
)、繪製(Draw
),其中,Measure的作用是測量要繪製View的大小,通過調用View.onMeasure()
方法實現;Layout的作用是明確要繪製View的具體位置,通過調用View.onDraw()
方法實現;Draw的作用就是繪製View,通過調用View.dispatchDraw()
方法實現。
2.1 measure過程
(1) View的measure過程
View的measure過程是從ViewRootImpl的performMeasure
方法開始的。performMeasure()方法實現非常簡單,就是去調用View的measure
方法,而這個方法再會繼續調用View的onMeasure
方法,來實現對View的測量。相關源碼如下:
//ViewRootImpl.performMeasure()方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// 執行mView的measure方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
// view.measure()方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) ==
PFLAG_FORCE_LAYOUT ? -1 :mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 調用View.onMeasure()
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
}
// view.onMeasure()方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// getSuggestedMinimumWidth方法會根據View是否有背景判斷
// 如果mBackground == null,則返回android:minWidth屬性值
// 否則,選取mBackground的getMinimumWidth()和android:minWidth屬性值最小值
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
接下來,我們進一步分析View得onMeasure()方法中,是怎麼對View進行測量的。從上述源碼可知,該方法實現非常簡單,主要是通過調用View.getDefaultSize()
方法來獲得最終的測量值,然後調用View.setMeasuredDimension()
方法進行設定。
首先,我們來看下getDefaultSize是如何獲得最終的測量值的。從getDefaultSize
源碼可知,該方法需要傳入兩個參數值,其中,第一個參數表示View可能被設的size,由View.getSuggestedMinimumWidth()
獲得,通過查看該方法源碼可知,它會根據當前View是否有BackGround,如果爲空則返回當前View的android:minWidth
或android:height
屬性值,否則,取android:minWidth或android:height
和BackGround.getMinimumWidth()或BackGround.getMinimumHeight()
的最大值;第二個參數是一個MeasureSpec,分爲是該View的widthMeasureSpec和heightMeasureSpec,它是衡量View的width、height測量規格,接下來我們會詳講。這裏我們只需要知道,從某些程度來說,MeasureSpec是一個int類型數值,佔32位,它的高2位表示測量的模式(UNSPECIFIED、AT_MOST、EXACTLY
),低30爲表示測量的大小。getDefaultSize()會根據View的測量模式specMode
來確定View的測量大小SpecSize
,其中,specSize由View本身LayoutParams和父容器的MeasureSpec共同決定,它可能是一個精確的數值,也可能是父容器的大小。具體操作如下所示:
- specMode=MeasureSpec.UNSPECIFIED,該模式下表示View的width、height要多大就有多大,通常用於系統內部,在該模式下測量的值爲getSuggestedMinimumWidth()或getSuggestedMinimumHeight()獲得的值;
- specMode=MeasureSpec.AT_MOST,該模式表示View的width、height儘可能的大,但是不能超過父容器的大小,它對應於
android:layout_width=wrap_content
或android:layout_height=wrap_content
屬性值,在該模式下測量的值爲specSize,且默認爲父容器的大小。基於此,當我們自定義View時,如果希望設置wrap_content屬性能夠獲得一個較爲合理的尺寸值,就必須重寫onMeasure方法來處理MeasureSpec.AT_MOST情況。 - specMode=MeasureSpec.EXACTLY,該模式下表示View的width、height是一個精確的值,它對應於精確的數值或者
match_parant
,在該模式下測量的值爲specSize;
View.getDefaultSize()源碼如下:
/*view的getDefaultSize方法*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//View的測量尺寸
int specMode = MeasureSpec.getMode(measureSpec);
//View的測量大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// UNSPECIFIED模式
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// AT_MOST模式和EXACTLY模式
// specSize可能是一個精確的數值,也可能是父容器的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
/*view的getSuggestedMinimumWidth方法*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
其次,我們再繼續分析onMeasure是如何設定View的width和height大小的。該方法會調用setMeasuredDimension
,setMeasuredDimension方法代碼很簡單,我們直接看最後一句setMeasuredDimensionRaw(measuredWidth, measuredHeight)
,這個方法最終完成將View的測量尺寸緩存到mMeasuredWidth和 mMeasuredHeight 字段,並將標誌設置爲已完成View測量工作。至此,通常情況下,我們應該可以通過View的getMeasuredWidth方法和getMeasureHeight獲取View的大小,雖然View的大小最終在onLayout方法執行完畢後才能確定,但是幾乎是一樣的。
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;
}
// 緩存View的widht和height尺寸大小
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
// 將尺寸大小緩存
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// 設置測量標誌,說明已經測量完畢
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
MeasureSpec和View的measureSpec獲取
前面說到,MeasureSpec是衡量View尺寸的測量規格,從計算上來說,它是一個int類型數值,佔32位,它的高2位表示測量的模式(UNSPECIFIED、AT_MOST、EXACTLY
),低30爲表示測量的大小。但是從源碼角度來說,它是View的一個內部類,提供了打包/解包MeasureSpec的方法。MeasureSpec類源碼如下:
// View.MeasureSpec內部類
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
// 11 位左移 30 位
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED模式
// 通常只有系統內部使用
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY模式
// child大小被限制,值爲精確值
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST模式
// child要多大有多大,最大不超過父容器的大小
public static final int AT_MOST = 2 << MODE_SHIFT;
// 構造measureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 提取measureSpec模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 提取measureSpec大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
}
使用MeasureSpec來測量頂層View和普通View稍微有點不同,但是測量的原理都是一樣的,即View的MeasureSpec創建受父容器和本身LayoutParams的影響,在測量的過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來測量出View的寬高。其中,這個父容器對於頂層View來說就是Window,對於普通View來說,就是ViewGroup。
使用MeasureSpec測量View的寬高在上面我們已經分析過來你,下面我們着重分析下對於頂層View和普通View是如何分別構建自己的MeasureSpec的。
- 頂層View
DecorView是Window的最頂層視圖,那麼,對DecorView的寬高的測量,會受本身LayoutParams和Window的影響。從ViewRootImpl的performTraversals()方法中,我們可以看到DecorView的寬高MeasureSpec的創建由getRootMeasureSpec()
方法實現,該方法需要傳入兩個參數,即mWidth/mHeight和lp.width/lp.height,其中,前者爲Window的寬高,後者爲DecorView的LayoutParams屬性值。
// ViewRootImpl.performTraversals()
WindowManager.LayoutParams lp = mWindowAttributes;
private void performTraversals() {
if (!mStopped || mReportNextDraw) {
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// 構造DecorView的widthMeasureSpec、heightMeasureSpec
// lp.width、lp.height分別是DecorView自身LayoutParams的屬性值
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
// 測量DecorView的寬高
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
....
}
// ViewRootImpl.getRootMeasureSpec()
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize,
MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize,
MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,
MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
接下來,我們分析構建DecorView的MeasureSpec過程。在ViewRootImpl.getRootMeasureSpec()中,根據DecorView的LayoutParams的width或height屬性值判斷,如果是MATCH_PARENT
,則DecorView的specSize爲window的尺寸且specMode設置爲MeasureSpec.EXACTLY
;如果是WRAP_CONTENT
,則DecorView的specSize爲window的尺寸且specMode設置爲MeasureSpec.AT_MOST
(這裏就證明了我們上面說的結論,當View的specMode爲AT_MOST時,specSize=父容器尺寸
);其他情況,則DecorView的specSize爲一個精確值=lp.width或lp.height,且specMode設置爲MeasureSpec.EXACTLY
。
- 普通View
對於普通View來說,它的父容器是ViewGroup,構建普通View的measureSpec是通過ViewGroup的measureChildWithMargins
方法實現的。在該方法中又調用了ViewGroup的getChildMeasureSpec
方法,這個方法接收三個參數,即父容器的parentWidthMeasureSpec、父容器的padding屬性值+子View的margin屬性值(left+right)以及子View的LayoutParams的width屬性值。(同理height)
// ViewGroup.measureChildWithMargins()方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 構建子View寬高的measureSpec
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);
// 調用子View的measure,測量它的尺寸(寬高)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// ViewGroup.getChildMeasureSpec()方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 獲得父容器的specMode、specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 獲得父容器specSize除去padding部分的空間大小
// 如果specSize - padding<=0,則取0
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
// 根據父容器的specMode
// 分情況構造子View的measureSpec
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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
從getChildMeasureSpec源碼可知,它首先會去獲取父容器measureSpec中的specMode和specSize,並計算父容器的specSize除去自身padding和子View的margin屬性值後的剩餘空間size;然後再根據父容器的specMode和子View的LayoutParams的width或height屬性值來最終確定子View的measureSpec。具體如下:
-
父容器specMode=MeasureSpec.EXACTLY,說明父容器的尺寸是確定的
(1) 如果childDimension >= 0,說明子View的尺寸是一個確切的數值,它的測量模式爲MeasureSpec.EXACTLY。
childSpecSize=childDimension,childSpecMode=MeasureSpec.EXACTLY
(2) 如果childDimension == LayoutParams.MATCH_PARENT,說明子View的尺寸是父容器尺寸的大小,但是需要注意的是,受子View本身margin參數和父容器padding參數的影響,這裏的尺寸是減去這兩個剩餘的空間。當然,儘管如此,它仍然是一個確切的值,它的測量模式爲MeasureSpec.EXACTLY
。 childSpecSize=size,childSpecMode=
MeasureSpec.EXACTLY
(3) 如果childDimension == LayoutParams.WRAP_CONTENT,說明子View的尺寸是不確定的,即儘可能的大,但是不能超過父容器的剩餘空間,它的測量模式爲MeasureSpec.AT_MOST。
childSpecSize=size,childSpecMode=MeasureSpec.AT_MOST
-
父容器specMode=MeasureSpec.AT_MOST,說明父容器的尺寸是不確定的
(1) 如果childDimension >= 0,說明子View的尺寸是一個確切的數值,它的測量模式爲MeasureSpec.EXACTLY。
childSpecSize=childDimension,childSpecMode=MeasureSpec.EXACTLY
(2) 如果childDimension == LayoutParams.MATCH_PARENT,說明子View的尺寸是父容器尺寸的大小,但是需要注意的是,受子View本身margin參數和父容器padding參數的影響,這裏的尺寸是減去這兩個剩餘的空間。然而,由於父容器的測量模式爲MeasureSpec.AT_MOST,導致父容器的尺寸不確定,從而導致子View尺寸的不確定,此時子View的測量模式爲MeasureSpec.AT_MOST。
childSpecSize=size,childSpecMode=
MeasureSpec.AT_MOST
(3) 如果childDimension == LayoutParams.WRAP_CONTENT,說明子View的尺寸是不確定的,即儘可能的大,但是不能超過父容器的剩餘空間,它的測量模式爲MeasureSpec.AT_MOST。
childSpecSize=size,childSpecMode=MeasureSpec.AT_MOST
-
父容器specMode=MeasureSpec.UNSPECIFIED,僅供系統內部使用,這裏就不說了。
注:childDimension即爲子View的lp.width或lp.height數值,可爲精確的數值、WRAP_CONTENT以及MATCH_PARENT。上述描述的尺寸,泛指普通View的寬或高。
2. ViewGroup的measure過程
ViewGroup繼承於View,是用於裝載多個子View的容器,由於它是一個抽象類,不同的視圖容器表現風格有所區別,因此,ViewGroup並沒有重寫View的onMeasure方法來測量ViewGroup的大小,而是將其具體的測量任務交給它的子類,以便子類實現其特有的功能屬性。
我們以常見的LinearLayout容器爲例,通過查閱它的源碼,可以知道LinearLayout繼承於ViewGroup,並且重寫了View的onMeasure()方法用來測量LinearLayout的尺寸(ViewGroup繼承於View
),該方法需要傳入widthMeasureSpec
和heightMeasureSpec
,從前面的分析可知,這兩個參數是測量LinearLayout尺寸的MeasureSpec,由其自身的LayoutParams和父容器計算得出。LinearLayout.onMeasure()源碼如下:
// LinearLayout.onMeasure()方法
// widthMeasureSpec和heightMeasureSpec是LinearLayout的測量規格
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
// 垂直佈局
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
// 水平佈局
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
由於LinearLayout垂直佈局和水平佈局測量邏輯大致一樣的,只是處理方式不同,這裏就以垂直佈局爲例進行分析,入口爲measureVertical方法。measureVertical方法
主要做了兩部分工作,即測量所有子View的大小和測量LinearLayout自身的大小
,其中,在測量所有子View的過程中,會不斷對子View的高度及其marginTop和marginBottom進行累加,用於計算存放所有子View時LinearLayout需要的長度mTotalLength;在測量LinearLayout自身大小時,mTotalLength還需計算自身的mPaddingTop和mPaddingBottom。也就是說,如果垂直方向放置所有的子View並全部顯示出來,LinearLayout所需的長度應爲所有子View高度 + 所有子View的marginTop和marginBottom + LinearLayout自身的mPaddingTop和mPaddingBottom。
(這裏忽略mDividerHeight)
// LinearLayout.measureVertical()方法
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// LinearLayout總長
mTotalLength = 0;
...
// 所有View的總寬度
int maxWidth = 0;
// 權重比總數
float totalWeight = 0;
// 獲取子View的數量
final int count = getVirtualChildCount();
// 獲取LinearLayout測量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
...
// See how tall everyone is. Also remember max width.
// 遍歷所有子View,對每個View進行測量
// 同時對所有子View的大小進行累加
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
// 如果View可見屬性設置爲GONE,不進行測量
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
// 如果子View之間設置了分割線,是需要計算的
// 由LinearLayout的divider屬性或setDrawableDivider獲得
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
// 獲取子View的LauyoutParams
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
// 累加權重比
totalWeight += lp.weight;
// 如果View的高爲0,只計算topMargin和bottomMargin佔用的空間
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
// 測量子View大小
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
...
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 獲取LinearLayout的最終長度
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// 計算heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
...
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 測量自己的大小
// 調用View的resolveSizeAndState方法獲取最終的測量值
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
從上面源碼可以看出,measureVertical將各種情況計算完畢後,會調用resolveSizeAndState方法
來決定LinearLayout自身的測量大小,然後,調用LinearLayout的setMeasuredDimension方法設定測量大小。
首先,我們通過分析resolveSizeAndState()
,看它是如何計算LinearLayout最終的測量大小。首先,該方法會去獲取LinearLayout的measureSpec中的specMode和specSize,然後根據specMode最終測量模式得到測量的大小數值。
- 當
specMode=MeasureSpec.AT_MOST
時,說明測量尺寸會盡可能的大,但仍然不能超過它的父容器的剩餘空間。因此,如果specSize(父容器剩餘空間)小於計算的size(所有子View高度/寬度等的總和)時,測量的尺寸應取specSize範圍內;否則,將測量尺寸直接設置爲計算的size(所有子View高度/寬度等的總和)。 - 當
specMode=MeasureSpec.EXACTLY
時,說明測量尺寸是一個精確的值,即爲specSize(父容器剩餘空間或精確值)。 - 當
specMode=MeasureSpec.UNSPECIFIED
時,這種測量模型通常內部使用,這裏不討論。
LinearLayout.resolveSizeAndState()方法源碼:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
// 獲取LinearLayout測量模式
final int specMode = MeasureSpec.getMode(measureSpec);
// 獲取LinearLayout測量大小
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
// 注:size爲heightSize或maxWidth,根據所有子View計算得來
// public static final int MEASURED_STATE_TOO_SMALL = 0x01000000;
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
2.3 Layout過程
根據ViewRootImpl的performTravels執行流程,當measure過程執行完畢後,接下來就是通過performLayout入口執行Layout過程。由於Layout過程的作用是ViewGroup用來確定子元素的位置
,只有當ViewGroup的位置被確定後,才它會在onLayout方法中遍歷所有子View並調用其layout方法,在layout方法中onLayout方法又會被調用。layout方法用於確定View本身的位置,onLayout方法則會確定所有子View的位置。似乎趕緊有點迷糊,那就直接上源碼吧,我們從ViewRootImpl.performLayout開始。源碼如下:
// ViewRootImpl.performLayout()方法
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
// 調用view的layout方法
// 傳入四個參數:left、top、right、bottom
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
// View.layout()方法
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// setFrame確定View本身的位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 調用自身onLayout
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
從上述源碼可知,在ViewRootImpl的performLayout
方法中,它會調用View的layout
方法,並向其傳入四個參數,這四個參數就是View的四個頂點的放置位置,其中,getMeasuredWidth()和getMeasuredHeight()就是我們在上一小節測量的尺寸值。在View的layout方法中,它首先會去調用setFrame
方法來實現確定View本身的放置位置,即mLeft/mTop/mRight/mBottom。然後,再調用onLayout
方法確定所有子View的放置位置。通過查看源碼發現,View和ViewGroup均沒有實現onLayout方法,其中,ViewGroup中onLayout爲抽象方法,也就是說,如果我們自定義一個ViewGroup,就必須要重寫onLayout方法,以便確定子元素的位置。
因此,同onMeasure一樣,ViewGroup的onLayout會根據具體的情況不同而不同,這裏我們仍然以LinearLayout舉例。LinearLayout.onLayout()源碼如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
// 垂直方向
layoutVertical(l, t, r, b);
} else {
// 水平方向
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
int childTop;
int childLeft;
...
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);
// 處理子元素的Gravity屬性
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);
}
}
}
從LinearLayout的layoutVertical()源碼可知,它回去遍歷自己所有的子元素,並調用setChildFrame
方法爲子元素指定對應的位置,其中childTop會逐漸增大,這就意味着後面的子元素會被放置在靠下的位置,即符合豎直方向的LnearLayout特性。至於setChildFrame,它僅僅是調用子元素的layout方法而已,這樣父元素在layout方法中完成自己的定位後,就通過onLayout方法去調用子元素的layout方法,子元素又會通過自己的layout方法來確定自己的位置,這樣一層一層的傳遞下去就完成了整個View樹的Layout過程。
private void setChildFrame(View child, int left, int top, int width, int height) {
// 設置子View位置
// 其中,width和height爲測量得到的大小值
child.layout(left, top, left + width, top + height);
}
2.4 Draw過程
根據ViewRootImpl的performTravels執行流程,當Layout過程執行完畢後,接下來就是通過performDraw
入口執行Draw過程,實現對View的繪製。相對於Measure、Layout過程而言,Draw過程就簡單很多了,通過之前的分析,在ViewRootImpl的performDraw方法中,它會去調用 自身的draw
方法,進而調用drawSoftware
,最終再該方法中調用View的draw方法完成最終的繪製。接下來,我們直接看View.draw()
方法完成了哪些工作。
@CallSuper
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
...
}
從上述源碼可知,draw過程主要遵循以下幾個步驟:
- 繪製背景(drawBackground)
private void drawBackground(Canvas canvas) {
// 判斷背景是否存在
final Drawable background = mBackground;
if (background == null) {
return;
}
// 設置背景邊界
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
// 繪製背景,調用Drawable的draw方法實現
// 該方法是一個抽象方法
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
- 繪製自己(onDraw)
// View.onDraw()
// 該方法是一個空方法,由View的子類實現
// 具體的繪製工作,假如我們自定義View,就需要重新該方法
protected void onDraw(Canvas canvas) {
}
- 繪製Children(dispatchDraw)
// View.dispatchDraw()
// 該方法是一個空方法,由View的子類實現,比如ViewGroup
// View繪製過程的傳遞就是通過該方法實現,它會遍歷調用所有子元素的draw方法
protected void dispatchDraw(Canvas canvas) {
}
- 繪製裝飾(onDrawForeground)
至此,對View的繪製原理分析就告一段落了。最後,我們再看下View的requestLayout
、invalidate
和postInvalidate
的區別,因爲在實際開發中,我們經常會用到這三個方法。requestLayout的作用是請求父佈局對重新其進行重新測量、佈局、繪製這三個流程,比如當View的LayoutParams發生改變時;invalidate的作用是刷新當前View,使當前View進行重繪,不會進行測量和佈局;postInvalidate與invalidate的作用一樣,都是使View樹重繪,但是該方法是在非UI線程中調用的,而invalidate是在UI線程中調用的。