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中可以做的两件坏事---破解锁屏密码和获取Wifi密码
- Android中使用语音引擎入门七步曲
- Android自定义View的实现方法,带你一步步深入了解View(三) 。
- Android(安卓)如何自定义一个简单的组件和自定义的点击事件(中级)
- Android五大基本组件
- 进度圈的显示
- Android(安卓)Graphics专题(1)--- Canvas基础
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用