Android:自定义view之onMeasure

本篇会讲解view的onMeasure的详细流程

onMeasure什么时候会被调用

onMeasure方法的作用是测量控件的大小,当我们创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入一个容器(父控件)中的时候才需要测量,而这个测量方法就是父控件唤起调用的。当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,然后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件拿着这些条件就能正确的测量自身的宽高了。

onMeasure方法执行流程

上面说到onMeasure方法是由父控件调用的,所有父控件都是ViewGroup的子类,ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的作用就是摆放它所有的子控件(安排位置),因为是抽象类,不能直接new对象,所以我们在布局文件中可以使用View但是不能直接使用 ViewGroup(代码中也一样)。
  在给子控件确定位置之前,必须要获取到子控件的大小,而ViewGroup并没有重写View的onMeasure方法,只是提供了(measureChildren\measureChild\measureChildWithMargins)等测量子view的相关方法.这样做也是因为不同的容器摆放子控件的方式不同,比如RelativeLayout,LinearLayout这两个ViewGroup的子类,它们摆放子控件的方式不同,有的是线性摆放,而有的是叠加摆放,这就导致测量子控件的方式会有所差别,所以ViewGroup就干脆不直接测量子控件,而是叫他的子类根据自己的布局特性重写onMeasure方法去测量。最后通过使用ViewGroup的measureChildxxx系列方法得到最终的子控件大小。
  测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。
举个栗子,看下图:


下面是测量的时序图:


MeasureSpec类

MeasureSpec约束是由父控件传递给子控件的,我们看一看源码:

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;
    /**
     * 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
     */
    public static final int EXACTLY = 1 << MODE_SHIFT;
    /**
     * 父控件会给子控件尽可能大的尺寸
     */
    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);
        }
    }
    /**
     * 从所提供的测量规范中提取模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 从所提供的测量规范中提取尺寸
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

从源码中我们知道,MeasureSpec其实就是尺寸和模式通过各种位运算计算出的一个整型值,它提供了三种模式(UNSPECIFIED ,EXACTLY ,AT_MOST )

约束:UNSPECIFIED(未指定)
布局参数:
值:0(00000000000000000000000000000000)
说明:父控件没有对子控件施加任何约束,子控件可以得到任意想要的大小(使用较少)。
约束:EXACTLY(完全)
布局参数:match_parent/具体宽高值
值:1073741824(01000000000000000000000000000000)
说明:父控件给子控件决定了确切大小。子控件将被限定在给定的边界里而忽略它本身大小。怎样理解忽略它本身大小呢?假设给自定义控件设置的大小为1dp,那可想而知,控件就看不到了,相比而言match_parent也是如此,假设父控件只有1dp,那子控件也就看不到了,所以这个模式子控件将被限定在给定的边界里而忽略它本身大小.

  • 如果是match_parent,说明父控件已经明确知道子控件想要多大的尺寸了(就是剩余的空间都要了)
  • 如果是设置的具体的值,那更应该说明父控件已经知道子控件大小了(具体的值)

约束:AT_MOST(至多)
布局参数:wrap-content
值:-2147483648(10000000000000000000000000000000)
说明:子控件至多达到指定大小的值。这种模式下父控件无法确定子控件的尺寸,这种情况下子控件要自己根据需求去设置大小.(我在模仿贪吃蛇的控件的时候默认给的是450px)

针对这几种模式,我在自定义控件中也定义了相应的尺寸(至于为什么要重写onMeasure我在下文中有提及):

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = getMySize(widthMeasureSpec);
        final int height = getMySize(heightMeasureSpec);
        final int min = Math.min(width, height);//保证控件为方形
        setMeasuredDimension(min, min);
    }
 /**
     * 获取测量大小
     */
    private int getMySize(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;//确切大小,所以将得到的尺寸给view
        } else if (specMode == MeasureSpec.AT_MOST) {
            //默认值为450px,此处要结合父控件给子控件的最多大小(要不然会填充父控件),所以采用最小值
            result = Math.min(DEFAULT_SIZE, specSize);
        } else {
            result = DEFAULT_SIZE;
        }
        return result;
    }
从ViewGroup的onMeasure到View的onMeasure
  • ViewGroup中三个测量子控件的方法:
    通过上面的介绍,我们知道,如果要自定义ViewGroup就必须重写onMeasure方法,在这里测量子控件的尺寸。子控件的尺寸怎么测量呢?ViewGroup中提供了三个关于测量子控件的方法:
/**
     * 遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //测量某一个子控件宽高
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    /**
     * 测量某一个child的宽高
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,
                                int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //获取子控件的宽高约束规则
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    /**
     * 测量某一个child的宽高,考虑margin值
     */
    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);
    }

其中measureChildren就是遍历所有子控件挨个测量,最终测量子控件的方法就是measureChildmeasureChildWithMargins 了,measureChildWithMarginsmeasureChild的区别就是父控件支持margin属性对子控件的测量有影响,比如我们的屏幕是1080x1920的,子控件的宽度为填充父窗体,如果使用了marginLeft并设置值为100; 在测量子控件的时候,如果用measureChild,计算的宽度是1080,而如果是使用measureChildWithMargins,计算的宽度是1080-100 = 980。

ViewGroup支持margin属性

ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams,MarginLayoutParams继承自LayoutParams ,这两个内部类就是VIewGroup的布局参数类,比如我们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性,最后要通过LayoutParams获取相应的属性。在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。
LayoutParams和MarginLayoutParams 的关系:
LayoutParams 中定义了两个属性(也就是layout_width\layout_hight的来头):

<declare-styleable name= "ViewGroup_Layout">
    <attr name ="layout_width" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
    <attr name ="layout_height" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
</declare-styleable >

MarginLayoutParams 是LayoutParams的子类,它当然也延续了layout_width\layout_hight 属性,但是它扩充了其他属性:

< declare-styleable name ="ViewGroup_MarginLayout">
    <attr name ="layout_width" />   <!--使用已经定义过的属性-->
    <attr name ="layout_height" />
    <attr name ="layout_margin" format="dimension"  />
    <attr name ="layout_marginLeft" format= "dimension"  />
    <attr name ="layout_marginTop" format= "dimension" />
    <attr name ="layout_marginRight" format= "dimension"  />
    <attr name ="layout_marginBottom" format= "dimension"  />
    <attr name ="layout_marginStart" format= "dimension"  />
    <attr name ="layout_marginEnd" format= "dimension"  />
</declare-styleable >

这就是我们使用的margin属性的由来。

  • 为什么LayoutParams 类要定义在ViewGroup中?
      众所周知ViewGroup是所有容器的基类,一个控件需要被包裹在一个容器中,这个容器必须提供一种规则控制子控件的摆放,所以ViewGroup提供一个布局属性类,用于控制子控件的布局(layout_)属性。

  • 为什么View中会有一个mLayoutParams 变量?
      在上篇自定义属性中我们在构造方法中初始化布局文件中的属性值,我们把属性分为两种。一种是本View的绘制属性,比如TextView的文本、文字颜色、背景等,这些属性是跟View的绘制相关的。另一种就是以“layout_”打头的叫做布局属性,这些属性是父控件对子控件的大小及位置的一些描述属性,这些属性都是用ViewGroup.LayoutParams定义的,所以用一个变量(mLayoutParams )保存着。

getChildMeasureSpec方法

measureChildWithMargins跟measureChild 都调用了这个方法,其作用就是通过父控件的宽高约束规则父控件加在子控件上的宽高布局参数生成一个子控件的约束。我们知道View的onMeasure方法需要两个参数(父控件对View的宽高约束),这个宽高约束就是通过这个方法生成的。为什么通过父类布局的约束生成子view约束呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是不是很有意思,父控件说我的宽高就是包裹我的子控件,而子控件说我的宽高填充父窗体。最后该怎么确定大小呢?所以我们需要为子控件重新生成一个新的约束规则。只要记住,子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。
measureChild源码,measureChildWithMargins类似

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //获取了子控件的layout_布局属性
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

getChildMeasureSpec方法代码不多,也比较简单,就是几个switch将各种情况考虑后生成一个子控件的新的宽高约束,这个方法的结果能够用一个表来概括:



进行了上面的步骤,接下来就是在measureChildWithMargins或者measureChild中 调用子控件的measure方法测量子控件的尺寸了。

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 的处理也将填充整个父控件。(上文中已经提到过WRAP_CONTENT的时候采用默认长度)

setMeasuredDimension

onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值,如果不调用这个方法,可能会产生不可预测的问题。

总结

最后总结一下

测量控件大小是父控件发起的
父控件要测量子控件大小,需要重写onMeasure方法,然后调用measureChildren或者measureChildWithMargins方法
View的onMeasure方法的参数是通过getChildMeasureSpec生成的
如果我们自定义控件需要使用wrap_content属性,我们需要重写onMeasure方法
测量控件的步骤:
父控件onMeasure->measureChildren/measureChildWithMargin->getChildMeasureSpec-> 子控件的measure->onMeasure->setMeasureDimension-> 父控件onMeasure结束调用setMeasureDimension
最后保存自己的大小

参考
自定义View目录

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

推荐阅读更多精彩内容