View那些事儿(2) -- 理解MeasureSpec

View的绘制的三大流程的第一步就是Measure(测量),想要理解View的测量过程,必须要先理解MeasureSpec,从字面上看,MeasureSpec就是“测量规格”的意思。其实它在一定程度上决定了View的测量过程,具体来讲它包含了一个View的尺寸规格信息。在系统测量View的尺寸规格的时候会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据MeasureSpec测量出View的宽高。

一.MeasureSpec的组成

MeasureSpec代表了一个32位的int值,高2位代表了SpecMode(测量模式),低30位代表了SpecSize(规格大小)。下面结合一段源码来分析:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;//二进制数左移的位数
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//将二进制数11左移动30位
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;//将二进制数00左移动30位          
    public static final int EXACTLY= 1 << MODE_SHIFT;//将二进制数01左移动30位
    public static final int AT_MOST= 2 << MODE_SHIFT;//将二进制数10左移动30位  
    //将SpecMode和SpecSize包装成MeasureSpec
    public static int makeMeasureSpec(int size, int mode ){
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    //从mesureSpec中取出SpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    //从measureSpec中取出SpecSize
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

通过以上源码,便可以很清晰地看出整个MeasureSpec的工作原理,其中涉及到了java里的按位运算操作,“&”是按位与的意思,“~”则是按位取反的意思,“<<”是左移运算符(x<<1就表示x的值乘2),这些运算符都是在2进制数的层面上进行运算的,具体的运算规则google上面一大堆,这里就不多说了。
MeasrureSpec类的三个常量:UNSPECIFIED、EXACTLY、AT_MOST分别代表的三种SpecMode(测量模式):

  • UNSPECIFIED(不指定大小的)
    父容器不对View有任何限制,想要多大有多大,这种情况一般属于系统内部,表示一种测量状态,几乎用不到。例如:系统对ScrollView的绘制过程。
  • EXACTLY(精确的)
    父容器已经检测出View所需要的精确的大小,这个时候View的最终大小就是SpecSize所指定的值。这里对应于:LayoutParams中的match_parent和具体的数值,如20dp。
  • AT_MOST(最大的)
    这种模式稍微难处理一点,它指的是父容器指定了一个可用的大小(SpecSize),View的大小不能超过这个值的大小。如果超过了,就取父容器的大小,如果没超过,就取自身的大小。这里对应于:LayoutParams中的wrap_content模式。

全部都是些理论,没点demo怎么行,下面直接上一段代码吧:

首先,要实现的效果很简单,就是一个圆形的自定义View(我们主要是要处理wrap_content的情况下的数据,必须给它一个默认值):

public class CircleView extends View {
    Paint mPaint;//画笔类
    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //第一步肯定是拿到View的测量宽高(SpecSize)和测量模式(SpecMode)
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //View显示的时候的实际的大小
        int width = 0;
        int height = 0;
        //开始处理宽度
        //默认的宽度(在wrap_content的情况下必须有个默认的宽高)
        int defaultWidth = 200;
        //判断SpecMode(在xml中指定View的宽度的时候就已经决定了View的SpecMode)
        switch (widthMode){
            case MeasureSpec.AT_MOST://这里指的是wrap_content,在这个模式下不能超过父容器的宽度
                width = defaultWidth;
                break;
            case MeasureSpec.EXACTLY://这里指的是match_parent或者具体的值,不需要做什么处理,width直接等于widthSize就可以了
                width = widthSize;
                break;
            case MeasureSpec.UNSPECIFIED://这个模式用不到,完全可以忽略
                width = defaultWidth;
                break;
            default:
                width = defaultWidth;
                break;
        }
        //开始处理高度
        int defaultHeight = 200;
        switch (heightMode){
            case MeasureSpec.AT_MOST://这里指的是wrap_content,在这个模式下不能超过父容器的高度
                height = defaultHeight;
                break;
            case MeasureSpec.EXACTLY://这里指的是match_parent或者具体的值,不需要做什么处理,height直接等于heightSize就可以了
                height = heightSize;
                break;
            case MeasureSpec.UNSPECIFIED://这个模式用不到,完全可以忽略
                height = defaultHeight;
                break;
            default:
                height = defaultHeight;
                break;
        }
        //最后必须调用父类的测量方法,来保存我们计算的宽度和高度,使得设置的测量值生效
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //初始化画笔,并进行一系列的设置,如颜色等。
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);//设置抗锯齿
        //canvas.drawCircle的几个参数分别是:圆心的x坐标,y坐标,半径,画笔
        canvas.drawCircle(getWidth()/2,getHeight()/2,Math.min(getWidth()/2,getHeight()/2),mPaint);
    }
}

显示效果如下:

处理wrap_content后的自定义View

大家可以试一下,如果把不在onMreasure中做以上处理(注释掉onMeasure方法即可看见效果),给CircleView设置的wrap_content会失效,实际的显示效果和match_parent没什么区别。
在这个地方提前写了一个自定义View是为了帮助读者理解MeasureSpec的相关用法,详细结合前一篇文章的讲解加上代码的注释,大家还是能很容易看懂的。
当然为了把这个过程表述清楚,我把代码写得很详细,显得有点累赘,实际中其实可以不用这么详细。

二.MeasureSpec和LayouParams的关系

上面都在讲MeasureSpec是什么,现在就该说说他是怎么来的了。
这里要分两部分说:第一是普通的View,第二是DecorView

  • 在普通View测量过程中,系统会将View自身的LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽和高;
  • 在DecorView(顶级View)的测量过程中,系统会将View自生的LayoutParams在窗口尺寸的约束下转换为对应的MeasureSpec 。

1.首先,重点说一下普通的View

对于普通的View(即在布局文中的View)来说,它的measure过程由ViewGroup传递而来,所以先来看看ViewGroup的measureChildWithMargins()方法:

//对ViewGroup的子View进行Measure的方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
    //获取子View的布局参数信息(MarginLayoutParams是继承自ViewGroup.LayoutParmas的)
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //获取子View的宽度的MeasureSpec,需要传入父容器的parentWidthMeasureSpec等信息
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //获取子View的高度的MeasureSpec,需要传入父容器的parentHeightMeasureSpec等信息    
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}  

结合注释,从上面这段测量子View的MeasureSpec的源码中可以看出,在测量的时候传入了父容器的MeasureSpec信息和子View自身的LayoutParams信息(如margin、padding),所以才说普通View的测量过程与父容器和自身的LayoutParams有关。
那么像知道测量的具体过程就得看看getChildMeasureSpec()这个方法了(看似代码较多,但是很简单):

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
          //还是先拿到specMode和specSize信息
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
          //padding指的是父容器已经占据的空间大小,所以,子View的大小因为父容器的大小减去padding
        int size = Math.max(0, specSize - padding);
          //测量后View最终的specSize和specMode
        int resultSize = 0;
        int resultMode = 0;
          //针对三种specMode进行判断
        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);
    }

上面的代码很清晰地展示这个测量过程,下面一个表格来梳理具体的判断逻辑。(parentSize是指父容器目前可使用的大小)

parentSpecMode →
childLayoutParams↓
EXACTLY AT_MOST UNSPICIFIED
dp/px(确定的宽/高) EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

根据表格,getChildMeasureSpec()方法的判断规则一目了然:

  • 当View的指定了确定的宽高的时候,无论父容器的SpecMode是什么,它的SpecMode都是EXACTLY,并且大小遵循LayoutParams中的大小;
  • 当View的宽/高指定成match_parent的时候,它的SpecMode与父容器相同,并且大小不能超过父容器的剩余空间大小;
  • 当View的宽/高指定成wrap_content的时候,它的SpecMode恒为(不考虑UNSPECIFIED的情况)AT_MOST,并且大小不能超过父容器的剩余空间大小。

由于UNSPECIFIED模式我们一般接触不到,故在这里不做讨论
从上面的总结来看,其实普通View的MeasureSpec的LayoutParams的关系还是很容易理解与记忆的。

2.下面该来看看DecorView(顶级View)了

对于DecorView来说,MeasureSpec是由窗口尺寸和自身的LayoutParams共同决定的。
还是来看看源码吧,在ViewRootImpl(这个类被隐藏了,需要手动搜索sdk目录找出ViewRootImpl.java才能看见源码)有一个meaureHierarchy()方法,其中有下面这段代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);      

其中,desiredWindowWidth和desiredWindowHeight分别指的是屏幕的宽高。
看到这里就隐约感觉到DecorView的MeasureSpec会和窗口尺寸有关,再来看看getRootMeasureSpec就更明了了:

private 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的MeasureSpec的确和窗口尺寸有关。

所以,Decor的MeasureSpec根据它的LayoutParams遵循以下规则:

  • LayoutParams.MATCH_PARENT: EXACTLY(精准模式),大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT: AT_MOST(最大模式),大小不定,但是不能超过窗口的大小;
  • 固定的大小(如200dp): EXACTLY(精准模式),大小为LayoutParams所制定的大小。

至此,对MeasureSpec的理解就结束了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容