View的 measure过程解析

前言

关于自定义view,我们都知道有三个最重要的方法onMeasure负责测量,onLayout负责放置位置(只有在ViewGroup时使用),onDraw负责绘制。今天就参照源码理解measure过程。

MeasureSpce

讲measure之前,先说这个measure时候用到的最基础的类,他是View的一个内部类。你可以将他理解为测量规则,我更喜欢将他理解为,测量约束,。他有两个最关键的定义(为什么是叫定义,因为它们不是参数),sizemodesize,就是具体尺寸,int类型。mode是测量模式,也可以说是约束模式。有三个值分别是UNSPECIFIEDEXACTLYAT_MOST

  • UNSPECIFIED
    不约束,比如ScrollView,ListView,他们都不会约束子View的高度,你要多高就给你多高。
  • EXACTLY
    精确约束,对应布局参数match_parent及精确值,比如layout_height=100dp。
  • AT_MOST
    给与最大尺寸以内的适应值,对应布局参数wrap_content。

对于他们为什么对应这些布局参数,在后面的源码中可以得到解释。

onMeasure()本质

既然讲测量过程,那么最关键肯定是onMeasure()方法,我们先看这个方法的源码。

  /**
   * Measure the view and its content to determine the measured width and the measured height.
   * This method is invoked by measure(int, int) and should be overridden by subclasses to
   * provide accurate and efficient measurement of their contents.
   * 
   * CONTRACT: When overriding this method, you must call setMeasuredDimension(int, int) to store
   * the measured width and height of this view. Failure to do so will trigger an 
   * IllegalStateException, thrown by measure(int, int). Calling
   * the superclass' onMeasure(int, int) is a valid use.
   * 
   * The base class implementation of measure defaults to the background size, unless a larger size
   * is allowed by the MeasureSpec. Subclasses should override onMeasure(int, int) to provide
   * better measurements of their content.
   */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    /**
     * This method must be called by {@link #onMeasure(int, int)} to store the
     * measured width and measured height. Failing to do so will trigger an
     * exception at measurement time.
     */
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ······
    }

setMeasuredDimension()很直接,注释都不用看,就是保存最终测量到的宽高值。这也是所有View的实现类的onMeasure()方法内都必须调用的。
是很显然,这个方法的作用,就是确定并保持测量的尺寸,也就是确定View本身的宽高。
emmm...这就完了?这篇文章就到这里了?这么说也完全可以(●ˇ∀ˇ●)。
到这里就该明白,其实重点是在这个测量过程,所以我们还要继续。

起点:ViewGroup.onMeasure()

上面的源码只是View(View的本身,并不包含其实现)的测量方式,我们都知道view必须放容器里面,比如LinearLayout,FramLayout等,所以我们应该从它的容器入手,而他们都有统一的父类ViewGroup,它是继承View,对View进行拓展的一个抽象类。但是ViewGroup是没有onMeasure()方法,准确的说,它本身没有重写onMeasure,也就是没有一个统一的测量过程,而是在他们的实现类中重写测量过程。
我们就以FrameLayout的 onMeasure源码为例。

首先从FrameLayout的onMeasure开始。(为什么从这里开始,其实这是个倒果为因,我理解测量过程后,才明白是从这里开始最合适(lll¬ω¬),这也是为什么大多数文章都是从这里开始讲,你看到最后就明白了)

  • onMeasure参数
    看方法先看参数。
    onMeasure(int widthMeasureSpec, int heightMeasureSpec)有两个参数,他们到底是什么?
    他们是父布局传递下来的,告诉当前View(ViewGroup)的对自身的约束,也就是MeasureSpec的sizemode信息。既然已经得到这些信息了,size也有了,那直接setMeasuredDimension()设值最终测量值不就行了吗?有些情况下是,比如EXACTLY,情况下,确实直接size是多少,measureDimension就是size,但是AT_MOST (wrap_content)呢。wrap_content是包裹内容,则就必须知道子view的宽高,才能确定包裹子view所需要的宽高。onMeasure()很大程度都是围绕wrap_content这个参数进行逻辑计算。

  • FrameLayout.onMeasure()源码

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                //手动重点,加黑加粗
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        //手动重点,加黑加粗
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        ······
    }

看到上面的重点部分没,关键方法measureChildWithMargins()setMeasuredDimension()
setMeasuredDimension()就不说了,onMeasure()方法内必须调用的。下一步就是分析measureChildWithMargins

  • measureChild
    measureChildWithMargins()看方法先看注释,这个方法太直接,注释就不用看了。他还有另外一个同类方法,measureChild,作用都是测量子类,一个是包含margin计算(支持Margin属性),另外一个是不包含margin计算。measureChild()measureChildWithMargins()都是由ViewGroup提供的,所以,ViewGroup虽然没有提供统一的onMeasure,但是提供了通用的子类测量方法,这也非常符合ViewGroup本身的意义和使命。并且,所有的ViewGroup实现,在measure的时候,都是调用ViewGroup的这两个测量方法中的一个对子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);
    }

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //手动重点,加粗加黑
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //手动重点,加粗加黑
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
  • getChildMeasureSpec
    从上面的源码知道重点方法getChildMeasureSpec,从命名就知道他的意义是,获取子View的测量约束。
    敲黑板,注意,重点来了!是时候展现真正的重点了!看源码
  public static int getChildMeasureSpec(int parentMeasureSpec, int padding, int childDimension) {
    //结合源码注释跟,我写的中文注释(不是翻译)看逻辑

    //获取FrameLayout测量约束(对应match_parent,wrap_content,或具体值),size
    int specMode = MeasureSpec.getMode(parentMeasureSpec);
    int specSize = MeasureSpec.getSize(parentMeasureSpec);

    //减去padding,就是能够给与子view的剩余尺寸大小
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
      // Parent has imposed(强加于) an exact size on us
      //FrameLayout有明确约束时,即size有准确值
      case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
          //若子view声明明确值时候,则子view要多少尺寸,给多少尺寸
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
          // Child wants to be our size. So be it.
          //若子View声明为match_parent,则他的尺寸应该就是FrameLayout的size
          resultSize = size;
          resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
          // Child wants to determine its own size. It can't be
          // bigger than us.
          //当子view为wrap_content属性时候,给子view传递的测量约束,是,最大尺寸(AT_MOST)不可以超过FrameLayout
          //可以给与的大小尺寸(size)
          resultSize = size;
          resultMode = MeasureSpec.AT_MOST;
        }
        break;

      // Parent has imposed(强加) a maximum size on us
      //当FrameLayout有最大值限制时,即属性为wrap_content
      case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
          // Child wants a specific size... so be it
          //子View要多少给多少
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == ViewGroup.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 == ViewGroup.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
      //FrameLayout的父类不指定约束FrameLayout时
      case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
          // Child wants a specific size... let him have it
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
          // Child wants to be our size... find out how big it should
          // be

          //sUseZeroUnspecifiedMeasureSpec 是否使用0,当未指定的测量约束
          //static boolean sUseZeroUnspecifiedMeasureSpec = false;
          //sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M
          //这里有一个版本问题,知道就行
          resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
          resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == ViewGroup.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);
  }

这里的逻辑不难理解,注释结合日常使用场景,就能明白。从这里,你应该明白了,EXACTLY为什么对应match_parent和准确值,而AT_MOST对应wrap_content了。同时也更深刻理解wrap_content在使用时候所代表的意义了。

  • measure()
    在上面的measureChildWithMargins方法内,另外一个重点,就是child.measure()。为了便于理解,只要知道,是这个方法调用了onMeasure()就行了。在onMeasure()注释也说过了,自己是由measure()调用的。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
         ······
         onMeasure(widthMeasureSpec, heightMeasureSpec);
         ······
    }

终点还是起点?

看到这里,是不是有种恍然的感觉,一切又回到onMeasure(),之前的一切都串联了起来。为什么onMeasure()传递的参数,是父布局给的自身约束,因为getChildMeasureSpec内调用的child.measure()
那么这里是终点,还是起点?
如果是View的实现,那么这里就是终点了,最后由View的实现比如TextView等,去实现onMeasure来确定自身的最终测量尺寸。
如果是ViewGroup的实现,那么这里可以说是一个新的起点,去继续遍历一个个子View进行测量。
那么一切的起点呢。
其实我有想过最顶的DecorView,但是去看了一遍源码,及PhoneWindow,还是没找到,谁给了DecorViewmeasure()的测量约束。不过应该能够基本确定,无论如何,都离不开屏幕的尺寸参数,如此才能确定它大小。

最后

总结一下过程:

  1. ViewGroupImpl.onMeasure()
    一切从这里开始吧
  2. ViewGroup.measureChild()/measureChildWithMargins()
    遍历所有子view,对每个子view进行测量
  3. ViewGroup.getChildMeasureSpec()
    根据自身的约束条件,及child的LayoutParams决定其child的测量约束
  4. View.measure()
    调起测量
  5. View.onMeasure()/ViewImpl.onMeasure()
    具体测量逻辑
  6. View.setMeasuredDimension()
    确认最终的测量宽高
  7. ViewGroup.setMeasuredDimension()
    确认最终的测量宽高

那么为什么自定义View需要重写onMeasure?你明白了吗。

参考

1
2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356