Measure要知道的點

1.measure和onMeasure

View中和測量過程相關的方法有三個,measure、onMeasure和setMeasuredDimension。


image

1.View與ViewGroup的不同

  • View的measure調(diào)用onMeasure.
  • View 中的onMeasure只調(diào)用了setMeasuredDimension來設置自身高度。
  • 在ViewGroup中,onMeasure方法并沒有被重寫法,所以繼承ViewGroup自定義ViewGroup一定要重寫onMeasure來測量子View,否則不會測量子View。
  • 在自定義View中,onMeasure方法是在一般情況下使用父容器提供的寬高,除了UNSPECIFIED情況下使用最小尺度。繼承View的自定義要根據(jù)業(yè)務onMeasure要選擇性重寫。

2.View的onMeasure

2.1 原生的onMeasure

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

其中獲取寬高的主要方法是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;
}

當父容器傳來的MeasureSpec為UNSPECIFIED時,數(shù)值取自己的最小值;為AT_MOST和EXACTLY時,取父容器傳遞的數(shù)值,也就是Wrap_Content和Match_Paraent

2.1.2 setMeasuredDimension

setMeasuredDimension方法是設置view寬高的方法,也是onMeasure必須要調(diào)用的方法。

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimensionRaw是最終被調(diào)用的方法,保存寬高,修改標識符。

如果在onMeasure中沒有調(diào)用setMeasuredDimension來設置寬高,在measure中,調(diào)用onMeasure后,會通過mPrivateFlags來判斷是否設置了寬高,沒有設置就會拋出異常。所以說setMeasuredDimension是必須要在onMeasure中被調(diào)用

2.2 自定義View重寫onMeasure

從getDefaultSize方法實現(xiàn)來看,直接繼承View的自定義控件要重寫onMeasure方法并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。

使用 resolveSize() 來讓子 View 的計算結果符合父 View 的限制(當然,如果你想用自己的方式來滿足父 View 的限制也行)。

public static int resolveSize(int size, int measureSpec) {
    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
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) {
            //標記量算完的尺寸沒有達到了View想要的寬度
                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);
}

相比原生的的onMeasure中的getDefaultSize,resolveSize對于Wrap_Content情況有了特別處理,在不大于父容器要求的尺寸下,使用用戶自己規(guī)定的尺寸。

上面的result就是保存的measureSpec的mode值,唯一不同的是第31位被用于標示尺寸是不是達到了View想要的寬度,如果不滿足,則標為1。下面把2位標示直觀表示下:

UNSPECIFIED 00
EXACTLY     01
AT_MOST     10
result = AT_MOST | MEASURED_STATE_TOO_SMALL  11

resolveSizeAndState的結果在resolveSize進行了截取,只取了數(shù)值部分,用來標記量算完的尺寸是不是達到了View想要的寬度的高八位沒有保留。

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredWidthAndState() {
    return mMeasuredWidth;
}

而getMeasuredWidth和getMeasuredWidthAndState的區(qū)別也在此,getMeasuredWidth是獲取存數(shù)值,而getMeasuredWidthAndState還帶有高位state。

線性布局中就用了resolveSizeAndState來獲取寬高。自己使用時可配合父容器使用。

3.ViewGroup的onMeasure

雖然ViewGroup沒有實現(xiàn)omMeasure的過程,但是它提供了兩個工具方法:measureChildren()和getChildMeasureSpec()。

我們都知道父容器會調(diào)用子View的measure方法來測量子View,那傳入的參數(shù)int widthMeasureSpec, int heightMeasureSpec是怎么來的呢?

3.1 理解MeasureSpec

系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)成對應的MeasureSpec,然后再根據(jù)這個MeasureSpec來測量出View的寬和高。

MeasureSpec代表一個32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規(guī)格大小。

SpecMode:UNSPECIFIED,EXACTLY, AT_MOST(wrap_content)

UNSPECIFIED 00
EXACTLY     01
AT_MOST     10
MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);

通過上面這個方法將尺寸和mode拼接成MeasureSpec。

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

可以看出,MODE_MASK就是32位int值,高兩位為1,通過與MODE_MASK的與或來獲取mode和size再拼接。

其實View中的MEASURED_SIZE_MASK = ~MODE_MASK,MEASURED_STATE_MASK = MODE_MASK;

public static final int MEASURED_SIZE_MASK = 0x00ffffff;
public static final int MEASURED_STATE_MASK = 0xff000000;

3.2 ViewGroup的getChildMeasureSpec

getChildMeasureSpec就是獲取要傳遞給子View的MeasureSpec的方法,要傳給子View的MeasureSpec就是通過這個獲取的。它通過
MeasureSpec與子View的寬度來獲取值。

public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;

所以在LayoutParams中,設置了固定長度(有意義)的View寬高是不為負數(shù)的。

3.2.1 子View設置了layout_width(height)
if (childDimension >= 0) {
    //子類自己決定size
    resultSize = childDimension;
    resultMode = MeasureSpec.EXACTLY;
}

無論哪種specMode,對于設置了精確長度的子View,取設置的寬高,且mode為EXACTLY。

3.2.2 layout_width(height)="wrap_content"
if (childDimension == LayoutParams.WRAP_CONTENT) {
    // 子類自己決定size,但不能超過父類
    resultSize = size;
    resultMode = MeasureSpec.AT_MOST;
}

除了specMode為UNSPECIFIED,子View為wrap_content時,取得寬高為方法參數(shù)中的尺寸(例如LinearLayout中為其父容器測量傳遞的MeasureSpec),mode為AT_MOST。

3.2.3 layout_width(height)="match_content"

不考慮UNSPECIFIED

switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension == LayoutParams.MATCH_PARENT) {
            // 等于父容器的寬度
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        }
        break;
    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension == LayoutParams.MATCH_PARENT) {
            // 子View想和父容器尺寸一樣,但父容器自己也還沒確定尺寸
            resultSize = size;
            // 子View尺寸肯定不能大于父容器
            resultMode = MeasureSpec.AT_MOST;
        }

父容器如果有確定的size,那子View就和父View一樣;如果沒有確定尺寸,那測量子View的MeasureSpec和父容器一樣。

4 參考

源碼解析Android中View的measure量算過程

一篇文章理解Android 視圖樹的測量過程

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容