View的测量过程


本文主要讲解Android View绘制三大流程(measure,layout,draw)中的measure流程

前言:

为什么需要measure过程?

我们在在绘制UI的时候,基本都是通过XML布局文件的方式来配置UI,而每个View必须要设置的两个群属性就是layout_width和layout_height,这两个属性代表着当前View的尺寸。

所以这两个属性的值是必须要指定的,这两个属性的取值只能为三种类型:

1、固定的大小,比如100dp

2、刚好包裹其中的内容,wrap_content

3、想要和父布局一样大,match_parent

由于Android希望提供一个更优雅的GUI框架,所以提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。

试想一下,那如果这些属性只允许设置固定的大小,那么每个View的尺寸在绘制的时候就已经确定了,所以可能都不需要measure过程。但是由于需要满足自适应尺寸的机制,所以需要一个measure过程。

 

measure的概念:

measure为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性: mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

如何合理的测量一颗View树?

因为measure过程最终是要确定每个View的实际大小,也就是准确的像素值。但是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值 。所以当一个View需要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他需要知道两个信息。

1、针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。

2、针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。 但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸(例如TextView的单行长度大于父View的话,那么他会进行换行处理)。

下面一个ViewGroup和2个View的简单场景。大概示意图如下:

 

自定义View的测量,需要重写onMeasure()方法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    
    // super
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
​

MeasureSpec关键字

1.如何理解MeasureSpec(测量规格)?

这个MeasureSpec 封装的是父容器传递给子容器的布局要求,而不是父容器对子容器的布局要求,“传递” 两个字很重要

更严谨的说法应该是这个MeasureSpec是由父View的MeasureSpec和子View(自身)的LayoutParams通过简单的计算得出一个针对子View的测量要求 ,这个测量要求就是MeasureSpec。

2.MeasureSpec的三种模式:

UPSPECIFIED : 父容器对于子容器没有任何限制,子容器想要多大就多大(不限制)

EXACTLY: 父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间(限制固定值)

AT_MOST:子容器可以是声明大小内的任意大小(限制上限)

3.MeasureSpecLayoutParams的联系:

在第一点已经提到MeasureSpec是由父View的MeasureSpec和子View(自身)的LayoutParams通过简单的计算得出一个针对子View的测量要求

然后从代码上来看view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个MeasureSpec是父类传递过来的,但并不是完全是父View的要求 ,而是父View的MeasureSpec和子View自己的LayoutParams共同决定的,而子View的LayoutParams其实就是我们在xml写的时候设置的layout_width和layout_height 转化而来的

下面通过查看ViewGroup的原码理解这两者之间的联系:

 

    protected void measureChildWithMargins(View child,
                                           int parentWidthMeasureSpec, int widthUsed,
                                           int parentHeightMeasureSpec, int heightUsed) {
        // 子View的LayoutParams 主要是用到xml中的layout_width和layout_height
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
​
        // 获取子View的MeasureSpec
        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);
​
        // 用子View的MeasureSpec(一个测量要求,比如不能超过多大)去测量自己,如果子View是ViewGroup 那还会递归往下测量。
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
​
    // measure方式是用final修饰的,如果自定义View需要使用测量,那么应该重写onMeasure方法
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ......
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ......
    }
​
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec); // 获取父View(当前)的Mode
        int specSize = MeasureSpec.getSize(spec); // 获取父View(当前)的大小
​
        int size = Math.max(0, specSize - padding); // 获取子View可用的实际大小
​
        int resultSize = 0;
        int resultMode = 0;
​
        // LayoutParams.MATCH_PARENT = -1
        // LayoutParams.WRAP_CONTENT = -2
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    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;// 暂时无法获取子View确切大小,前提是不能比父View大,默认设置和父View一样大
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            /**
             *  总结:
             *  如果父View的MeasureSpec 是EXACTLY,说明父View的大小是确切的,
             *  确切的意思很好理解,如果一个View的MeasureSpec 是EXACTLY,那么它的size是多大,最后展示到屏幕就一定是那么大。
             */
​
            // Parent has imposed a maximum size on us
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    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;
            /**
             * 总结:
             * 如果父View的MeasureSpec 是AT_MOST,说明父View的大小是不确定,最大的大小是MeasureSpec 的size值,不能超过这个值。
             */
​
            // 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;
​
            /**
             * 总结:
             * 如果父View的MeasureSpec 是UNSPECIFIED(未指定),表示没有任何束缚和约束,不像AT_MOST表示最大只能多大,
             * 不也像EXACTLY表示父View确定的大小,子View可以得到任意想要的大小,不受约束
             *
             * View.sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
             * 6.0以下的一些列表嵌套需要手动计算高度(猜的)
             */
        }
        //根据上面获取的mode和size构建MeasureSpec对象。
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

提出疑问!

按照上述的逻辑 有没有可能子View比父View大?

android:clipChildren = "false"

 

什么情况下产生MeasureSpec.UNSPECIFIED?

适用于会滚动的View

ViewGroup的测量过程:

ViewGroup是一个抽象类它并没有实现onMeasure,我们知道测量过程其实都是在onMeasure方法里面做的,我们来看下FrameLayout 的onMeasure 方法,具体分析

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        int maxHeight = 0;
        int maxWidth = 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 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            }
        }
        
        // setMeasuredDimension()
    }
​

大致描述一下测量步骤:

 

1.测量(绘制)入口描述:

既然我们知道整个View的Root是DecorView,那么View的绘制是从哪里开始的呢,我们知道每个Activity 均会创建一个 PhoneWindow对象,是Activity和整个View系统交互的接口 ,每个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系,对于Activity来说,ViewRootImpl是连接WindowManager和DecorView的纽带, 绘制的入口是由ViewRootImpl的performTraversals方法来发起Measure,Layout,Draw等流程

    private void performTraversals() {
        ......
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值