图+源码,读懂View的MeasureSpec
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情
读懂 View 三大绘制方法的文章
图+源码,读懂View的MeasureSpec - 掘金 (juejin.cn)
图+源码,读懂View的Measure方法 - 掘金 (juejin.cn)
前置知识
- 有Android开发基础
- 了解 View 体系
前言
在青训营系列的文章中,我们用两篇文章讲解了 View体系 的知识。但是碍于时间和篇幅的限制,我们还未将该知识点完全打通,所以在这篇文章中,笔者将给大家继续讲述 View 的知识,本文使用 图+源码 的形式给大家讲解,希望这样子能讲得更加明白,大家也能有所收获。
阅读本系列,你可以学到在Android中, View 三大绘制方法 Measure 、Layout 和 Draw 的原理。
而今天这篇文章,我们讲解的是 Measure 方法的前置知识,View的MeasureSpec类。
何为 MeasureSpec
在学习三大方法之前,我们先来了解一下 View 的内部类,MeasureSpec 的作用和原理。
MeasureSpec 在 View 的测量(measure)过程中,担任着存储 View 的规格尺寸的作用。每次启动 Measure 方法的时候,我们都会需要传入对应的 MeasureSpec 参数才能执行。所以说,在 View 的绘制中,我们必须先获取每个 View 的 MeasureSpec 参数,才能执行 Measure 方法。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode { } //1
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
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);
}
}
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide internal use only for compatibility with system widgets and older apps
*/
@UnsupportedAppUsage
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
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);
}
/**
* Returns a String representation of the specified measure
* specification.
*
* @param measureSpec the measure specification to convert to a String
* @return a String with the following format: "MeasureSpec: MODE SIZE"
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
上述是 MeasureSpec 的源码,首先,我们看到了 MeasureSpecMode (标注1处) 这个注解定义,注解中表明了有三个枚举类。从这三个枚举类的常量定义中,我们发现他们使用了位计算,0,1,2 三个数全部左移 30 位,其二进制刚好就是int32位的高两位。可见,该类是想将32位二进制数的前两位赋予 MeasureSpecMode 。这三个常量称之为 **specMode(测量规格模式)**,占据了 32位int值的高两位;而其余低的30位,称之为 **specSize(测量规格大小)**。
这三种模式释义如下。
UNSPECIFIED
未指定模式。View可以任意大,父布局不对其进行约束。多用于系统内部测量。
EXACTLY
精确模式。子View的大小已被父View确定,确定的值为是 specSize ,子View大小是被限制,无法被更改的。对应了match_parent和具体的数值。
AT_MOST
最大模式。子View可以任意大,最终可达到最终指定的specSize值,但是不能大于这个值。对应wrap_content属性。
然后,我们看到有 makeMeasureSpec
、getMode
和 getSize
这些方法,他们的作用也正如方法名一样,分别是载入保存模式和大小,获得模式、获得大小。
DecorView 如何生成 MeasureSpec
我们知道,每一个View中都会持有自己的一个 MeasureSpec 信息。那在现在介绍完 MeasureSpec 类的构成之后,我们来看一下,顶层 View (DecorView) 是如何获得自己的 MeasureSpec 信息的。
我们看一下 View 工作流开启的方法 performTraversals()
,我们看到注释1和2处,可以看到是 getRootMeasureSpec
方法生成了对应的childWidthMeasureSpec
和 childHeightMeasureSpec
值,再载入到注释3的 performMeasure
中执行对应的 Measure 方法。
private void performTraversals() {
...
// TODO: In the CL "ViewRootImpl: Fix issue with early draw report in
// seamless rotation". We moved processing of RELAYOUT_RES_BLAST_SYNC
// earlier in the function, potentially triggering a call to
// reportNextDraw(). That same CL changed this and the next reference
// to wasReportNextDraw, such that this logic would remain undisturbed
// (it continues to operate as if the code was never moved). This was
// done to achieve a more hermetic fix for S, but it's entirely
// possible that checking the most recent value is actually more
// correct here.
if (!mStopped || mReportNextDraw) {
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()
|| dispatchApplyInsets || updatedConfiguration) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width,
lp.privateFlags);//1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height,
lp.privateFlags);//2
if (DEBUG_LAYOUT) Log.v(mTag, "Ooops, something changed! mWidth="
+ mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ " mHeight=" + mHeight
+ " measuredHeight=" + host.getMeasuredHeight()
+ " dispatchApplyInsets=" + dispatchApplyInsets);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//3
...
}
if (didLayout) {
performLayout(lp, mWidth, mHeight);
...
if (!performDraw() && mSyncBufferCallback != null) {
mSyncBufferCallback.onBufferReady(null);
}
...
}
}
}
那么, getRootMeasureSpec
方法又做了上什么呢?我们可以查看一下源码
private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
int measureSpec;
final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
? MATCH_PARENT : measurement;//1
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;
}
我们发现 getRootMeasureSpec
这个函数传入了三个值。第一个是窗口大小,第二个是测量信息,第三个是私有标签。
在这段代码的注释1处,我们可以看到第二、第三个参数最后生成了 rootDimension
,这个其实就是DecorView 的 MeasureSpec 类里面的布局参数。由此可得,DecorView 的 MeasureSpec 类是由 DecorView 自身的布局参数和窗口大小决定的。
出于好奇心,我们可以再看一下 performMeasure
方法是执行了什么?我们可以看到,它是直接执行 View 的 measure
方法了。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);//执行View的measure方法
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
对应执行图
参考
View.java - Android Code Search
ViewRootImpl.java - Android Code Search
《Android进阶之光》