Measure要知道的点

1.measure和onMeasure

View中和测量过程相关的方法有三个,measure、onMeasure和setMeasuredDimension。


image

1.View与ViewGroup的不同

  • View的measure调用onMeasure.
  • View 中的onMeasure只调用了setMeasuredDimension来设置自身高度。
  • 在ViewGroup中,onMeasure方法并没有被重写法,所以继承ViewGroup自定义ViewGroup一定要重写onMeasure来测量子View,否则不会测量子View。
  • 在自定义View中,onMeasure方法是在一般情况下使用父容器提供的宽高,除了UNSPECIFIED情况下使用最小尺度。继承View的自定义要根据业务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时,数值取自己的最小值;为AT_MOST和EXACTLY时,取父容器传递的数值,也就是Wrap_Content和Match_Paraent

2.1.2 setMeasuredDimension

setMeasuredDimension方法是设置view宽高的方法,也是onMeasure必须要调用的方法

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

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

setMeasuredDimensionRaw是最终被调用的方法,保存宽高,修改标识符。

如果在onMeasure中没有调用setMeasuredDimension来设置宽高,在measure中,调用onMeasure后,会通过mPrivateFlags来判断是否设置了宽高,没有设置就会抛出异常。所以说setMeasuredDimension是必须要在onMeasure中被调用

2.2 自定义View重写onMeasure

从getDefaultSize方法实现来看,直接继承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情况有了特别处理,在不大于父容器要求的尺寸下,使用用户自己规定的尺寸。

上面的result就是保存的measureSpec的mode值,唯一不同的是第31位被用于标示尺寸是不是达到了View想要的宽度,如果不满足,则标为1。下面把2位标示直观表示下:

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

resolveSizeAndState的结果在resolveSize进行了截取,只取了数值部分,用来标记量算完的尺寸是不是达到了View想要的宽度的高八位没有保留。

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

而getMeasuredWidth和getMeasuredWidthAndState的区别也在此,getMeasuredWidth是获取存数值,而getMeasuredWidthAndState还带有高位state。

线性布局中就用了resolveSizeAndState来获取宽高。自己使用时可配合父容器使用。

3.ViewGroup的onMeasure

虽然ViewGroup没有实现omMeasure的过程,但是它提供了两个工具方法:measureChildren()和getChildMeasureSpec()。

我们都知道父容器会调用子View的measure方法来测量子View,那传入的参数int widthMeasureSpec, int heightMeasureSpec是怎么来的呢?

3.1 理解MeasureSpec

系统会将View的LayoutParams根据父容器所施加的规则转成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高。

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。

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宽高是不为负数的。

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时,取得宽高为方法参数中的尺寸(例如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 视图树的测量过程

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容