Android控件的测量流程梳理

总体流程

Android控件的测量从根布局开始,根布局即DecorView ;
测量开始的地方,由ViewRootImol类的performMeasure方法开启测量,调用了DecorView的onMeasure方法,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
其中参数widthMeasureSpec和heightMeasureSpec里包含的信息有两个,宽高尺寸尺寸模式,其中宽高为屏幕的尺寸,尺寸模式为MeasureSpec.EXACTLY
在DecorView的onMeasure方法中,根据传入的两个参数、子View的尺寸信息以及自身的布局逻辑,来判断需要给子View设定的宽高尺寸和尺寸模式,然后继续调用子View的measure方法;
子view在measure方法中会处理一系列逻辑,调用到自身的onMeasure方法,然后根据父布局传入的宽高尺寸、尺寸模式以及自身的具体需求来确定自己最终的尺寸大小;
在上一步中如果子View也是一个ViewGroup子类,则根据传入的参数和自身的逻辑继续测量自己的子View,直到最后一层。

以上就是整个流程的简要归纳,如果上面那段话中让你觉得云里雾里,或者你感觉有很多东西不知道从哪冒出来的,不要紧,我们一个一个点来说,捋清其中的细节。这个结论可以留到最后再来看,到时会更加清晰。

根布局

在Android应用中,你所写的每一个页面,都有一个根布局,这个根布局不是你调用setContentView()时设置的那个,而是DecorView。
我们来捋一捋Activity,DecorView,你填入的布局,还有一个:Window,这几个东西之间的联系。
Window是一个顶级窗口,它定义了窗体样式和行为,提供标准的UI规则,例如背景、标题、默认关键过程等。实际上,每当你写一个新的Activity,在Activity的 attach方法中,都会初始化一个PhoneWindow实例。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
      //省略部分代码...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
      //省略部分代码...
    }

在你调用setContentView方法的时候,实际上调用的是Window的setContentView方法:

 public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

然后PhoneWindow的setContentView方法中调用了installDecor方法:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
 //省略部分代码...
    }

installDecor()再调用generateLayout方法,这个方法中做了很多事情,会根据设置的主题样式来设置DecorView的风格,比如有没有TitleBar,有没有ActionBar等等;
这个方法中也为DecorView添加了子View,即你通过setContentView设置进来的布局。

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    TypedArray a = getWindowStyle();
    //省略好多好多代码...
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //省略好多好多代码...
    return contentParent;
}

onResourcesLoaded代码如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

        mStackId = getStackId();
        //省略部分代码...
      //根据id实例化你填入的布局
      final View root = inflater.inflate(layoutResource, null);
      if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            //将你的布局加入到DecorView
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        //省略部分代码...
}

所以,到这里,我们应该明白了,一个页面中的几层关系:

  • 每个Activity会持有一个Window实例
  • Window下有DecorView
  • DecorView下是你设置的页面内容布局

开启测量的起始点

现在我们知道根布局在哪里,然后我们来看从哪里开始测量。
View的绘制过程,是由ViewRootImpl这个类来完成的,测量工作当然也包含在其中。ViewRootImpl是连接WindowManager和DecorView的纽带,负责向DecorView分发收到的用户发起的event事件(如按键,触屏等),也负责完成View的绘制。

具体处理绘制流程是在一个performTraversals方法中,这个方法被调用的时候很多:控件焦点变化被调用、显示状态变化被调用、绘制刷新被调用、等等等等...
performTraversals方法内部逻辑相当相当多&复杂,截取相关部分代码:

private void performTraversals() {  
    // 省略巨多的代码…
  
    if (!mStopped) {  
        // ……省略一些代码 
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
        // ……省省省  
  
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  
    }  
  
    // 省略巨多的代码…
}  

可以看到在performTraversals方法中通过getRootMeasureSpec获取原始的测量规格并将其作为参数传递给performMeasure方法处理,这里我们重点来看getRootMeasureSpec方法是如何确定测量规格的,首先我们要知道mWidth, lp.width和mHeight, lp.height这两组参数的意义,其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定,mWidth和mHeight表示当前窗口的大小,其值由performTraversals中一系列逻辑计算确定,这里跳过,而在getRootMeasureSpec中作了如下判断:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {  
    int measureSpec;  
    switch (rootDimension) {  
  
    case ViewGroup.LayoutParams.MATCH_PARENT:  
        // Window不能调整其大小,强制使根视图大小与Window一致  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
        break;  
    case ViewGroup.LayoutParams.WRAP_CONTENT:  
        // Window可以调整其大小,为根视图设置一个最大值  
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
        break;  
    default:  
        // Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸  
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
        break;  
    }  
    return measureSpec;  
}  

所以最终的测量规格的确定走的是这一步:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
这里解释一下传入onMeasure()的这个参数,measureSpec 这个整型值,包含了两个信息,一个是具体尺寸,一个是尺寸模式。具体尺寸很好理解,尺寸模式包括三种:

  • MeasureSpec.EXACTLY 代表父View已经为子View规定好了具体的尺寸,是否遵循看子view的意愿;
  • MeasureSpec.AT_MOST 代表父View为子View圈定了一片地方,子View最大不能超过这片地方;
  • MeasureSpec.UNSPECIFIED 代表父view对子view没有任何约束,子view想多大就多大;
    (具体这个整形值如何包含着两个信息的自行查找资料学习,这里略过)

所以回到计算中,这里的结果很明显:尺寸是窗口大小,模式是MeasureSpec.EXACTLY ,顶层DecorView接收到的参数就是这两个。也就是说不管如何,我们的根视图大小必定都是全屏的…

好了,到这一步,我们测量的起点也找到了,顶层View接收到的尺寸参数也明确了,接下来我们看看它如何把测量流程继续往下走。

ViewGroup测量子View

顶层的ViewGroup从onMeasure方法中接收到了尺寸参数,大小确定为屏幕宽高,模式是MeasureSpec.EXACTLY 。
好了,现在顶层的DecorView要开始自己的工作了:我有这么大块地盘,我要好好安置我的儿子们,每人划一片地儿...安置的妥妥的......
为了理清脉络,DecorView自己的布局逻辑我们摒开不看,它在onMeasure方法中调用了父类的onMeasure方法,也就是FrameLayout的onMeasure方法。现在我们把常用的几个Layout拎出来,看看他们的onMeasure方法,比对一下,可以发现,大致都有这么一段逻辑:
1.对子View进行遍历;
2.调用measureChildWithMargins()方法获取对子view的建议尺寸规格(这个是ViewGroup本身的方法);
3.用获取到的尺寸规格,调用子view的measure方法;
由于各种布局自身的排列逻辑不同,相关的实现细节必定差异极大,但是测量的流程却都是几乎相同的,measureChildWithMargins()方法就是其中的共同点之一,来看看这个方法:

 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);
    }

很简单,获取子View的测量规格,然后调用子View的measure方法。接着看getChildMeasureSpec():

 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的尺寸模式,再判断子view的宽高值:具体值、WRAP_CONTENT、MATCH_PARENT,根据这两个值的具体情况来返回相应的MeasureSpec信息。
然后用这个确定好的MeasureSpec信息传给子View,调用其measure方法,让它确定自身的尺寸。
到这里已经很清晰了...
接下来,就是View拿到父View建议的尺寸规格,结合自身情况,设置自身的具体尺寸大小。

View设定具体宽高

终于到了View这一层了。
View的measure方法逻辑中,会调用到onMeasure方法,其默认的实现是:

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

//onMeasure内用到的的相关方法
protected int getSuggestedMinimumWidth() {  
    //mMinWidth和mMinHeight 好像都是100px
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());  
}  

public static int getDefaultSize(int size, int measureSpec) {  
    // 将我们获得的最小值赋给result  
    int result = size;  
  
    // 从measureSpec中解算出测量规格的模式和尺寸  
    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不限制,就按自己的背景大小或者最小值来显示,如果父view有限制,就按父view给的尺寸来显示。
按照这个逻辑,如果要自己写一个自定义View,大小可以在布局中确定的话,一般不用再重新onMeasure 再做什么工作了。
但是如果自己的自定义View在布局中使用WRAP_CONTENT,并且内容大小并不确定的话,还是要根据自己的显示逻辑做一些工作的。
比如,自己写一个显示图片的控件,布局中使用WRAP_CONTENT,那么根据以上的逻辑梳理,父view很可能就扔给你一个尺寸模式:大小是父view本身的大小,模式是MeasureSpec.AT_MOST;这样的话即使你布局里写的是WRAP_CONTENT,你也会使用父view建议给你的尺寸,占满父view全部的空间了,即使你的图片并没有那么大~是不是会很奇怪?
所以,一般情况下,展示内容尺寸不确定的自定义View,onMeasure可以作如下类似的逻辑:

@Override  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 声明一个临时变量来存储计算出的测量值  
    int resultWidth = 0;  
  
    // 获取宽度测量规格中的mode  
    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);  
  
    // 获取宽度测量规格中的size  
    int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);  
  
    /* 
     * 如果爹心里有数 
     */  
    if (modeWidth == MeasureSpec.EXACTLY) {  
        // 那么儿子也不要让爹难做就取爹给的大小吧  
        resultWidth = sizeWidth;  
    }  
    /* 
     * 如果爹心里没数 
     */  
    else {  
        // 那么儿子可要自己看看自己需要多大了  
        resultWidth = getSelfContentWidth();//自己实现...
  
        /* 
         * 如果爹给儿子的是一个限制值 
         */  
        if (modeWidth == MeasureSpec.AT_MOST) {  
            // 那么儿子自己的需求就要跟爹的限制比比看谁小要谁  
            resultWidth = Math.min(resultWidth, sizeWidth);  
        }  
    }  
  
    int resultHeight = 0;  
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);  
    int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);  
  
    if (modeHeight == MeasureSpec.EXACTLY) {  
        resultHeight = sizeHeight;  
    } else {  
        resultHeight = getSelfContentHeight();//自己实现...
        if (modeHeight == MeasureSpec.AT_MOST) {  
            resultHeight = Math.min(resultHeight, sizeHeight);  
        }  
    }  
  
    // 设置测量尺寸  
    setMeasuredDimension(resultWidth, resultHeight);  
}  

这样,既考虑了自身内容的尺寸,也适应了View的测量流程,就可以正确的显示大小了。当然,具体情况还是要看自定义view 的具体逻辑,这里只是一个示例,不一定适合各种场合。

那么,到这里,关于Android中控件尺寸测量流程的梳理,差不多就都结束了。现在你再去看开头的结论,会不会清晰一些了?
最后,如发现有问题,请斧正,不胜感激!
最后的最后,感谢AIGE的博客, 学习了很多,本篇文章很多的参考了AIGE的博客,甚至摘抄了部分他的代码和描述片段,当做笔记,往后以方便查阅。原文地址,请点击这里。

你可能感兴趣的:(Android控件的测量流程梳理)