自定义view-Measure

281 阅读4分钟

前言

一个界面就像一张报纸,如果你想让他合理的排版,首先需要测量每个板块(子view)的大小,然后根据大小布局合适的位置,最后在绘制内容。

测量阶段:从上到下递归地调用每个 View 或者 ViewGroup 的 measure() 方法,测量他们的尺寸并计算它们的位置;

View 或 ViewGroup 的测量过程

测量阶段,measure() 方法被父 View 调用,在 measure() 中做一些准备和优化工作后,调用 onMeasure() 来进行实际的自我测量。 onMeasure() 做的事,View 和 ViewGroup 不一样:

  • View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;

  • ViewGroup:ViewGroup 在 onMeasure() 中会调用所有子 View 的 measure() 让它们进行自我测量,并根据子 View 计算出的期望尺寸来计算出它们的实际尺寸和位置(实际上 99.99% 的父 View 都会使用子 View 给出的期望尺寸来作为实际尺寸,原因在下期或下下期会讲到)然后保存。同时,它也会根据子 View 的尺寸和位置来计算出自己的尺寸然后保存;

测量View

1.重写 onMeasure() 来修改已有的 View 的尺寸;

具体步骤:

  1. 重写 onMeasure() 方法,并在里面调用 super.onMeasure(),触发原有的自我测量;
  2. 在 super.onMeasure() 的下面用 getMeasuredWidth() 和 getMeasuredHeight() 来获取到之前的测量结果,并使用自己的算法,根据测量结果计算出新的结果;
  3. 调用 setMeasuredDimension() 来保存新的结果。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //1.触发原有的自我测量
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //2.1获取原有测量结果
        var resultWidth = measuredWidth
        var resultHeight = measuredHeight
        "start resultWidth $resultWidth resultHeight $resultHeight ".e()
        //2.3重新计算宽高
        if (resultWidth > resultHeight){
            resultWidth = resultHeight
        }else{
            resultHeight = resultWidth
        }
        "end resultWidth $resultWidth resultHeight $resultHeight ".e()
        //3.保存新的结果
        setMeasuredDimension(resultWidth,resultHeight)
    }

2.重写 onMeasure() 来全新定制自定义 View 的尺寸

全新定制尺寸和修改尺寸的最重要区别

需要在计算的同时,保证计算结果满足父 View 给出的的尺寸限制

父 View 的尺寸限制

  1. 由来:开发者的要求(布局文件中 layout_ 打头的属性)经过父 View 处理计算后的更精确的要求;
  2. 限制的分类:
UNSPECIFIED:不限制
AT_MOST:限制上限
EXACTLY:限制固定值

具体步骤:

  1. 重新 onMeasure(),并计算出 View 的尺寸;
  2. 使用 resolveSize() 来让子 View 的计算结果符合父 View 的限制(当然,如果你想用自己的方式来满足父 View 的限制也行)。

resolveSize最终调用源码

//size View测量自身的大小
//measureSpec 父View的限制
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            //限制上限
            case MeasureSpec.AT_MOST:
                //如果大于上限,返回上限大小,否返回view自身测量的大小
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
                //父设置了精确值,就得按照精确值大小
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
                //父没有设置,想多大就多大,就得按照自身大小
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

使用时,只需要将自身测量的大小和父view的限制传给resolveSize即可

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        var resultWidth = 200
        var resultHeight = 300
        resultWidth = resolveSize(resultWidth,widthMeasureSpec)
        resultHeight = resolveSize(resultHeight,heightMeasureSpec)
        setMeasuredDimension(resultWidth,resultHeight)
    }

MeasureSpec是怎么来的?

继续往下看

不按照父限制会有什么后果?

会出bug,比如你在布局设置了match_parent,由于你测量的结果是400dp,而父view剩余空间有500dp,就会出现空隙等问题。所以要满足父view的限制。

重写 onMeasure() 测量ViewGroup

具体步骤

  1. 调用每个子 View 的 measure() 来计算子 View 的尺寸
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    for (i in 0 until childCount){
        val childView = getChildAt(i)
        childView.measure(childWidthSpec,childHeightSpec)
    }
}

重点来了childWidthSpec、childHeightSpec是父View对子View的尺寸限制,这两个并不是现成的,那怎么来的呢?

这个限制是根据开发者的需求即xml中layout_开头的配置和ViewGroup的剩余空间,结合起来计算得到的。这里要注意开发者的要求在地位上是绝对高于剩余空间的。例如开发者写了layout_width = "48dp",那么就不用去计算了,直接以开发者的要求48dp作为测量的大小

怎么获取xml中layout_width、layout_height?

       for (i in 0 until childCount){
            val childView = getChildAt(i)
            val lp = childView.layoutParams
//            lp.width --> layout_width
//            lp.height --> layout_height
        }

ViewGroup的可用空间则么算?

 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

通过参数widthMeasureSpec,heightMeasureSpec可以获取自己的可用空间。不过这里也分集中情况。

widthMeasureSpec 封装了父View对子View的尺寸限制,这里的限制分为三种情况。如果是EXACTLY、AT_MOST 那么剩余空间就是widthMeasureSpec中的size,如果是UNSPECIFIED 那么剩余空间就是无限大。

 when (modeWidth) {
     MeasureSpec.EXACTLY,
     MeasureSpec.AT_MOST -> {
         剩余空间 = modeWidth
     }
     MeasureSpec.UNSPECIFIED -> {
         剩余空间 = 无限大
     }
 }

得到了开发者的要求和父view的剩余空间,来给子view设置MeasureSpec尺寸限制。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
        val sizeWidth = MeasureSpec.getSize(widthMeasureSpec)
        val modeheight = MeasureSpec.getMode(heightMeasureSpec)
        val sizeheight = MeasureSpec.getSize(heightMeasureSpec)

        for (i in 0 until childCount) {
            val childView = getChildAt(i)
            val lp = childView.layoutParams
            var childWidthSpec: Int
            when (lp.width) {
                MATCH_PARENT ->{
                    //沾满父View剩余空间,剩余空间根据widthMeasureSpec
                    if (modeWidth == MeasureSpec.EXACTLY || modeWidth == MeasureSpec.AT_MOST){
                        childWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth - usedWidth, MeasureSpec.EXACTLY)
                    }else{
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                    }
                }
                WRAP_CONTENT ->{
                    //沾满父View剩余空间,剩余空间根据widthMeasureSpec
                    if (modeWidth == MeasureSpec.EXACTLY || modeWidth == MeasureSpec.AT_MOST){
                        childWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth - usedWidth, MeasureSpec.AT_MOST)
                    }else{
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                    }
                }
                else -> {
                    //精确值 如48dp
                    childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY)
                }
            }
        }
    }
  1. 计算子 View 的位置并保存子 View 的位置和尺寸
  2. 计算自己的尺寸并用 setMeasuredDimension() 保存

参考资料 扔物线