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的绘制流程是从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位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。MeasureSpec 通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配。为了方便操作,其提供了打包和解包的方法。
SpecMode 有三类,每一类都表示特殊的含义,如下所示。
- UNSPECIFIED 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,我们可以不用关心。
- EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小模式就是SpecSize所指定的值。通常对应于LayoutParams 中的 match_parent 和具体的数值这两种模式。
- AT_MOST 这种情况父容器指定了一个可用大小的SpecSize,View的大小不能超过这个值。通常对应LayoutParams 中的wrap_content这种模式。
MeasureSpec的产生
如前所述,MeasureSpec由两部分决定,分别是View的LayoutParams 和其所在的ViewGroup的ViewGroup 的LayoutParams。 确定了View的MeasureSpec 后即可计算测量相应的宽和高。另外,对于顶级View(DecorView)和普通View来说MeasureSpec生成的过程略有不同,此处只阐述普通View 的MeasureSpec的生成过程。
对于普通的View来说,MeasureSpec 由其所在ViewGroup传递而来,首先看一下ViewGroup的measureChildWithMargins 方法:
/** * 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有关,具体情况我们可以深入到ViewGroup 的getChildMeasureSpec方法,如下所示。
/** * 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);
根据此处代码,我们可以得出一个如下表格,表明View 和ViewGroup 的LayoutParams 是如何共同决定子View的* MeasureSpec*:
由上表可知:
当子View的采用固定宽和高时,不论其ViewGroup 的SpecMode 是什么,View 的尺寸是固定的。
当子View的SpecMode 为match_parent 时,不论ViewGroup 的SpecMode 是什么,View 的尺寸与ViewGroup 相同。
值得注意的是,当View 的LayoutParams 为wrap_content 时,此时虽然其SpecMode 均为AT_MOST, 但是尺寸却全与ViewGroup 一样,所以当我们自定义view时,为了让其能够使用正常wrap_content 属性,我们需要注意对View 的MeasureSpec进行一些处理,以满足我们的预期。
重写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清醒,沿用系统的测量值,而这个默认值得设定并没有固定的依据,应该依据实际情况进行灵活调整。
- View的工作原理(1)--Measure
- View的工作原理:measure、layout、draw
- View的工作原理之measure过程
- View的工作原理之View的measure、layout、draw
- View的工作原理(measure、layout、draw)
- View工作原理(measure、layout、draw)
- View的工作原理(二)--从measure说View的测量流程
- View的工作流程---Measure过程
- Android View工作原理(一)----子View的measure(即子View的尺寸确定)
- Android View的工作流程总结分析(二)-Measure
- View的工作流程-measure、layout、draw三大流程
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View的工作原理
- View 的工作原理
- View的工作原理
- 开源SIFT特征库OpenSIFT: An Open-Source SIFT Library
- CUnit-2.1-3在Linux系统上的安装
- 导入css的三种方式
- 代码时间换空间以及空间换时间
- hdu 5706 GirlCat【暴力DFS】
- View的工作原理(1)--Measure
- 【android】:android系统里面的系统时间获取
- 边界表示的数据结构
- Universal-Image-Loader(android图片缓存)
- rails入门
- 最小费用流模板
- maven学习(3):本地仓库和镜像仓库的修改
- C初级阶段练习题目(二)
- 常用的iOS第三方资源