Android自定义View简单指南(03测量)

878 阅读4分钟

流程

  1. 解释为什么会出现上一节出现的问题?

  2. 为什么需要测量?

  3. measure机制是怎样的?

  4. 实现对 view 的正确测量。

上节中的 View 和预期不一样?

上节中我们将mian_layout 布局中的自定义控件的高度设置为 wrap_content ,但是最终呈现的效果并不是我们所预期的那样,为什么呢?

这是因为 Android View 的 measure 机制 造成的。

View 从加载到呈现到屏幕上的过程需要经过三个阶段:

  • measure 机制测量每个 View 的大小,保证 View 的大小显示正常。

  • layout 机制(ViewGroup特有,一般 View 没有)将每个子view放在合适的位置。

  • draw 机制,精准的绘制每个 View 的内容。

上一节的问题就出现在 measure 机制中,当时没有并没有处理 view 的测量。

measure 过程大概

View 的最终的大小不仅仅决定与 View 自身的测量,与其父 View 有着直接的关系。

这里涉及源码分析,暂时没写,只讲大概过程。具体看这里,比较简单可以看

  1. 父 View 获取子 View 的高宽(就是laytout_width与layout_height中设置的值),然后结合父 View 的测量模式决定子 View 的测量模式与初始的宽高(因为在onMeasure中可以更改)。

    三种宽高模式,最终给子view的模式由父view和子View结合得出。

    • EXACTLY : 精准模式,给定了 View 指定的大小,对应。
    • AT_MOST : 最大值为父View的尺寸,然后尽可能小的测量吱=自身的值。
    • UNSPECIFIED :当前View的父View不对View的尺寸作限制。
  2. 父 View 调用子view的 measure() 方法,然后转调用 onMeasure() 方法执行测量。

  3. 如果自定View没有重写 onMeasure 方法,则会使用基类View的 onMeasure() 方法处理测量。

    上一节中我们没有重写 onMeasure 方法,而系统就直接使用基类View进行处理了,这里给出 ViewGroup 根据自身测量模式以及子view的高宽决定子view测量模式的源码,明白为什么会出现上节出现的问题。

    19年6月 更新,重点!!!!不同的 ViewGroup 实现这里有差异,比如可滚动的 ViewGroup 不会去限制子 View 模式为 WRAP_CONTENT 时滚动方向的尺寸(可以参考 RecyclerView)。这里有洋神的问答贴,这个对这个模式做了详细的解答。

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
     
    
        	// 父 View 获取自身的测量模式和尺寸
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    		
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
    
        	// 父 View 根据自身的测量模式和子view的测量模式决定子view最终的测量模式和尺寸
            switch (specMode) {
            // Parent has imposed an exact size on us
            case MeasureSpec.EXACTLY:
                    // 父view是精准模式,对应准确数值和match_parent
                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.
                    // 这里,上节我们将自定义view的高度设置为 wrap_content 模式了,父view再这里直接将子view的最终尺寸设置为自身的尺寸。而在自定义view中我们没有处理 onMeasure ,因此自view的最终高度为父view的高度。
                    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);
        }
    

    下面正式处理 measure 过程。

measure 处理很简单

一般的 View 测量过程其实是一套模板代码,如无特殊测量需求基本代码是一致的,过程也比较简单。

1. 覆写 onMeasure 方法

覆写 onMeasure 方法,不用调用超类的方法,整个测量过程我们要精准控制。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       
    }	

2. 分别处理高度和宽度

高度和宽度的处理逻辑一致,代码细微不同而已。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureWidth(widthMeasureSpec);
    }

    private int measureWidth(int widthMeasureSpec) {
        // 测量宽度
        // 获取宽度模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        // 获取父View建议宽度
        int recommendWidth = MeasureSpec.getSize(widthMeasureSpec);
        // 最终的宽度
        int finalWidth = recommendWidth;
        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                // 已经准确的给了值,直接使用即可
                break;
            case MeasureSpec.AT_MOST:
                // 这里对应 wrap_content,但是父view将尺寸设为了自身对应的尺寸,需要我们自行处理
                // 处理逻辑是我们自身设定的最小需要尺寸+对应尺寸的内边距,外边距不用考虑
                finalWidth = radius*2 + getPaddingLeft() + getPaddingRight();
                break;
            case MeasureSpec.UNSPECIFIED:
                // 没有限制尺寸,保持父view大小即可
                break;
        }
        return finalWidth;
    }

3. 调用设置尺寸函数,完成测量

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec),measureHieght(heightMeasureSpec));
    }

效果如下:

ViNVNd.md.png