提示:本文的源码均取自Android 7.0(API 24)
自定义View是Android进阶路线上必须攻克的难题,而在这之前就应该先对View的工作原理有一个系统的理解。本系列将分为4篇博客进行讲解,本文主要对View的测量流程进行讲解。相关内容如下:
- Android View原理解析之基础知识(MeasureSpec、DecorView、ViewRootImpl)
- Android View原理解析之测量流程(measure)
- Android View原理解析之布局流程(layout)
- Android View原理解析之绘制流程(draw)
在上一篇文章讲到整个视图树(ViewTree)的根容器是DecorView,ViewRootImpl通过调用DecorView的measure方法开启测量流程。measure是定义在View中的方法,我们就先从View的角度来看看测量过程中发生了什么。
首先来看一下measure
方法中的逻辑,关键代码如下:
/**
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
*
* 这个方法将调用onMeasure方法完成真正的测量工作
* 因此View的派生类只需要也只能重写onMeasure方法完成布局逻辑
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
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);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
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) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// ② 调用onMeasure方法,将在onMeasure方法中真正地设置自身的大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
可以看到measure是被final
修饰的,说明View的子类是无法重写这个方法的,也就是说ViewGroup及其派生类调用的都是View中的measure方法。在这个方法中先是针对存在特殊边界的情况,对MeasureSpec进行了调整。随后在代码①的位置判断是否需要进行测量流程,最后在代码②的位置调用onMeasure
方法。接下来我们继续看一下View#onMeasure
方法,代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在onMeasure方法中调用了setMeasuredDimension
方法,这个方法用于设置View测量后的宽高。我们通过View#getMeasuredWidth
和View#getMeasuredHeight
获取的就是这个方法设置的值。这里的宽高都是通过getDefaultSize
方法获取的,下来让我们来看看这个方法中都做了什么:
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size View的默认宽度/高度
* @param measureSpec 父容器传入的MeasureSpec
* @return View最终的size(测量后的宽/高)
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
// ① 对MeasureSpec进行解包
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进行解包操作,获取specSize和specMode。然后在代码②的位置判断测量模式(specMode),只有在测量模式为MeasureSpec.UNSPECIFIED
时使用传入的默认大小,否则使用解包出来的specSize。这也说明默认情况下,View在测量模式为AT_MOST或EXACTLY时都会直接使用MeasureSpec中的宽/高。UNSPECIFIED一般是系统内部使用的测量模式,所以大部分情况下这个方法都会返回从MeasureSpec解包出来的specSize。
另外,这个方法中使用的默认大小(size)是通过getSuggestedMinimumWidth
和getSuggestedMinimumHeight
获得的,我们也来看一眼这两个方法中都做了什么:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
这两个方法的逻辑是相似的,这里仅分析getSuggestedMinimumWidth方法。首先会判断View是否存在背景(无非就是各种Drawable),如果不存在就直接返回mMinWidth,对应着XML中的android:minWidth
属性;否则返回mMinWidth和背景最小宽度中的较大值。getMinimumWidth
是Drawable中的方法,Drawable的子类都有自己的实现。
仅仅从View的角度来看,测量流程到此就结束了。因为不需要测量子View的大小,只需要确定自身的大小就行了。由此可见,如果我们想要通过继承View的方式实现自定义View,只需要重写onMeasure方法,并在这个方法中根据不同的情况为自己设置合适的宽/高,就可以保证测量流程正确进行。
ViewGroup需要承担测量子View的责任,而View#measure
方法又是无法被重写的,那么可以很自然地想到去ViewGroup#onMeasure
方法中寻找相应的测量逻辑。但是当我们兴致勃勃地在ViewGroup中寻觅时,会发现ViewGroup根本就没有重写onMeasure方法。
仔细想想也很正常,ViewGroup是一个抽象类,它的派生类们实现布局的方式也是多种多样,ViewGroup作为父类是无法提供一个统一的测量方案的。当然啦,ViewGroup确实也提供了很多方便测量的方法,下面我们就来一个一个地认识它们:
首先来看看ViewGroup#measureChild
方法:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// ① 获取子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
// ② 生成测量子View需要的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// ③ 调用子View的measure方法开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
首先在代码①的位置获取子View的LayoutParams,里面封装着子View对父容器的期望,也就是告诉父容器自己期望的宽高。在代码②的位置调用了getChildMeasure
方法分别获取子View宽高对应的MeasureSpec。这里传入了3个参数,分别是父容器的MeasureSpec、父容器的左右/上下内间距以及子View的LayoutParams中封装的width/height。最后在代码③的位置调用子View的measure方法开启子View的测量流程。getChildMeasureSpec是一个非常重要的方法,接下来我们就来分析一下这个方法的逻辑:
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* 这个方法将根据父容器的MeasureSpec和子View LayoutParams中的宽/高
* 为子View生成最合适的MeasureSpec
*
* @param spec 父容器的MeasureSpec
* @param padding 父容器的内间距(可能还会加上子View的外间距)
* @param childDimension 子View的LayoutParams中封装的width/height
* @return 子View的MeasureSpec
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// ① 对父容器的MeasureSpec进行解包
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// ② 减去间距(可以简单认为size就是父容器剩余可用的空间)
int size = Math.max(0, specSize - padding);
// 记录子View最终的大小和测量模式
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// ③ 父容器是精准测量模式
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ④ 父容器指定了一个最大可用的空间
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// ⑤ 父容器不对子View的大小作出限制
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;
}
// ⑥ 将最终的size和mode打包为子View需要的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这个方法里的代码比较多,但都是有迹可循的,咱们来一点一点捋一遍。首先在代码①的位置,对父容器的MeasureSpec进行解包,获取specSize和specMode。然后在代码②的位置用specSize减去padding,其实就是获取父容器剩余的空间,并用resultSize和resultMode记录子View最终的大小和测量模式。接下来,就需要依照具体的测量模式和子View的LayoutParams进行分析了。
在代码③的位置,父容器的测量模式为MeasureSpec.EXACTLY
,也就是说父容器的大小已经确定了,那么我们只需要参考子View的LayoutParams就好了:
如果LayoutParams中的宽/高是一个确定的值(比如20dp这样的形式),即childDimension>0,那就说明这是一个有着强烈自我意识的子View,它知道自己想要多大的空间。系统将充分尊重子View的需求,resultSize就将被赋值为子View声明的宽/高(childDimension),测量模式也会被赋值为MeasureSpec.EXACTLY。当然,如果子View声明的宽/高大于父容器剩余的空间,最终显示的时候超出的部分是会被裁剪的;
如果LayoutParams中的宽/高是LayoutParams.MATCH_PARENT
,说明子View想要和父容器一样大,那就将父容器剩余的空间(size)赋给resultSize就好了。此时子View的宽/高依旧是确定的,测量模式同样会被赋值为MeasureSpec.EXACTLY;
如果LayoutParams中的宽/高是LayoutParams.WRAP_CONTENT
,说明子View自己也不清楚想要多大的空间,那父容器也无可奈何。此时会将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,也就是为子View指定了一个最大可用的空间;
在代码④的位置,父容器的测量模式为MeasureSpec.AT_MOST
,也就是说父容器只知道自己可以使用的最大空间,并不知道精确的大小,接下来结合子View的LayoutParams进行讲解:
如果LayoutParams中的宽/高是一个确定的值(childDimension>0),那就将resultSize赋值为子View声明的宽/高(childDimension),测量模式也会被赋值为MeasureSpec.EXACTLY;
如果LayoutParams中的宽/高是LayoutParams.MATCH_PARENT
,说明子View想要和父容器一样大。但是此时父容器也不确定自己有多大,所以只能将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,也就是为子View指定了一个最大可用的空间;
如果LayoutParams中的宽/高是LayoutParams.WRAP_CONTENT
,说明父容器和子View都不清楚自己想要多大的空间,那就直接将resultSize赋值为父容器剩余的空间(size),并将测量模式赋值为MeasureSpec.AT_MOST,同样为子View指定了一个最大可用的空间。
可以看到在父容器的测量模式为MeasureSpec.AT_MOST
时,无论子View的LayoutParams使用WRAP_CONTENT
还是MATCH_PARENT
,结果都是一样的。
在代码⑤的位置,父容器的测量模式为MeasureSpec.UNSPECIFIED
,也就是不限制子View的大小。这一般是系统内部使用的测量模式,我们就不再重点讲解了。只说明一下如果LayoutParams中的宽/高是一个确定的值,那就将resultSize赋值为childDimension,测量模式也会被赋值为MeasureSpec.EXACTLY。
getChildMeasureSpec是一个非常重要的方法,希望大家可以好好理解这个过程。
接下来,我们再来一起看看ViewGroup#measureChildWithMargins
方法:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child 被测量的子View(必须要有MarginLayoutParams)
* @param parentWidthMeasureSpec 父容器的MeasureSpec(针对width)
* @param widthUsed 在水平方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)
* @param parentHeightMeasureSpec 父容器的MeasureSpec(针对height)
* @param heightUsed 在垂直方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// ① 获取子View的LayoutParams,并强制转型为MarginLayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// ② 生成测量子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);
// ③ 调用子View的measure方法开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
首先在代码①的位置获取子View的LayoutParams,并强制转型MarginLayoutParams,这也就说明使用这个方法的父容器要支持margin属性(即父容器的LayoutParams要继承MarginLayoutParams)。
后面两步就和measureChild相似了,先在代码②的位置使用getChildMeasureSpec
方法生成测量子View需要的MeasureSpec。这里和measureChild不同的是除了传入父容器的内边距之外,还传入了子View的外边距(margin)以及widthUsed/heightUsed。widthUsed和heightUsed是在水平/垂直方向上被使用的额外空间(可能是被父容器或其他子View使用的空间)。
最后在代码③的位置调用子View的measure方法开始测量。其实看方法名也能明白这个方法和measureChild的区别,无非就是在测量的时候会考虑到外边距的影响。当我们需要考虑子View的margin时,就可以使用这个方法进行测量。
最后我们再来看看ViewGroup#measureChildren
方法:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec 父容器的MeasureSpec(针对width)
* @param heightMeasureSpec 父容器的MeasureSpec(针对height)
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount; // 子View的数量
final View[] children = mChildren; // 包含子View的数组
for (int i = 0; i < size; ++i) { // 逐个对子View进行测量
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // 只测量visibility不为GONE的子View(提高效率)
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
可以看到,这个方法的目的是对父容器所有的子View进行测量,其实就是逐次对每个visibility不为GONE的子View调用了measureChild方法。所以我们也就知道了,如果希望在测量过程中考虑子View的外间距的话,是不可以使用这个方法的。
到这里,我们基本就把ViewGroup与测量流程相关的方法分析完了。仔细想来,似乎ViewGroup并没有做出什么实质性的测量工作。毕竟ViewGroup是一个抽象的父类,确实也不能决定具体的测量步骤。
如果通过继承ViewGroup实现自定义View,就应该重写onMeasure方法,并在这个方法中合理利用ViewGroup提供的measureChild、measureChildWithMargins、measureChildren和getChildMeasureSpec等方法完成对子View的测量工作,并通过setMeasuredDimension
方法设置自身的宽高。
上面分别从View和ViewGroup的角度讲解了测量流程,这里再以流程图的形式归纳一下整个measure过程,便于加深记忆:
测量流程在三大流程中相对是比较复杂的,如果看完本文后依旧有些疑惑,不如打开AndroidStudio,沿着文章的脉络亲自探索一下整个measure过程的逻辑,可能学习效果会更好一点。
https://blog.csdn.net/a553181867/article/details/51494058
https://blog.csdn.net/xmxkf/article/details/51490283
https://blog.csdn.net/lfdfhl/article/details/51347818