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