自定义 view - 测量 onMeasure

自定义 view 的3个核心方法

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

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

自定义 View 绘制流程

《自定义 view - 测量 onMeasure》 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个测量模式,看下图:

《自定义 view - 测量 onMeasure》 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 还有更多详细的东西,比如源码分析,搭建看我下面提供的链接吧,如果你有兴趣额的话:

    原文作者:前行的乌龟
    原文地址: https://www.jianshu.com/p/a50db94ed04b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞