AndroidQ View的测量流程onMeasure

本篇文章来分析一下View的测量流程,在写自定义控件时往往需要重写View的onMeasure来定义自己的测量规矩,如果我们不熟悉测量流程,就算根据别人的自定义控件学习,最后写出来了,但是再遇到新的自定义控件可能还是无从下手

我们都知道View的测量,布局,绘制的入口在ViewRootImpl的performTraversals方法开始,而View的测量则是从最顶层的DecorView开始的,先测量子View,在测量父View,测量的规则其实就是根据父View的MeasureSpec和子View的LayoutParams再根据一个特定规则算出子View的大小

MeasureSpec

MeasureSpec中包含一个View的测量模式和size,这个size并不一定是最终View的大小

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

测量模式有三种:
UNSPECIFIED: 不限制View的大小,如ListView,scrollView,无法进行限制
EXACTLY: 精确模式,如在xml中写死View的大小或者在父View模式为EXACTLY时自己在xml中写的match_parent
AT_MOST: 在xml中写的wrap_content或者在父View模式为AT_MOST时自己在xml中写的match_parent

我们前面说了测量一个View是用的父View的MeasureSpec和子View的LayoutParams来计算的,那么最顶层的DecorView没有父View,它的MeasureSpec哪来的呢?

我们从测量的入口来看看DecorView的MeasureSpec的获取

performTraversals

private void performTraversals() {
	  ......
	  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	  ......

}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {

        }
    }

在ViewRootImpl的performTraversals方法中调用performMeasure开始测量,performMeasure中调用mView的measure方法,这里的mView就是DecorView,我们来看看childWidthMeasureSpec和childHeightMeasureSpec

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

childWidthMeasureSpec和childHeightMeasureSpec的获取是通过调用getRootMeasureSpec方法,传递了mWidth,mHeight以及lp.width,lp.height,lp是在ViewRootImpl中创建的LayoutParams,调用了WindowManager.LayoutParams()的无参构造函数,默认值是:MATCH_PARENT

public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        }

而mWidth和mHeight则是手机屏幕的宽高,再看看getRootMeasureSpec方法

 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

getRootMeasureSpec方法中合成了DecoreView的MeasureSpec,根据传递的lp.width,lp.height为MATCH_PARENT得到传递到DecorView的MeasureSpec模式为EXACTLY,size为手机屏幕的宽高,接着看performMeasure中调用的mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

DecoreView是一个FrameLayout,所以会调用FrameLayout的measure方法,而measure方法定义在View中,是final修饰的,所以子类无法继承,实际上View的measure方法中调用了onMeasure,这个方法是给子类实现自己的测量规则的

FrameLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		......
	     for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            //mMeasureAllChildren代表是否测量所有View
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                 ...
                }
            }
		......
}

遍历DecoreView中的所有子View,如果mMeasureAllChildren为true或者当前子View不是Gone状态则进一步调用measureChildWithMargins测量子View,到这里我们先说一下DecoreView的内部布局,DecoreView内部会在PhoneWindow的setContentView方法中创建DecoreView时加载一个如下xml布局,不考虑任何样式的情况下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

所以可以简单认为DecoreView中包含一个LinearLayout,这个LinearLayout中包含一个actionbar和一个id为content的FrameLayout,通过setContentView填充的就是这个FrameLayout,忽略actionbar的测量

好了,了解了DecoreView内部布局后我们再回到其onMeasure方法中,遍历DecoreView的子View,调用measureChildWithMargins开始测量,这里子View就是这个LinearLayout

measureChildWithMargins

此方法定义在ViewGroup中

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        //(1)
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //(2)
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        //(3)
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //(4)
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

我们分几步来看看,首先:
(1)获取LinearLayout的LayoutParams,获取LayoutParams的目的其实是需要获取xml中定义的Margin以及layout_width,layout_height

(2)(3)分别计算LinearLayout宽高的MeasureSpec,计算MeasureSpec时需要父View的MeasureSpec和父View的Padding和自己的Margin再加上一个widthUsed或者heightUsed,被其他View使用过的空间

(4)得到了LinearLayout的MeasureSpec之后会传给自己内部的measure,进而测量自身

getChildMeasureSpec

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //获取父View的测量模式
        int specMode = MeasureSpec.getMode(spec);
        //获取父View的size
        int specSize = MeasureSpec.getSize(spec);
        //由父View的size减去父View的padding和自己的margin以及Used
        //得到最终size(为了方便后面依然统称为父View的size)
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;
        //根据父View的测量模式以及自己LayoutParams计算MeasureSpec
        switch (specMode) {
        // 如果父View的模式为EXACTLY
        case MeasureSpec.EXACTLY:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式就是精确的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size就是父View的size
                resultSize = size;
                //自己的模式就是精确的,因为父View的模式是精确的
                resultMode = MeasureSpec.EXACTLY;
                //如果自己在xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size暂时定为父View的size,因为此时还没有
                //对自己进行具体测量,无法得到具体值
                resultSize = size;
                //自己的模式为AT_MOST,因为就算还没有具体测量但最终
                //肯定不能超过父View的大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父View的测量模式为AT_MOST,即父View此时也不能确定自己的size
        case MeasureSpec.AT_MOST:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size就是父View的size
                resultSize = size;
                //自己的模式为AT_MOST,因为父View都不能确定自己大小
                resultMode = MeasureSpec.AT_MOST;
                //如果自己的xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size暂定为父View的size
                resultSize = size;
                //自己的模式为AT_MOST,自己虽然为WRAP_CONTENT,
                //但最终还是不能超多父View大小
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //如果父View模式为UNSPECIFIED
        case MeasureSpec.UNSPECIFIED:
         //如果自己的xml中写的值是一个不是match_parent或者wrap_content
            if (childDimension >= 0) {
                //则自己的size就是xml中写的值
                resultSize = childDimension;
                //自己的模式为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
                //如果自己的xml中写的是MATCH_PARENT
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //则自己的size取决与sUseZeroUnspecifiedMeasureSpec,
                //这个值在View中初始化,当sdk版本小于M时为true,大于为false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式为UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
                //如果自己的xml中写的是WRAP_CONTENT
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //则自己的size取决与sUseZeroUnspecifiedMeasureSpec,
                //这个值在View中初始化,当sdk版本小于M时为true,大于为false
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                //自己的模式为UNSPECIFIED
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //根据计算出来的size和mode生成自己的MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

getChildMeasureSpec这个方法看着很多代码,其实仔细看,逻辑很清晰,只要记住自己的MeasureSpec一定是根据父View的MeasureSpec和自己的LayoutParams来计算的,是两个条件共同决定,所以有很多判断,大致对这些判断进行分类其实有三个大分支:

  1. 父View的测量模式为EXACTLY时
    子View只有在xml中写了WRAP_CONTENT时才需要在自己onMeasure方 法中定义测量规则
  2. 父View的测量模式为AT_MOST时
    子View在xml写了WRAP_CONTENT和MATCH_PARENT时都需要在自己onMeasure方法中定义测量规则
  3. 父View的测量模式为UNSPECIFIED时
    不用管,这一般是系统的View

总结:View需要测量的情况其实就是自己的mode计算出来为AT_MOST时,如果是EXACTLY的模式,根本不需要我们定义测量规则

好了由DecoreView的MeasureSpec就计算出了它里面LinearLayout的MeasureSpec了,这个LinearLayout的layout_width和layout_height是match_parent,根据上面分析的测量规则,所以这个LinearLayout的mode就是EXACTLY,size为DecoreView的size减去DecoreView内部padding和自己的margin

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

得到了MeasureSpec之后,measureChildWithMargins方法的(4)步会调用LinearLayout的measure方法继续进行测量,前面已经说过measure这个方法定义在View中,不可继承,实际上是在measure中调用子类onMeasure方法

LinearLayout.onMeasure

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

这里分为垂直布局和水平布局,随便看一个

measureHorizontal

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
     for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            //useExcessSpace代表是否使用权重的计算方式
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
               ...
            } else {
                ...
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);        
   }
}

同样是对LinearLayout内部的View进行遍历,如果不是采用权重的方式计算则调用measureChildBeforeLayout方法测量LinearLayout的子View,measureChildBeforeLayout方法中同样是调用了我们前面分析的measureChildWithMargins来测量,传递的是LinearLayout的MeasureSpec,我们可以发现其实是按一种递归的方法,从最顶层的DecoreView到LinearLayout到里面的子View,如果继续还是ViewGroup,可以想到,肯定还是遍历其子View调用measureChildWithMargins,直到最终找到非ViewGroup的View,调用其onMeasure方法,此时的onMeasure肯定就不会遍历子View了,而是实实在在的测量自己,当所以子View测量完成之后,再对那些xml中写的是WRAP_CONTENT的父View进行进一步测量,所以我们前面说measureChildWithMargins测量的size并不一定就是最终View的size,
然后按不同的规则,比如LinearLayout可能是子View的宽或者高累加,FrameLayout可能是按它里面最大的View的宽高得到的,这就需要去看它的源码如何定义的规则

我们在自定义View时,重写onMeasure方法时,它里面的widthMeasureSpec和heightMeasureSpec其实就是通过ViewRootImpl的performMeasure方法开始,从DecoreView开始测量,计算的MeasureSpec一步一步,慢慢计算传递过来的

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

说白了,测量流程其实就是为了给那些mode为AT_MOST的自定义View规定一个测量规则好计算自己的size,mode为AT_MOST时并不是一个准确的值,只是暂时的值,所以需要你重写onMeasure方法,告诉系统,你的View的测量规则,对于那些自定义View没有重写onMeasure方法的View,Android提供了一个默认的测量方法,我们来看下

View.onMeasure

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         //设置View的宽高,此方法调了之后,测量流程就结束了
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

先看看getDefaultSize方法

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //获取自己的测量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        //获取自己MeasureSpec的size
        int specSize = MeasureSpec.getSize(measureSpec);
        /*
           只要自己的模式不为UNSPECIFIED,则自己的宽高就等于
           MeasureSpec保存的宽高
        */
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

所以对一般的自定义View来说,如果你不重写onMeasure方法就默认使用MeasureSpec的size,而MeasureSpec的size根据我们前面getChildMeasureSpec方法的分析,不重写onMeasure情况下,WRAP_CONTENT等价于MATCH_PARENT

总结一下View的测量过程:

  1. View的测量是从ViewRootImpl的performMeasure为入口,首先会根据手机屏幕的宽高和默认值MATCH_PARENT给DecoreView创建一个MeasureSpec
  2. 接着调用DecoreView的measure实际调用FrameLayout的onMeasure方法进行测量,onMeasure中会遍历子类View,以此调用measureChildWithMargins测量子View
  3. 接着又会调用id为content的FrameLayout的onMeasure方法测量其子View,所以View的测量其实就是一个递归的过程
  4. measureChildWithMargins测量子View其实就是根据父View的MeasureSpec和子View自己的LayoutParams计算得到子View自己的MeasureSpec,这里面包含了子View的测量模式和暂时的size
  5. 自定义View或者自定义ViewGroup时一定要重写onMeasure方法,只要你打算在xml中使用WRAP_CONTENT

分析完整个View测量流程之后我们再来聊聊为什么Activity的onCreate方法中获取不到View的宽高,我们能想到要获取View宽高一定要等View测量完成才行,毕竟没有测量完谁知道大小呢,所以我们能够进一步想到onCreate方法一定是在View的测量之前执行的,就是这个原因,Activity执行onCreate回调时View的测量还没开始呢

View的测量是在Activity的onResume回调才开始执行的

后面会再写一篇UI刷新机制详细分析View测量流程是怎么开始的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值