本篇文章主要介紹以下幾個知識點:
- 初識 ViewRoot 和 DecorView阀溶;
- 理解 MeasureSpec身冬;
- View 的工作流程:measure筐摘、layout卒茬、draw。
4.1 初識 ViewRoot 和 DecorView
為更好的理解 View 的三大流程(measure
圃酵、layout
、draw
)馍管,先了解一些基本的概念郭赐。
ViewRoot 對應(yīng)于 ViewRootImpl
類,是連接 WindowManager
和 DecorView
的紐帶确沸,View 的三大流程都是通過 ViewRoot
來完成的捌锭。
View 的繪制流程從 ViewRoot 的 performTraversals
方法開始,它經(jīng)過 measure
(測量 View 的寬高)罗捎,layout
(確定 View 在父容器的位置) 和 draw
(負責將 View 繪制在屏幕上) 三個過程才能將一個 View 繪制出來观谦,如下:
DecorView 是一個 FrameLayout
,View 層的事件都先經(jīng)過 DecorView
桨菜,再傳遞給 View豁状。
DecorView 作為頂級 View,一般它內(nèi)部會包含一個豎直方向的 LinearLayout倒得,上面是標題欄泻红,下面是內(nèi)容欄。在 Activity 中通過 setContentView
設(shè)置的布局文件就是被加到內(nèi)容欄中霞掺,而內(nèi)容欄的 id 為 content谊路,可通過 ViewGroup content = findviewbyid(android.R.id.content)
得到 content,通過 content.getChildAt(0)
得到設(shè)置的 View菩彬。其結(jié)構(gòu)如下:
4.2 理解 MeasureSpec
MeasureSpec 很大程度上決定了一個 View 的尺寸規(guī)格缠劝。在 View 的測量過程中,系統(tǒng)會將 View 的 LayoutParams
根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的 MeasureSpec
挤巡,再根據(jù)這個 measureSpec
來測量出 View 的寬高(測量寬高不一定等于 View 的最終寬高)剩彬。
4.2.1 MeasureSpec
MeasureSpec 代表一個32位 int 值,高兩位代表 SpecMode(測量模式)矿卑,低30位代表 SpecSize(某個測量模式下的規(guī)格大泻砹怠),MeasureSpec 內(nèi)部的一些常量定義如下:
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;
// MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內(nèi)存分配
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 解包:獲取其原始的 SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 解包:獲取其原始的 SpecSize
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
SpecMode 有三類,其含義分別如下:
UNSPECIFIED
父容器不對 View 有任何的限制(一般用于系統(tǒng)內(nèi)部)轻黑,表示一種測量的狀態(tài)EXACTLY
父容器檢測出 View 的精度大小糊肤,此時 View 的最終大小就是 SpecSize 所指定的值。它對應(yīng)于 LayoutParams 中的match_parent
和具體的數(shù)值這兩種模式AT_MOST
父容器指定一個可用大小即SpecSize氓鄙,View 的大小不能大于這個值馆揉。它對應(yīng)于 LayoutParams 中的wrap_content
4.2.2 MeasureSpec 和 LayoutParams 的對應(yīng)關(guān)系
Layoutparams 需要和父容器一起才能決定 View 的 MeasureSpec,一旦確定 MeasureSpec 后抖拦,onMeasure 中就可以確定 View 的測量寬高升酣。
頂級 View(DecorView),其 MeasureSpec 由窗口的尺寸和自身的 Layoutparams 來共同決定态罪;普通 View噩茄,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 Layoutparams 來決定。
對于 DecorView复颈,在 ViewRootImpl
中的 measureHierarchy
方法中的一段代碼展示了其 MeasureSpec 的創(chuàng)建過程:
// 其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth , lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接下來看下 getRootMeasureSpec
方法的實現(xiàn):
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 的 MesourSpec 的產(chǎn)生過程绩聘,根據(jù)其 Layoutparams 的寬高的參數(shù)來劃分,遵守如下規(guī)則:
LayoutParams.MATCH_PARENT:
精確模式耗啦,大小就是窗口的大小LayoutParams.WRAP_CONTENT:
最大模式凿菩,大小不定,但是不能超出屏幕的大小固定大兄慕病(比如100dp):
精確模式衅谷,大小為 LayoutParams 中指定的大小
對于 普通的 View,指布局中的 View似将,其 measure 過程由 ViewGroup 傳遞而來会喝,先看下 ViewGroup 的 measureChildWithMargins
方法:
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);
// 調(diào)用子元素的 measure 方法前會通過上面的 getChildMeasureSpec 方法得到子元素的 MesureSpec
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上述對子元素進行 measure,顯然玩郊,子元素的 MesureSpec 的創(chuàng)建和父容器的 MesureSpec 、子元素的 LayoutParams 有關(guān)和 View 的 margin 有關(guān)枉阵,其中 getChildMeasureSpec
方法如下:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 參數(shù)中的 pading 是指父容器中已占有的控件大小
// 因此子元素可以用的大小為父容器的尺寸減去 pading
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);
}
上述方法主要作用是根據(jù)父容器的 MeasureSpec 同時結(jié)合 View 本身的 Layoutparams 來確定子元素的 MesureSpec译红。
上面getChildMeasureSpec
展示了普通 View 的 MeasureSpec 創(chuàng)建規(guī)則,也可參考下表(表中的 parentSize 指父容器中目前可使用的大行肆铩):
當 View 采用固定寬/高時侦厚,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精確模式并且其大小遵循 LayoutParams 中的大小拙徽。
當 View 的寬/高是 match_parent
時刨沦,若父容器是精準模式,那么 View 也是精準模式并且其大小是父容器的剩余空間膘怕;若父容器是最大模式想诅,那么 View 也是最大模式并且其大小不會超過父容器的剩余空間。
當 View 的寬/高是 wrap_content
時,不管父容器的模式是精準還是最大化来破,View 的模式總是最大化篮灼,并且大小不能超過父容器的剩余空間。
注:UNSPECIFIED 模式主要用于系統(tǒng)內(nèi)部多次 Measure 的情形徘禁,一般不需關(guān)注此模式诅诱。
綜上,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams送朱,就可以快速地確定出子元素的 MeasureSpec 了娘荡,有了 MeasureSpec 就可以進一步確定出子元素測量后的大小了。
4.3 View 的工作流程
View 的工作流程主要是指 measure
(測量驶沼,確定 View 的測量寬/高)炮沐、layout
(布局,確定 View 的最終寬/高和四個頂點的位置)商乎、draw
(繪制央拖,將 View 繪制到屏幕上)這三大流程。
4.3.1 measure 過程
若只是一個原始的 View鹉戚,那么通過 measure 方法就完成了其測量過程鲜戒,若是一個 ViewGroup,除了完成自己的測量過程外抹凳,還會遍歷去調(diào)用所有子元素的 measure 方法遏餐,各個子元素再遞歸去執(zhí)行這個流程。
4.3.1.1 View 的 measure 過程
View 的 measure 過程由其 measure 方法來完成赢底,measure 方法中會去調(diào)用 View 的 onMesure
方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設(shè)置 View 寬/高的測量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中 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 這兩種情況失都,可理解為 getDefaultSize
返回的大小就是 mesourSpec 中的 specSize,而這個 specSize 就是 View 測量后的大行叶场(測量大小不一定等于 View 的最終大写馀印)。
至于 UNSPECIFIED 這種情況洽损,一般用于系統(tǒng)內(nèi)部的測量過程庞溜,View 的大小為 getDefaultSize
的第一個參數(shù)是 size,其寬/高獲取方法如下:
protected int getSuggestedMinimumWidth() {
// 1. 若 View 沒有設(shè)置背景碑定,View 的寬度為 mMinwidth流码,
// 而 mMinwidth 對應(yīng)于 android:minwidth 這個屬性所指定的值,
// 因此 View 的寬度即為 android:minwidth 屬性所指定的值延刘,
// 若這個屬性不指定漫试,那么 mMinWidth 則默認為0;
// 2. 若 View 指定了背景碘赖,則View的寬度為max(mMinwidth驾荣,mbackground().getMininumwidth)
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
上面注釋分析了 getSuggestedMinimumWidth
方法的實現(xiàn)外构,getSuggestedMinimumHeight
和它的原理一樣。注釋中未說明的 mBackground.getMinimumWidth()
方法(即 Drawable 的 getMinimumWidth
方法)如下:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
// 返回 Drawable的原始寬度(有原始寬度的話)秘车,否則就返回0
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
總結(jié) getSuggestedMinimumWidth
的邏輯:
若 View 沒設(shè)背景典勇,那么返回 android:minwidth
所指定的值(可為0);
若 View 設(shè)了背景叮趴,則返回 android:minwidth
和背景的最小寬度這兩者中的最大值割笙。
View 在 UNSPECIFIED 情況下的測量寬/高即為 getSuggestedMinimumWidth
和getSuggestedMinimumHeight
的返回值 。
結(jié)論:直接繼承 View 的自定義控件需要重寫 onMeasure
方法并設(shè)置 wrap_content
時的自身大小眯亦,否則在布局中使用 wrap_content
就相當于使用 match_parent
伤溉。
從上述代碼中知道,若 View 在布局中使用 wrap_content
妻率,那么它的 specMode 是 AT_MOST 模式乱顾,它的寬/高等于 specSize;此情況下 View 的 specSize 是 parentSize宫静,而 parentSize 是父容器中目前可以使用的大小走净,即父容器當前剩余的空間大小。顯然孤里,View 的寬/高就等于父容器當前剩余的空間大小伏伯,這種效果和在布局中使用 match_parent
完全一致。
解決上述問題代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
// 給 View 指定一個默認的內(nèi)部寬/高(mWidth, mHeight)捌袜,并在 wrap_content 時設(shè)置此寬/高即可
// 對于非 wrap_content 情形说搅,沿用系統(tǒng)的測量值即可
//(注:TextView、ImageView 等針對 wrap_content 情形虏等,它們的 onMeasure 方法做了特殊處理)
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);
}
}
4.3.1.2 ViewGroup 的 measure 過程
和 View 不同的是弄唧,ViewGroup 是一個抽象類,它沒有重寫 View 的 onMeasure
方法霍衫,但它提供了一個 measureChildren
方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
// ViewGroup 在 measure 時候引,會對每一個子元素進行 measure
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
上述代碼中的 measureChild
方法如下:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 取出子元素的 LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 2. 通過 getChidMeasureSpec 來創(chuàng)建子元素的 MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 3. 將 MeasureSpec 直接傳遞給 View 的 measure 方法來進行測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
上面代碼注釋說明了 measurechild
的思想。
由于 ViewGroup 是一個抽象類敦跌,其測量過程的 onMeasure 方法需要各個子類去具體實現(xiàn)背伴;不同的 ViewGroup 子類有不同的布局特性蔚润,它們的測量細節(jié)各不相同经宏,如 LinearLayout 和 RelativeLayout 這兩者的布局特性不同卤妒,因此 ViewGroup 無法對其 onMeasure 方法做統(tǒng)一實現(xiàn)。
下面通過 LinearLayout 的 onMeasure 方法來分析 ViewGroup 的 measure 過程携兵,先來看一下 LinearLayout 的 onMeasure 方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
這里選擇查看豎直方向的 LinearLayout 測量過程,即 measureVertical
方法(其源碼比較長就不貼了)搂誉,這里只描述其大概邏輯:系統(tǒng)會遍歷子元素并對每個子元素執(zhí)行 measureChildBeforeLayout
方法徐紧,此方法內(nèi)部會調(diào)用子元素的 measure 方法,當子元素測量完畢之后,LinearLayout 會根據(jù)子元素的情況來測量自己的大小并级。
View 的 measure 過程完成后拂檩,通過 getMeasureWidth/Height
可以正確地獲取到 View 的測量寬/高。但在系統(tǒng)要多次 measure 才能確定最終的測量寬/高的情況下嘲碧,在 onMeasure 方法中拿到的測量寬/高可能是不準確的稻励。因此建議在 onLayout 方法中去獲取 View 的測量寬/高或者最終寬/高。
問題:如何在 Activity 已啟動的時候獲取某個 View 的寬/高愈涩?
注:由于 View 的 measure 過程和 Activity 的生命周期方法不是同步執(zhí)行的望抽,無法保證 Activiy 執(zhí)行了 onCreate、onStart履婉、onResume
時某個 View 已經(jīng)測量完畢了煤篙,從而在 onCreate、onStart毁腿、onResume
中均無法正確得View的寬/高信息(若 View 還沒測量完畢辑奈,那么獲得的寬/高就是0)。
這里給出四種方法:
(1)Activity/View#onWindowFocusChanged
onWindowFocusChanged
方法是指:View 已初始化完畢已烤,寬/高已準備好鸠窗,此時去獲取寬/高是沒問題的(注:當 Activity 繼續(xù)執(zhí)行和暫停執(zhí)行時,onWindowFocusChanged
均會被調(diào)用草戈,若頻繁地進行 onResume
和 onPause
塌鸯,那么 onWindowFocusChanged
也會被頻繁地調(diào)用)。典型代碼如下:
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2)view.post(runnable)
通過 post 可將一個 runnable 投遞到消息隊列的尾部唐片,然后等待 Lopper 調(diào)用此 runnable 時丙猬,View 就初始化好了。典型代碼如下:
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
(3)ViewTreeObserver
使用 ViewTreeObserver
的眾多回調(diào)可完成這個功能费韭,典型代碼如下:
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
(4)view.measure(int widthMeasureSpec , int heightMeasureSpec)
通過手動測量 View 的寬高茧球,此方法較復(fù)雜,根據(jù) View 的LayoutParams 來分情況來處理:
match_parent:無法測量出具體的寬高
具體的數(shù)值(dp/px):如寬高都是100dp星持,如下 measure:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
- wrap_content:如下measure:
// View 的尺寸使用30位的二進制表示抢埋,即最大是30個1(即 2^30-1),也就是 (1<<30)-1
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
關(guān)于 View 的 measure,網(wǎng)絡(luò)上有兩個錯誤的用法督暂。為什么說是錯誤的揪垄,首先其違背了系統(tǒng)的內(nèi)部實現(xiàn)規(guī)范(因為無法通過錯誤的 MeasureSpec 去得出合理的 SpecMode,從而導(dǎo)致 measure 過程出錯)逻翁,其次不能保證 measure 出正確的結(jié)果饥努。
- 第一種錯誤的方法:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1, View.MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
- 第二種錯誤的方法:
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
4.3.2 layout 過程
Layout 是 ViewGroup 用來確定子元素的位置的,當 ViewGroup 的位置被確定后八回,它在 onLayout 中會遍歷所有的子元素并調(diào)其 layout 方法酷愧,在 layout 方法中 onLayout 又被調(diào)用驾诈。layout 方法確定 View 本身的位置,而 onLayout 方法則會確定所有子元素的位置溶浴,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;
// 1. 通過 setFrame 方法來設(shè)定 View 的四個頂點的位置乍迄,
// 即初始化 mLeft,mTop,mRight,mBottom 這四個值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 2. View 的四個頂點一旦確定,那么 View 在父容器的位置也就確定了士败,
// 接下來會調(diào)用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;
}
和 onMeasure 類似闯两,onLayout 的具體位置實現(xiàn)同樣和具體布局有關(guān),所有 View 和 ViewGroup 均沒有真正的實現(xiàn) onLayout 方法拱烁。 LinearLayout 的 onLayout 如下:
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);
}
}
LinearLayout 的 onLayout 和 onMeasure 的實現(xiàn)邏輯類似生蚁,就 layoutVertical 來說,其主要代碼如下:
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();
. . .
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
// 調(diào)用 setChildFrame 為子元素指定對應(yīng)的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
上述方法中的 setChildFrame
方法戏自,僅僅是調(diào)用子元素的 layout 方法而已邦投,如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
這樣父元素在 layout 方法中完成自己的定位后,就通過 onLayout 方法去調(diào)用子元素的 layout 方法擅笔,子元素又會通過自己的 layout 方法來確定自己的位置志衣,這樣一層一層傳遞下去完成整個 View 樹的 layout 過程。
問題:View 的測量寬/高和最終寬/高有什么區(qū)別猛们?(即:View 的 getMeasureWidth
和getWidth
這兩個方法有什么區(qū)別念脯?)
為了回答這個問題,先看下 getWidth
和 getHeight
方法的實現(xiàn):
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
可以看出弯淘,getWidth
绿店、getHeight
返回的剛好是 View 的測量寬度、高度庐橙。
對于上面的問題:在 View 的默認實現(xiàn)中假勿,View 的測量寬/高和最終寬/高是相等的,只不過測量寬/高形成于 View 的 measure 過程态鳖,一個是 layout 過程转培,而最終寬/高形成于 View 的 layout 過程,即兩者的賦值時機不同浆竭,測量寬/高的賦值時機稍微早一些浸须。
日常開發(fā)中可用認為 View 的測量寬/高 = 最終寬/高,但某些特殊情況下邦泄,如重寫 View 的 layout 方法如下:
public void layout(int l,int t,int r, int b){
super.layout(l, t, r + 100, b + 100);
}
上述代碼會導(dǎo)致在任何情況下 View 的最終寬/高總是比測量寬/高大 100px删窒。
4.3.3 draw 過程
Draw 過程其作用是將 View 繪制到屏幕上面。View 的繪制過程遵循如下幾步:
(1)繪制背景 background.draw(canvas)
(2)繪制自己 (onDraw)
(3)繪制 children (dispatchDraw)
(4)繪制裝飾 (onDrawSrcollBars)
這一點通過 draw 方法的源碼可看出來:
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;
}
. . .
}
View 繪制過程的傳遞是通過 dispatchDraw 來實現(xiàn)的顺囊,dispatchDraw 會遍歷所有子元素的 draw 方法易稠,如此 draw 事件就一層層地傳遞下去。View 有一個特殊的方法 setwilINotDraw
:
public void setwilINotDraw(boolean willNotDraw){
// 若一個 View 不需要繪制任何內(nèi)容包蓝,那么設(shè)置這個標記位為 true 以后驶社,系統(tǒng)會進行相應(yīng)的優(yōu)化。
// 默認情況下测萎,View 沒有啟用這個型龅纾化標記位,但 ViewGroup 會默認啟用這個優(yōu)化標記位硅瞧。
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
實際開發(fā)中份乒,自定義控件繼承于 ViewGroup 并且本身不具備繪制功能時,就可以開啟這個標記位從而便于系統(tǒng)進行后續(xù)的優(yōu)化腕唧。若明確知道一個 ViewGroup 需要通過 onDraw 來繪制內(nèi)容時或辖,需要顯式地關(guān)閉 WILL_NOT_DRAW
這個標記位。