1. 程式人生 > >Android學習筆記之MeasureSpec

Android學習筆記之MeasureSpec

什麼是MeasureSpec

Android系統在繪製View的時候,過程是十分複雜的,其中頻繁的使用到了MeasureSpec。那麼MeasureSpec是什麼?有什麼用?簡單點說,它是一個int值的中間變數,用來儲存View的尺寸規格。再說細點,在測量過程中,系統會將View的LayoutParams根據父容器所施加的約束規則轉換成對應的MeasureSpec。

MeasureSpec代表一個32位int值,高兩位代表SpecMode,低30位代表SpecSize,SpecMode是指測試模式,而SpecSize是指在某中測試模式下的規格大小。原始碼如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    //高兩位11
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //高兩位00
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //高兩位01
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //高兩位10
    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 makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
   
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //取高2位得模式
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        //取低30位得規格大小
        return (measureSpec & ~MODE_MASK);
    }
}

什麼是約束規則

上面解釋MeasureSpec時,提及到父容器的約束規則,可能會讓我們聯想到 match_parent 和 wrap_content,但這裡說的約束規則並不完全是這樣。具體來說,約束規則指的是MeasureSpec中的三種模式,即SpecMode,它是通過與 match_parent 和 wrap_content 相關的邏輯判斷最後決定下來的。每一種模式的含義如下:

  • UNSPECIFIED:父容器不對View有任何限制,要多大給多大,一般用於系統內部,表示一種測試狀態。
  • EXACTLY:表示View的規格為確切值,即SpecSize所指定的值。它對應於match_parent和具體的數值這兩種模式。
  • AT_MOST:表示View的規格不能超過某個值,具體是什麼值要看不同View的具體實現。它對應於wrap_content。

MeasureSpec和LayoutParams的關係

上面提到, 在測量過程中,系統會將View的LayoutParams根據父容器所施加的約束規則轉換成對應的MeasureSpec。這裡需要注意的是,對於頂級View(DecorView)來說,其MeasureSpec由視窗的尺寸和其自身的LayoutParams來共同決定;對於普通View來說,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定。

對DecorView來說,建立MeasureSpec的原始碼如下:(其中desireWindowWidth和desireWindowHeight是螢幕的尺寸)

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的LayoutParams中的寬/高參數為match_parent,那麼它的MeasureSpec的模式為精確模式,尺寸為視窗大小
  • 如果DecorView的LayoutParams中的寬/高參數為wrap_content,那麼它的MeasureSpec的模式為最大模式,最大尺寸為視窗大小
  • 如果DecorView的LayoutParams中的寬/高參數為具體值(例如100dp),那麼它的MeasureSpec的模式為精確模式,尺寸為指定的具體值(100dp)

對普通View來說,View的measure過程是由它的父容器ViewGroup呼叫的,如下:

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方法獲取子元素的寬高MeasureSpec,然後呼叫子元素的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:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } 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
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // 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);
}

上述程式碼不難理解,總的思路是按照父容器的SpecMode分為三個分支,然後在每個分之中結合子元素本身的LayoutParams來確定子元素的MeasureSpec。通過上述程式碼可以得出以下結論:

  • 當子View採用固定寬/高時,不管父容器的MeasureSpec是什麼模式,View的MeasureSpec都是精確模式並且大小為LayoutParams中指定的具體值
  • 當子View的寬/高是 match_parent 時,如果父容器的模式是精確模式,那麼子View也是精確模式並且大小是父容器的剩餘空間;如果父容器的模式是最大模式,那麼子View也是最大模式並且大小不會超過父容器的剩餘空間
  • 當子View的寬/高是 wrap_content 時,不管父容器是最大模式還是精確模式,子View的模式總是最大模式並且大小不會超過父容器的剩餘空間

PS:上面的總結沒有考慮UNSPECIFIED模式,是因為該模式主要用於系統內部多次Measure的情形,通常情況下我們不需要關注它。