View的绘制-measure流程详解
目录

作用
用于测量View的宽高,在执行 layout 的时候,根据测量的宽高去确定自身和子 View 的位置。
基础知识
在 measure 过程中,设计到 LayoutParams 和 MeasureSpec 这两个知识点。 这里我们简单说一下,假如还有不明白之处,Google it!
LayoutParams
简单来说就是布局参数,包含了 View 的宽高等信息。每一个 ViewGroup 的子类都有相对应的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以看出 LayoutParams 是 ViewGroup 子类的内部类。
| 值 | 含义 |
|---|---|
| LayoutParams.MATCH_PARENT | 等同于在 xml 中设置 View 的属性为 match_parent 和 fill_parent |
| LayoutParams.WRAP_CONTENT | 等同于在 xml 中设置 View 的属性为 wrap_content |
MeasureSpec
MeasureSpec 是 View 的测量规则。通常父控件要测量子控件的时候,会传给子控件 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的值。这个值里面包含两个信息,SpecMode 和 SpecSize。一个 int 值怎样会包含两个信息呢?我们知道 int 是一个4字节32位的数据,在这两个 int 类型的数据中,前面高2位是 SpecMode ,后面低30位代表了 SpecSize。

mode 有三种类型:UNSPECIFIED,EXACTLY,AT_MOST
| 测量模式 | 应用 |
|---|---|
| EXACTLY | 精准模式,当 width 或者 height 为固定 xxdp 或者者为 MACH_PARENT 的时候,是这种测量模式 |
| AT_MOST | 当 width 或者 height 设置为 warp_content 的时候,是这种测量模式 |
| UNSPECIFIED | 父容器对当前 View 没有任何显示,子 View 可以取任意大小。一般用在系统内部,比方:Scrollview、ListView。 |
我们怎样从一个 int 值里面取出两个信息呢?别担心,在 View 内部有一个 MeasureSpec 类。这个类已经给我们封装好了各种方法:
//将 Size 和 mode 组合成一个 int 值int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);//获取 size 大小int size = MeasureSpec.getSize(measureSpec);//获取 mode 类型int mode = MeasureSpec.getMode(measureSpec);具体实现细节,可以查看源码,or Google it!
执行流程
注:以下涉及到源码的,都是版本27的。
我们知道,一个视图的根 View 是 DecorView。在我们开启一个 Activity 的时候,会将 DecorView 增加到 window 中,同时会创立一个 RootViewImpl对象,并将 RootViewImpl 对象和 DecorView 对象建立关联。RootViewImpl 是连接 WindowManager 和 DecorView 的纽带。具体 DecorView 详解可以看 这篇文章
View的绘制流程就是从 RootViewImpl 开始的。在它的 performTraversals()方法中执行了 performMeasure()、performLayout、performDraw方法。而这三个方法又分别执行了view.measure()、view.layout()、view.draw()方法,从而开始执行整个 View 树的绘制流程

ViewGroup 中 measure 的执行流程
ViewGroup 本身是继承 View 的,这是我们大家都知道的。在 ViewGroup 中并没有找到 measure 方法,那么就在它的父类 View 中找,具体源码如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { /*....省略代码....*/ if (forceLayout || needsLayout) { /*....省略代码....*/ if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back //执行 onMeasure 方法 onMeasure(widthMeasureSpec, heightMeasureSpec); } /*....省略代码....*/ } /*....省略代码....*/}我们可以看出,measure 方法是被 final 修饰了,子类不能重写。measure 方法中调用了 onMeasure 方法。
而后我们继续寻觅 onMeasure 方法,会发现在 ViewGroup 中并没有实现 onMeasure 方法,只有在 View 中发现了 onMeasure 方法。WTF?难道 ViewGroup 的 onMeasure 也会走 View 中的方法?并不是的,ViewGroup 本身是一个笼统类,在 Android SDK 中有很多它的子类,如:LinearLayout、RelativeLayout、FrameLayout等等,这些控件的特性都是不一样的,测量规则自然也都不一样。它们都各自实现了 onMeasure 方法,而后去根据自己的特定测量规则进行控件的测量。(PS:假如我们的自己设置控件继承 ViewGroup 的时候,肯定要重写 onMeasure 方法的,根据需求来制定测量规则)
这里我们以 LinearLayout 为例,来进行源码分析:
//LinearLayout 类@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { //假如方向是垂直方向,就进行垂直方向的测量 measureVertical(widthMeasureSpec, heightMeasureSpec); } else { //进行水平方向的测量 measureHorizontal(widthMeasureSpec, heightMeasureSpec); }}measureVertical 和 measureHorizontal 过程相似,我们对 measureVertical 进行分析。(以下源码有所删减)
//LinearLayout 类void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; float totalWeight = 0; final int count = getVirtualChildCount(); //获取 LinearLayout 的宽高模式 SpecMode final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean skippedMeasure = false; // See how tall everyone is. Also remember max width. //遍历子 View ,查看每一个子类有多高,并且记住最大的宽度。 for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { //measureNullChild() 恒返回 0, mTotalLength += measureNullChild (i); continue; } //假如子控件时 GONE 状态,就跳过,不进行测量。 //也可以看出,假如子 View 是 INVISIBLE 也是要测量大小的。 if (child.getVisibility() == View.GONE) { //getChildrenSkipCount 也是恒返回为 0 的。 i += getChildrenSkipCount(child, i); continue; } //获取子控件的参数信息。 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; //子控件能否设置了权重 weight final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) { final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); //假如设置了权重,就将 skippedMeasure 标记为 true。 //后面会根据 skippedMeasure 的值和其余条件来决定能否进行重新绘制。 //所以说,在 LinearLayout 中使用了 weight 权重,会导致测量两次,比较耗时。 //可以考虑使用 RelativeLayout 或者者 ConstraintLayout skippedMeasure = true; } else { if (useExcessSpace) { lp.height = LayoutParams.WRAP_CONTENT; } //计算已经使用过的高度 final int usedHeight = totalWeight == 0 ? mTotalLength : 0; /*这句代码是关键,从字面意思即可以了解出,该方法是在 layout 之前进行子 View 的测量。*/ measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); } }}那么我们在查看 measureChildBeforeLayout 方法:
//LinearLayout 类void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);}再查看 measureChildWithMargins 方法,最终来到了 ViewGroup 类:
//ViewGroup 类protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { /*获取子 View 的布局参数 MarginLayoutParams 可以获取子 View 设置的 margin 属性。*/ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //获取子 View 宽度的 MeasureSpec 值。 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); //获取子 View 高度的 MeasureSpec 值。 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}在 ViewGroup 中还有一个方法为
measureChild(int widthMeasureSpec, int heightMeasureSpec)。这个方法和measureChildWithMargins作用一致,都是生成子 View 的 measureSpec。只是传参不同。
里面在获取子 View 宽高属性的时候,都是通过 getChildMeasureSpec 方法来获取的。这个方法是 ViewGroup 具体实现根据自身的 measureSpec 和子 View 的 LayoutParams 来设置子 View 的 measureSpec 的主要过程。
//ViewGroup 类/** * @param spec 父类的 measureSpec * @param padding 父类的 padding + 子类的 margin * @param childDimension 子 View 的 LayoutParams.width/LayoutParams.height 属性 */public static int getChildMeasureSpec(int spec, int padding, int childDimension) { //获取父控件的测量模式 specMode int specMode = MeasureSpec.getMode(spec); //获取父控件的测量大小 SpecSize int specSize = MeasureSpec.getSize(spec); //获取父控件剩余的宽度/高度大小 int size = Math.max(0, specSize - padding); //子 View 的测量大小 int resultSize = 0; //子 View 的测量模式 int resultMode = 0; switch (specMode) { // 父控件的宽高模式是精准模式 EXACTLY case MeasureSpec.EXACTLY: if (childDimension >= 0) { //假如子 View 的宽/高是具体的值(具体的 xxdp/px) //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //假如子 View 的宽/高是 MATCH_PARENT //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是父控件剩余的空间 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //假如子 View 的宽/高是 WRAP_CONTENT /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间, 子控件可以在在这个size大小范围内设置宽高*/ resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us //父控件测量模式为 AT_MOST,会给子 View 一个最大的值 case MeasureSpec.AT_MOST: if (childDimension >= 0) { //假如子 View 的宽/高是具体的值(具体的 xxdp/px) //模式 mode 就设置为精准模式 EXACTLY,大小 size 就是具体设置的大小 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //假如子 View 的宽/高是 MATCH_PARENT /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间, 子控件可以在在这个size大小范围内设置宽高*/ resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //假如子 View 的宽/高是 MATCH_PARENT /*模式 mode 就设置为精准模式 AT_MOST,大小 size 就是父控件剩余的空间, 子控件可以在在这个size大小范围内设置宽高*/ resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be //父控件不限制子 View 的宽高,一般用于 ListView、Scrollview //平常基本不用,暂不分析 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; } //生成子 View 的 measSpec return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}以上就是 ViewGroup 根据自身 measureSpec 和 子 View 的 LayoutParams 生成子 View 的 measureSpec 的过程。具体总结如下:

以上就是 LinearLayout 测量子控件宽高的过程。
从上述表格我们也可以看出,当我们在自己设置控件继承 View 的时候,还是要重写 View 的 onMeasure 方法来解决 wrap_content 的情况,假如不解决 wrap_content 的情况,wrap_content 的效果是和 match_parent 一样的,都是填充满父控件。可以在 xml 布局中直接增加一个
<View android:layout_width="match_parent" android:layout_height="wrap_content"/>控件自行感受一下。
LinearLayout 测量完子控件后,根据子控件的宽高来设置自身的宽高:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { // Add in our padding //增加自身的 padding 值 mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height //从 最小建议高度 和 heightSize 中取最大值,getSuggestedMinimumHeight 在后面有分析 heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); /*....省略代码....*/ //遍历完子控件后,来设置自身的宽高 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);}//假如 LinearLayout 高为具体值,heightSizeAndState 就是具体的值//否则是 子控件 的高度之和,但是也不能超过它的父容器的剩余空间。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: if (specSize < size) { result = specSize | MEASURED_STATE_TOO_SMALL; } else { result = size; } break; case MeasureSpec.EXACTLY: result = specSize; break; case MeasureSpec.UNSPECIFIED: default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK);}至此,我们可以得知,当 ViewGroup 生成子 View 宽/高的 measureSpec 后,开始调用子 View 进行测量。假如子 View 继承了 ViewGroup 就重复执行上述流程(各个不同的 ViewGroup 子类执行各自的 onMeasure 方法);假如是具体的 View,就开始执行具体 View 的 measure 过程。最后根据子控件的宽高和其余条件来决定自身的宽高。
View 中 measure 的执行流程
View 的 measure 具体源码在 ViewGroup 中已经分析过,这里主要分析 View 的 onMeasure 过程。
//View 类protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //通过 getDefaultSize 获取宽高大小,设置为测量值。 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}getDefaultSize 具体源码
//View 类/** * @param size 通过 getSuggestedMinimumWidth 获取的建议最小宽度 * @param measureSpec 通过父控件生成的 measureSpec */public static int getDefaultSize(int size, int measureSpec) { //宽/高值 int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: //假如是 UNSPECIFIED 就设置为建议最小值 result = size; break; /*否则就都设置为通过父控件生成的值(假如子控件为具体的 xxdp/px值,就是具体的值,假如不是就是父控件的剩余空间。具体可以查看上面的分析)*/ case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result;}//建议最小的值
//View 类protected int getSuggestedMinimumWidth() { //判断能否有设置背景 Background 假如没有,建议最小值就是设置的 minWidth; //假如有,就取 mMinWidth 和 背景最小值 两者的最大值。 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());}背景最小值是多少呢?点击查看源码,就来到了 Drawable 类。
//Drawable 类public int getMinimumWidth() { //首先获取 Drawable 的原始宽度 final int intrinsicWidth = getIntrinsicWidth(); //假如有原始宽度,就返回原始宽度;假如没有,就返回 0 //注: 比方 ShapeDrawable 就没有原始宽度,BitmapDrawable 有原始宽高(图片尺寸) return intrinsicWidth > 0 ? intrinsicWidth : 0;}至此,View的 measure 就分析完了。
DecorView 的 measureSpec 计算逻辑
可能我们会有疑问,假如所有子控件的 measureSpec 都是父控件结合自身的 measureSpec 和子 View 的 LayoutParams 来生成的。那么作为视图的顶级父类 DecorView 怎样获取自己的 measureSpec 呢?下面我们来分析源码:(以下源码有所删减)
//ViewRootImpl 类private void performTraversals() { //获取 DecorView 宽度的 measureSpec int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); //获取 DecorView 高度的 measureSpec int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be //开始执行测量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}//ViewRootImpl 类private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; 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;}windowSize 是 widow 的宽高大小,所以我们可以看出 DecorView 的 measureSpec 是根据 window 的宽高大小和自身的 LayoutParams 来生成的。
总结

最后相关安卓资料领取:
点赞+加群免费获取 Android IOC架构设计
加群 Android IOC架构设计领取获取往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,群内还有技术大牛一起探讨交流处理问题。


1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » View的绘制-measure流程详解