自定义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开发艺术探索》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android相关

LinearLayout.onMeasure-声明变量

mTotalLength:表示所有子View所需要的高度 maxWidth:表示这个LinearLayout的宽度,最后设置宽度的时候用到的 childSt...

1182
来自专栏恰同学骚年

Web前端温故知新-CSS基础

  定义:CSS成为层叠样式表,它主要用于设置HTML页面中的文本内容(字体、大小、对齐方式等)、图片的外形(宽高、边框样式、边距等)以及版面的布局等外观显示样...

1283
来自专栏ytkah

marquee一行代码实现滚动跑马灯效果无需js

  最近ytkah决定拓展一下业务,贴补一些家用,~(@^_^@)~,将以前做的网站建设案例展现出来,有这方面需求的朋友可以扫一下二维码加我哈,或者推荐朋友给我...

7785
来自专栏HTML5学堂

CSS3 -webkit-filter 滤镜

HTML5学堂:在早期网页要实现图片的色相旋转、灰色度的变化,只能用ie的滤镜来实现。直到CSS3出来之后,可以用filter来实现了,接下来详细的了解filt...

3475
来自专栏前端小叙

js焦点轮播图

汇集网上焦点轮播图的实现方式,自己试了下,不过鼠标悬浮停止动画和鼠标离开动画播放好像没生效,不太明白,最后两行代码中,为什么可以直接写stop和play。不用加...

62913
来自专栏我的博客

安卓开发之简单组件使用

一、TextView组件(文本框) <TextView android:id=”@+id/firstText” android:text=”第一行“ andro...

2786
来自专栏Hellovass 的博客

手动测量 View 的宽高

手动调用 View 的 measure(int widthMeasureSpec,int heightMeasureSpec) 方法来得到 View 的宽高。

2016
来自专栏AndroidTv

前端入门4-CSS属性样式表声明正文-CSS属性样式表

作为一个前端小白,入门跟着这四个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

1123
来自专栏贾鹏辉的技术专栏@CrazyCodeBoy

React Native布局详细指南

本文出自《React Native学习笔记》系列文章。 一款好的APP离不了一个漂亮的布局,本文章将向大家分享React Native中的布局方式FlexBox...

4004
来自专栏学海无涯

Android开发之自定义View(一)

Android常见的自定义控件有三种方式: 继承View 继承原有的控件,在原有控件的基础上进行修改 重新拼装组合 今天先来简单说一说第一种也是最复杂的一种~~...

2877

扫码关注云+社区

领取腾讯云代金券