自定义View之onMeasure()方法

前言

一个View从创建到被绘制到屏幕上,需要完成measure(测量)、layout(布置)、draw(绘制)三个步骤,分别对应View中的measure()、layout()、draw()三个方法。网上关于这三个方法的源码解析文章有很多,而且一般情况下也不会去重写它们(measure()方法还无法覆盖),因此本文不打算将其作为重点。本文以及接下来的几篇文章会详细介绍和编程人员关系更大的onMeasure()、onLayout()与onDraw()的具体实现方法,以及过程中会涉及到的一些知识。

MeasureSpec

View中的onMeasure()方法是这样的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...}

可以看到,它的参数是widthMeasureSpec与heightMeasureSpec两个int值。这两个参数实质上是由View的静态内部类MeasureSpec管理的两个特殊的“对象”(并非真正的对象),包含了父view关于子view应当如何测量自身给出的“指示”。为了提升效率,Android系统采用位运算的方式,将模式SpecMode(2位)与尺寸SpecSize(30位)拼接成了一个int值,并传递这个int值作为测量时使用的参数。
MeasureSpec类的实现基本都是依靠位运算,没什么实质性内容。直接上一些结论:
(1)SpecMode分三种:UNSPECIFIED、EXACTLY、AT_MOST。UNSPECIFIED表示不指定具体测量模式,EXACTLY表示父View希望子view的尺寸取精确值(即等于SpecSize),AT_MOST表示父View希望子view的尺寸不超过SpecSize。
(2)使用MeasureSpec.getMode(int measureSpec)与MeasureSpec.getSize(int measureSpec)获取SpecMode与SpecSize。
(3)使用MeasureSpec.makeMeasureSpec(int size,int mode)生成一个measureSpec值。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法

下面回到onMeasure()方法。顾名思义,这个方法是在该view需要测量自身时调用的。具体来说,当这个view的父view对其调用measure()方法时,onMeasure()方法会在过程中被调用。下面分别看看View与ViewGroup分别应当怎么实现这个方法。

View的onMeasure()实现

view只需要根据自身情况,计算出自己的尺寸就可以了。步骤如下:
(1)使用MeasureSpec.getMode()与MeasureSpec.getSize()获取父view要求的SpecMode与SpecSize。
(2)根据上面的参数确定自己的实际尺寸(width与height)。一般来说,如果SpecMode是EXACTLY,那么直接取尺寸值=SpecSize即可。如果SpecMode是AT_MOST,那么就需要根据自身特点计算出一个尺寸值,并保证最终尺寸值不超过SpecSize。当然了,你也可以完全无视父view的要求,自顾自地进行测量,不过这种方式显然是不推荐的。
(3)使用setMeasuredDimension(int measuredWidth, int measuredHeight)设置最终测量尺寸。这个方法被调用之后,view的getMeasuredWidth()方法与getMeasuredHeight()方法才能生效(在之前调用会返回0)。
实际上,对于不那么复杂的自定义view,View类提供的默认实现已经可以满足大部分需求了。下面看看它是怎么做的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
    getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

getSuggestedMinimumWidth()与getSuggestedMinimumHeight()是根据view是否设置了BackgroundDrawable确定一个最小尺寸值。重点看一下getDefaultSize()方法:

public static int getDefaultSize(int size, int measureSpec) {
//size是view根据自身需求提供的一个尺寸值,measureSpec来自父view。
    //最终尺寸值
    int result = size;
    //获取父布局要求的测量模式与测量尺寸
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //如果模式为UNSPECIFIED则不作限制
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    //AT_MOST直接当做EXACTLY处理
    case MeasureSpec.AT_MOST:
    //如果模式为EXACTLY,则根据父布局要求确定最终尺寸值
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

解释见代码注释。这里需要记住的是,如果使用这个方法计算尺寸值的话,AT_MOST模式不会生效。AT_MOST一般是在view的layout_width与layout_height为WRAP_CONTENT时使用的。因此,如果想要自定义view支持WRAP_CONTENT属性,就必须自己对AT_MOST的情况作出处理。

ViewGroup的onMeasure()实现

不同于View,ViewGroup需要负责子view的测量。具体来讲,就是为子view提供合适的MeasureSpec,并调用子view的measure()(注意,不是onMeasure())方法。至于自身的尺寸,则需要结合更高一层的父view的指示以及子view的情况来确定。
为了给子view提供合适的MeasureSpec,ViewGroup中提供了一个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);
}

英文注释是自带的,已经很详细了。参数spec是高层view提供的MeasureSpec,padding为需要扣除的padding部分尺寸,childDimension为子view需求的尺寸(一般直接传MarginLayoutParams.width)。实质上,这个方法就是综合考虑了高层view的指示以及低层view的需求,分9种情况构建了一个合适的MeasureSpec。下面的图来自Android View系统解析(下) ,任玉刚总结

总结

关于onMeasure()方法的实现差不多就这些内容了。可以看出,MeasureSpec是父view与子view沟通的桥梁。实现onMeasure()方法的关键点就在于如何响应父view的MeasureSpec,以及如何为子view构建合适的MeasureSpec。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值