温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

MeasureSpec在View测量中的作用是什么

发布时间:2021-07-05 16:57:47 来源:亿速云 阅读:169 作者:chen 栏目:web开发

本篇内容主要讲解“MeasureSpec在View测量中的作用是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“MeasureSpec在View测量中的作用是什么”吧!

介绍

首先,我们看下这个类:

public static class MeasureSpec {        private static final int MODE_SHIFT = 30;        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;         //00后面跟30个0        public static final int UNSPECIFIED = 0 << MODE_SHIFT;        //01后面跟30个0        public static final int EXACTLY     = 1 << MODE_SHIFT;        //10后面跟30个0        public static final int AT_MOST     = 2 << MODE_SHIFT;         public static int makeMeasureSpec(int size, int mode) {            if (sUseBrokenMakeMeasureSpec) {                return size + mode;            } else {                return (size & ~MODE_MASK) | (mode & MODE_MASK);            }        }         //获取mode        public static int getMode(int measureSpec) {            //保留高2位,剩下30个0            return (measureSpec & MODE_MASK);        }         //获取size        public static int getSize(int measureSpec) {         //替换高两位00,保留低30位            return (measureSpec & ~MODE_MASK);        }     }

我留下了比较重要的三个方法:

  • makeMeasureSpec。用于生成一个MeasureSpec,生成的方式就是size+mode,得到一个32位的int值。

  • 获取mode。也就是取前2位的值作为mode。

  • 获取size。也就是取后30位的值作为size。

至此,我们至少知道了MeasureSpec是一个32位的int值,高2位为mode(测量模式),低30位为size(测量大小)。

这么做的目的主要是避免过多的对象内存分配。

所以我们可以大致猜测,这个MeasureSpec就是用来标记View的测量参数,其中测量模式可能和View具体怎么显示有关,而测量大小就是值的View实际大小。

当然,这只是我们的初步猜测。

要搞清楚具体信息,就要从View树的绘制测量开始说起。

DecorView的测量

上文说到,测量代码是从ViewRootImpl的measureHierarchy开始的,然后会执行到performMeasure方法:

private void measureHierarchy(){  childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); }      private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {        try {            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);        } finally {            Trace.traceEnd(Trace.TRACE_TAG_VIEW);        }    }

很明显,在这里就会进行第一次MeasureSpec的计算,并且传给了下层的mView,也就是DecorView。

那我们就来看看DecorView的MeasureSpec测量规格计算方式:

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;    }

所以DecorView是和它的LayoutParams有关,其实也就是跟Window的调整有关,如果Window是子窗口,那么就可以调整,比如Dialog的宽高设置为WRAP_CONTENT,那么DecorView对应的测量规格就是AT_MOST。

到此,我们也可以初步得到这个测量规格mode的含义:

  • 如果View的值是确定大小,比如MATCH_PARENT或者固定值,那么它的测量模式就是MeasureSpec.EXACTLY。

  • 如果View的值是自适应,比如WRAP_CONTENT,那么它的测量模式就是 MeasureSpec.AT_MOST。

具体是不是这样呢?我们继续到下层View一探究竟。

View/ViewGroup的测量

对于具体的View/ViewGroup  测量,就涉及到另外的一个方法measureChildWithMargins,这个方法也是在很多布局中会看到,比如LinearLayout。

protected void measureChildWithMargins(View child,             int parentWidthMeasureSpec, int widthUsed,             int parentHeightMeasureSpec, int heightUsed) {         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();          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);          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);     }

代码不多,首先获取子View的LayoutParams。然后根据 padding、margin、width 以及  parentWidthMeasureSpec 算出宽的测量模式&mdash;&mdash;childWidthMeasureSpec。

高度测量模式同理。

到此,我们的认识又前进了一步,对于子View的测量模式MeasureSpec肯定是和两个元素有关:

  • 子View的LayoutParams(包括margin,width)

  • 父View的MeasureSpec (再加上padding)

继续看看getChildMeasureSpec方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {         int specMode = MeasureSpec.getMode(spec);         int specSize = MeasureSpec.getSize(spec);          int size = Math.max(0, specSize - padding);          int resultSize = 0;         int resultMode = 0;          switch (specMode) {         // Parent has imposed an exact size on us         case MeasureSpec.EXACTLY:             if (childDimension >= 0) {                 resultSize = childDimension;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                 // Child wants to be our size. So be it.                 resultSize = size;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                 // Child wants to determine its own size. It can't be                 // bigger than us.                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             }             break;          // Parent has imposed a maximum size on us         case MeasureSpec.AT_MOST:             if (childDimension >= 0) {                 // Child wants a specific size... so be it                 resultSize = childDimension;                 resultMode = MeasureSpec.EXACTLY;             } else if (childDimension == LayoutParams.MATCH_PARENT) {                 // Child wants to be our size, but our size is not fixed.                 // Constrain child to not be bigger than us.                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             } else if (childDimension == LayoutParams.WRAP_CONTENT) {                 // Child wants to determine its own size. It can't be                 // bigger than us.                 resultSize = size;                 resultMode = MeasureSpec.AT_MOST;             }             break;          // Parent asked to see how big we want to be         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;         }         //noinspection ResourceType         return MeasureSpec.makeMeasureSpec(resultSize, resultMode);     }

代码其实很简单,就是对子View的LayoutParams和父View的specMode、specSize,共同计算出子View的MeasureSpec。

举其中一个例子,当父view的测量模式为MeasureSpec.EXACTLY,子View宽的LayoutParams为MATCH_PARENT。想象一下,这种情况,子View的宽肯定就会占满父View的大小,所以子View的测量模式中的mode肯定就是确定值,为MeasureSpec.EXACTLY,而大小就是父View的大小了。对应的代码就是:

case MeasureSpec.AT_MOST: if (childDimension == LayoutParams.MATCH_PARENT) {     // Child wants to be our size. So be it.     resultSize = size;     resultMode = MeasureSpec.EXACTLY; }

综合所有的情况,很经典的一张表格就来了:

MeasureSpec在View测量中的作用是什么

这里我们也可以明确了MeasureSpec中mode的含义:

  • MeasureSpec.EXACTLY。父View可以确定子View的精确大小,比如子View大小是固定的值,在所有的情况下都会是EXACTLY模式。

  • MeasureSpec.AT_MOST。父View给定一个最大的值,意思是子View大小可以不确定,但是肯定不能超过某个最大的值,例如窗口的大小。

  • MeasureSpec.UNSPECIFIED。父View对子View完全没限制,要多大给多大。这个模式似乎听起来有点奇怪?待会我们再细谈。

到此,似乎就结束了?当然没啦,获取子View的MeasureSpec之后,子View又会怎么处理呢?

View对于MeasureSpec的处理

继续上文,测量子View的测量规格之后,会调用child.measure方法。

protected void measureChildWithMargins() {         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();          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);          child.measure(childWidthMeasureSpec, childHeightMeasureSpec);     }  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  onMeasure(widthMeasureSpec, heightMeasureSpec); }

child.measure方法也就是View的measure方法,也就是走到了onMeasure方法,继续看看:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {      setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),              getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  }   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {      if (optical != isLayoutModeOptical(mParent)) {          measuredWidth  += optical ? opticalWidth  : -opticalWidth;          measuredHeight += optical ? opticalHeight : -opticalHeight;      }      setMeasuredDimensionRaw(measuredWidth, measuredHeight);  }

哦~最后原来是给子View的measuredWidth和measuredHeight赋值了,所赋的值就是getDefaultSize方法返回的大小。

而这个measuredWidth是干嘛的呢?搜索一下:

public final int getMeasuredWidth() {  //MEASURED_SIZE_MASK用于限制大小的         return mMeasuredWidth & MEASURED_SIZE_MASK;     }

这不就是我们获取view的大小调用的方法吗?所以小结一下:

  • 父view通过父View的MeasureSpec和子View的LayoutParams算出了子View的MeasureSpec。

  • 然后子View通过MeasureSpec计算了measuredWidth

  • 而这个measuredWidth也就是我们可以获取View宽高所调用的方法。

最后就是看看getDefaultSize方法干了啥,也就是验证MeasureSpec中size是不是就是我们要获取的View的宽高呢?

getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)     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;    }

可以看到,在AT_MOST和EXACTLY这两种常用的情况下,确实是等于测量大小specSize的。

只是在一个特殊情况,也就是UNSPECIFIED的时候,这个大小会等于getSuggestedMinimumWidth()方法的大小。

问题来了,UNSPECIFIED模式到底是啥,getSuggestedMinimumWidth()方法又做了什么?

UNSPECIFIED

很多文章会忽略这个模式,其实它也是很重要的,在前两天的讨论群中,我们还讨论了这个问题,一起看看吧~

首先,我们看看什么时候会存在UNSPECIFIED模式呢?它的概念是父View对子View的大小没有限制,很容易想到的一个控件就是ScrollView,那么在ScrollView中肯定有对这个模式的设置:

@Override    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,            int parentHeightMeasureSpec, int heightUsed) {        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                        + widthUsed, lp.width);        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +                heightUsed;        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),                MeasureSpec.UNSPECIFIED);         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);    }

没错,在ScrollView中重写了measureChildWithMargins方法,比对下刚才ViewGroup的measureChildWithMargins方法,发现有什么不对了吗?

childWidthMeasureSpec的计算没有什么变化,还是调用了getChildMeasureSpec方法,但是childHeightMeasureSpec不对劲了,直接调用了makeSafeMeasureSpec方法生成了MeasureSpec,而且!而且!直接把SpecMode设置成了MeasureSpec.UNSPECIFIED。

也就是对于子View的高度是无限制的,这也符合ScrollView的理念。

所以当ScrollView嵌套一个普通View的时候,就会触发刚才getDefaultSize中UNSPECIFIED的逻辑,也就是View的实际大小为getSuggestedMinimumWidth的大小。

继续看看getSuggestedMinimumWidth到底获取的是什么大小:

protected int getSuggestedMinimumWidth() {         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());     }

就一句代码:

  • 如果view的背景为null,则等于最小宽度mMinWidth。

  • 如果view的背景不为null,则等于最小宽度和 背景的最小宽度 中取较大值。

所以如果View没有设置背景,没有设置mMinWidth,那么ScrollView嵌套View的情况,View的宽度就是为0,即使设置了固定值也没用。

这只是UNSPECIFIED在普通View中的处理情况,不同的情况对UNSPECIFIED的处理方式都不一样,比如TextView、RecycleView等等。

下次会专门出一篇UNSPECIFIED的文章,到时候见。

总结

今天回顾了MeasureSpec的相关知识点:

  • MeasureSpec的基本概念:

  • MeasureSpec为一个32位的int值。

  • SpecMode为高两位,一共三种模式,代表父View对子View的大小限制模式,比如最大可用大小&mdash;&mdash;AT_MOST。

SpecSize为低30位,代表父View给子View测量好的宽高。这个宽高大概率等于View的实际宽高,但是也有例外情况,也就是UNSPECIFIED的情况。

测量流程中的MeasureSpec:

  • View输的测量流程开始于ViewRootImpl的measureHierarchy,也是在这里开始了第一次MeasureSpec的计算。

  • 第一次MeasureSpec的计算也就是DecorView的MeasureSpec计算,是通过自身的LayoutParams相关,也就是和Window大小有关。

  • 然后就开始子View/ViewGroup的MeasureSpec计算,是通过父View的MeasureSpec和子View的LayoutParams相关。

  • 计算完子View的MeasureSpec之后,就开始调用onMeasure方法,计算出View的实际大小。

  • 如果是UNSPECIFIED模式,实际大小为。否则实际大小就等于计算好的specSize。

到此,相信大家对“MeasureSpec在View测量中的作用是什么”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI