Measure

作者: 夜沐下的星雨 | 来源:发表于2020-08-11 18:58 被阅读0次

本文主要android View绘制三大流程(measure、layout、draw)中的measuer流程。
如何通过measure来确定自己的宽高?

主要问以下三部分:

  • 名词定义
  • measure过程是什么样的,它是如何确定一个View/ViewGroup的宽高
  • 总结

一、名词定义

View是一个矩形区域,它有自己的位置、大小与边距。

View位置

View位置:有左上角坐标(getLeft(), getTop())决定,该坐标是以它的父View的左上角为坐标原点,单位是pixels。

View大小

View大小:View的大小有两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示了该View在它的父View里期望的大小值,在measure()方法完成后可获得。 getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。

View内边距

View内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理 padding,否则它不会生效,这一块的内容我们会在View自定义实践系列的文章中展开。

View外边距

View内边距:View的外边距用margin来表示,它表示View的边缘离它相邻的View的距离。
Measure过程决定了View的宽高,该过程完成后,通常都可以通过getMeasuredWith()/getMeasuredHeight()获得宽高。

MeasureSpec

MeasureSpec从父View生成传递给子View。View的大小最终由子View的LayoutParams与传递给子View的MeasureSpec共同决定。它包含两部分:
高2位:SpecMode,测量模式
低30位:SpecSize,某种测量模式下的规格大小

SpecMode的三种模式

UNSPECIFIED:这种模式表明parent对它的child的大小没有限制,child可以告诉parent它自己所希望的尺寸。
EXACTLY:这种模式表明parentchild设置了一个确切的值,child必须使用这个值,并且需要保证child的后代节点都要符合这个值的设置。
AT_MOST:这种模式表明parentchild设置了一个最大值,child可以是它想要的任何值,但child以及它的后代节点的尺寸大小都必须保证在这个最大值内。

二、measure过程

image

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">View树(自上而下遍历)</figcaption>

具体分析

measure 过程由measure(int, int)方法发起,从上到下有序的测量 View,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。

measure 过程会为一个 View 及所有子节点的 mMeasuredWidth 和 mMeasuredHeight 变量赋值,该值可以通过 getMeasuredWidth()getMeasuredHeight()方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于 Measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次 measure。比如,父视图可以先根据未给定的 dimension 去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。

下面我们对ViewGoup的 Measure 流程做一个分析

image

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">流程图</figcaption>

核心方法1:ViewGroup.getChildMeasureSpec(int spec, int padding, int childDimension)

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
     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);
    }
}

该方法用来获取子View的MeasureSpec,由参数我们就可以知道子View的MeasureSpec由父容器的spec,父容器中已占用的的空间大小 padding,以及子View自身大小childDimension共同来决定的。

通过上述方法,我们可以总结出普通View的MeasureSpec的创建规则。

  • 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY。
  • 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST 则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。
  • 当View的宽高是wrap_content时,不管父容器的模式是MeasureSpec.EXACTLY还是MeasureSpec.AT_MOST,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过父类的剩余空间。

了解了MeasureSpec的概念之后,我就就可以开始分析测量流程了。

  • 对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
  • 对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。

View的绘制会先调用View的measure()方法,measure()方法用来测量View的大小,实际的测量工作是由View的onMeasure()来完成的。我们来看看 onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的实现。

核心方法2:View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

       protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
           setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                   getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
       }

       //设置View宽高的测量值
       protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);

       //measureSpec指的是View测量后的大小
       public static int getDefaultSize(int size, int measureSpec) {
           int result = size;
           int specMode = MeasureSpec.getMode(measureSpec);
           int specSize =  MeasureSpec.getSize(measureSpec);

           switch (specMode) {
           //MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
           case MeasureSpec.UNSPECIFIED:
               result = size;
               break;
           //我们主要关注着两种情况,它们返回的是View测量后的大小
           case MeasureSpec.AT_MOST:
           case MeasureSpec.EXACTLY:
               result = specSize;
               break;
           }
           return result;
       }

       //如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
       //如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
       protected int getSuggestedMinimumHeight() {
           int suggestedMinHeight = mMinHeight;

           if (mBGDrawable != null) {
               final int bgMinHeight = mBGDrawable.getMinimumHeight();
               if (suggestedMinHeight < bgMinHeight) {
                   suggestedMinHeight = bgMinHeight;
               }
           }

           return suggestedMinHeight;
       }
}

View的onMeasure()方法实现比较简单,它调用setMeasuredDimension()方法来设置View的测量大小,测量的大小通过getDefaultSize()方法来获取。

如果我们直接继承View来自定义View时,需要重写onMeasure()方法,并设置wrap_content时的大小。为什么呢?

通过上面的描述我们知道,当LayoutParams为wrap_content时,SpecMode为AT_MOST,而在关于getDefaultSize(int size, int measureSpec) 方法需要说明一下,通过上面的描述我们知道getDefaultSize()方法中AT_MOST与EXACTLY模式下,返回的 都是specSize,这个specSize是父View当前可以使用的大小,如果不处理,那wrap_content就相当于match_parent。

如何处理?

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);

      //指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
      //控件的大小应该设置多大了
      int mWidth = 200;
      int mHeight = 200;

      int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
      int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

      int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
      int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

      if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
          setMeasuredDimension(mWidth, mHeight);
      } else if (widthSpecMode == MeasureSpec.AT_MOST) {
          setMeasuredDimension(mWidth, heightSpecSize);
      } else if (heightSpecMode == MeasureSpec.AT_MOST) {
          setMeasuredDimension(widthSpecSize, mHeight);
      }
  }

注:你可以自己尝试一下自定义一个View,然后不重写onMeasure()方法,你会发现只有设置match_parent和wrap_content效果是一样的,事实上TextView、ImageView 等系统组件都在wrap_content上有自己的处理,可以去翻一翻源码。

看完了View的measure过程,我们再来看看ViewGroup的measure过程。ViewGroup继承于View,是一个抽象类,它并没有重写onMeasure()方法,因为不同布局类型的测量 流程各不相同,因此onMeasure()方法由它的子类来实现。

三、总结
Measure过程是对View尺寸的测量过程,View通过onMeasure方法确定自己的尺寸,ViewGroup在确定自己尺寸的同时,要正确调用子View的measure()方法,让子View正确测量。自定义View和ViewGroup的时候,也是通过onMeasure方法完成measure过程。

相关文章

  • 常用英语单词分类速记(111)

    Measure 测量 Measure 测量(动) Measure 计量单位/测量标准 Measurement 测量...

  • Android View的绘制流程

    一、measure过程 ViewGroup measure过程 View的measure()方法是final的,无...

  • View的绘制原理

    一、measure过程 1、View的measure过程 View的measure方法是一个final方法,不可重...

  • Android布局绘制

    二、View的测量Measure 父View的measure方法会调用子View的measure方法,让子View...

  • Linux指令

    vcgencmd命令 vcgencmd measure_temp vcgencmd measure_volts core

  • measure

    if you can not measure it, you can not improve it

  • measure

    @The act or process of assigning numbers to phenomena acc...

  • Measure

    A box This box has three dimensions,length、width and heig...

  • Measure

    本文主要android View绘制三大流程(measure、layout、draw)中的measuer流程。如何...

  • 4源码的角度分析View

    内容:View的三大工作流程源码分析 measure过程 1.View的measure过程 由measure方法来...

网友评论

      本文标题:Measure

      本文链接:https://www.haomeiwen.com/subject/pgiydktx.html