自定義控件(三) 源碼分析measure流程

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

自定義控件(一) Activity的構成(PhoneWindow、DecorView)android

自定義控件(二) 從源碼分析事件分發機制bash

自定義控件(四) 源碼分析 layout 和 draw 流程ide


在以前的文章中,咱們比較清晰的瞭解了Activity的構成和事件分發機制的原理, 從這篇文章咱們開始分析 view 的三個流程:測量,佈局,繪製。oop

  • 在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值。post

SpecMode 的三種類型:

  • UNSPECIFIED: 父容器不對 view 有任何限制,view 要多大給多大。通常用於系統內部,能夠不用特別關注學習

  • EXACTLY: 父容器檢測到 view 所須要的精確大小,這時view的最終測量結果就是 specSize 指定的值。它對應於 LayoutParams 中的 match_parent 和 具體數值這兩種狀況ui

  • 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 的知識點已經總結完了, 若是你以爲有不理解的地方或者有更好的看法還請提出來, 讓咱們共同窗習一塊兒成長。

若是以爲收穫,點個贊再走唄~

相關文章
相關標籤/搜索