文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

温故而知新 MeasureSpec在View测量中的作用

2024-12-03 05:26

关注

介绍

首先,我们看下这个类:

  1. public static class MeasureSpec { 
  2.        private static final int MODE_SHIFT = 30; 
  3.        private static final int MODE_MASK  = 0x3 << MODE_SHIFT; 
  4.  
  5.        //00后面跟30个0 
  6.        public static final int UNSPECIFIED = 0 << MODE_SHIFT; 
  7.        //01后面跟30个0 
  8.        public static final int EXACTLY     = 1 << MODE_SHIFT; 
  9.        //10后面跟30个0 
  10.        public static final int AT_MOST     = 2 << MODE_SHIFT; 
  11.  
  12.        public static int makeMeasureSpec(int sizeint mode) { 
  13.            if (sUseBrokenMakeMeasureSpec) { 
  14.                return size + mode; 
  15.            } else { 
  16.                return (size & ~MODE_MASK) | (mode & MODE_MASK); 
  17.            } 
  18.        } 
  19.  
  20.        //获取mode 
  21.        public static int getMode(int measureSpec) { 
  22.            //保留高2位,剩下30个0 
  23.            return (measureSpec & MODE_MASK); 
  24.        } 
  25.  
  26.        //获取size 
  27.        public static int getSize(int measureSpec) { 
  28.         //替换高两位00,保留低30位 
  29.            return (measureSpec & ~MODE_MASK); 
  30.        } 
  31.  
  32.    } 

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

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

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

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

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

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

DecorView的测量

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

  1. private void measureHierarchy(){ 
  2.  childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); 
  3.        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
  4.        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 
  5.  
  6.  
  7.    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { 
  8.        try { 
  9.            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  10.        } finally { 
  11.            Trace.traceEnd(Trace.TRACE_TAG_VIEW); 
  12.        } 
  13.    } 

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

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

  1. private static int getRootMeasureSpec(int windowSize, int rootDimension) { 
  2.        int measureSpec; 
  3.        switch (rootDimension) { 
  4.  
  5.        case ViewGroup.LayoutParams.MATCH_PARENT: 
  6.            // Window can't resize. Force root view to be windowSize. 
  7.            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 
  8.            break; 
  9.        case ViewGroup.LayoutParams.WRAP_CONTENT: 
  10.            // Window can resize. Set max size for root view
  11.            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 
  12.            break; 
  13.        default
  14.            // Window wants to be an exact sizeForce root view to be that size
  15.            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 
  16.            break; 
  17.        } 
  18.        return measureSpec; 
  19.    } 

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

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

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

View/ViewGroup的测量

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

  1. protected void measureChildWithMargins(View child, 
  2.             int parentWidthMeasureSpec, int widthUsed, 
  3.             int parentHeightMeasureSpec, int heightUsed) { 
  4.         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 
  5.  
  6.         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
  7.                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 
  8.                         + widthUsed, lp.width); 
  9.         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
  10.                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 
  11.                         + heightUsed, lp.height); 
  12.  
  13.         child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  14.     } 

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

高度测量模式同理。

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

继续看看getChildMeasureSpec方法:

  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
  2.         int specMode = MeasureSpec.getMode(spec); 
  3.         int specSize = MeasureSpec.getSize(spec); 
  4.  
  5.         int size = Math.max(0, specSize - padding); 
  6.  
  7.         int resultSize = 0; 
  8.         int resultMode = 0; 
  9.  
  10.         switch (specMode) { 
  11.         // Parent has imposed an exact size on us 
  12.         case MeasureSpec.EXACTLY: 
  13.             if (childDimension >= 0) { 
  14.                 resultSize = childDimension; 
  15.                 resultMode = MeasureSpec.EXACTLY; 
  16.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  17.                 // Child wants to be our size. So be it. 
  18.                 resultSize = size
  19.                 resultMode = MeasureSpec.EXACTLY; 
  20.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  21.                 // Child wants to determine its own size. It can't be 
  22.                 // bigger than us. 
  23.                 resultSize = size
  24.                 resultMode = MeasureSpec.AT_MOST; 
  25.             } 
  26.             break; 
  27.  
  28.         // Parent has imposed a maximum size on us 
  29.         case MeasureSpec.AT_MOST: 
  30.             if (childDimension >= 0) { 
  31.                 // Child wants a specific size... so be it 
  32.                 resultSize = childDimension; 
  33.                 resultMode = MeasureSpec.EXACTLY; 
  34.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  35.                 // Child wants to be our size, but our size is not fixed. 
  36.                 // Constrain child to not be bigger than us. 
  37.                 resultSize = size
  38.                 resultMode = MeasureSpec.AT_MOST; 
  39.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  40.                 // Child wants to determine its own size. It can't be 
  41.                 // bigger than us. 
  42.                 resultSize = size
  43.                 resultMode = MeasureSpec.AT_MOST; 
  44.             } 
  45.             break; 
  46.  
  47.         // Parent asked to see how big we want to be 
  48.         case MeasureSpec.UNSPECIFIED: 
  49.             if (childDimension >= 0) { 
  50.                 // Child wants a specific size... let him have it 
  51.                 resultSize = childDimension; 
  52.                 resultMode = MeasureSpec.EXACTLY; 
  53.             } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  54.                 // Child wants to be our size... find out how big it should 
  55.                 // be 
  56.                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  57.                 resultMode = MeasureSpec.UNSPECIFIED; 
  58.             } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  59.                 // Child wants to determine its own size.... find out how 
  60.                 // big it should be 
  61.                 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  62.                 resultMode = MeasureSpec.UNSPECIFIED; 
  63.             } 
  64.             break; 
  65.         } 
  66.         //noinspection ResourceType 
  67.         return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
  68.     } 

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

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

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

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

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

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

View对于MeasureSpec的处理

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

  1. protected void measureChildWithMargins() { 
  2.         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 
  3.  
  4.         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
  5.                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 
  6.                         + widthUsed, lp.width); 
  7.         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
  8.                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 
  9.                         + heightUsed, lp.height); 
  10.  
  11.         child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  12.     } 
  13.  
  14. public final void measure(int widthMeasureSpec, int heightMeasureSpec) { 
  15.  onMeasure(widthMeasureSpec, heightMeasureSpec); 
  16.      

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

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  2.      setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
  3.              getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
  4.  } 
  5.  
  6.  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { 
  7.      if (optical != isLayoutModeOptical(mParent)) { 
  8.          measuredWidth  += optical ? opticalWidth  : -opticalWidth; 
  9.          measuredHeight += optical ? opticalHeight : -opticalHeight; 
  10.      } 
  11.      setMeasuredDimensionRaw(measuredWidth, measuredHeight); 
  12.  } 

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

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

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

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

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

  1. getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec) 
  2.  
  3.    public static int getDefaultSize(int sizeint measureSpec) { 
  4.        int result = size
  5.        int specMode = MeasureSpec.getMode(measureSpec); 
  6.        int specSize = MeasureSpec.getSize(measureSpec); 
  7.  
  8.        switch (specMode) { 
  9.        case MeasureSpec.UNSPECIFIED: 
  10.            result = size
  11.            break; 
  12.        case MeasureSpec.AT_MOST: 
  13.        case MeasureSpec.EXACTLY: 
  14.            result = specSize; 
  15.            break; 
  16.        } 
  17.        return result; 
  18.    } 

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

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

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

UNSPECIFIED

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

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

  1. @Override 
  2.    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 
  3.            int parentHeightMeasureSpec, int heightUsed) { 
  4.        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 
  5.  
  6.        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
  7.                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 
  8.                        + widthUsed, lp.width); 
  9.        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + 
  10.                heightUsed; 
  11.        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( 
  12.                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal), 
  13.                MeasureSpec.UNSPECIFIED); 
  14.  
  15.        child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  16.    } 

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

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

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

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

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

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

就一句代码:

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

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

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

总结

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

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

测量流程中的MeasureSpec:

参考

《Android开发艺术探索》

本文转载自微信公众号「码上积木」,作者积木zz。转载本文请联系码上积木公众号。

 

来源:码上积木内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯