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

来源:互联网 发布:锦衣卫同知是什么官职 编辑:程序博客网 时间:2024/06/05 04:16

在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过程,以满足我们的需求:

@Overridepublic 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清醒,沿用系统的测量值,而这个默认值得设定并没有固定的依据,应该依据实际情况进行灵活调整。

0 0
原创粉丝点击