自定義控制元件(三) 原始碼分析measure流程

飯小龍發表於2017-12-22

系列文章傳送門 (持續更新中..) :

自定義控制元件(一) 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,大體流程如下圖

借用一下剛哥《Android開發藝術探索》裡的圖

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 的知識點已經總結完了, 如果你覺得有不理解的地方或者有更好的見解還請提出來, 讓我們共同學習一起成長。

如果覺得收穫,點個贊再走唄~

相關文章