前言
自定义 View
知识作为 Android
的一大基础,重要性不言而喻。而对 View
的测量则是第一步,因此我们必须掌握好它。而且,也只有理解了 View
的测量规则,然后我们才能更好地编写测量自定义控件的逻辑。
注:本源码基于SDK25
我们先来看看 View
中的测量相关代码。
MeasureSpec解析
MeasureSpec
封装了父 ViewGroup
传递给子 View
的一些布局必要条件。其代表了宽度和高度信息。而这宽度或者高度的信息都是由测量尺寸 size
和测量模式 mode
组成。那么,有了父ViewGroup
要求的测量信息 MeasureSpec
,子 View
就能够知悉自己应该能有多大,测出的结果才能符合 ViewGroup
对自身的约束。总之,该类在父 ViewGroup
和子 view
之间进行信息传递。
源码
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
//移位的掩码,掩码的作用是屏蔽一部分二进制位,获取另一部分二进制位信息
//此处将0x3(11)左移30位后得到 32位二进制 1100 0000 0000 0000 0000...(共30个0)正数左移低位补0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* 测量模式其一,parent对子view没有约束信息,子view想多大就多大,
* 此时view就不需要考虑parent给的size了。
* 左移后32位二进制 0000 0000 0000 0000 0000...(共30个0)
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* 测量模式其二,parent已经为子view确定了精确的尺寸信息,子view必须按照parent的约束
* 进行测量。其中精确的尺寸信息包括宽高为xxxdp和match_parent两种布局参数。
* 比如parent确定size为100dp给子view,那么子view就只能是100dp或者自己确定的尺寸(>0dp)。
* 左移后32位二进制 0100 0000 0000 0000 0000...(共30个0)
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* 测量模式其三,子view能够尽可能的大,但不能超过parent约束的尺寸。
* 左移后32位二进制 1000 0000 0000 0000 0000...(共30个0)
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* 此方法为基于给定的测量尺寸size和测量模式mode重新生成新的测量规格信息.
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
//此处考虑兼容API17及以下,不再深究
return size + mode;
} else {
//利用掩码分别取出size信息(低30位),以及高2位mode信息,再进行位与操作,得到新的测量规格信息
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);
}
/**
* 利用掩码取出测量规格的高2位信息得到测量模式并返回。
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* 利用掩码取出测量尺寸的低30位信息得到测量模式并返回。返回的尺寸单位为px
*/
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) {//此模式下不需要调整测量规格直接返回。
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);
}
measure()源码解析
此方法作用是根据 parent
提供的宽高测量规格参数,来搞清view究竟有多大。但真正测量的方法是调用 onMeasure()
,measure()帮我们做了一些逻辑处理,包括测量结果缓存等等,因此我们只要重写onMeasure()
方法即可,接下来我们来具体看下源码,分析分析流程。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//指定Parent的layoutMode为optical bound 时额外的判断,以修正测量规格。但此特性很少用,略过
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
//1.将int型widthMeasureSpec强转为64位的long型并左移32位,此时宽度信息处于高32位
//2.再将int型heightMeasureSpec强转为64位的long型并和0xffffffffL进行位与,屏蔽高32为,得到了低32位的高度信息
//3.最后两者位或后组成了一个64位的long型变量。里面存储了宽度和高度的规格信息。
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//定义缓存的集合
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//1.mPrivateFlags是一个包含view的各种状态信息的变量,一般都是通过和相应状态的掩码位与,来检查是否具备相应的状态信息。
//2.此处就是检测view是否有强制布局的标记
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//1.检测测量规格是否有变化
//2.检测测量模式是否为MeasureSpec.EXACTLY
//3.检测当前控件的大小是否匹配parent的测量尺寸
//4.根据1、2、3判断是否需要布局,其中sAlwaysRemeasureExactly为一个兼容API23及以下的变量,表示都需要重新布局
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
if (forceLayout || needsLayout) {//强制布局或者需要布局时都进入
//先去除view中测量尺寸已设置的标记
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
//rtl相关略过,感兴趣了解即可
resolveRtlPropertiesIfNeeded();
//接下来主要判断是从缓存中获取测量信息呢,还是执行测量工作呢
//1.若view标记了强制布局,那么进入第一个条件,调用onMeasure(),开始真正的测量工作,并且必须设置测量尺寸返回
//接下来去除布局之前需要测量的标记,以便layout()过程中知道view已经完成测量了。
//2.根据key从缓存集合中取出含有宽高测量规格信息的64为long值value.
//首先,对value右移32位,高位补0,再向下强转得到32位int型的mMeasuredWidth,
// 其次,直接对value进行向下强转得到原64位value的低32位信息,即mMeasuredHeight,
//最后,调用setMeasuredDimensionRaw()设置view的测量尺寸,并加入布局之前需要测量标记
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
//检查上述流程是否都有调用设置view尺寸的方法setMeasuredDimension(),否则报错
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
//加入需要布局的标记
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
//记录宽度和高度的测量规格
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//以键值对形式,mMeasuredWidth信息存long型高32位,mMeasuredHeight信息存long型低32位,并位与为一个64位值,缓存到集合中
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL);
}
onMeasure源码分析
此方法的作用是测量 view
及其内容,以确定测量宽度以及测量高度。并且必须调用 setMeasuredDimension(int, int)
以保存测量宽高信息。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据默认的view的宽高大小和对应的parent的测量规格,设置测量尺寸并保存宽高信息
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//判断view的背景是否为空,若是返回xml设置的minWidth,否则返回背景的最小宽度
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight() 同理
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根据size和测量规格中的测量尺寸和测量模式分情况设置结果尺寸
switch (specMode) {
//parent未指定约束信息,直接取size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//parent的约束信息为如下两种时,直接设置测量尺寸作为结果尺寸
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//指定layoutMode为optical bound 时额外的判断,以修正测量规格。但此特性很少用,略过
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
//真正设置view尺寸的方法
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
//保存当前view的测量宽度和高度
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
//设置view的测量尺寸已设置标记(此标记若不设置,则会导致measure()方法中报错)
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
resolveSize源码解析
此方法是能够使你期望的大小和状态与 MeasureSpec
追加的约束信息一致的工具方法,也就是说仍是基于当前传入的约束信息去计算最终的大小。除非约束的测量尺寸不同于你所期望的,否则将会默认以你期望的尺寸返回。
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://parent约束的测量模式AT_MOST,specSize为view的最大尺寸
if (specSize < size) {//取specSize为结果尺寸,并加入MEASURED_STATE_TOO_SMALL标记
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {//否则返回期望的结果尺寸
result = size;
}
break;
case MeasureSpec.EXACTLY://parent约束了尺寸,以该测量尺寸返回
result = specSize;
break;
case MeasureSpec.UNSPECIFIED: //此模式下同默认返回的情况
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);//返回高8为状态位信息的结果尺寸
}
注意:上述方法中涉及到两张尺寸,不要搞混。一个是
parent
约束child
的测量尺寸,而另一个是你自己计算后或者你期望child
多大传入的尺寸,最终计算结果需要综合两者,进而得到一个最符合的尺寸。
接下来我们再来看看 ViewGroup
中有关测量的源码。
measureXXX()
我们先看看几个以 measure
开头的方法及一些涉及到的常用方法。
measureChildren()
此方法为 ViewGroup
测量 children
的方法。主要就是遍历每个 child
进行测量。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
//获取child的个数 我们会常利用getChildCount()来返回child的个数
final int size = mChildrenCount;
//ViewGroup中children的集合
final View[] children = mChildren;
//遍历每个child
for (int i = 0; i < size; ++i) {
final View child = children[i];
//如果child的可见性不是GONE,则开始测量child
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
measureChild()
该方法结合 ViewGroup
对 child
的宽高约束信息,测量自己。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取child的布局参数
final LayoutParams lp = child.getLayoutParams();
//调用getChildMeasureSpec()重新确定child的宽高约束信息,此处考虑了parent的padding对测量child的影响
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//child根据约束信息开始测量自身
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildWithMargins()
此方法考虑了 ViewGroup
的 padding
以及 children
的 margin
等因素,以便对children
更加精确的测量,大体流程无异于 measureChild()
,后者其实亦包含了前者。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//为了获取margin我们需要拿到child的MarginLayoutParams实例
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//重新生成对child的测量约束信息。比前者多了margin,以及Parent中已使用的空间。
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);
//child根据约束信息开始测量自身
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec()
此方法返回了对 child
的一个最佳的测量规格。因为它不仅结合了 ViewGroup
对 child
的约束信息,也结合了自身 LayoutParams
的信息,最终计算得到了一个 child
最佳的尺寸和模式,也即child
的最佳测量规格,我们先来看看源码:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
获取ViewGroup对children的测量模式和测量尺寸约束信息
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//考虑了ViewGroup padding属性额外占据的空间,我们先去掉,得到一个children有效的测量尺寸
int size = Math.max(0, specSize - padding);
//记录child的最终测量尺寸和测量模式的变量
int resultSize = 0;
int resultMode = 0;
//根据ViewGroup的测量模式,讨论children最终的约束信息
switch (specMode) {
//若是MeasureSpec.EXACTLY,ViewGroup的layoutParams可以是xxxdp或者MATCH_PARENT
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//child的宽度或高度值为具体的xxxdp时,即child确定需要多大了,那么直接给它分配。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child的宽度或高度值为LayoutParams.MATCH_PARENT时,表示child想要和ViewGroup一样大,那就满足它。
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child的宽度或高度值为LayoutParams.WRAP_CONTENT,表示child决定自己的大小,有多少内容就占用多大,但是你不能超过ViewGroup
给你约束的尺寸。
resultSize = size;//分配最大的尺寸给child,不能再多了
resultMode = MeasureSpec.AT_MOST;
}
break;
//ViewGroup的layoutParams只能是WRAP_CONTENT,它对child约束了一个最大容纳尺寸
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {//此处同上,
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//此处大致同上,唯一的区别在于测量模式。虽然child的LayoutParams是MATCH_PARENT,我们可能会认为测量模式应该是MeasureSpec.EXACTLY,
但是你想啊ViewGroup的测量模式是MeasureSpec.AT_MOST,意味者ViewGroup的大小是由自身内容实际占用的大小决定,而这并不是精确的尺寸,
那么它的child就更不可能是精确模式了,即MeasureSpec.EXACTLY,至多只能和ViewGroup测量模式一样咯。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//此处同上
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ViewGroup对child不施加约束信息,意味着child要多大都可以。
//除非child具体指定大小,否则都是给child设置resultSize为0,其模式为 MeasureSpec.UNSPECIFIED,表示child想多大就多大,ViewGroup不控制。
而此处的sUseZeroUnspecifiedMeasureSpec为兼容性处理变量,再SDK_INT<M,始终是true,而高于此则为false,可能在高版本上有更好的解决方案吧。
我们暂且考虑resultSize为0的情况吧
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//此处同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//最后返回child新的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
其实以上方法的流程,我们可以总结为这样一张表格,可以显得更加直观。
EXACTLY | AT_MOST | UNSPECIFIED | |
---|---|---|---|
具体大小(100dp) | childDimension+EXACTLY | childDimension+EXACTLY | childDimension+EXACTLY |
MATCH_PARENT | size+ EXACTLY | size+AT_MOST | size(0)+UNSPECIFIED |
WRAP_CONTENT | size+AT_MOST | size+AT_MOST | size(0)+UNSPECIFIED |
表格的第一行为 ViewGroup
对 children
对测量模式,第一列为 children
具体的 LayoutParams
,结果为 children
最终的测量尺寸和测量模式。
总结
在第一部分,首先,我们从 MeasureSpec
这个类切入,详细解析了三大测量模式的含义,以及指定新测量规格的方法等;其次,分析了 measure()
,知道了 View
中其实是有缓存测量过的信息的,否则 view
才会重新测量,也知道了真正的测量工作其实是在 onMeasure ()
方法中进行,因此我们可以重写该方法,重新定义 view
的测量逻辑;再来,我们通过调用 setMeasureDimension()
为 view
重新定义了新的宽度和高度;最后,我们又解析了方法 resolveSize()
的作用,它作为一个工具方法能够辅助你进行测量工作。
在第二部分,首先我们研究了 ViewGroup
中测量 children
的方法 measureChildren()
,知道其实它是通过遍历每个child进行测量的,然后进一步研究了测量单个 child
的方法 measureChild()
以及 measureChildWithMargins()
,而两者内部其实关键在于 getChildMeasureSpec()
的调用,该方法对于 ViewGroup
与 Children
之间可能存在的各种布局关系进行了分类讨论,以至于能够得到一个最佳的 child
测量规格。
感谢
源码解析Android中View的measure量算过程
感谢您看到这里,期望留下您的印迹。