接著上篇 View 基礎(chǔ) 來講 View 的工作原理,View 的工作原理中最重要的就是測(cè)量、布局、繪制三大過程,而其中測(cè)量是最復(fù)雜的,今天的主要任務(wù)是講明白 View 的測(cè)量過程 ,不過在講測(cè)量過程之前還會(huì)將一些有關(guān) View 測(cè)量中使用到的其他知識(shí)進(jìn)行講解,例如 View 的展示過程、MeasureSpec 類的原理和使用等。所以整篇內(nèi)容會(huì)稍多,不過條理很簡(jiǎn)單。讓我們開始吧。
View的展示過程
首先呢,讓我們對(duì)一個(gè)界面的根View從創(chuàng)建到顯示到屏幕的具體過程有一個(gè)簡(jiǎn)單的認(rèn)識(shí),從而了解 View 的測(cè)量、布局、繪制過程從哪里來的。
創(chuàng)建一個(gè) Window ,因?yàn)榻缑嫔巷@示的內(nèi)容都是通過 Window 來展示的,所以需要先創(chuàng)建一個(gè) Window
創(chuàng)建 DecorView(頂級(jí)View) ,就是說得到我們需要展示的根 View,一般情況下是一個(gè) Framlayout
Window 要展示到屏幕上是通過 WindowManager.addView() 方法完成的,該方法中會(huì)創(chuàng)建會(huì)為每個(gè) Window 創(chuàng)建 ViewRootImpl 對(duì)象用于連接 WindowManager 和 DecorView ,并且所有 Window 想要對(duì) View 的進(jìn)行的操作都是通過 ViewRootImpl 來完成的
ViewRootImpl 創(chuàng)建之后,會(huì)調(diào)用 performTraversals 方法,其中又會(huì)依次調(diào)用,performMeasure、performLayout、performDraw 方法,performMeasure 方法中會(huì)調(diào)用 DecorView 的 measure 方法,measure 方法中又會(huì)調(diào)用 onMeasure 方法將測(cè)量過程傳遞到子 View;performLayout 則會(huì)調(diào)用 layout 方法,layout 方法中會(huì)調(diào)用 onLayout 方法將布局過程傳遞到子 View;performDarw 方法中會(huì)調(diào)用 darw 方法,draw 方法中調(diào)用 dispatchDraw 方法將繪制過程傳遞到子 View 中
ViewRootImpl 將測(cè)量、布局、繪制完成之后,就會(huì)通過 IPC 遠(yuǎn)程調(diào)用 WindowManagerService 的方法,將 window 和 view 連接,完成顯示。
View 顯示更新時(shí),系統(tǒng)也是通過 ViewRootImpl 來重新完成 測(cè)量、布局、繪制 三大過程。
View的測(cè)量 目的:確定View的測(cè)量寬高
上面提到了 View 要顯示到界面上必須經(jīng)過測(cè)量、布局、繪制三個(gè)階段,其中的測(cè)量是第一個(gè)過程,其主要目的是要確定 View 的測(cè)量寬高,也就是 View 的 getMeasuredWidth()/getMeasuredHeight() 方法得到的結(jié)果
測(cè)量的過程是這樣的,父 View 調(diào)用 measure 方法開始測(cè)量,measure 方法中又會(huì)調(diào)用 onMeasure 方法將測(cè)量過程傳遞到子 View 中,也就是調(diào)用子 View 的 measure 方法,子 View 的 measure 方法中調(diào)用子 View 的 onMeasure 方法,onMeasure 方法中完成子 View 的測(cè)量,子 View 測(cè)量完成之后,父 View 根據(jù)子 View 的測(cè)量結(jié)果確定自己的測(cè)量結(jié)果
measure 的過程是在 onMeasure() 方法中完成的,父容器根據(jù)自己的測(cè)量模式和測(cè)量大小以及子 view 的 LayoutParams 來確定子 View 的測(cè)量模式和測(cè)量大小,用到了 MeasureSpec 這個(gè)類,父容器再根據(jù)子的大小確定最終自己的大小,先了解一下 MeasureSpc 這個(gè)類
MesureSpc 是 View 中的一個(gè)內(nèi)部類,里面封裝了View測(cè)量方式和測(cè)量大小的信息
MesureSpec 是一個(gè)32位的int值,其中高 2 位為測(cè)量模式,低 30 位為測(cè)量的大小,在計(jì)算中使用位運(yùn)算的原因是為了提高優(yōu)化效率。
MeasureSpec 封裝了父布局傳遞給子布局的布局要求,每個(gè) MeasureSpec 代表了一組寬度或高度的要求及大小,測(cè)量過程中每個(gè) View 的寬高都由 MeasureSpec 來確定
每個(gè) Viwe 寬高的 MeasureSpec 的確定都由其父 View 的 MeasureSpec 和該 View 的 LayoutParams 確定
三種mode
UNSPECIFIED 父沒有對(duì)子施加任何約束,子可以是任意大小,使用較少,一般用于系統(tǒng)內(nèi)部
EXACTLY 確切大小,子被限定在給定值,即在 xml 中設(shè)置了寬高的確定值或者是 match_parent 時(shí),模式為 EXACTLY
-
AT_MOST 表示子 view 的大小不確定,但最多是父 View 目前可使用的大小 會(huì)根據(jù)這個(gè)上限來設(shè)置自己的尺寸。表示子布局限制在一個(gè)最大值內(nèi),一般為 WARP_CONTENT ,此時(shí) MeasureSpc 中的Size值即為父可允許的最大值,展示效果為占滿父控件剩余位置
public class MeasureSpec {
// 進(jìn)位大小為2的30次方(int的大小為32位,所以進(jìn)位30位就是要使用int的最高位和倒數(shù)第二位也就是32和31位做標(biāo)志位) private static final int MODE_SHIFT = 30; // 0x3為16進(jìn)制,10進(jìn)制為3,二進(jìn)制為11。3向左進(jìn)位30,就是 11 00000000000...(11后跟30個(gè)0) private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0向左進(jìn)位30,就是00 00000000000...(00后跟30個(gè)0) public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1向左進(jìn)位30,就是01 00000000000(01后跟30個(gè)0) public static final int EXACTLY = 1 << MODE_SHIFT; // 2向左進(jìn)位30,就是10 00000000000(10后跟30個(gè)0) public static final int AT_MOST = 2 << MODE_SHIFT; /** * 根據(jù)提供的size和mode得到一個(gè)詳細(xì)的測(cè)量結(jié)果 * measureSpec = size + mode; (注意:二進(jìn)制的加法,不是10進(jìn)制的加法!) * 這里設(shè)計(jì)的目的就是使用一個(gè)32位的二進(jìn)制數(shù),32和31位代表了mode的值,后30位代表size的值 */ public static int makeMeasureSpec(int size, int mode) { return size + mode; } /** * 通過詳細(xì)測(cè)量結(jié)果獲得mode * * mode = measureSpec & MODE_MASK; * MODE_MASK = 11 00000000000(11后跟30個(gè)0),原理是用MODE_MASK后30位的0替換掉measureSpec后30位中的值,保留32和31位的mode值。 */ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * 通過詳細(xì)測(cè)量結(jié)果獲得size * * size = measureSpec & ~MODE_MASK; * 原理同上,不過這次是將MODE_MASK取反,也就是變成00 111111(00后跟30個(gè)1),將32,31替換成0也就是去掉mode,保留后30位的size */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } /** * 重寫的toString方法,打印mode和size的信息,這里省略 */ public static String toString(int measureSpec) { return null; }
}
DecorView 的 MeasureSpc的確定
上面提到了 View 測(cè)量的過程中,是從 DecorView 開始的,所以我們先看 DecorView 的 MeasureSpc 的是如何確定的,其代碼在 ViewRootImpl 中
// ViewRootImpl
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;
}
其中 windowSize 代表窗口大小,rootDimension 代表 LayoutParams 中的寬高參數(shù),由代碼可以看出 DecorView 的 MeasureSpc 單純的由其 LayoutParams 確定
match_parent 精確模式,大小就是窗口的大小
wrap_content 最大模式,大小不定,最大不能超過窗口大小
固定值 精確模式,大小為 LayoutParams 中指定的寬高大小
普通 View 的 MeasureSpc 的確定
DecorView 的 MeasureSpec 確定之后,ViewRootImpl 就會(huì)調(diào)用 DecorView 的 measure 方法,measure 方法中會(huì)調(diào)用 onMeasure 方法,DecorView 是 GroupView 的子類,GroupView 的 onMeasure 方法中會(huì)遍歷子 View ,并依次調(diào)用 measureChildWithMargins 方法
// ViewGroup
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);
}
// ViewGroup
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);
}
以上代碼非常清晰,就是根據(jù)父 View 的 MeasureSpc 和子 View 的 LayoutParams 來共同確定子 View 的 MeasureSpec ,其邏輯可以用一下圖片總結(jié)
測(cè)量過程
好了,經(jīng)過上面那么多的介紹和分析,終于要進(jìn)入今天的正題了,那就是 View 的測(cè)量過程,由上面的分析可以知道 ViewRootImpl 中的 performMeasure 方法啟動(dòng) View 的測(cè)量,其中會(huì)將測(cè)量過程由 ViewGrop 逐級(jí)傳遞,上面分析子 View 的 MeasureSpec 的確定過程中,子 View 的 MeasureSpec 確定之后,接下來有一行代碼:
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
這行代碼則將測(cè)量過程由 ViewGroup 傳遞到了子 View,假定該子 View 并不是 ViewGroup ,我們知道 View 的 measure 方法會(huì)調(diào)用 onMeasure 方法,接下來分析 View 的測(cè)量過程
View 的測(cè)量過程
/**
* 這個(gè)方法需要被重寫,應(yīng)該由子類去決定測(cè)量的寬高值
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure 中調(diào)用了 setMeasuredDimension() 方法,將 View 的測(cè)量大小保存在 View 的 mMeasuredWidth 和 mMeasuredHeight 中
onMeasure() 方法應(yīng)該被子類重寫從而確定 View 的測(cè)量寬高值,如果子類不重寫,則 View 的寬高值為根據(jù) View 的背景大小以及 MeasureSpec 確定的值,其中 View 的測(cè)量大小通過 getDefaultSize 方法確定。
默認(rèn) View 測(cè)量寬高的確定規(guī)則為:
如果 MeasureSpec 的 SpecMode 為無限制條件,就以最小的寬度作為測(cè)量結(jié)果
如果 MeasureSpec 的 SpecMode 為 MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY,就以 MeasureSpec 中的 SpecSize 為測(cè)量結(jié)果
/**
* 返回 View 默認(rèn)的最小寬度
*/
protected int getSuggestedMinimumWidth() {
// 如果沒有給View設(shè)置背景,那么就返回View本身的最小寬度mMinWidth,如果給View設(shè)置了背景,那么就取View本身最小寬度mMinWidth和背景的最小寬度的最大值
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
/**
* 作用是返回一個(gè)默認(rèn)的值,父沒有給子設(shè)置限制條件,子就以自己想要的尺寸 size 作為測(cè)量的結(jié)果,有限制時(shí)View 必須使用其父ViewGroup指定的尺寸
* 第一個(gè)參數(shù)size為View的設(shè)置大小,第二個(gè)參數(shù)為測(cè)量的大小
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec); // 得到 MeasureSpc 的限制方式
int specSize = MeasureSpec.getSize(measureSpec); // 得到 MeasureSpc 的尺寸
switch (specMode) {
case MeasureSpec.UNSPECIFIED: // 無限制條件,就以最小的寬度 mMinWidth作為測(cè)量結(jié)果
result = size;
break;
case MeasureSpec.AT_MOST: // View 使用其父View為其設(shè)定的可達(dá)的最大尺寸
case MeasureSpec.EXACTLY: // View 必須使用指定的尺寸
result = specSize;
break;
}
return result;
}
mMinWidth 來源
第一種情況是,mMinWidth是在View的構(gòu)造函數(shù)中被賦值的,可以為0,View 構(gòu)造函數(shù)中通過讀取XML中定義的 minWidth 的值來設(shè)置 View 的最小寬度 mMinWidth,如果不設(shè)置默認(rèn)該值為 0
第二種情況是調(diào)用 View 的 setMinimumWidth 方法給 View 的最小寬度 mMinWidth 賦值
最后再來看一下 setMeasuredDimension 方法,其只是將測(cè)量結(jié)果保存并修改 View 的狀態(tài)位
/**
* 這個(gè)方法必須由onMeasure(int, int)來調(diào)用,來存儲(chǔ)測(cè)量的寬,高值。
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
//layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情況,我們不考慮
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
// 最終將測(cè)量的結(jié)果保存
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//最后將View的狀態(tài)位mPrivateFlags設(shè)置為已量算狀態(tài)
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
由以上的分析可以得出結(jié)論,onMeasure() 的任務(wù)就是計(jì)算準(zhǔn)確的 measuredWidth 和 measuredHeight ,源碼中的 getDefaultSize() 只是簡(jiǎn)單的測(cè)量了默認(rèn)寬高值,具體的測(cè)量任務(wù)就交給了子類中重寫的 onMeasure() 方法。所以子 View 需要根據(jù)需求重寫 onMeasure 方法來保存自己想要的測(cè)量值。
在子類中重寫 onMeasure() 方法
如果不重寫 onMeasure 之前的 MeasureSpec 中保存的是根據(jù)父容器以及 View 的 LayoutParams 確定的大小
由上分析可得,在默認(rèn)的 onMeasure() 方法中支持 EXACTLY 和 UNSPECIFIED 時(shí)的準(zhǔn)確測(cè)量,EXACTLY 即 xml 中設(shè)置大小或設(shè)置為 match_parent 占滿父控件
AT_MOST 情況需要我們?cè)谥貙?onMeasure() 方法時(shí)處理,即屬性設(shè)置為 wrap_content 時(shí)或 match_parent 但父控件測(cè)量模式為 AT_MOST,我們需要在 onMeasure() 方法中計(jì)算內(nèi)容的寬高,從而設(shè)置準(zhǔn)確的測(cè)量值。
結(jié)論:重寫 onMeasure() 方法,即通過判斷測(cè)量的模式,給出不同的測(cè)量值。
到這里 View 的測(cè)量過程就已經(jīng)結(jié)束了,測(cè)量過程的任務(wù)就是確定了 View 的測(cè)量寬高值。
ViewGroup的測(cè)量
ViewGroup 是一個(gè)抽象類,其默認(rèn)的測(cè)量過程只是按照 View 的測(cè)量過程完成自己的測(cè)量過程
ViewGrop 的子類實(shí)現(xiàn),一般會(huì)先測(cè)量所有的子 View ,由所有子 View 的測(cè)量大小再根據(jù) ViewGrop 要實(shí)現(xiàn)的效果來確定測(cè)量大小。
具體對(duì)子元素 measure() 方法的調(diào)用是在 ViewGroup() 的實(shí)現(xiàn)類的 onMeasure() 方法中, measureChildWithMargins() 或者 MeasureChildren() 方法,這兩個(gè)方法中都會(huì)確定子元素的寬高的 MeasureSpc 并調(diào)用子元素的 measure() 進(jìn)行子元素的測(cè)量。獲取子元素寬高的方法為 getChildMeasureSpc(),這個(gè)方法中會(huì)根據(jù) ViewGrop 的 MeasureSpc,父元素的 padding、子元素的 LayoutParams、以及父元素的可用空間等信息確定子元素的 MeasureSpc。確定子 View 的 MeasureSpec 之后則會(huì)調(diào)用子 View 的 measure 方法。