系列文章傳送門 (持續更新中..) :
自定義控制元件(一) Activity的構成(PhoneWindow、DecorView)
自定義控制元件(四) 原始碼分析 layout 和 draw 流程
在之前的文章中,我們比較清晰的瞭解了Activity的構成和事件分發機制的原理, 從這篇文章我們開始分析 view 的三個流程:測量,佈局,繪製。
- 在Android的知識體系中, 自定義控制元件扮演著很重要的角色, 可以說, view的重要性不低於Activity, 在和使用者的各種互動中離不開各式各樣的view。Android提供了一套GUI庫,裡面有很多控制元件,但是我們日常開發中有時並不能滿足於此,對於很多五花八門的效果,我們常常需要通過自定義控制元件去實現,創造出和別人不一樣的炫酷效果。
自定義view是有一定難度的,尤其是複雜的自定義view,僅僅瞭解普通控制元件的基本使用是無法完成複雜的自定義空間的。為了更好的完成自定義view,我們必須去掌握它的底層工作原理,即三個步驟:測量流程,佈局流程,繪製流程,分別對應 measure、layout 和 draw。
- 測量:決定 View 的尺寸大小;
- 佈局:決定 View 在父容器中的位置;
- 繪製:決定怎麼繪製這個 View。
(一)理解 MeasureSpec
MeasureSpec 的作用:
在view的measure過程中, MeasureSpec 參與了很重要的角色, 所以首先要理解 MeasureSpec 是個什麼. 從字面上看, 是 Measure 、Specification 兩個單詞的縮寫,直譯貌似大約像是“測量規格”。在原始碼中,它用於處理兩個資訊:尺寸大小和測量模式。
- MeasureSpec 代表一個 32 位的int值,高2位代表 specMode 即測量模式,低30位代表 specSize 即尺寸大小,我們看一下 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( int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
複製程式碼
可以看到 MeasureSpec 通過把 specMode 和 specSize 打包成一個int值來避免過多的記憶體分配,內部也提供了打包和解包的方法,即可以把 specMode、specSize 打包為一個 MeasureSpec 的32位int值,也可以通過解包 MeasureSpec 得到 specMode、specSize 的int值。
SpecMode 的三種型別:
-
UNSPECIFIED: 父容器不對 view 有任何限制,view 要多大給多大。一般用於系統內部,可以不用特別關注
-
EXACTLY: 父容器檢測到 view 所需要的精確大小,這時view的最終測量結果就是 specSize 指定的值。它對應於 LayoutParams 中的 match_parent 和 具體數值這兩種情況
-
AT_MOST: 父容器指定了一個可用大小即 specSize,子view 大小不能大於這個值。對應 LayoutParams 中的 wrap_content
MeasureSpec 的生成 :
MeasureSpec 的生成是由父容器的 MeasureSpec 和當前 view 的LayoutParams 共同決定的,但是對於頂級VIew (DecorView)和普通 View 來說它的轉換過程則有所不同。對於 DecorView,它的 MeasureSpec 由視窗的尺寸和自身的 LayoutParams 來決定。而普通 View,則是由父容器的 MeasureSpec 和自身的 LayoutParams 來決定。
- 如果這段話你看的糊里糊塗腦闊子疼,請先往下看,瞭解了 DecorView 和 普通 View 的測量過程後,這段話就很明朗了
(二)瞭解 ViewRoot
在介紹View的三大流程前,首先需要了解 ViewRoot,它對應 ViewRootImpl 這個類,它是連線 WindowManager 和 DecorView 的紐帶,View的三大流程是由 ViewRootImpl 來完成的。在 ActivityThread 中, 當 Activity 物件被建立完畢後,會將 DecorView 新增到 Window 中,同時會建立 ViewRootImpl 物件,並將 ViewRootImpl 和 DecorView 相關聯
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
複製程式碼
- View 的繪製流程是從 ViewRootImpl 的 performTraversals() 開始的,這個方法巨長,我就挑幾個大家看一下就明白了
private void performTraversals() {
...
measureHierarchy(host, lp, mView.getContext().getResources(),desiredWindowWidth, desiredWindowHeight);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
複製程式碼
如上可以清晰的看到, 方法內部會依次呼叫 performMeasure、performLayout、performDraw,這三個方法分別完成頂級 View 的 measure、layout、draw,大體流程如下圖
performMeasure 方法中會呼叫 measure 方法, measure 方法又呼叫 onMeasure 方法, 在 onMeasure 中遍歷所有子元素並對子元素進行 measure 過程, 這時 measure 流程就從父容器傳遞到子元素中了, 這樣就完成了一次 measure 流程。接著子元素重複進行父容器的 measure 過程, 如此反覆直到完成整個 view 樹的遍歷。performLayout 和 performDraw 的傳遞流程是類似的,唯一不同的是 performDraw 的傳遞是在 draw 方法中通過 dispatchDraw 來實現的,不過這沒有本質區別。
而在performTraversals 的 measureHierarchy() 方法中, 可以看到 DecorView 的 MeasureSpec 建立過程, 其中 desiredWindowWidth 和 desiredWindowHeight 是螢幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
複製程式碼
看一下 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 產生過程是由它的 LayoutParams 中的寬/高引數來劃分:
- ViewGroup.LayoutParams.MATCH_PARENT:精確模式,大小就是視窗的大小
- ViewGroup.LayoutParams.WRAP_CONTENT:最大模式,大小不定但不能超過視窗的大小
- 固定大小(dp、px):大小為 LayoutParams 中指定的大小
(三)measure 流程
-
什麼時候需要呼叫 onMeasure( )? : 當父容器要放置該View時呼叫View的onMeasure()。ViewGroup會問子控制元件View一個問題:“你想要用多大地方啊?”,然後傳入兩個引數 —— widthMeasureSpec 和 heightMeasureSpec;這兩個引數指明控制元件可獲得的空間大小 (SpecSize) 以及關於這個空間描述 (SpecMode) 的後設資料。然後子控制元件把自己的尺寸儲存到 setMeasuredDimension() 裡,告訴父容器需要多大的控制元件放置自己。在 onMeasure() 的最後都會呼叫 setMeasuredDimension();如果不呼叫,將會由 measure() 丟擲一個 IllegalStateException()。
-
setMeasuredDimension(): 可以簡單理解為給 mMeasuredWidth 和 mMeasuredHeight 設值,如果這兩個值一旦設定了,則意味著對於這個View的測量結束了,View的寬高已經有了測量的結果。如果我們想設定某個View的高寬,完全可以直接通過setMeasuredDimension(100,200)來設定死它的高寬(不建議),但是 setMeasuredDimension 方法必須在 onMeasure 方法中呼叫,不然會拋異常。
1. View 的 測量過程 :
View 的測量過程比較簡單,因為沒有子元素,通過 measure 方法就完成了其的測量過程,而 measure 方法是被 final 修飾的, 意味著子類不能重寫這個方法。在 measure() 方法中則會去呼叫 onMeasure() 方法, 我們主要看一下 onMeasure() 方法內部的實現:
/**
* 引數 widthMeasureSpec 和 heightMeasureSpec 是父容器當前剩餘控制元件的大小,即子元素的可用尺寸
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
內部很簡潔,呼叫 setMeasuredDimension 會設定 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 的情況,則 getDefaultSize 的返回值就是 specSize,而 specSize 就是 View 測量後的尺寸大小 (注意區分測量後的大小和最終的大小, 最終的大小是在 layout 流程結束後確定的,雖然幾乎所有的情況下兩個值是相等的)。
至於 UNSPECIFIED 一般用於系統內部的測量過程,這時 getDefaultSize 的返回值是傳入的第一個引數 size,此時這個 size 的值則由 getSuggestedMinimumWidth() 方法決定,看一下內部實現:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
#Drawable.java
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
複製程式碼
getSuggestedMinimumWidth 的返回值和View設定的背景有關, 如果沒有設定背景, 則返回 mMinWidth 的值, 即對應 xml 中 android:minWidth 屬性的值, 沒設定預設是0。設定了背景則呼叫它(Drawable)的 getMinimumWidth 方法,該方法獲取的是 Drawable 的原始尺寸值,沒有的原始尺寸值則為0。
-
從上述程式碼中我們可以得出:直接繼承 View 的自定義控制元件,需要重寫 onMeasure 方法並設定在 wrap_content 時自身的尺寸大小,否則在 xml 佈局中使用 wrap_content 相當於使用 match_parent 。
-
為啥?: 從 getDefaultSize 方法中清晰的看到,當 AT_MOST 情況即佈局是 wrap_content 時,getDefaultSize 返回的結果是 specSize 也就是父容器當前剩餘的控制元件大小,這和在佈局中使用 match_parent 的效果完全一致。
-
怎麼處理?: 解決也很簡單,在 onMeasure 中對於佈局中使用 wrap_content 的情況,即 mode = MeasureSpec.AT_MOST 時, 呼叫 setMeasuredDimension() 給 View 的寬和高設定一個預設的尺寸, 對於其它情況則沿用系統的測量值即可。具體的預設尺寸看實際需求就可以。
2. ViewGroup 的 測量過程 :
測量子元素的過程: measureChildren
在 ViewGroup 的測量過程中,需要先遍歷並測量子View (通過呼叫它們的 measure 方法, 然後各個子元素再去遞迴執行這個過程),等子View測量結果出來後,再對自己進行測量。而 ViewGroup 是一個抽象類,它並沒有重寫 onMeasure 方法,但是它提供了一個 measureChildren 方法, 是用來遍歷子元素並進行測量的方法, 方法內部呼叫 measureChild 測量子元素, 看一下 measureChildren 的內部實現 :
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
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);
}
複製程式碼
在 measureChildren 方法中, 先遍歷所有的子元素, 然後執行 measureChild 方法對子元素進行測量。在實際情況中,ViewGroup 的實現子類 (例如FrameLayout、LinearLayout) 則是直接使用它封裝的另外一個方法 measureChildWithMargins 來測量某個子元素, 該方法實現和 measureChild 方法基本類似,所以這裡直接分析 measureChildWithMargins 方法:
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
int widthUsed, nt parentHeightMeasureSpec, int heightUsed) {
// 先提取子元素的 LayoutParams, 即在xml中設定的 你在xml的layout_width和
// layout_height, layout_xxx的值最後都會封裝到這個個LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 呼叫 getChildMeasureSpec 方法, 傳入父容器的 MeasureSpec ,父容器自己的padding
// 和子元素的margin以及已經用掉的大小(widthUsed), 來計算出子元素的 MeasureSpec
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);
// 接著把 MeasureSpec 傳給子元素的 measure 方法進行測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製程式碼
在 measureChildWithMargins方法中,先提取子元素的 LayoutParams,再通過 getChildMeasureSpec 來建立子元素的 MeasureSpec,然後把 MeasureSpec 直接傳遞給子元素的 measure 方法進行測量。繼續看 getChildMeasureSpec 方法內部實現:
/**
* spec: 父容器的 MeasureSpec
* padding: 父容器的Padding + 子View的Margin + 已經用掉的大小(widthUsed)
* childDimension: 表示該子元素的 LayoutParams 屬性的值(lp.width、lp.height)
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
// specSize 是父容器的尺寸
int specSize = MeasureSpec.getSize(spec);
// size 是子元素可用的尺寸, 即父容器減去padding剩下的尺寸大小
int size = Math.max(0, specSize - padding);
// resultSize 和 resultMode 是最終要返回的結果
int resultSize = 0;
int resultMode = 0;
// 根據父容器的 specMode 測量模式進行分別處理
switch (specMode) {
// Parent has imposed an exact size on us
// 父容器的測量模式是EXACTLY
case MeasureSpec.EXACTLY:
// 根據子元素的 LayoutParams 屬性分別處理
if (childDimension >= 0) {
// 子元素的 LayoutParams 是精確值(dp/px)
resultSize = childDimension; // 等於設定的尺寸
resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
// 子元素的 LayoutParams 是MATCH_PARENT
resultSize = size; // 等於父容器尺寸
resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
// 子元素的 LayoutParams 是WRAP_CONTENT
resultSize = size; // 暫時等於父容器尺寸
resultMode = MeasureSpec.AT_MOST; // Mode是AT_MOST
}
break;
// Parent has imposed a maximum size on us
// 父容器的測量模式是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
// 子元素的 LayoutParams 是精確值(dp/px)
resultSize = childDimension; // 等於設定的尺寸
resultMode = MeasureSpec.EXACTLY; // Mode是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; // Mode是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; // Mode是AT_MOST
}
break;
// Parent asked to see how big we want to be
// 父容器的測量模式是UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension; // 等於設定的尺寸
resultMode = MeasureSpec.EXACTLY; // Mode是EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = ? 0; // 暫等於0, 值未定
resultMode = MeasureSpec.UNSPECIFIED; // Mode是UNSPECIFIED
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0; // 暫等於0, 值未定
resultMode = MeasureSpec.UNSPECIFIED; // Mode是UNSPECIFIED
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製程式碼
上面清楚展示了普通 View 的 MeasureSpec 建立規則,通過下面的表,可以對該內容進行清晰的梳理:
通過之前 View 對自身的測量過程,和 ViewGroup 對子元素的測量過程,可以清楚的看到 View 的 MeasureSpec 的生成,是由父容器的 MeasureSpec 和當前 view 的LayoutParams 共同決定的, 驗證了我之前說的那一段話。
-
另外需要注意的是, 當父容器是 AT_MOST 而子元素的 LayoutParams 是 WRAP_CONTENT 時, 父View的大小是不確定(只知道最大隻能多大),子View又是WRAP_CONTENT,那麼在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。這是 View 中的預設實現。
-
而對於其他的一些View的派生類,如TextView、Button、ImageView等,它們的onMeasure方法系統了都做了重寫,不會這麼簡單直接拿 MeasureSpec 的size來當大小,而去會先去測量字元或者圖片的高度等,然後拿到View本身content這個高度(字元高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那麼可以直接用View本身content的高度(字元高度等),而不是像 View.java 中直接用MeasureSpec的size做為View的大小。
測量自己的過程 : onMeasure (通過 LinearLayout 分析)
onMeasure ( )
在 ViewGroup 中沒有定義其測量的具體過程, 它本身是一個抽象類, 它的測量過程需要子類去具體實現。因為不同的子類有不同的佈局特性,從而導致它們的測量過程各不相同,VIewGroup 無法對此做統一實現。下面通過 LinearLayout 的 onMeasure 方法來分析 ViewGroup 的測量過程。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
複製程式碼
measureVertical( )
方法比較簡潔,明顯是根據設定的 orientation 來對應不同的測量方法,measureVertical 和 measureHorizontal 內部實現類似,我們選擇看一下 measureVertical 的內部,即豎直佈局的情況, 方法比較長, 這裡我分段去分析一下:
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
// 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).
// 遍歷子元素並測量它們
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// mTotalLength 是用來儲存 LinearLayout 在豎直方向上的高度
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
// 每測量一個子元素,mTotalLength 會儲存它的高度以及它豎直方向上的 margin
mTotalLength = Math.max(totalLength, totalLength + childHeight +
lp.topMargin +lp.bottomMargin + getNextLocationOffset(child));
複製程式碼
從上面一段程式碼可以看出來, 這裡先遍歷子元素, 然後執行 measureChildBeforeLayout 方法, 在方法內部會去執行 measureChildWithMargins 對子元素進行測量, 這個方法我們剛分析過。接著看 mTotalLength 則是用來儲存 LinearLayout 在豎直方向上的高度, 它會儲存每一個測量完的子元素的高度和它豎直方向上的 margin。
在測量完子元素之後, LinearLayout 會對自己進行測量並儲存尺寸, 繼續看 measureVertical 方法中後面的程式碼:
// 加上自己豎直方向上的 padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec,
childState), heightSizeAndState);
複製程式碼
對於豎直的 LinearLayout 在測量自己的尺寸時, 它水平方向上的測量過程會遵循 View 的測量過程, 而豎直方向的測量則有所不同, 然後執行 resolveSizeAndState 方法來生成豎直高度的 MeasureSpec ,即程式碼中的變數 heightSizeAndState , 我們看一下它的實現過程 :
resolveSizeAndState( )
/**
* size: 是 mTotalLength, 即豎直方向上所有子元素的高度總和
* measureSpec: 父容器傳過來的期望尺寸, 即剩餘空間
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
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);
}
複製程式碼
可以看到, 如果 LinearLayout 的佈局高度是 match_parent 或者 具體數值, 則它的測量過程和 View 是一致的, 高度是 specSize。如果佈局高度是 wrap_content, 則它的高度是豎直方向左右子元素高度的總和, 但這個值仍不能大於 specSize
(四)獲取 View 的測量寬/高
- 到這裡 View 的測量流程就結束了,在三大流程中 measure 是最複雜的一個,在 measure 結束後就可以通過 getMeasuredWidth/Height() 正確的獲得 View 的測量寬/高。但是據說在某些極端情況下,系統需要多次呼叫 measure 才能準備的測量出結果,所以一般比較穩妥的做法是在 onLayout 方法中去獲取測量寬/高或者最終寬/高。
現在有這樣一個問題:怎樣在 Activity 啟動時,即在 onCreate 方法中獲取 View 的寬高呢? 如果直接在 onCreate 中呼叫 getMeasuredWidth/Height() 是不能正確獲取它的尺寸值的, 而且同樣在 onResume 和 onStart 中都是不準確的,因為你無法保證此時 View 的測量過程已經完成了,如果沒有完成,得到的值則為0。
1. Activity/View 的 onWindowFocusChanged(boolean hasFocus) onWindowFocusChanged 表示 View 已經初始化完畢了, 這時獲取它的寬/高是沒問題的。 這個方法是當 Activity/View 得到焦點和失去焦點時都會呼叫一次, 在 Activity 中對應 onResume 和 onPause ,如果頻繁的進行 onResume 和 onPause, 則 onWindowFocusChanged 也會被頻繁的呼叫。
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
複製程式碼
2. view.post(runnable): 通過 post 將一個 runnable 訊息投遞到訊息佇列的底部,然後等待 Looper 呼叫此 runnable 的時候,View 已經初始化好了
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
view.post(new Runnable(){
@Override
public void run(){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
複製程式碼
3. ViewTreeObserver ViewTreeObserver 的眾多回撥可以完成這個需求, 例如使用 OnGlobalLayoutListener 這個介面, 當 view 樹的狀態改變或者 view 樹內部 view 的可見性改變, 都會回撥 onGlobalLayout 方法。
// 方法1:增加整體佈局監聽
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
}
});
// 方法2:增加元件繪製之前的監聽
ViewTreeObserver vto =view.getViewTreeObserver();
vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
}
});
複製程式碼
4. view.measure(int widthMeasureSpec, int heightMeasureSpec) 這是通過手動觸發對 View 進行 measure 來得到 View 的寬/高的方法。需要根據 View 的 LayoutParams 情況來分別處理:
-
**match_parent:**無法測量寬/高,根據前面分析的 View 測量過程,此時構造它的 MeasureSpec 需要知道父容器的剩餘控制元件,而此時我們無法獲取,則理論上講無法測出 View 的大小。
-
具體的數值(dp / px): 比如寬高都是200, 直接通過 MeasureSpec.makeMeasureSpec 手動構造它的寬和高尺寸, 然後傳入 view.measure 方法觸發測量 :
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(200, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
複製程式碼
- wrap_content
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);
複製程式碼
1 << 30 - 1 就是30位 int 值的最大值, 也就是30個1。前面介紹 MeasureSpec 時說到 View 的尺寸用30位的int值表示,此時我們是用 View 理論上能支援的最大值去構造 MeasureSpec ,相當於給 View 一個足夠的範圍空間去完成自己的測量並儲存自己的測量結果, 是可行的。
- 有兩個錯誤用法: 違背了系統的內部實現規範, 因為無法通過錯誤的 MeasureSpec 去得到合法的 SpecMode, 導致測量過程有錯。
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(-1 , View.MeasureSpec.UNSPECIFIED
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(- 1, View.MeasureSpec.UNSPECIFIE
view.measure(widthMeasureSpec, heightMeasureSpec);
// 這個我自己在7.0版本的編譯環境下已經編譯不通過了,在 makeMeasureSpec
// 方法的第一個引數需要傳入 0 ~ 1073741823 範圍的值, -1 不合法。
複製程式碼
view.measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
// measure 方法引數不合法
複製程式碼
看到這裡, 三大流程中關於 measure 的知識點已經總結完了, 如果你覺得有不理解的地方或者有更好的見解還請提出來, 讓我們共同學習一起成長。
如果覺得收穫,點個贊再走唄~