系列文章傳送門 (持續更新中..) :
自定義控件(一) 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 = ;
private static final int MODE_MASK = << MODE_SHIFT;
public static final int UNSPECIFIED = << MODE_SHIFT;
public static final int EXACTLY = << MODE_SHIFT;
public static final int AT_MOST = << 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 > ? intrinsicWidth : ;
}
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 = ; 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(, specSize - padding);
// resultSize 和 resultMode 是最終要傳回的結果
int resultSize = ;
int resultMode = ;
// 根據父容器的 specMode 測量模式進行分别處理
switch (specMode) {
// Parent has imposed an exact size on us
// 父容器的測量模式是EXACTLY
case MeasureSpec.EXACTLY:
// 根據子元素的 LayoutParams 屬性分别處理
if (childDimension >= ) {
// 子元素的 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 >= ) {
// 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 >= ) {
// 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, 值未定
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, 值未定
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 = ; 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, ,
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, );
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(, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
- wrap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec( << - , View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec( << - , 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(- , View.MeasureSpec.UNSPECIFIED
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(- , View.MeasureSpec.UNSPECIFIE
view.measure(widthMeasureSpec, heightMeasureSpec);
// 這個我自己在版本的編譯環境下已經編譯不通過了,在 makeMeasureSpec
// 方法的第一個參數需要傳入 ~ 範圍的值, - 不合法。
view.measure(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
// measure 方法參數不合法
看到這裡, 三大流程中關于 measure 的知識點已經總結完了, 如果你覺得有不了解的地方或者有更好的見解還請提出來, 讓我們共同學習一起成長。
如果覺得收獲,點個贊再走呗~