自定义View(八)-View的工作原理- View的measure

前言

从上一篇中。同Activity的布局加载了解了整个View树加载的流程。最后是通过View的三大流程来实现布局的显示的。那么我们这篇来讲下布局的三大流程之一-->measure


1.MeasureSpec
在讲解测量之前我们要先清楚什么是MeasureSpec?MeasureSpaec可以理解为测量规格。在View.measure()中多次被用到。它是有一个32位的int值,高2位代表SpecMode(指测量模式),低30位代表SpecSize(在指定模式下的规格大小)。

他们对应的二进制值分别是:
UNSPECIFIED=00000000000000000000000000000000
EXACTLY =01000000000000000000000000000000
AT_MOST =10000000000000000000000000000000
由于最前面两位代表模式,所以他们分别对应十进制的0,1,2;

在测量中,会根据子View的LayoutParames与父容器的MeasureSpec的规格来生成子View的MeasureSpec然后根据它来测量出View的宽/高。所以这个概念该是非常重要的。下面我来看下它的具体模式的含义。

SpecMode SpecSize
MeasureSpec.UNSPECIFIED 不确定模式:子视图View请求多大就是多大,父容器不限制其大小范围,一般用于系统内部
MeasureSpec.EXACTLY 精确模式,父容器已经检测View所需要的精确大小,View的最终大小就SpecSize所指定的值。对应Layout中的match_parent和具体的数值两种模式
MeasureSpec.AT_MOST 最大模式,父容器制定一个可用大小SpecSize,子View不能大于这个值。对应LayoutParames中的warp_content

2.View#measure()
measure测量分成两种一种是原始View,那么通过measure方法就完成了其自己的测量,如果是ViewGroup,除了完成自己的测量完还要遍历子元素的measure方法,各个子元素如果是View就测量自己,如果是ViewGroup就接着遍历,最后都是调用View的measure。

因为View是所有View与ViewGroup的老祖宗,那么我们先抛开ViewGroup先直接来了解下View.measure()方法:

    int mOldWidthMeasureSpec = Integer.MIN_VALUE;

    int mOldHeightMeasureSpec = Integer.MIN_VALUE;

 public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

        ..................
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //如果上一次的测量规格和这次不一样,则条件满足,重新测量视图View的大小
        if (forceLayout || needsLayout) {

            int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                    mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

    }

这段代码比较简单,首先判断与上次测量的MeasureSpec是否相等,不等就重新测量。可以看到此方法调用了onMeasure()方法,并将传来的值直接传递下去,那么就说明测量的主要的逻辑都在此方法中,我们跟往下走View#onMeasure():

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

通过注释我们知道,参数中的MeasureSpec是父布局给我们传递过来的。这点我们要清楚。这段代码看起来比较简单,但是实际理解起来却不容易。可以看到在onMeasure()只调用了setMeasuredDimension();就结束了了。那么我们就可以知道当盗用此方法的时候就证明测量流程结束。那么我们来看下他里面参数分别是measuredWidth,measuredHeight。并通过getDefaultSize()方法来计算的,进入此方法:

/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     *
     * @param size Default size for this view 这个view的默认尺寸大小
     * @param measureSpec Constraints imposed by the parent 这个参数是父容器提供的子View的MeasureSpec
     * @return The size this view should be.  这个view在经过此方法后返回的view的尺寸大小
     */
    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        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的测量宽高。下面我们进入getSuggestedMinimumWidth()方法看看是如何获得View的尺寸大小的:

   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

如果View没有设置背景那么返回mMinWidth(它对应XML中的android:minWidth,如果不指定默认为0),如果设置了背景就为Drawable的原始高度。

总结: 在通常情况下我们没有设置android:minWidth属性,那么getDefaultSize()的返回值就为specSize(父容器提供的)那我们通过getDefaultSize()方法知道了,在自定义View的时候如果直接继承View要重写onMeasure()方法,否者warp_content和match_parent效果相同

sizeSpec大小是有父容器决定的,我们由上篇文章知道知道父容器DecorView的测量模式是MeasureSpec.EXACTLY,测量大小sizeSpec是整个屏幕的大小。

到这里我们就把View的绘制流程梳理完成了。下面我们就接着上篇讲解从performTraversals()方法触发查看view的三大流程。


3.ViewGroup测量

从上一篇文章我们知道顶级View(DecorView)继承FrameLayout(ViewGroup)。那我们继续上一篇的中performTraversals()方法中的performMeasure()测量这个方法看下:

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

那么我们进入到FrameLayout(ViewGroup),为了完全理清流程我们先来看下它父类ViewGroup#onMearsure()方法发现ViewGroup是一个抽象类,它里面没有实现onMearsure(),这也能理解,因为ViewGroup是所有空间容器的父类,具体的测量方式应该是子类容器控件实现的。比如LinearLayout与RelativeLayout他们的方法都是不一样的。但是它有一下两个方法:

  protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed)
            
 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec)

在measureChildren方法中会循环遍历子View,然后调用measureChild()方法,通过对measureChild()与measureChildWithMargins()方法的比较发现两者基本相同,只不过后者加入了边距的运算。不管那种方法最后都会调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法也就是View.measure(),之后就会走View#measure流程。那么我们现在回过头来进入FrameLayout#onMearsure()方法看他是如何实现的:

·
    @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());
        }

        // //设置当前FrameLayout测量结果,此方法的调用表示当前View测量的结束。
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }
    
    ......

上面讲到在调用setMeasuredDimension()方法后就表示测量完成了,所以我们主要看它上面的代码。首先取出容器下的所有子控件,然后调用 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的LayoutParams,然后通过getChildMeasureSpec()方法来得到子View的MeasureSpec,最后调用View.measure()完成测量。子View还是ViewGroup继续走ViewGroup的测量,如果是子控件是View就会测量自己完成整个测量过程。那么我在跟踪代码看下getChildMeasureSpec()方法做了什么:

/
    * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
 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:
            //子View的宽/高是具体的值
            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
        //MeasureSpec.makeMeasureSpec--> 根据大小个模式生成一个MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

这个方法首先获取了当前DecorView容器的测量模式,然后减去传进来的padding参数,得到一个子元素可用的大小size,代码如下:

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

int size = Math.max(0, specSize - padding);//(padding表示不可用的范围,由上面的代码可知padding=当前容器(FrameLayout)的padding+子元素的margins)

有上一篇我们知道我们DecorView是match_parent,所以直接看MeasureSpec.EXACTLY:分支,其他分支是一样的,通过观察我们可以将上面MeasureSpec.EXACTLY:分支下的三个if语句总结如下:

  • LayoutParams.MATCH_PARENT(精确模式): 当子View宽/高为LayoutParams.MATCH_PARENT 大小就是父容器大小。
  • LayoutParams.WRAP_CONTENT(最大模式): 大小不定,但是不能超过父容器的大小。
  • 固定模式(比如100dp)(精确模式): 大小为LayoutParams指定大小。(这里注意如果你的大小超过窗口大小比如1200dp,那么你的大小就是1200dp,虽然窗口显示不下,但是窗口会能显示多少显示多少)

总结:

  • 1.MeasurseSpec 中我们讲到这么一句话在测量中,会根据子View的LayoutParames与父容器的MeasureSpec的规格来生成子View的MeasureSpec然后根据它来测量出View的宽/高。 通过上面的代码和下面的总结,现在我们在来理解这句话就很好理解了。

    • measureChildWithMargins():子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本省的LayoutParames以及父容器的padding与子元素的margins决定。
    • getChildMeasureSpec():通过传入子元素LayoutParames(也就是XML中宽/高所具体定义的),来决定自己的MearsureSpec。
  • 对于顶级View(即DecorView)和普通View来说MearsureSpec转换过程略有不同,对于DecorView,其MearsureSpec是窗口尺寸和其自己的LayoutParames共同决定,对于普通View,MearsureSpec是由父容器的MearsureSpec和自身的LayoutParames共同决定。同时对于普通View针对不同的父容器和View本身不同的LayoutParames,View就可以有多重MeasureSpec具体不同参照下表:

    图片.png

      普通View的MeasureSpec的创建规则 (此图来自Android开发艺术探索)


  • 4.顶级DecorView测量
    对于好奇的小伙伴可能会问:上面提到DecorView的MearsureSpec是窗口尺寸和其自己的LayoutParames共同决定。那么是如何决定的呢?

其实在ViewRootImpl中的measureHierarchy中展示了MeasureSpec创建过程(此方法在performTraversals()被调用同时在三大流程之前):

childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);//1139
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  

getRootMeasureSpec()方法中的一个参数就是窗口的尺寸大小,第二个就是当前View(顶级DecorView)的LayoutParames,他的方法如下:

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

与getChildMeasureSpec的原理是一样的具体看getChildMeasureSpec关于固定大小、精确模式、最大模式总结。我认为在测量时先经过ViewRootImpl#measureHierarchy方法测量出DecorView的


5.整个View三大流程之测量概括总结

上面我们把整个View的测量相关流程基本上都滤清了关于这些纯概念源码的东西看着乏味,也不容易理解,偏底层也没有什么程序运行效果。那么我用流程图来梳理下整个流程:

View的测量(1).png


结语

View的测量基本上就是这样了。通过本章的学习,我们应该掌握测量的流程和里面重要的方法,这样我们在自定义View的时候才会更的得心应手。希望这篇文章对大家有所帮助。如果有错误希望可以指出,觉得对你有所帮助就支持下,关注走一波!下篇View的布局(layout)见。

感谢

《Android开发艺术探索》

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

推荐阅读更多精彩内容