自定义View控件之onMeasure方法详解

前言

转载请注明出处!
http://blog.csdn.net/u011692041/article/details/76093920

这类的文章很多很多,其实我也是不想写的.但是说起来我虽然看了很多很多的文章,但是对于View控件的measure方法还是一知半解的.那么今天我就来做一个总结,并且解决很多人问我的一些常见的问题.下面先把一些常见的问题罗列一遍
View控件中的measure方法被父容器调用,会引发测量的整个过程,也就有了onMeasure方法
父容器调用measure方法放在下一篇自定义ViewGroup中赘述

常见问题

  • 1.为什么我没有重写onMeasure方法,自定义控件能显示
  • 2.为什么我的自定义控件不显示
  • 3.我的自定义控件如何才能支持包裹(wrap_content)
  • 4.为什么我的自定义控件在xml中写 wrap_content 和写 match_parent 效果是一样的
  • 5.我的自定义控件如何才能支持内边距
  • 6.我的自定义控件在平常视图使用是正常的,在列表中就不见啦?

带着上述的问题,今天小金子来掰扯掰扯,下面分两个方面来分析这些问题

onMeasure默认实现


我们知道自定义View(? extends View)的时候,我们需要自己绘制内容.和自定义ViewGroup不太一样,自定义ViewGroup主要是如何排放每一个子View的位置.

我们先观望一下系统的View默认的onMeasure方法是怎么样的

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;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

因为onMeasure方法涉及到其他的两个方法,所以代码都完整的贴出来了.在onMeasure方法中有一个setMeasuredDimension(int measuredWidth, int measuredHeight)方法,这个方法是很关键的.系统的注释我就不贴出来了.这里直接给出博主的解释.
当我们的自定义View测量自己的宽和高的时候,一定要通过此方法保存测量好的宽和高.
没有调用此方法保存测量好的宽和高或者调用了但是调用此方法的时候传入的两个值有一个是0的时候就是导致问题2出现的原因的其中两个原因

getSuggestedMinimumWidth方法 和 getSuggestedMinimumHeight方法中的代码很简单,就是获取自定义控件最小大小

getDefaultSize 方法很重要.从代码中我们可以看到 getDefaultSize 方法中有两个入参
第一个是最小的值,也就是getSuggestedMinimum*方法获取到的值
第二个是父容器推荐的值和模式(MeasureSpec不懂的同学自行搜索下)
从swich中我们可以明显的看出,这里根据推荐的测量模式,进行不一样的取值

最后onMeasure方法直接保存getDefaultSize方法防护的值到View内部,以供后续使用n

那么我们可以总结出两点:
1.当模式是 MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY 的时候,两者取得值是相同的,都是父容器推荐的值
2.当模式是MeasureSpec.UNSPECIFIED的时候,取的值是最小的值,这个模式一般只会出现在列表或者ScrollView中,因为这类容器由于本身视图是可以滚动的,拿ListView来说,纵向的高度可以认为是无限大,那么它就不可能有一个最大值可以推荐给你,所以这个模式就是用来描述父容器已经没法给你一个推荐的值了,你得自己计算,如果不计算,视图就看不见了
这也是问题6的根本原因

其实这里就是问题4的根本原因

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

这两行代码获取到父容器推荐的值和计算模式

而这里所谓的父容器推荐的模式,其实其中的两个 MeasureSpec.AT_MOSTMeasureSpec.EXACTLY 是和我们在xml中写的 wrap_contentmatch_parent 息息相关的。但是只是相关,并不能决定,为什么呢?.

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

这是ViewGroup 中的一段代码,用于向子View推荐模式和值,那么就简单的从代码中可以看出,子View中的被推荐的值和模式和 子View本身的LayoutParams(也就是你在xml中写的宽和高)、measureChild方法中传入的 parentWidthMeasureSpec, parentHeightMeasureSpec都息息相关
所以这里只是先告诉你影响子View的推荐的模式和值的因素有哪些,在下一篇中会具体的讲述
自定义ViewGroup测量方法的使用

影响测量推荐的值和模式的因素总结

以下是默认的情况下,系统的一些布局RelativeLayout之类的可能对测量流程做了更改,所以发现不符合的情况,应该是系统的对默认的做了更改

我们就根据上面的默认实现的方法做一个小总结先,先有一个先入为主的认识,下一篇还会中点讲述

下面的 父View推荐模式 指的是上面的measureChild 方法传入的parentWidthMeasureSpec 或者 parentHeightMeasureSpec中获取出来的推荐模式,这样子获取

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

1.
子View: wrap_content
父View推荐模式: MeasureSpec.AT_MOST
推荐结果: 模式:MeasureSpec.AT_MOST ,值:父View所能给的最大值
(推荐值用不用是你的事情)

2.
子View: wrap_content
父View推荐模式: MeasureSpec.EXACTLY
推荐结果: 模式:MeasureSpec.AT_MOST ,值:父View所能给的最大值
(推荐值用不用是你的事情)

3.
子View: wrap_content
父View推荐模式: MeasureSpec.UNSPECIFIED
推荐结果: 模式:MeasureSpec.UNSPECIFIED ,值:0或者父容器推荐的值 // 这种情况就是引起一个自定义控件在列表或者ScrollView中不可见的原因

4.
子View: match_parent
父View推荐模式: MeasureSpec.AT_MOST
推荐结果: 模式:MeasureSpec.AT_MOST ,值:父View所能给的最大值
(推荐值用不用是你的事情)
和第一种运行结果是一样的,不一样的是父视图的大小,不在讨论范围内,下一篇介绍

5.
子View: match_parent
父View推荐模式: MeasureSpec.EXACTLY
推荐结果: 模式:MeasureSpec.EXACTLY ,值:父View所能给的最大值
(推荐值用不用是你的事情)

6.
子View: match_parent
父View推荐模式: MeasureSpec.UNSPECIFIED
推荐结果: 模式:MeasureSpec.UNSPECIFIED ,值:0或者父容器推荐的值 // 这种情况就是引起一个自定义控件在列表或者ScrollView中不可见的原因

7.
子View: 40dp // 举例这种情况
父View推荐模式: MeasureSpec.AT_MOST
推荐结果: 模式:MeasureSpec.EXACTLY ,值:40dp

8.
子View: 40dp // 举例这种情况
父View推荐模式: MeasureSpec.EXACTLY
推荐结果: 模式:MeasureSpec.EXACTLY ,值:40dp

9.
子View: 40dp // 举例这种情况
父View推荐模式: MeasureSpec.UNSPECIFIED
推荐结果: 模式:MeasureSpec.UNSPECIFIED ,值:40dp

以上结论并不是我个人得出的,而是有依据的,代码在ViewGroup类中的一个静态的方法中

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);
    }

从这里我们清晰的看到所有情况的判断,这也是我总结的出处

下一篇自定义ViewGroup下一篇中我们会详细的再说明的,暂时看不懂的小伙伴不用担心.先往下看剩下的吧

上面也就解释了为什么一个自定义控件,默认的时候,在xml写 wrap_content 和 写
match_parent 是一样的效果了.
也解释了为什么一个自定义View控件在没有重写onMeasure方法的时候,能显示,就是因为有一个默认的实现.过程不再赘述

onMeasure 自定义实现

上面讲了整个onMeasure 方法的默认实现,我们有一点必须要很重视

那就是无论父视图是如何推荐的,我们都应该根据推荐的模式和值对自身View的大小有一个精准的测量

那么我们如何来自定义测量的代码呢?下面切看我花样作死~~~~

自定义View控件之onMeasure方法详解_第1张图片



首先我们新建一个MyView类继承View,然后重写onMeasure方法,先从父容器推荐的传入的两个参数中获取出计算模式和相应的值,这都是套路,学着点~~~

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);


    }

}

然后我们上面说过必须调用* setMeasuredDimension(int measuredWidth, int measuredHeight)*方法来保存计算好的宽和高

第一浪

一直让我们的自定义View显示100px * 100px的宽和高

答案:

setMeasuredDimension(100,100); // 直接设置测量的宽和高是100px,不管什么情况下

宽和高写 wrap_content 的效果:

自定义View控件之onMeasure方法详解_第2张图片

宽和高写上500dp的效果

自定义View控件之onMeasure方法详解_第3张图片

宽和高写 match_parent 的效果

自定义View控件之onMeasure方法详解_第4张图片

我们可以看到,我们的大小根本不随着xml的宽和高而有任何的改变,这就是因为我们在onMeasure 方法中直接写死了一个宽和高导致的!

第二浪

假如我们需要绘制一个100px * 100px的区域,实现xml中写wrap_content的时候是包裹100px * 100px的区域的,其他情况大小由xml写的为准,也就是40dp就显示40dp大小,match_parent就是最大的大小

答案:

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        p.setColor(Color.GREEN);
        p.setStyle(Paint.Style.FILL);
    }

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeWidth == MeasureSpec.AT_MOST) { // wrap_content
            sizeWidth = Math.min(100,sizeWidth);
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.AT_MOST) { // wrap_content
            sizeHeight = Math.min(100,sizeHeight);
            modeHeight = MeasureSpec.EXACTLY;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    private Paint p = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // draw a green rect(绘制一个100*100的正方形)
        canvas.drawRect(0,0,100,100,p);

    }
}

效果:

自定义View控件之onMeasure方法详解_第5张图片

自定义View控件之onMeasure方法详解_第6张图片

自定义View控件之onMeasure方法详解_第7张图片

自定义View控件之onMeasure方法详解_第8张图片

自定义View控件之onMeasure方法详解_第9张图片

重点关注我们的onMeasure方法.由于我们上面分析得知,我们的自定义控件默认的实现支持xml中写40dp这种固定的大小,也支持match_parent 的效果,就是不支持wrap_content,当wrap_content的时候应该是包裹着要绘制的视图内容的,而我们上面的需求就是有个100px*100px的绘制区域,我们在里面绘制了一个绿色的矩形.
所以我们在onMeasure只需要在默认的实现前面做一个小动作即可.判断推荐的模式是MeasureSpec.AT_MOST 的时候把推荐的值和100做一个取小的操作即可
然后把计算模式改为MeasureSpec.EXACTLY就行啦
为什么要100和推荐的值取小的操作,因为有可能父容器推荐给你的值是小于100的,那么你肯定不能让自身的大小超过给你的值,当然了实际运行中,不进行如此细致的判断也是可以的,直接采用100,但是作为程序员还是得思考的严谨些

onMeasure 支持内边距

其实这个很简单,因为我们在xml中为View写的内边距都会在创建这个View的时候转化为View控件的属性

自定义View控件之onMeasure方法详解_第10张图片

我们在测量的时候直接拿过来计算就行

自定义View控件之onMeasure方法详解_第11张图片

其实我们只需要在包裹的情况下添加内边距的值就行了,试想一下,如果一个View的绘制区域是100*100,而上下左右都有一个20的边距,那么这个View的大小理所应当应该是140 * 140
其他情况就不需要考虑内边距啦

然后我们的绘制代码也要改变了,因为有内边距的存在我们需要一个绘制偏移量,这也就是为什么下面的绘制代码那么写的原因

总结

  • 1.onMeasure 方法就是让你测量自身的大小并且存储测量的大小的一个地方
  • 2.一定要注意,计算测量的大小后,一定要保存测量的值。不管是调用父类的onMeasure方法还是自己调用setMeasuredDimension(int measuredWidth, int measuredHeight)
  • 3.系统默认的实现中,包裹(MeasureSpec.AT_MOST)的情况是不支持的,因为系统的View根本就不知道要绘制的内容的大小,所以包裹内容无从谈起,而你自定义了View,那你就有这个责任支持包裹(MeasureSpec.AT_MOST)的情况,因为你自己定义的View你自己知道绘制的大小
  • 4.保存的测量大小仅供父容器在排列子View的时候使用,或者压根就不使用,所以切记在onDraw(Canvas canvas)方法中使用自己测量的 measuredWidthmeasuredHeight,
    因为这个时候你已经知道了自身的实际大小,这个大小和父容器有关(后续文章跟进),所以你应该在onDraw(Canvas canvas)方法中直接使用getWidth()getHeight() 的值!
  • 5.当模式是MeasureSpec.UNSPECIFIED的时候,拿ListView来说,纵向的高度可以认为是无限大,那么它就不可能有一个最大值可以推荐给你,所以这个模式就是用来描述父容器已经没法给你一个推荐的值了,你得自己计算,如果不计算,视图就看不见了,这也就是为什么有些自定义控件在普通情况下是可见的,好使的,但是一放进列表中就鸡鸡了,原因就在于你在onMeasure中没有判断模式是MeasureSpec.UNSPECIFIED的情况,如果使用默认的,那就是上述中讲的使用View的getSuggestedMinimumWidth方法 和 getSuggestedMinimumHeight方法中返回的值作为测量的高度.其实作为一个自定义控件,理应在MeasureSpec.UNSPECIFIED能呈现一个包裹视图的状态!所以这种情况和MeasureSpec.AT_MOST 类似处理即可

自定义View控件之onMeasure方法详解_第12张图片

上述写的100是因为我的控件绘制区域就是100,你们可一定要按照你们自己的绘制区域来计算哈,别傻敷敷的瞎写,哈哈

暂时就写这么多吧,如有不足后续再跟进吧,看到这里的童鞋,如果喜欢小生,评论一下撒,反正我也不会回复你,哈哈哈

贴出全部代码(以供复制,ps:我多贴心!!)

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        p.setColor(Color.GREEN);
        p.setStyle(Paint.Style.FILL);
    }

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeWidth == MeasureSpec.UNSPECIFIED) { // this view used in scrollView or listview or recyclerView
            int wrap_width = 100 + getPaddingLeft() + getPaddingRight();
            sizeWidth = wrap_width;
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.UNSPECIFIED) { // this view used in scrollView or listview or recyclerView
            int wrap_height = 100 + getPaddingTop() + getPaddingBottom();
            sizeHeight = wrap_height;
            modeHeight = MeasureSpec.EXACTLY;
        }

        if (modeWidth == MeasureSpec.AT_MOST) { // wrap_content
            int wrap_width = 100 + getPaddingLeft() + getPaddingRight();
            sizeWidth = Math.min(wrap_width,sizeWidth);
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.AT_MOST) { // wrap_content
            int wrap_height = 100 + getPaddingTop() + getPaddingBottom();
            sizeHeight = Math.min(wrap_height,sizeHeight);
            modeHeight = MeasureSpec.EXACTLY;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    private Paint p = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // draw a green rect(绘制一个100*100的正方形)
        canvas.drawRect(getPaddingLeft(),getPaddingTop(),getPaddingLeft() + 100,getPaddingTop() + 100,p);

    }
}

自定义View控件之onMeasure方法详解_第13张图片

你可能感兴趣的:(总结,自定义,Android,Android,Studio,玩转自定义控件)