View系列 (二) — MeasureSpec 详解

一、概述

在 View 的测绘 (measure()) 过程中,会用到 MeasureSpe (测量规格)。

MeasureSpec的作用

确切来说,MeasureSpec 在很大程度上决定了一个 View 的尺寸,因为父容器会影响 子View 的MeasureSpec 创建过程。
在测量过程中,系统会将子 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后再根据这measureSpec 来测量出View的宽/高。

版本: Android SDK 29
关联文章:

  1. 《View系列 (一) — Android 坐标系》
  2. 《View系列 (二) — MeasureSpec 详解》
  3. 《View系列 (三) — Measure 流程详解》
  4. 《View系列 (四) — Layout 流程详解》
  5. 《View系列 (五) — Draw 流程详解》

二、MeasureSpec 的构成

1. MeasureSpec 中的常量

MeasureSpec.class

// MeasureSpec.class
// int的大小为32位,高两位作为标记位,其余30位表示尺寸大小
private static final int MODE_SHIFT = 30;
// 3向左进位30 = 11 00000000000(11后跟30个0)  
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

// UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY     = 1 << MODE_SHIFT;
// AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST     = 2 << MODE_SHIFT;

从 MeasureSpec 的常量可以看出,它代表了32位的 int 值, 其中高 2 位代表了 SpecMode,低30位则代表 SpecSize。SpecMode指的是测量模式, SpecSize指的是测量大小。

SpecMode 分为三类:

UNSPECIFIED: 不对View进行任何限制,要多大给多大,一般用于系统内部。
EXACTLY: 对应 LayoutParams 中的 match_parent 和 具体数值这两种模式。检测到 View 所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
AT_MOST: 对应 LayoutParams 中的 wrap_content。子View 不能大于其父容器的大小。

2. MeasureSpec 提供的方法

既然 MeasureSpec 由两部分组成,那么在 MeasureSpec 中就会提供获取这两部分值的方法,以及将这两部分值合成 MeasureSpec 的方法。

  1. 获取 SpecMode: getSize(int measureSpec)
  2. 获取 SpecSize: getMode(int measureSpec)
  3. 合成 MeasureSpec : makeSafeMeasureSpec(int size, int mode)
public static class MeasureSpec {
	// 根据传入的View的测量模式和测量值大小来生成一个 MeasureSpec。
    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);
        }
    }

    @UnsupportedAppUsage
    public static int makeSafeMeasureSpec(int size, int mode) {
        if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
            return 0;
        }
        return makeMeasureSpec(size, mode);
    }
	
	// 通过位运算,获取View的测量模式 
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

	// 通过位运算,获取View的测量值大小 
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return makeMeasureSpec(size, UNSPECIFIED);
        }
        size += delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                    ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }
}

三、MeasureSpec 的使用

下面来说一下子View的大小到底如何确定下来的,这里要分两种情况讨论:根View (即:DecorView)非根View

  1. 根View 的大小: 由 自身的LayoutParams 和 窗口的尺寸 来决定。
  2. 非根View的大小: 由自身的LayoutParams 和 父容器的 MeasureSpec 决定。

1. 根 View 大小的测量

已知每个视图都有一个根节点 (即 DecorView) ,它最终在 ViewRootImpl 中触发了 DecorView 的测量操作。
在这里插入图片描述

// ViewRootImpl.class
private void performTraversals() {
	//...省略...
	WindowManager.LayoutParams lp = mWindowAttributes;
	//...省略...
	if (layoutRequested) {
		//...省略...
	   	// 这里执行了测量操作
	   	windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight);
	}
}

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
		final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;
    //...省略...
    
	// 测量 根View 的大小,lp为根View的LayoutParams
    childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
    childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
    // 从根View开始,触发View的测量操作
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
   
	//...省略...
    return windowSizeMayChange;
}

// 这是计算根View大小的核心逻辑:根据窗口大小和根节点的LayoutParams,来获取根View的尺寸大小
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;
}

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
	// 开始从根View进行测量,此处 mView 即为 DecorView
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View 自身的 LayoutParams模式 + 尺寸
LayoutParams.MATCH_PARENTMeasureSpec.EXACTLY + 窗口尺寸
LayoutParams.WRAP_CONTENTMeasureSpec.AT_MOST + 窗口尺寸
固定的尺寸(如50dp)MeasureSpec.EXACTLY + View自身的尺寸

2. 非根 View 大小的测量

非根View的大小: 由自身的LayoutParams 和 父容器的 MeasureSpec 决定。
在这里插入图片描述

这里,我们选取 DecorView 下的子视图 View (非根 View) 来作为分析。步骤如下所述:

  1. 根View计算结束后,会返回根View的宽高和测量模式 (childWidthMeasureSpecchildHeightMeasureSpec)。
  2. 执行 ViewRootImpl.performMeasure() 方法,其内部调用了 mView.measure() 方法会触发 View.measure() 方法。
  3. View.measure() 方法中,会触发 View.onMeasure() 方法,由于 步骤2 中的 mView 代表的是 DecorView,而 DecorView 是 FrameLayout 的子类,且 FrameLayout 重写了 onMeasure() 方法,所以真正触发的是 FrameLayout.onMeasure() 方法。
  4. FrameLayout.onMeasure() 方法中会触发 ViewGroup.measureChildWithMargins() 方法。
  5. ViewGroup.measureChildWithMargins() 方法中,最终会调用 ViewGroup.getChildMeasureSpec() 方法,获取每个子 View 的大小。

下面看一下步骤5中涉及的代码:

// ViewGroup.class
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 1.获取子View的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
	// 2.根据父容器的MeasureSpec和子View的LayoutParams来计算子View的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);
	// 3.子View继续进行测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// 这是计算非根View大小的核心逻辑:根据父容器的MeasureSpec和子View自身的LayoutParams,来获取子View的尺寸大小
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
	// 1.父容器的测量模式和尺寸大小
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
	// 2.计算子控件可用的最大尺寸(父容器-父容器内边距)
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;
    
	// 根据下面9种情况来设置子View的MeasureSpec(3种父容器的测量模式 * 3种子View的LayoutParams参数)
    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
    // 将上面获取到的子View测量模式和测量大小合成一个MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

小结:
针对 ViewGroup.getChildMeasureSpec() 种的9种情况,作如下归纳:

子View的LayoutParams参数(纵坐标)
/
父容器测量模式(横坐标)
EXACTLYAT_MOSTUNSPECIFIED
具体数值(如50dp)EXACTLY + childSizeEXACTLY + childSizeEXACTLY + childSize
MATCH_PARENTEXACTLY + parentSize
(父容器剩余空间)
AT_MOST + parentSize
(父容器剩余空间)
UNSPECIFIED + 0
WRAP_CONTENTAT_MOST + parentSize
(父容器剩余空间)
AT_MOST + parentSize
(父容器剩余空间)
UNSPECIFIED + 0

根据上面的表格可知:

  1. 当子View的大小是确定值时,子View的MeasureSpec值恒为EXACTLY + childSize
  2. 当子View的大小为MATCH_PARENT时,子View的测量模式与父容器保持一致。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值