自定义 view - 测量 onMeasure

自定义 view 的3个核心方法

  • onMeasure
    根据 view 的测量模式计算确定 view 的宽高
  • onLayout
    ViewGroup 中对所有的子 view 排版,决定子 view 的位置
  • onDraw
    具体绘制 view

本节我们来说说 onMeasure ,view 的宽高的大小如何决定


自定义 View 绘制流程

005Xtdi2jw1f638wreu74j30fc0heaay.jpg

看完上面的图,那么今天我们呢就来说说 view 的测量 onMeasure


onMeasure 的经典写法

在自定义 view 的 onMeasure 测量方法中,所有的资料都是建议下面这种经典写法

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

    // 获取宽的测量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 获取符控件提供的 view 宽的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

先不要问为什么,先熟悉下代码,一会会有用到


view 的测量模式

view 的测量涉及到一个重要的点,测量模式,view 有3个测量模式,看下图:


944365-e631b96ea1906e34.png
  • USPENCIFIED
    是不限制子 view 大小的,我们在自定义 view 时用不到,一般也不用处理

  • EXACTLY :精准模式
    当 view 的宽高设置为 MATCH_PARENT 或者固定大小时,view 的测量就是 EXACTLY 类型的。

  • AT_MOST :最大值模式
    当 view 的宽高设置为 WARP_CONTENT 时,ew 的测量就是 AT_MOST 类型的。这是我们需要返回给系统一个值,已通知系统这个 view 的宽高应该是多少,若是我们不做处理,那么就会按 EXACTLY 测量,最大值不会查过父控件的宽高

清楚了这3个模式,尤其是 EXACTLY 和 EXACTLY 代表什么意思之后,我们要来说一说这个测量模式了。

测量模式对应的 matchParent,warp_content,具体宽高数值,都是写在 android:layout_width,android:layout_height xml 属性里面的,注意这都是 layout 的 xml 标签, layout 的 xml 标签最终都会把数据写入到 LayoutParams 里面,LayoutParams 是给 view 的父控件 ViewGorup 用的,父控件解析所有子 view 的 LayoutParams 参数,然后把这些参数经过处理包装到 widthMeasureSpec,heightMeasureSpec 里面传递给自子 view 的,自定义 view onMeasure 方法里面的参数就是这么来的。

这就带出一个问题,一个 view 的宽高不仅仅是自己决定的,也是父控件决定的。一个 view 先估算自己的宽高,然后告知父控件,父控件再最终决定view 的宽高是多少。这里面起决定作用的还是 view 自己的估算,父控件只是做一个最终的上限复核,子 view 的大小不嗯呢乖超过父控件的,这下大家都懂了吧


view 自己估算自家的宽高

还是上面哪段经典代码,我们放出来,方便观看

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

    // 获取宽的测量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 获取符控件提供的 view 宽的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

在上面的经典代码中,我们其实干了几件很简单的事:

  1. 拿到 view 宽高的测量模式和父控件允许 view 宽高的最大值

  2. 根据 view 宽高不同的测量模式区分对待

  3. 当宽和高是 EXACTLY 时,表示 宽高已经设定了一个固定值,match_parent 其实也是表示6个固定值,就是用父控件允许 view 宽高的最大值(符空间在宽高方向上剩余的最大值),就是一个具体的,父控件返回给我们什么值,我们用什么值就行了。当我们在 xml 中给宽高设置一个具体的值时,比如 35dp,那么父控件就会把 35dp 返回给我们,而不是 match_parent 时返回给我们允许的最大值,这点区别注意下

  4. onMeasure 里面最值得我们费脑子的就是 AT_MOST 了,当 AT_MOST 出现时,就表示我们给宽高使用了 warp_content,这个时候父控件是不知道子 view 宽高应该是多少的,就需要子 view 明确声明自己的宽高是多少了。一个典型的例子,textview 就是根据文字矩阵的宽高来计算具体宽高的值的,但是自定义 view 的宽和高同时是 warp_content 是不多的,多数时都是宽是 match_parent 的,高是 warp_content 的,我们在设计一个自定义 view 时,自定义 view 图案的宽高总是成比例的,这样我们就可以根据宽高一方的具体值按比例计算出宽高中的另一个值。但是我们碰到了宽高都是 warp_content 时呢,我们根据宽高的比例和父控件宽高允许的最大值计算出 子 view 不出父控件时成比例的宽高值。warp_content 时我们计算宽高值的基础就是 自定义 view 的图案必须成比例,要不谁知道这个 view 应该有多大呢。

  5. 最后用 setMeasuredDimension 通知父控件子 view 的大小。

基本上自定义 view 计算自己的宽高就是这么搞的,说的简单,但是碰到 warp_content 时计算真的不是很轻松。


Margin 和 padding 的问题

这个我们根据需要处理

  • Margin
    外边局的处理很简单,Margin 是会写到 LayoutParams 里面的,在逻辑上都是交给父控件 ViewGroup 在 onLayout 方法中处理的

  • padding
    内边距就得我们自己在测量中处理了。我们使用这几个 api 就能在 view 中拿到 padding 的大小,getPaddingLeft() 、getPaddingRight 、getPaddingTop() 、getPaddingBottom(),然后把的 padding 数值加到 view 的宽高里面去,不难处理,详细处理可以看篇文章:


onSizeChange

onSizeChange 方法很好理解,在 view 大小改变时会调用,我们来看看这个方法

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
}

四个参数,分别为 宽度,高度,上一次宽度,上一次高度,我们只需关注 宽度(w), 高度(h) 即可,这两个参数才是 View 的最终大小

onSizeChange 可能会多次触发,view 会缓存上一次的大小,在 view 的大小改变时就会触发这个回调了。触发 onSizeChange 回调的方法挺多,比如 settop ,setleft 这类改变 view 的大小方法, addView,removeview 也会触发 onSizeChange


父控件 ViewGroup 对子 view 测量的影响

ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是为用于告诉容器的),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ;决定childView的位置;为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。

然后 view 根据测量模式和ViewGroup给出的建议的宽和高,计算出自己的宽和高;同时还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形态。

这里面最重要的点是 view 的测量模式不是自己解析出来的,是父控件 ViewGroup 传递给子 view 的,父控件 ViewGroup 传递给子 view 之前,是经过处理的,这个处理我们需要清楚,不难,实际页没啥用,但是我们得知道。另外这一段是我摘抄过来的,看着有不通顺的地方大家脑补一下就都能理解了。

ViewGroup的测量过程主要用到了三个方法:

  • measureChildren()
    遍历所有的childView
  • getChildMeasureSpec()
    确定测量规格
  • measureChild()调用测量规格

measureChildren() 调用了 measureChild() ,measureChild() 又调用了 getChildMeasureSpec(),getChildMeasureSpec() 是核心,需要看一下的,看过我们就可以知道 view 的测量模式不仅收自己影响,还受到父控件影响

/**
  * 源码分析:getChildMeasureSpec()
  * 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //参数说明
         * @param spec 父view的详细测量值(MeasureSpec) 
         * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
         * @param childDimension 子视图的布局参数(宽/高)

            //父view的测量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的实际大小和模式(需要计算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


            // 当父view的模式为EXACITY时,父view强加给子view确切的值
           //一般是父view设置为match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 当子view的LayoutParams>0,即有确切的值  
                if (childDimension >= 0) {  
                    //子view大小为子自身所赋的值,模式大小为EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为MATCH_PARENT时(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小为父view大小,模式为EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
            // 多见于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小为子自身所赋的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }

xml 中所有 layout 的属性都不是给 view 看的,是给 view 的父控件 ViewGorup 看的,layout 的参数封装在 LayoutParams 里面。ViewGorup 父控件遍历所有的子 view 的测量模式,然后结合自身测量模式,决定子 view 的测量模式及传给子 view 的宽高建议值

在上面代码中我们可以看到:

  • 当父控件的测量模式是 EXACTLY 精确值时,子 view 在 layout 中设置的是什么测量模式就是什么测量模式。
  • 当父控件的测量模式是 AT_MOST 包括内容时,即便子 view 在 layout 中设置的是 MATCH_PARENT ,父控件也会把子 view 的测量模式设置为 AT_MOST
  • 父控件返回给子 view 宽高的建议值时,除了子 view 在 layout 中声明了一个确切的数时,返回这给子 view 这个确切的数,其实都是返回的父控件最大的宽高值

父控件宽高是 AT_MOST 时,父控件也不知道自己的宽高应该是多少,这时他要依赖子 view 的大小才能确定自己的宽高,所以会给子 view 设置成 AT_MOST 的测量模式,就是希望子 view 明确zi view 自己具体的大小,以便父控件计算自己的大小。所以我们在 自定义 view 碰到 AT_MOST 时,就当 warp_content 处理就行了,经典的 onMeasure 方法是久经考验的

onMearsu 还有更多详细的东西,比如源码分析,搭建看我下面提供的链接吧,如果你有兴趣额的话:


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