接上篇 繪制優(yōu)化-原理篇2-DecorView布局加載流程 講到的ViewRootImpl衷敌,在ViewRootImpl的setView()方法里主要做兩件事:
1.執(zhí)行requestLayout()方法完成view的繪制流程
2.通過WindowSession將View和InputChannel添加到WmS中草冈,從而將View添加到Window上并且接收觸摸事件乃摹。
2的部分 window加載視圖已經(jīng)介紹了术奖,那么今天就來講講1的部分:執(zhí)行requestLayout()方法完成view的繪制流程
//ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
//在 Window add之前調(diào)用现柠,確保 UI 布局繪制完成 --> measure , layout , draw
requestLayout();//View的繪制流程
...
//通過WindowSession進(jìn)行IPC調(diào)用裆赵,將View添加到Window上
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
...
}
一逢并、從requestLayout開始
從requestLayout代碼一層層往下追(具體源碼不貼了,非常簡單)郭卫,最終確認(rèn)view的繪制流程是從performTraversals開始筒狠。順一下整個流程:
1.1 performTraversals
private void performTraversals() {
//獲得view寬高的測量規(guī)格,mWidth和mHeight表示窗口的寬高箱沦,lp.width和lp.height表示DecorView根布局寬和高
WindowManager.LayoutParams lp = mWindowAttributes;
...
//頂層視圖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;
}
}
...
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, mWidth, mHeight);
...
//執(zhí)行繪制操作
performDraw();
}
performTraversals()中做了非常多的處理谓形,代碼接近800行,這里我們重點(diǎn)關(guān)注繪制相關(guān)流程疆前。
1.2 MeasureSpec
在分析繪制過程之前寒跳,我們需要先了解MeasureSpec,它是干什么的呢竹椒?簡而言之童太,MeasureSpec 是View的尺寸一種封裝手段。
MeasureSpec代表一個32位int值胸完,高2位代表SpecMode,低30位代表SepcSize. 這樣的打包方式好處是避免過多的對象內(nèi)存分配书释。為了方便操作,其提供了打包和解包的方法:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
...
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
...
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK); //高2位運(yùn)算
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);//低30位運(yùn)算
}
}
getMode方法中ModeMask 為 0x3 << 30 轉(zhuǎn)換成二進(jìn)制為 0011 << 30 赊窥,也就是向左移動30位 則 ModeMask高兩位為1爆惧,低三十位為0,整形measureSpec 為32位锨能, measureSpec & mode_mask 就是高2位的運(yùn)算扯再,getSize方法中 ~Mode_MASK 則是除了高兩位為0 外剩下的低30位均為 1,那么和measure 進(jìn)行 & 運(yùn)算就是在求得低30為中存儲的值址遇。
SpecMode:測量模式
模式 | 描述 |
---|---|
UNSPECIFIED | 父容器不作限制熄阻,一般用于系統(tǒng)內(nèi)部 |
EXACTLY | 精確模式,大小為SpecSize倔约,對應(yīng)LayoutParams中的match_parent或者具體數(shù)值 |
AT_MOST | 最大模式秃殉,大小不能大于SpecSize,對應(yīng)于LayoutParams中的warp_content |
SpecSize:對應(yīng)某種測量模式下的尺寸大小
下面針對DecorView和普通View分別來看看其MeasureSpec的組成:
//ViewRootImpl
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同決定跺株,
}
//ViewGroup
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
普通View复濒,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。
}
對普通View的MeasureSpec的創(chuàng)建規(guī)則進(jìn)行總結(jié):
這個表怎么用呢乒省?舉個例子:
如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,這種模式下巧颈,它的寬高為specSize, 而查表可得View的specSize是parentSize,而parentSize是當(dāng)前父容器剩余空間大小袖扛,這種效果和在布局中使用match_parent完全一致砸泛,所以如果是對尺寸有具體要求的自定義控件需要指定specSize大小十籍。
注:
LayoutParams類是用于子視圖向父視圖傳達(dá)自己尺寸意愿的一個參數(shù)包,包含了Layout的高唇礁、寬信息勾栗。LayoutParams在LayoutInflater.inflater過程中與View一起被解析成對象,保存在WindowManagerGlobal集合中盏筐。
二围俘、View繪制流程
performTraversals里面執(zhí)行了三個方法,分別是performMeasure()琢融、performLayout()界牡、performDraw()這三個方法,這三個方法分別完成DecorView的measure漾抬、layout宿亡、和draw這三大流程,其中performMeasure()中會調(diào)用measure()方法纳令,在measure()方法中又會調(diào)用onMeasure()方法挽荠,在onMeasure()方法中會對所有子元素進(jìn)行measure過程,這個時候measure流程就從父容器傳遞到子元素中了平绩,這樣就完成了一次measure過程圈匆。接著子元素會重復(fù)父容器的measure過程,如此反復(fù)就實(shí)現(xiàn)了從DecorView開始對整個View樹的遍歷測量捏雌,measure過程就這樣完成了臭脓。同理,performLayout()和performDraw()也是類似的傳遞流程腹忽。針對performTraveals()的大致流程来累,可以用以下流程圖來表示:
以上的流程圖只是一個為了便于理解而簡化版的流程,真正的流程應(yīng)該分為以下五個工作階段:
預(yù)測量階段:這是進(jìn)入performTraversals()方法后的第一個階段窘奏,它會對View樹進(jìn)行第一次測量嘹锁。在此階段中將會計算出View樹為顯示其內(nèi)容所需的尺寸,即期望的窗口尺寸着裹。(調(diào)用measureHierarchy())
窗口布局階段:根據(jù)預(yù)測量的結(jié)果领猾,通過IWindowSession.relayout()方法向WMS請求調(diào)整窗口的尺寸等屬性,這將引發(fā)WMS對窗口進(jìn)行重新布局骇扇,并將布局結(jié)果返回給ViewRootImpl摔竿。(調(diào)用relayoutWindow())
測量階段:預(yù)測量的結(jié)果是View樹所期望的窗口尺寸。然而由于在WMS中影響窗口布局的因素很多少孝,WMS不一定會將窗口準(zhǔn)確地布局為View樹所要求的尺寸继低,而迫于WMS作為系統(tǒng)服務(wù)的強(qiáng)勢地位,View樹不得不接受WMS的布局結(jié)果稍走。因此在這一階段袁翁,performTraversals()將以窗口的實(shí)際尺寸對View樹進(jìn)行最終測量柴底。(調(diào)用performMeasure())
布局階段:完成最終測量之后便可以對View樹進(jìn)行布局了。(調(diào)用performLayout())
繪制階段:這是performTraversals()的最終階段粱胜。確定了控件的位置與尺寸后柄驻,便可以對View樹進(jìn)行繪制了。(調(diào)用performDraw())
下面分別來闡述:
2.1 預(yù)測量階段(performTraversals())
這個階段在performTraversals中最先發(fā)生焙压,對View樹進(jìn)行第一次測量鸿脓,會判斷當(dāng)前期望窗口尺寸是否能滿足布局要求。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
// 表示測量結(jié)果是否可能導(dǎo)致窗口的尺寸發(fā)生變化
boolean windowSizeMayChange = false;
//goodMeasure表示了測量是否能滿足View樹充分顯示內(nèi)容的要求
boolean goodMeasure = false;
//測量協(xié)商僅發(fā)生在LayoutParams.width被指定為WRAP_CONTENT的情況下
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
//第一次協(xié)商涯曲。measureHierarchy()使用它最期望的寬度限制進(jìn)行測量答憔。
//這一寬度限制定義為一個系統(tǒng)資源。
//可以在frameworks/base/core/res/res/values/config.xml找到它的定義
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
// 寬度限制被存放在baseSize中
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//第一次測量。調(diào)用performMeasure()進(jìn)行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//View樹的測量結(jié)果可以通過mView的getmeasuredWidthAndState()方法獲取。
//View樹對這個測量結(jié)果不滿意丈冬,則會在返回值中添加MEASURED_STATE_TOO_SMALL位
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true; // 控件樹對測量結(jié)果滿意回官,測量完成
} else {
//第二次協(xié)商。上次的測量結(jié)果表明View樹認(rèn)為measureHierarchy()給予的寬度太小城榛,在此
//在此適當(dāng)?shù)胤艑拰挾鹊南拗凭纠褂米畲髮挾扰c期望寬度的中間值作為寬度限制
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
//第二次測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 再次檢查控件樹是否滿足此次測量
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
// 控件樹對測量結(jié)果滿意,測量完成
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
//最終測量狠持。當(dāng)View樹對上述兩次協(xié)商的結(jié)果都不滿意時疟位,measureHierarchy()放棄所有限制
//做最終測量。這一次將不再檢查控件樹是否滿意了喘垂,因?yàn)榧幢闫洳粷M意甜刻,measurehierarchy()也沒
//有更多的空間供其使用了
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//如果測量結(jié)果與ViewRootImpl中當(dāng)前的窗口尺寸不一致,則表明隨后可能有必要進(jìn)行窗口尺寸的調(diào)整
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
// 返回窗口尺寸是否可能需要發(fā)生變化
return windowSizeMayChange;
}
measureHierarchy()方法最終也是調(diào)用了performMeasure()方法對View樹進(jìn)行測量正勒,只是多了協(xié)商測量的過程得院。
2.2 窗口布局階段(relayoutWindow())
調(diào)用relayoutWindow()來請求WindowManagerService服務(wù)計算Activity窗口的大小以及過掃描區(qū)域邊襯大小和可見區(qū)域邊襯大小。計算完畢之后章贞,Activity窗口的大小就會保存在成員變量mWinFrame中祥绞,而Activity窗口的內(nèi)容區(qū)域邊襯大小和可見區(qū)域邊襯大小分別保存在ViewRoot類的成員變量mPendingOverscanInsets和mPendingVisibleInsets中。
這部分不在這細(xì)講了鸭限,有耐心的可以把老羅的文章看完:Android窗口管理服務(wù)WindowManagerService計算Activity窗口大小的過程分析
2.3 測量過程(performMeasure())
WMS的布局結(jié)果已經(jīng)確定了蜕径,不管是否滿意都得開始終極布局過程了,下面介紹下measure:
measure是對View進(jìn)程測量败京,確定各View的尺寸的過程,這個過程分View和ViewGroup兩種情況來看兜喻,對于View,通過measure完成自身的測量就行了赡麦,而ViewGroup除了完成自身的測量外虹统,還需要遍歷去調(diào)用所有子view的measure方法弓坞,各個子view遞歸去執(zhí)行這個過程。
那么先從performMeasure開始:
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);
}
}
這里的mView是 ViewRootImpl setView傳進(jìn)來的rootView.
接下來我們看下View的measure()方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//根據(jù)widthMeasureSpec和heightMeasureSpec計算key值车荔,在下面用key值作為鍵渡冻,緩存我們測量得到的結(jié)果
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
…
//forceLayout 是通過上次的mPrivateFlags標(biāo)記位來判斷這次是否需要觸發(fā)重繪
//View中有個forceLayout()方法可以設(shè)置mPrivateFlags.
// needsLayout 簡單看就是spec發(fā)生了某些規(guī)則約束下的變化
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
//在View真正進(jìn)行測量之前,View還想進(jìn)一步確認(rèn)能不能從已有的緩存mMeasureCache中讀取緩存過的測量結(jié)果 //如果是強(qiáng)制layout導(dǎo)致的測量忧便,那么將cacheIndex設(shè)置為-1族吻,即不從緩存中讀取測量結(jié)果 //如果不是強(qiáng)制layout導(dǎo)致的測量,那么我們就用上面根據(jù)measureSpec計算出來的key值作為緩存索引cacheIndex,這時候有可能找到相應(yīng)的值,找到就返回對應(yīng)索引;也可能找不到,找不到就返回-1
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//在緩存中找不到相應(yīng)的值或者需要忽略緩存結(jié)果的時候,重新測量一次 //此處調(diào)用onMeasure方法珠增,并把尺寸限 制條件widthMeasureSpec和heightMeasureSpec傳入進(jìn)去 //onMeasure方法中將會進(jìn)行實(shí)際的測量工作超歌,并把測量的結(jié)果保存到成員變量中
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
//如果運(yùn)行到此處,那么表示當(dāng)前的條件允許View從緩存成員變量mMeasureCache中讀取測量過的結(jié)果
//用上面得到的cacheIndex從緩存mMeasureCache中取出值蒂教,不必在調(diào)用onMeasure方法進(jìn)行測量了
long value = mMeasureCache.valueAt(cacheIndex);
//一旦我們從緩存中讀到值巍举,我們就可以調(diào)用setMeasuredDimensionRaw方法將當(dāng)前測量的結(jié)果保存到成員變量中
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
}
...
}
...
}
先判斷一下是否有必要進(jìn)行測量操作,如果有,先看是否能在緩存mMeasureCache中找到上次的測量結(jié)果,如果找到了那直接從緩存中獲取就可以了凝垛,如果找不到懊悯,那么乖乖地調(diào)用onMeasure()方法去完成實(shí)際的測量工作,并且將尺寸限制條件widthMeasureSpec和heightMeasureSpec傳遞給onMeasure()方法梦皮。
另外炭分,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法剑肯,說明Android是不允許我們改變View的measure框架. 主要看onMeasure()方法捧毛,這里才是真正去測量并設(shè)置View大小的地方。
2.3.1 View的measure過程:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法會設(shè)置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;
}
很顯然看出:
AT_MOST 和 EXACTLY兩種情況返回的就是specSize呀忧。
UNSPECIFIED返回的是size, 即getSuggestedMinimumWidth 和 getSuggestedMinimumHeight的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
mMinWidth 對應(yīng)于android:minWidth屬性指定的值溃睹〖雠埃總結(jié):如果View沒有設(shè)置背景,那么返回mMinWidth的值丸凭,否則返回mMinWidth和背景最小寬度的最大值福扬。
2.3.2 ViewgGroup的measure過程:
ViewGroup 繼承自 View,我們知道View的 measure是 final方法惜犀,那這個方法是肯定會走的铛碑,但是具體實(shí)現(xiàn)是在onMeasure中,ViewGroup提供了幾個方法來幫助ViewGroup的子類來實(shí)現(xiàn)onMeasure邏輯虽界,包括:
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);
}
仔細(xì)看其實(shí)最終還是讓child去執(zhí)行自己對于的measure汽烦,只是getChildMeasureSpec有差別,這里加上了margin 和 padding.
具體onMeasure的實(shí)現(xiàn)可以參考LinearLayout莉御、FrameLayout撇吞、RelativeLayout等俗冻。
另外需要關(guān)注的是ViewGroup 的 getChildMeasureSpec方法,我們從上面代碼中很明顯看出牍颈,傳入的Spec是父容器的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來說,getChildMeasureSpec的邏輯是通過其父View提供的MeasureSpec參數(shù)得到specMode和specSize煮岁,然后根據(jù)計算出來的specMode以及子View的childDimension(layout_width或layout_height)來計算自身的measureSpec 讥蔽。
measure總結(jié):
MeasureSpec 由specMode和specSize組成:
DecorView, 其MeasureSpec由窗口尺寸和其自身LayoutParams共同決定。
普通View画机,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定冶伞。View的measure方法是final的,不允許重載步氏,View子類只能重載onMeasure來完成自己的測量邏輯响禽。
ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法荚醒,以及getChildMeasureSpec方法 芋类,供具體實(shí)現(xiàn)ViewGroup的子類重寫onMeasure的時候方便使用。
只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams腌且,否則無法使用layout_margin參數(shù)。
使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高榛瓮,必須保證這兩個方法在onMeasure流程之后被調(diào)用才能返回有效值铺董。
比較常用的方式:
view.post(runnable)
view.measure(0,0)之后 get
measure整體執(zhí)行流程:
2.4 布局過程 (performLayout())
Layout的作用是ViewGroup用來確定子view的位置,當(dāng)ViewGroup的位置被確定之后禀晓,它在onLayout中會遍歷所有子view并調(diào)用其layout方法精续,在layout方法中onLayout又被調(diào)用。
先從performLayout看起:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
...
//標(biāo)記當(dāng)前開始布局
mInLayout = true;
//mView就是DecorView
final View host = mView;
...
//DecorView請求布局 layout參數(shù)分別是 左 上 右 下
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//標(biāo)記布局結(jié)束
mInLayout = false;
...
}
跟蹤代碼進(jìn)入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;
}
//保存上一次View的四個位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//設(shè)置當(dāng)前視圖View的左粹懒,頂重付,右,底的位置凫乖,并且判斷布局是否有改變
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果布局有改變确垫,條件成立,則視圖View重新布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//調(diào)用onLayout帽芽,將具體布局邏輯留給子類實(shí)現(xiàn)
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;
}
首先關(guān)注下需要重新layout的條件:
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
其中setOpticalFrame內(nèi)部也會調(diào)用setFrame删掀,所以就看下setFrame好了:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
return changed;
}
通過setFrame方法設(shè)定View的四個頂點(diǎn)的位置,并更新本地值导街,同時判斷頂點(diǎn)位置較之前是否有變化披泪,并return是否有變化的boolean值,如果有變化還會執(zhí)行invalidate(sizeChanged)搬瑰。
然后款票,咱們再看看onLayout方法:
View中的onLayout是個空方法控硼,可實(shí)現(xiàn)可不實(shí)現(xiàn)
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup中是個抽象方法,子類必須實(shí)現(xiàn)
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
下面以RelativeLayout為例艾少,對onLayout具體實(shí)現(xiàn)做簡單的分析:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// The layout has actually already been performed and the positions
// cached. Apply the cached values to the children.
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() != GONE) {//只有不為GONE的才會執(zhí)行布局
RelativeLayout.LayoutParams st =
(RelativeLayout.LayoutParams) child.getLayoutParams();
child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
}
}
}
遍歷所有子view卡乾,并通過其LayoutParams 獲取四個方向的位置值,將位置信息傳入子view的layout方法進(jìn)行布局姆钉。
layout總結(jié):
Layout的作用是ViewGroup用來確定子view的位置, 是ViewGroup需要干的活说订,View不需要,所以View中是空方法潮瓶,而ViewGroup中是抽象方法陶冷,但是View你也可以重寫,大多數(shù)是利用這個生命周期階段加寫邏輯操作毯辅。
當(dāng)我們的視圖View在布局中使用 android:visibility=”gone” 屬性時埂伦,是不占據(jù)屏幕空間的,因?yàn)樵诓季謺rViewGroup會遍歷每個子視圖View思恐,判斷當(dāng)前子視圖View是否設(shè)置了 Visibility==GONE,如果設(shè)置了沾谜,當(dāng)前子視圖View就不會添加到父容器上,因此也就不占據(jù)屏幕空間胀莹。具體可以參考RelativeLayout的onLayout.
必須在View布局完之后調(diào)用getHeight( )和getWidth( )方法獲取到的View的寬高才大于0.
layout的整體執(zhí)行流程:
2.5 繪制過程 (performDraw())
Draw作用是將View繪制到屏幕上.過程相對比較簡單基跑。
draw是從performDraw開始
//ViewRootImpl
private void performDraw() {
...
draw(fullRedrawNeeded);
...
}
然后看ViewRootImpl的draw方法:
//ViewRootImpl
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
...
}
再看ViewRootImpl的drawSoftware方法:
//ViewRootImpl
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
...
mView.draw(canvas);
...
}
最終在drawSoftware方法中,會走到View的draw并傳入了canvas畫布描焰。這部分先不細(xì)說媳否,之后的Surface部分會分析。
那么接著往下的話就是真正View繪制的部分了:
//View
public void draw(Canvas canvas) {
......
/*
* 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
......
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
......
// Step 2, save the canvas' layers
......
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
......
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
......
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
......
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
......
}
從摘要可以看出荆秦,繪制過程分如下幾步:
- 繪制背景 background.draw(canvas)
private void drawBackground(Canvas canvas) {
//獲取xml中通過android:background屬性或者代碼中setBackgroundColor()篱竭、setBackgroundResource()等方法進(jìn)行賦值的背景Drawable
final Drawable background = mBackground;
......
//根據(jù)layout過程確定的View位置來設(shè)置背景的繪制區(qū)域
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
......
//調(diào)用Drawable的draw()方法來完成背景的繪制工作
background.draw(canvas);
......
}
-
繪制自己(onDraw)
View中onDraw是一個空方法,ViewGroup也沒有重新實(shí)現(xiàn)步绸。
protected void onDraw(Canvas canvas) {
}
-
繪制children(dispatchDraw)
View的dispatchDraw()方法是一個空方法掺逼,而且注釋說明了如果View包含子類需要重寫他。所以ViewGroup肯定重寫了瓤介,來看看:
@Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
可以看見吕喘,ViewGroup確實(shí)重寫了View的dispatchDraw()方法,該方法內(nèi)部會遍歷每個子View刑桑,然后調(diào)用drawChild()方法兽泄,我們可以看下ViewGroup的drawChild方法,如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
- 繪制裝飾(onDrawScrollBars)
可以看見其實(shí)任何一個View都是有(水平垂直)滾動條的漾月,只是一般情況下沒讓它顯示而已病梢。這部分不做詳細(xì)分析了。
draw拓展點(diǎn):
1)View的setWillNotDraw方法:
如果一個View不需要繪制任何內(nèi)容,那么設(shè)置這個標(biāo)記位為true后蜓陌,系統(tǒng)會進(jìn)行相應(yīng)的優(yōu)化觅彰,如果需要通過onDraw繪制內(nèi)容,則需要設(shè)置為false钮热。(默認(rèn)View是false ViewGroup是true)這是個優(yōu)化手段填抬。
2)區(qū)分View動畫和ViewGroup布局動畫,前者指的是View自身的動畫隧期,可以通過setAnimation添加飒责,后者是專門針對ViewGroup顯示內(nèi)部子視圖時設(shè)置的動畫,可以在xml布局文件中對ViewGroup設(shè)置layoutAnimation屬性(譬如對LinearLayout設(shè)置子View在顯示時出現(xiàn)逐行仆潮、隨機(jī)宏蛉、下等顯示等不同動畫效果)。
3)默認(rèn)情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致性置,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序拾并。
draw整體執(zhí)行流程:
三、forceLayout 鹏浅、invalidate 嗅义、requestLayout簡述
在之前分析的繪制流程中,我們或多或少都見過這三個方法隐砸,他們到底是干什么的之碗,下面做下簡單說明:
View#forceLayout( )
public void forceLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
}
官方描述:強(qiáng)制此視圖在下一次布局傳遞期間進(jìn)行布局。此方法不調(diào)用父類的requestLayout()或forceLayout()季希。
每個View都有個成員變量:mPrivateFlags褪那,在不同的繪制執(zhí)行路徑會對它賦值。在measure方法內(nèi)部forceLayout用來判斷是否執(zhí)行onMeasure:
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
View#invalidate( ) 和 View#postInvalidate( )
invalidate和postInvalidate:都是用來重繪View胖眷,區(qū)別就是invalidate只能在主線程中調(diào)用武通,postInvalidate可以在子線程中調(diào)用.
View#requestLayout
requestLayout: 當(dāng)前view及其以上的viewGroup部分都重新走ViewRootImpl 重新繪制 霹崎,分別重新onMeasure onLayout onDraw ,其中onDraw比較特殊珊搀,有內(nèi)容變化才會觸發(fā)。
最后一張圖總結(jié)下invalidate/postInvalidate 和 requestLayout
參考:
https://blog.csdn.net/yanbober/article/details/46128379
https://blog.csdn.net/feiduclear_up/article/details/46772477
http://www.reibang.com/p/4a68f9dc8f7c
http://www.reibang.com/p/a65861e946cb
《Android開發(fā)藝術(shù)探索》