View的工作原理(1)--Measure

原创 2016年05月30日 17:57:03

在Android中,View扮演着很重要的角色,任何一个App都离不开View控件。Android内置了一整套GUI库,供我们选择。但是在很多应用场景下,我们并不满足于只使用这些控件;原因有二:第一是容易引起界面的同类化,第二是有时候我们需要功能更强大的View控件。解决这些问题的最终途径是自定义View。为了学会自定义View,首先要了解一些有关View工作的基础和原理。本篇博客基于此目的,介绍了View工作的基本机制和View工作的第一流程Measure。

View树结构

Android中的每个控件都会在界面中占得一块矩形的区域,而在Android中,控件大致被分为两类,即ViewGroup控件和View控件。ViewGroup控件作为父控件可以包含多个View控件,并管理其包含的View控件。通过ViewGroup,整个界面上的控件形成了一个树形结构,这也就是我们常说的控件树,上层控件负责下层子控件的测量,布局和绘制,并传递交互事件。通常在Activity中使用的findViewById()方法,就是控件树中以树的深度优先遍历来查找对应元素。
View树结构图

View绘制流程

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上。其流程图如下所示:
这里写图片描述
其中measure过程决定了View的宽/高,measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽和高。layout过程决定了View的四个顶点的坐标和实际的View的宽和高,完成以后,可以通过getTop、getBottom、getLeft和getRight来拿到View的四个顶点的位置。并可以通过getWidth和getHeight方法来拿到View的最终宽和高.Draw过程则决定了View的显示,只有draw方法完成以后View的内容才能呈现在屏幕上。

MeasureSpec

要理解View的测量过程,最关键在于理解MeasureSpec的生成和使用。简单来说MeasureSpec实际是View的LayoutParams和该View所在ViewGroup的LayoutParam共同决定的产物。在View实际测量过程中,只需要对MeasureSpec进行一定的解析,即可获得View测量后的width和height了。
首先看一下MeasureSpec的源码:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(int size, int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

        /**
         * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
         * will automatically get a size of 0. Older apps expect this.
         *
         * @hide internal use only for compatibility with system widgets and older apps
         */
        public static int makeSafeMeasureSpec(int size, int mode) {
            if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                return 0;
            }
            return makeMeasureSpec(size, mode);
        }

        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
   }

MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSizeSpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。MeasureSpec 通过将SpecModeSpecSize打包成一个int值来避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法。

SpecMode 有三类,每一类都表示特殊的含义,如下所示。

  1. UNSPECIFIED 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,我们可以不用关心。
  2. EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小模式就是SpecSize所指定的值。通常对应于LayoutParams 中的 match_parent 和具体的数值这两种模式。
  3. AT_MOST 这种情况父容器指定了一个可用大小的SpecSize,View的大小不能超过这个值。通常对应LayoutParams 中的wrap_content这种模式。

MeasureSpec的产生

如前所述,MeasureSpec由两部分决定,分别是View的LayoutParams 和其所在的ViewGroup的ViewGroupLayoutParams。 确定了View的MeasureSpec 后即可计算测量相应的宽和高。另外,对于顶级View(DecorView)和普通View来说MeasureSpec生成的过程略有不同,此处只阐述普通View 的MeasureSpec的生成过程。
对于普通的View来说,MeasureSpec 由其所在ViewGroup传递而来,首先看一下ViewGroupmeasureChildWithMargins 方法:

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    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);
    }

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,很显然,子元素的MeasureSpec的创建于父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和view的margin以及padding有关,具体情况我们可以深入到ViewGroupgetChildMeasureSpec方法,如下所示。

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @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:
            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;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代码很长,但是逻辑比较简单。即根据父容器的MeasureSpec 同时结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding,具体代码如下所示:

int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);

根据此处代码,我们可以得出一个如下表格,表明ViewViewGroupLayoutParams 是如何共同决定子View的* MeasureSpec*

由上表可知:
当子View的采用固定宽和高时,不论其ViewGroupSpecMode 是什么,View 的尺寸是固定的。
当子View的SpecModematch_parent 时,不论ViewGroupSpecMode 是什么,View 的尺寸与ViewGroup 相同。
值得注意的是,当ViewLayoutParamswrap_content 时,此时虽然其SpecMode 均为AT_MOST, 但是尺寸却全与ViewGroup 一样,所以当我们自定义view时,为了让其能够使用正常wrap_content 属性,我们需要注意对ViewMeasureSpec进行一些处理,以满足我们的预期。

重写onMeasure

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在view的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,View的onMeasure方法如下所示。

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

其中setMeasuredDimension会设置View宽/高的测量值,因此我们只需要看getDefaultSize 这个方法即可:

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

对我们来说,只需要关心后两个case。简单来说,这两种情况,直接返回了MeasureSpec中的SpecSize,由上一节分析可知,当View的LayoutParams 为wrap_content时,最终的size可能并不符合预期,所以此处我们需要改写一下view的onMeasure过程,以满足我们的需求:

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    setMeasuredDimension(measureLength(widthMeasureSpec), measureLength(heightMeasureSpec));
    }

由代码可知,我们在把参数传递给setMeasureDimension 之前进行了一些处理,代码如下:

private int measureLength(int measureSpec){
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        if(specMode == MeasureSpec.EXACTLY){
            result = specSize;
        }else{
            result = 200;
            if(specMode == MeasureSpec.AT_MOST){
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

在上面的代码中,我们只需要给View指定一个默认的内部宽/高,并在wrap_content时设置此宽高即可。对于非wrap_content清醒,沿用系统的测量值,而这个默认值得设定并没有固定的依据,应该依据实际情况进行灵活调整。

相关文章推荐

View工作原理(三)视图大小计算过程(measure过程)

一、android视图measure过程总概 视图大小计算的过程是从根视图measure()方法开始,接着该方法会调用根视图的onMeasure()方法,onMeasure()方法会对所包含的子视图...

View的工作原理之View的measure、layout、draw

View作为Android在视觉上的呈现,在Android的体系中承担着重要的作用。在我们平时的开发中,会用到各种各样的View,在有特别的需求时还要用到自定义View。所以,掌握View的底层工作原...

View的工作原理(二)--从measure说View的测量流程

前言 上篇博客主要介绍了我们了解View工作原理前应该掌握的三个基础概念:ViewRoot,DecorVIew,MeasureSpec。这之后几篇博客主要介绍View的三大流程(Measure ,la...

Android View工作原理(一)----子View的measure(即子View的尺寸确定)

1. 什么是MeasureSpec? android官方文档是这样描述的:A MeasureSpec encapsulates the layout requirements passed from ...
  • yzf0011
  • yzf0011
  • 2017年02月17日 20:16
  • 171

View工作原理PPT

  • 2016年12月01日 23:28
  • 82KB
  • 下载

View工作原理

  • 2016年12月01日 23:22
  • 25.24MB
  • 下载

Android艺术开发探索第四章——View的工作原理(上)

Android艺术开发探索第四章——View的工作原理(上) 这章就比较好玩了,主要介绍一下View的工作原理,还有自定义View的实现方法,在Android中,View是一个很重要的角色,...

Android View的工作原理详解

  • 2016年10月24日 15:19
  • 503KB
  • 下载

View工作原理【触摸消息派发】

原文:《Android内核剖析》读书笔记 第13章 View工作原理【触摸消息派发】
  • ymangu
  • ymangu
  • 2014年09月11日 09:55
  • 436
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:View的工作原理(1)--Measure
举报原因:
原因补充:

(最多只允许输入30个字)