Android自定义View基础之onMeasure详解

上一篇文章,介绍了MeasureSpec类的基础知识,包括三种模式及子view的measureSpec的生成过程,搞懂了这个,那么我们下面就可以进入到onMeasure流程中了,同时,也会在上一篇的基础上做一下关于自定义view wrap_content和match_parent的补充。

onMeasure源码分析

onMeasure

/**
 * <p>
 * Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overriden by subclasses to provide accurate and efficient  measurement of their contents.
 * </p>
 * @param widthMeasureSpec horizontal space requirements as imposed by the parent.The requirements are encoded with {@link android.view.View.MeasureSpec}.
 * @param heightMeasureSpec vertical space requirements as imposed by the parent. The requirements are encoded with  {@link android.view.View.MeasureSpec}.
 *
 */
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
 }

通过源码可以看出,onMeasure流程为:

  • 通过setMeasureDimension设置measure阶段view的宽和高
  • 通过getDefaultSize方法获取默认的宽度和高度
  • 通过getSuggestedMinimumHeight/width方法获取建议的最小宽度或高度值

下面,一次进入对应的方法,看一下内部实现。

getSuggestedMinimumWidth(height)

/**
 * Returns the suggested minimum height that the view should use. This
 * returns the maximum of the view's minimum height
 * and the background's minimum height
 * ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
 * <p>
 * When being used in {@link #onMeasure(int, int)}, the caller should still
 * ensure the returned height is within the requirements of the parent.
 *
 * @return The suggested minimum height of the view.
 */
 protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
  }
  • 其中mBackground为view的背景,如果有背景的话,可以获取到其最小高 宽度值
  • mMinHeight 此值可以在xml中通过minHeight设置,也可以通过view的setMinimumHeight()方法设置

getDefaultSize

/**
 * Utility to return a default size. Uses the supplied size if the
 * MeasureSpec imposed no constraints. Will get larger if allowed
 * by the MeasureSpec.
 *
 * @param size Default size for this view
 * @param measureSpec Constraints imposed by the parent
 * @return The size this view should be.
 */
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的宽度或高度值,通过switch,可以看出有两种返回,分别如下:

  • measureSpec的mode为unspecified时,返回的就是view的最小宽度或高度值,这种情况一般很少见
  • measureSpec的mode为exactly或at_most时,返回的是view measureSpec中的specSize

由此,也得知,在measure阶段中,view的大小宽高,由其measureSpec中的specSize决定的。

解决自定义view之wrap_content问题

问题及MeasureSpec规则回顾

首先回顾一下上篇文章中,绘制的measureSpec形成图,如下
这里写图片描述

由图可以看到,当子view的宽高为warp_content时,不管父容器的specMode为exactly还是at_most,其占据的空间都为parentLeftSize,显然,这不是我们所期望的,那么,我们就应该对wrap_content的情况在onMeasure阶段进行特殊处理。

解决方案

  • 如果在xml中,宽高均为wrap_content,需要设置view的宽高为mWidth mHeight
  • 如果在xml中,宽高有一个被设置为wrap_content,那么就将该值设置为默认值,另一个采用系统测量的specSize即可,代码示例如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //获取宽高的size mode
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSise = MeasureSpec.getSize(heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //宽高都为wrap_content
    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //宽度为wrap_content
        setMeasuredDimension(mWidth, heightSise);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //高度为wrap_content
        setMeasuredDimension(widthSize, mHeight);
    }
 }

好了,现在就解决了自定义view宽高为wrap_content的问题了,不过。。。。

引发的另一个“血案”

上面成功解决了wrap_content问题,不过,不知道注意到没有,在子view的layoutParams为match_parent,父容器的specMode为at_most时,子view的specMode也为at_most,其size也为parentLeftSize,那这种情况下,子view match_parent的case也会走到刚才解决wrap_content的代码中,那么本来宽高想要填充父容器,现在却被设置了默认值,这样也不合理的,有木有,有木有!!!

不过呢,仔细推理一下,就会发现,原来。。。搜噶。

问:什么情况下,父容器的specMode为at_most呢
答:两种情况:
a. 当父容器的layoutParams为wrap_content,系统给父容器的specMode为at_most
b. 父容器的layoutParams为match_parent,系统给父容器的specMode为at_most

下面,来分别分析一下这两种情况。
A. 父容器为wrap_content,子view为match_parent,子view为包裹内容,想和父容器一样大,而父容器又不知道自己知道多大,那么两者就陷入死循环了,谁也决定不了谁,这种case理论上可能出现,但是实际中一般是不可取的。
B. 既然父容器是match_parent,那么爷(父容器的父容器)应该为什么呢?下面,排除法,列举一下

  • 爷容器为wrap_content,此case同A,不可取,不正确的
  • 精确的值,比如200dp。试想一下,如果爷容器为精确的值,爷mode为exactly,父为match_parent,那么父的mode就不可能为at_most,而应该是exactly,所以这种也是不可能的
  • 爷容器为match_parent,爷容器的mode可能为exactly或at_most,那么,分别分析一下
    • 爷容器mode为exactly,这种情况下,父容器的mode应该为exactly,而不是at_most,所以这种case是不可能的。
    • 爷容器mode为at_most,大小为match_parent,那么父容器的mode为at_most,这是唯一可能存在的情况。仔细分析一下,这样就会陷入一个死循环,子view大小是match_parent,mode为at_most,父容器大小是match_parent,mode为at_most,爷容器大小也是match_parent,mode为at_most,如此下去,直到rootview,如果根view的大小为match_parent,那么其对应的mode应该为exactly,所以这种case也是不可能的。

由上面推测发现,如果子view的大小为match_parent,并且父容器的mode为at_most,那么此时子view的mode也为at_most其大小为parentLeftSize,这种情况是不合理的,不可取的。

ViewGroup的measure

viewgroup是一个抽象类,他提供了测量child的measureChildren方法,在该方法里,又会调用measureChild方法,在measureChild方法里,会执行child.measure()方法,就回到我们前面分析的流程中了。
执行步骤:measureChildren()—>measureChild()—>child.measure()

ViewGroup是一个抽象类,没有实现onMeasure()方法,那么其子类就应该根据自身的特点去测量子view的,测量好了子view的大小,那么其自身的大小也就明确了。比如linearLayout根据布局方向完成测量,relativeLayout根据子view的相对位置完成测量,等等。

结束语

至此,关于自定义view宽高为wrap_content的情况解决了,onMeasure()的调用过程也简单的分析了一下,欢迎指出不足和问题,不定时再更新自己的理解和补充。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值