1. 程式人生 > >自定義View(一)View工作原理之測量 measure

自定義View(一)View工作原理之測量 measure

在Android中,一個View繪製出來要經過三大流程,分別用measure來測量View的寬高,用layout來確定View在父容器中的位置,最終用draw將View繪製到螢幕上。本章節主要,通過自己的理解來講解一下第一個流程measure的相關知識點。

measure方法在View類中,在此方法中會 呼叫onMeasure方法,一般我們通過重寫onMeasure方法去自定義View的寬高。ViewGroup類中沒有實現onMeasure方法,所以在自定義ViewGroup的時候,我們要重寫onMeasure方法,在onMeasure方法中對其所有子元素進行measure過程,通過這種方式將測量行為傳遞子view,如此類推view的測量行為,從而完成整個View樹的測量工作。

講到view的measure行為,我們首先要一個重要的類MeasureSpec,這個類主要用來儲存測量模式SpecMode,規格大小SpecSize,通過一個32位的int值的高2位和低30去儲存它們的值。其中SpecMode有三種模式:UNSPECIFUED、EXACTLY、AT_MOST。其中我們主要理解EXACTLY和AT_MOST:

1. EXACTLY 表示父容器已經檢測出當前view的大小,就是view在SpecSize中所指定的值,它對應於LayoutParams中的match_parent和具體數值兩種模式

2. AT_MOST 表示當前view的大小不超過父容器的可用大小,它對應於LayoutParams中的wrap_content。

接下來我們解決一下ViewGroup的measure過程,雖然ViewGroup沒有實現onMeasure方法,但它提供了一些重要的方法用來測量child,先看一下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);
        }
    }
} 

           從上述程式碼可以看出,ViewGroup會對每一個子元素進行measure,我們再看measureChild方法,程式碼如下

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);
}

            上面程式碼中主要是通過呼叫getChildMeasureSpec方法來獲取子元素的測量值,然後通過child的measure方法將測量行為傳遞下去,我們看看getChildMeasureSpec方法的程式碼

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: //父容器精確模式
        //子view是精確值
        if (childDimension >= 0) { 
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } 
        // 子view模式MATCH_PARENT,大小為父容器可用的最大空間
        else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } 
        // 子view模式WRAP_CONTENT,大小為父容器可用的最大空間
        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
    //父容器AT_MOST模式
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) { //子view精確值
            // 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);
}

           程式碼有點長,在這裡我們可以清晰地看到,child的MeasureSpec是怎麼來的,怎麼通過child的layoutparams值和父容器的MeasureSpec去計算得到當前child的MeasureSpec的,有一點要注意的是,預設的情況下,child的layoutparams值不管是match_parent還是wrap_content,其resultSize都是父容器可用空間的大小。

          上面分析了ViewGroup的相關measure行為,主要是對子元素的測量,具體ViewGroup的大小該怎麼確定,這個要自定義的時候在onMeasure方法中計算,因為ViewGroup是一個view容器,所以它的寬高是很難有統一的計算方法,不同的情況計算方法又不一樣,所以ViewGroup沒有去實現onMeasure方法,留給繼承者去實現。

           下面我們看一下View類的onMeasure方法,程式碼如下

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

            可以看到,預設通過getDefaultSize去獲取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這兩種情況,所以其實此方法返回的就是specSize,而這個值就是view測量後的大小,因為specSize的值在AT_MOST的情形下,是等於父容器可用大小的,而如果在佈局中使用wrap_content,specSize大小和match_parent是一樣的,所以我們一般在自定義view的時候,對於warp_content的情形,都會給view指定一個自定義的大小,以免和match_parent一樣。

            總結一下,通過上面的分析,我們知道view的measure流程大概是怎麼的一個過程,即我們首先會在ViewGroup的繼承類的onMeasure方法中,要分別去測量父容器的子view,具體怎麼通過測量出的child的寬高去計算ViewGroup的寬高,這個需要自己根據具體需求去實現。如果我們要子view中進行自定義的measure行為,就要重寫其onMeasure方法,根據需求是實現相關測量邏輯。整個測量流程是從ViewGroup到view的從上到下的行為,從而完成了我們整個view樹的測量工作。