3.3onMeasure流程设计
目录介绍
- 01.Measure流程分析
- 02.MeasureSpec的确定
- 03.View的测量流程
- 04.View的onMeasure源码
- 05.NestedScrollView测量过程
- 06.看几个思考题分析
- 07.View中onMeasure方法
- 08.ViewGroup中onMeasure方法
01.Measure流程分析
- 顾名思义,就是测量每个控件的大小。调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { }
- measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
- 由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?
- 如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //super.onMeasure(widthMeasureSpec, heightMeasureSpec); //从中获取View的宽/高 int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); //指定View的宽高,完成测量工作 setMeasuredDimension(width,height); }
02.MeasureSpec的确定
2.1 MeasureSpec是什么
- 先介绍下什么是MeasureSpec?
img
- MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。
- 其中,Mode模式共分为三类
- UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
- EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值
- AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
- 其中,Mode模式共分为三类
- 那么MeasureSpec又是如何确定的?
- 对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。
- 这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。
- 对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。
- 这部分比较复杂。以下列图表表示不同的情况:
img - 当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。
- 从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。
2.2 MeasureSpec静态类
- 代码如下所示
- MeasureSpec总结起来就是:
- 它由2部分数据组成,分别为定义了View测量的模式和View的测量尺寸大小
- 其中EXACTLY精确模式表示的是match_parent和具体值;AT_MOST最大模式表示的是wrap_content的情况
//view.class public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** * 这种模式不用关心 */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * 精确模式,对应的是match_parent和具体值,比如100dp public static final int EXACTLY = 1 << MODE_SHIFT; /** * 最大模式,对应的就是wrap_content */ public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * 获取测量的模式 */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /** * 获取测量到的尺寸大小 */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }
03.View的测量流程
- View的测量流程如下所示
img
04.View的onMeasure源码
- View中onMeasure方法已经默认为我们的控件测量了宽高,我们看看它做了什么工作:
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } /** * 为宽度获取一个建议最小值 */ protected int getSuggestedMinimumWidth () { return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth()); } /** * 获取默认的宽高值 */ 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: result = size; break; case MeasureSpec. AT_MOST: case MeasureSpec. EXACTLY: result = specSize; break; } return result; }
- 从源码可以知道:
- 如果View的宽高模式为未指定,他的宽高将设置为android:minWidth/Height =”“值与背景宽高值中较大的一个;
- 如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
- 如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
- 如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。
- 也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者MATCH_PARENT 的情况,我们可以不用重写onMeasure方法。但如果自定义控件需要设置包裹内容WRAP_CONTENT ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下WRAP_CONTENT 的处理也将填充整个父控件。
05.NestedScrollView测量过程
- NestedScrollView是继承FrameLayout的控件,然后先看看onMeasure方法中的源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //通过super方法先测量父布局 super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (this.mFillViewport) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != 0) { if (this.getChildCount() > 0) { //获取自布局,NestedScrollView下面只能嵌套一个线性布局或者相对布局。 View child = this.getChildAt(0); LayoutParams lp = (LayoutParams)child.getLayoutParams(); int childSize = child.getMeasuredHeight(); int parentSpace = this.getMeasuredHeight() - this.getPaddingTop() - this.getPaddingBottom() - lp.topMargin - lp.bottomMargin; if (childSize < parentSpace) { int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, 1073741824); //测量子类child的控件 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } } }
06.看几个思考题分析
6.1 getWidth()方法和getMeasureWidth()
- getWidth()方法和getMeasureWidth()区别是什么?
- getMeasureWidth()
- getMeasureWidth()方法在measure()过程结束后就可以获取到了,另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的
- 这里mMeasuredWidth & MEASURED_SIZE_MASK表示的是测量阶段结束之后,view真实的值。
public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; }
- getWidth()
- getWidth()方法要在layout()过程结束后才能获取到,getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
- mRight和mLeft是什么值,是在什么时候被设置的。具体看layout()过程中源码
@ViewDebug.ExportedProperty(category = "layout") public final int getWidth() { return mRight - mLeft; }
- getMeasureWidth()
6.2 getWidth()或者getMeasureWidth()得到0
- 为什么有时候用getWidth()或者getMeasureWidth()得到0
- 问题描述:使用getMeasuredWidth()和getMeasuredHeight()方法,无论是在onCreate()、onStart()、onResume()中调用,都无法得到控件的长度、和宽度。如下图,测量的结果为0。
- 解释:技术博客大总结
- 因为View的Measure过程和Activity的生命周期方法不是同步执行的,所以无法保证Activity执行了onCreate()、onStart()、onResume()时某个View已经测量完毕了,如果View还没有测量完毕,那么获得宽/高就是0。
- 解决控件测量宽高问题
- 如下所示
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); test(); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); getWidth(4); } private void test(){ getWidth(1); marqueeView.measure(0, 0); getWidth(2); marqueeView.post(new Runnable() { @Override public void run() { getWidth(3); } }); } private void getWidth(int a){ int width2 = marqueeView.getWidth(); int measuredWidth2 = marqueeView.getMeasuredWidth(); Log.e(a+"MainActivity-----",width2+"-----"+measuredWidth2); } //11-28 17:03:17.559 15990-15990/com.yc.cn.ycbanner E/1MainActivity-----: 0-----0 //11-28 17:03:17.567 15990-15990/com.yc.cn.ycbanner E/2MainActivity-----: 0-----760 //11-28 17:03:17.684 15990-15990/com.yc.cn.ycbanner E/3MainActivity-----: 960-----960 //11-28 17:03:17.685 15990-15990/com.yc.cn.ycbanner E/4MainActivity-----: 960-----960
- 什么时候测量宽高不等于实际宽高?
- MeasuredWidth/height!=getWidth/Height()的场景:更改View的布局参数并进行重新布局后,就会导致测量宽高!=实际宽高
07.View中onMeasure方法
- 下面是真是开发案例中的代码,如下所示
//Android7.0以后,优化了View的绘制,onMeasure和onSizeChanged调用顺序有所变化 //Android7.0以下:onMeasure--->onSizeChanged--->onMeasure //Android7.0以上:onMeasure--->onSizeChanged @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); /* * onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值 * MeasureSpec.EXACTLY 是精确尺寸 * MeasureSpec.AT_MOST 是最大尺寸 * MeasureSpec.UNSPECIFIED 是未指定尺寸 */ if (heightMode == MeasureSpec.EXACTLY) { heightSize = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY); } else if (heightMode == MeasureSpec.AT_MOST && getParent() instanceof ViewGroup && heightSize == ViewGroup.LayoutParams.MATCH_PARENT) { heightSize = MeasureSpec.makeMeasureSpec(((ViewGroup) getParent()).getMeasuredHeight(), MeasureSpec.AT_MOST); } else { int heightNeeded; if (gravity == Gravity.CENTER) { if (tickMarkTextArray != null && tickMarkLayoutGravity == Gravity.BOTTOM) { heightNeeded = (int) (2 * (getRawHeight() - getTickMarkRawHeight())); } else { heightNeeded = (int) (2 * (getRawHeight() - Math.max(leftSB.getThumbScaleHeight(), rightSB.getThumbScaleHeight()) / 2)); } } else { heightNeeded = (int) getRawHeight(); } heightSize = MeasureSpec.makeMeasureSpec(heightNeeded, MeasureSpec.EXACTLY); } super.onMeasure(widthMeasureSpec, heightSize); }
- 下面是ImageView的源代码
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { resolveUri(); int w; int h; // Desired aspect ratio of the view's contents (not including padding) float desiredAspect = 0.0f; // We are allowed to change the view's width boolean resizeWidth = false; // We are allowed to change the view's height boolean resizeHeight = false; final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (mDrawable == null) { // If no drawable, its intrinsic size is 0. mDrawableWidth = -1; mDrawableHeight = -1; w = h = 0; } else { w = mDrawableWidth; h = mDrawableHeight; if (w <= 0) w = 1; if (h <= 0) h = 1; // We are supposed to adjust view bounds to match the aspect // ratio of our drawable. See if that is possible. if (mAdjustViewBounds) { resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; desiredAspect = (float) w / (float) h; } } final int pleft = mPaddingLeft; final int pright = mPaddingRight; final int ptop = mPaddingTop; final int pbottom = mPaddingBottom; int widthSize; int heightSize; if (resizeWidth || resizeHeight) { /* If we get here, it means we want to resize to match the drawables aspect ratio, and we have the freedom to change at least one dimension. */ // Get the max possible width given our constraints widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec); // Get the max possible height given our constraints heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec); if (desiredAspect != 0.0f) { // See what our actual aspect ratio is final float actualAspect = (float)(widthSize - pleft - pright) / (heightSize - ptop - pbottom); if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { boolean done = false; // Try adjusting width to be proportional to height if (resizeWidth) { int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) + pleft + pright; // Allow the width to outgrow its original estimate if height is fixed. if (!resizeHeight && !sCompatAdjustViewBounds) { widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec); } if (newWidth <= widthSize) { widthSize = newWidth; done = true; } } // Try adjusting height to be proportional to width if (!done && resizeHeight) { int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) + ptop + pbottom; // Allow the height to outgrow its original estimate if width is fixed. if (!resizeWidth && !sCompatAdjustViewBounds) { heightSize = resolveAdjustedSize(newHeight, mMaxHeight, heightMeasureSpec); } if (newHeight <= heightSize) { heightSize = newHeight; } } } } } else { /* We are either don't want to preserve the drawables aspect ratio, or we are not allowed to change view dimensions. Just measure in the normal way. */ w += pleft + pright; h += ptop + pbottom; w = Math.max(w, getSuggestedMinimumWidth()); h = Math.max(h, getSuggestedMinimumHeight()); widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); } setMeasuredDimension(widthSize, heightSize); }
08.ViewGroup中onMeasure方法
- 下面是真是开发案例中LinearLayout的代码,如下所示
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int myWidth = -1; int myHeight = -1; int width = 0; int height = 0; final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // Record our dimensions if they are known; if (widthMode != MeasureSpec.UNSPECIFIED) { myWidth = widthSize; } if (heightMode != MeasureSpec.UNSPECIFIED) { myHeight = heightSize; } if (widthMode == MeasureSpec.EXACTLY) { width = myWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = myHeight; } View ignore = null; int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.START && gravity != 0; gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0; int left = Integer.MAX_VALUE; int top = Integer.MAX_VALUE; int right = Integer.MIN_VALUE; int bottom = Integer.MIN_VALUE; boolean offsetHorizontalAxis = false; boolean offsetVerticalAxis = false; if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) { ignore = findViewById(mIgnoreGravity); } final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY; final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY; // We need to know our size for doing the correct computation of children positioning in RTL // mode but there is no practical way to get it instead of running the code below. // So, instead of running the code twice, we just set the width to a "default display width" // before the computation and then, as a last pass, we will update their real position with // an offset equals to "DEFAULT_WIDTH - width". final int layoutDirection = getLayoutDirection(); if (isLayoutRtl() && myWidth == -1) { myWidth = DEFAULT_WIDTH; } View[] views = mSortedHorizontalChildren; int count = views.length; for (int i = 0; i < count; i++) { View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); int[] rules = params.getRules(layoutDirection); applyHorizontalSizeRules(params, myWidth, rules); measureChildHorizontal(child, params, myWidth, myHeight); if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { offsetHorizontalAxis = true; } } } views = mSortedVerticalChildren; count = views.length; final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; //省略部分代码 setMeasuredDimension(width, height); }
其他介绍
01.关于博客汇总链接
02.关于我的博客
- github:https://github.com/yangchong211
- 知乎:https://www.zhihu.com/people/yczbj/activities
- 简书:http://www.jianshu.com/u/b7b2c6ed9284
- csdn:http://my.csdn.net/m0_37700275
- 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
- 开源中国:https://my.oschina.net/zbj1618/blog
- 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 邮箱:yangchong211@163.com
- 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
- 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e