Android View measure流程详解
Posted 低调小一
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View measure流程详解相关的知识,希望对你有一定的参考价值。
android View measure流程详解
Android中View绘制的流程包括:measure(测量)->layout(布局)->draw(绘制).
因为Android中每个View都占据了一块矩形的空间,当我们要在屏幕上显示这个矩形的View的时候
- 首先我们需要知道这个矩形的大小(宽和高)这就对应了View的measure流程.
- 有了View的宽和高,我们还需要知道View左上角的起点在哪里,右下角的终点在哪里,这就对应了View的layout流程.
- 当矩形的区域在屏幕上确定之后,相当于屏幕上有了一块属于View的画布,接下来通过draw方法就可以在这块画布上画画了.
本文重点介绍View的measure流程.一般情况下,我们都是在xml文件里去定义一个布局文件,针对每个View来说,是一定要声明layout_width和layout_height的,不然编译期就会报错.
那layout_width和layout_height数值包括:
- 具体的长度单位,例如20dp或者20px.
- match_parent,代表充满父控件.
- wrap_content,代表能包含View中内容即可.
从layout_width和layout_height的取值来看,如果Android规定只能使用具体的长度,那可能就不需要measure流程了,因此宽和高已经知道了.因此,从取值上我们知道了measure方法的作用是:将match_parent和wrap_content转成具体的度量值.
measure方法
接下来,我们从源码的角度去分析measure方法.
/**
* <p>
* 这个方法是用来测试View的宽和高的. View的父附件提供了宽和高的限制参数,即View的最值.
* </p>
*
* <p>
* View的真正测量工作是在 {@link #onMeasure(int, int)} 函数里进行的. 因此, 只有
* {@link #onMeasure(int, int)} 方法才能被子类重写.
* </p>
*
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 判断View的layoutMode是否为LAYOUT_MODE_OPTICAL_BOUNDS
boolean optical = isLayoutModeOptical(this);
// 子View是LAYOUT_MODE_OPTICAL_BOUNDS,父附件不是LAYOUT_MODE_OPTICAL_BOUNDS的情况很少见,不需要去care
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);
}
// 生成View宽高的缓存key,并且如果缓存Map为null,则构建缓存Map.
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// 判断是否为强制布局或者宽、高发生了变化
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// 清除PFLAG_MEASURED_DIMENSION_SET标记,表示该View还没有被测量.
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
// 解析从右向左的布局
resolveRtlPropertiesIfNeeded();
// 判断是否能用cache缓存中view宽和高
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// View宽和高真正测量的地方
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// 获取缓存中的View宽和高
long value = mMeasureCache.valueAt(cacheIndex);
// long占8个字节,前4个字节为宽度,后4个字节为高度.
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 无论是调用onMeasure还是使用缓存,都应该设置了PFLAG_MEASURED_DIMENSION_SET标志位.
// 没有设置,则说明测量过程出了问题,因此抛出异常.
// 并且,一般出现这种情况一般是子类重写onMeasure方法,但是最后没有调用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;
}
// 记录View的宽和高,并将其存储到缓存Map里.
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL);
}
从measure方法中,我们可以看出,measure方法只要是加了一个缓存机制,真正的测量还是在onMeasure去执行.为了防止缓存的滥用,Android系统直接将measure设置为final类型,即子类不能重写,意味着子类只需要提供测量的具体实现,不需要care缓存等提速功能的实现.
onMeasure方法
onMeasure的默认实现非常简单,注释源码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 将getDefaultSize函数处理后的值设置给宽和高的成员变量
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 特殊处理LAYOUT_MODE_OPTICAL_BOUNDS的情况
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成员变量中
mMeasuredWidth = measuredWidth;
// 保存高度到View的mMeasuredHeight成员变量中
mMeasuredHeight = measuredHeight;
// 设置View的PFLAG_MEASURED_DIMENSION_SET,即代表当前View已经被测量过.
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
通过源码,我们发现onMeasure只是给mMeasuredWidth和mMeasuredHeight两个成员变量赋值.那这个值是如何获取到的呢?和Parent提供的限制又有神马关系呢?
想回答这个问题,需要先跟踪一下getDefaultSize的源码.
public static int getDefaultSize(int size, int measureSpec) {
// size表示View想要的尺寸信息,比如最小宽度或者最小高度
int result = size;
// 解析SpecMode信息
int specMode = MeasureSpec.getMode(measureSpec);
// 解析SpecSize信息
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 如果View的度量值设置为WARP_CONTENT的时候
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
// 如果View的度量值设置为具体数值或者MATCH_PARENT的时候
result = specSize;
break;
}
return result;
}
同时,需要看一下getSuggested一系列方法,以getSuggestedMinimumWidth为例:
protected int getSuggestedMinimumWidth() {
// 如果View没有设置背景,则返回View本身的最小宽度mMinWidth.
// 如果View设置了背景,那么就取mMinWidth和背景宽度的最大值.
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
这里有同学会奇怪View的mMinWidth(最小宽度)是什么时候设置的?我们可以看一下View的构造函数(截取部分代码):
case R.styleable.View_minWidth:
mMinWidth = a.getDimensionPixelSize(attr, 0);
break;
可以看到,和我们自定义一个View是一样的,mMinWidth成员变量对应着的自定义属性是minWidth,如果xml中未定义则默认值是0.
示例设置代码:
<TextView
android:minWidth=100dp
android:id="@+id/id_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Hello World!"
android:textColor="#ffffff"
android:textSize="30sp" />
或许还有同学会对上面的MeasureSpec感到陌生,不要着急,这就带来MeasureSpec的详解.
MeasureSpec
MeasureSpec的值由specSize和spceMode共同组成,其中specSize记录的是大小,specMode记录的是规格.
我们来看一下MeasureSpec的源码定义,他是View的内部类,源码位于: /frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// specMode一共有三种类型
public static final UNSPECIFIED = 0 << MODE_SHIFT; // 任意大小
public static final int EXACTLY = 1 << MODE_SHIFT; // 表示父容器希望子视图的大小由specSize来决定
public static final int AT_MOST = 2 << MODE_SHIFT; // 表示子视图最多只能是specSize中指定的大小
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
public static int getMode(int measureSpec) {
return measureSpec & MODE_MASK;
}
public static int getSize(int measureSpec) {
return measureSpec & ~MODE_MASK;
}
}
只看源码可能大家还比较陌生,这里结合源码讲解一下.首先,Android只所以提供MeasureSpec类,我认为Android开发人员觉得View尺寸最大不超过2^30,所以将剩余的2位来表示模式,这样节约了空间.
前面说过,View的宽和高只有三种情况,分别是:具体数值,WARP_CONTENT,MATCH_PARENT.而measure就是将WARP_CONTENT,MATCH_PARENT转换为具体数值的方法,所以需要有MeasureSpec.getSize()方法获取具体数值.
那MeasureSpec.getMode的几种类型和View的赋值也是有对应关系的:
- EXACTLY: 对应着具体数值或者MATCH_PARENT
- AT_MOST: 对应着WRAP_CONTENT
- UNSPECIFIED: 父容器对View无限制,这中情况一般出现在Android系统内部.
ViewGroup的onMeasure流程
对于非ViewGroup的View,上面介绍的measure->onMeasure方法足以完成View的测量.
但是对于自定义ViewGroup,我们不仅要测量自己的宽和高,同时还需要负责测量child view的宽和高.
测量child view通常会用到measureChild方法.源码实现如下:
/**
* 测量每个子View的宽高.测量时需要排除padding的影响,如果需要排除margins,则调用measureChildWithMargins方法.
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到,measureChild方法只要是获取子View的widthMeasureSpec和heightMeasureSpec,然后调用measure方法设置子View的实际宽高值.同时,从getChildMeasureSpec的方法传递中,我们可以看出child View的值是由父View和child view共同决定的.
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父容器的MeasureSpecMode
int specMode = MeasureSpec.getMode(spec);
// 获取父容器的具体大小
int specSize = MeasureSpec.getSize(spec);
// 子view的可能大小=父View的大小-padding
int size = Math.max(0, specSize - padding);
// 设置子View的初始MeasureSpecSize和MeasureSpecMode均为0
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 当父容器有固定大小(具体数值或MATCH_PARENT)的情况下
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// view的宽或高赋值为具体数值,例如20dp
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// view的宽或高赋值为MATCH_PARENT
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// view的宽或高赋值为WRAP_CONTENT,这时View的最大值就是父容器大小-padding.所以MeasureSpecSize=父容器大小-padding.
// 同时,View的最大值为MeasureSpecSize,所以MeasureSpecMode设置为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父容器是AT_MOST情况下(即父容器大小赋值为WRAP_CONTENT)
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 由于父容器的mode为AT_MOST,虽然子View设置为MATCH_PARENT,但是父容器大小不是精确的,所以子View也只能是AT_MOST了.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 这个case不需要care,自定义控件不会遇到这种case的.
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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec的总体思路:
通过其父容器提供的MeasureSpec参数得到specMode和specSize,并根据计算出来的specMode以及子视图的childDimension(layout_width和layout_height中定义的值)来计算子View自身的measureSpec和measureMode.
以上是关于Android View measure流程详解的主要内容,如果未能解决你的问题,请参考以下文章
Android View 测量流程(Measure)完全解析
Android View体系从源码解析View的measure流程
Android应用层View绘制流程之measure,layout,draw三步曲