Android中View框架的工做机制中,主要有三个过程:html
一、View树的测量(measure)Android View框架的measure机制
java
二、View树的布局(layout) Android View框架的layout机制
数据结构
三、View树的绘制(draw)Android View框架的draw机制
框架
View框架的工做流程为:测量每一个View大小(measure)-->把每一个View放置到相应的位置(layout)-->绘制每一个View(draw)。ide
本文主要讲述三大流程中的measure过程。布局
开发人员在绘制UI的时候,基本都是经过XML布局文件的方式来配置UI,而每一个View必需要设置的两个群属性就是layout_width和layout_height,这两个属性表明着当前View的尺寸。spa
官方文档截图:.net
因此这两个属性的值是必需要指定的,这两个属性的取值只能为三种类型:3d
一、固定的大小,好比100dp。code
二、恰好包裹其中的内容,wrap_content。
三、想要和父布局同样大,match_parent / fill_parent。
因为Android但愿提供一个更优雅的GUI框架,因此提供了自适应的尺寸,也就是 wrap_content 和 match_parent 。
试想一下,那若是这些属性只容许设置固定的大小,那么每一个View的尺寸在绘制的时候就已经肯定了,因此可能都不须要measure过程。可是因为须要知足自适应尺寸的机制,因此须要一个measure过程。
因为上面提到的自适应尺寸的机制,因此在用自适应尺寸来定义View大小的时候,View的真实尺寸还不能肯定。可是View尺寸最终须要映射到屏幕上的像素大小,因此measure过程就是干这件事,把各类尺寸值,通过计算,获得具体的像素值。measure过程会遍历整棵View树,而后依次测量每一个View真实的尺寸。具体是每一个ViewGroup会向它内部的每一个子View发送measure命令,而后由具体子View的onMeasure()来测量本身的尺寸。最后测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的数据单位是像素。
系统在遍历完布局文件后,针对布局文件,在内存中生成对应的View树结构,这个时候,整棵View树种的全部View对象,都尚未具体的尺寸,由于measure过程最终是要肯定每一个View打的准确尺寸,也就是准确的像素值。可是刚开始的时候,View中layout_width和layout_height两个属性的值,都只是自适应的尺寸,也就是match_parent和wrap_content,这两个值在系统中为负数,因此系统不会把它们当成具体的尺寸值。因此当一个View须要把它内部的match_parent或者wrap_content转换成具体的像素值的时候,他须要知道两个信息。
一、针对于match_parent,父布局当前具体像素值是多少,由于match_parent就是子View想要和父布局同样大。
二、针对wrap_content,子View须要根据当前本身内部的content,算出一个合理的能包裹全部内容的最小值。可是若是这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不该该超过这个尺寸。
因为树这种数据结构的特殊性,咱们在研究measure的过程时,能够只研究一个ViewGroup和2个View的简单场景。大概示意图以下:
也就是说,在measure过程当中,ViewGroup会根据本身当前的情况,结合子View的尺寸数据,进行一个综合评定,而后把相关信息告诉子View,而后子View在onMeasure本身的时候,一边须要考虑到本身的content大小,一边还要考虑的父布局的限制信息,而后综合评定,测量出一个最优的结果。
谈到传递限制信息,那就是MeasureSpec类了,该类贯穿于整个measure过程,用来传递父布局对子View尺寸测量的约束信息。简单来讲,该类就保存两类数据。
一、子View当前所在父布局的具体尺寸。
二、父布局对子View的限制类型。
那么限制类型又分为三种类型:
一、UNSPECIFIED,不限定。意思就是,子View想要多大,我就能够给你多大,你放心大胆的measure吧,不用管其余的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
二、EXACTLY,精确的。意思就是,根据我当前的情况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在MeasureSpec的尺寸属性中,本身去查看吧,你也不要管你的content有多大了,就用这个尺寸吧。
三、AT_MOST,最多的。意思就是,根据我当前的状况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个刚好能包裹你内容的尺寸就能够了。
在View的源代码中,提取到了下面一些关于measure过程的信息。
咱们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,因此它是一个ViewGroup,因此整棵View树的测量是从一个ViewGroup对象的measure方法开始的。
/** 开始测量一个View有多大,parent会在参数中提供约束信息,实际的测量工做是在onMeasure()中进行的,该方法会调用onMeasure()方法,因此只有onMeasure能被也必需要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);
父布局会在本身的onMeasure方法中,调用child.measure ,这就把measure过程转移到了子View中。
/** 具体测量过程,测量view和它的内容,来决定测量的宽高(mMeasuredWidth mMeasuredHeight )。该方法中必需要调用setMeasuredDimension(int, int)来保存该view测量的宽高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
子View会在该方法中,根据父布局给出的限制信息,和本身的content大小,来合理的测量本身的尺寸。
/** 保存测量结果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);
当View测量结束后,把测量结果保存起来,具体保存在mMeasuredWidth和mMeasuredHeight中。
/** 让全部子view测量本身的尺寸,须要考虑当前ViewGroup的MeasureSpec和Padding。跳过状态为gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
测量全部的子View尺寸,把measure过程交到子View内部。
/** 测量单个View,须要考虑当前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();
对每个具体的子View进行测量。
/** 测量单个View,须要考虑当前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();
对每个具体的子View进行测量。可是须要考虑到margin等信息。
/** measureChildren过程当中最困难的一部分,为child计算MeasureSpec。该方法为每一个child的每一个维度(宽、高)计算正确的MeasureSpec。目标就是把当前viewgroup的MeasureSpec和child的LayoutParams结合起来,生成最合理的结果。
好比,当前ViewGroup知道本身的准确大小,由于MeasureSpec的mode为EXACTLY,而child但愿可以match_parent,这时就会为child生成一个mode为EXACTLY,大小为ViewGroup大小的MeasureSpec。
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension);
根据当前自身的情况,以及特定子View的尺寸参数,为特定子View计算一个合理的限制信息。
源代码:
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); }
伪代码:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 获取限制信息中的尺寸和模式。 switch (限制信息中的模式) { case 当前容器的父容器,给当前容器设置了一个精确的尺寸: if (子View申请固定的尺寸) { 你就用你本身申请的尺寸值就好了; } else if (子View但愿和父容器同样大) { 你就用父容器的尺寸值就好了; } else if (子View但愿包裹内容) { 你最大尺寸值为父容器的尺寸值,可是你仍是要尽量小的测量本身的尺寸,包裹你的内容就足够了; } break; case 当前容器的父容器,给当前容器设置了一个最大尺寸: if (子View申请固定的尺寸) { 你就用你本身申请的尺寸值就好了; } else if (子View但愿和父容器同样大) { 你最大尺寸值为父容器的尺寸值,可是你仍是要尽量小的测量本身的尺寸,包裹你的内容就足够了; } else if (子View但愿包裹内容) { 你最大尺寸值为父容器的尺寸值,可是你仍是要尽量小的测量本身的尺寸,包裹你的内容就足够了; } break; case 当前容器的父容器,对当前容器的尺寸不限制: if (子View申请固定的尺寸) { 你就用你本身申请的尺寸值就好了; } else if (子View但愿和父容器同样大) { 父容器对子View尺寸不作限制。 } else if (子View但愿包裹内容) { 父容器对子View尺寸不作限制。 } break; } return 对子View尺寸的限制信息; }
当自定义View的时候,也须要处理measure过程,主要有两种状况。
一、继承自View的子类。
须要覆写onMeasure来正确测量本身。最后都须要调用setMeasuredDimension来保存测量结果
通常来讲,自定义View的measure过程伪代码为:
int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); int viewSize = 0; swith (mode) { case MeasureSpec.EXACTLY: viewSize = size; //当前View尺寸设置为父布局尺寸 break; case MeasureSpec.AT_MOST: viewSize = Math.min(size, getContentSize()); //当前View尺寸为内容尺寸和父布局尺寸当中的最小值 break; case MeasureSpec.UNSPECIFIED: viewSize = getContentSize(); //内容有多大,就设置尺寸为多大 break; default: break; } setMeasuredDimension(viewSize);
二、继承自ViewGroup的子类。
不但须要覆写onMeasure来正确测量本身,可能还要覆写一系列measureChild方法,来正确的测量子view,好比ScrollView。或者干脆放弃父类实现的measureChild规则,本身从新实现一套测量子view的规则,好比RelativeLayout。最后都须要调用setMeasuredDimension来保存测量结果。
通常来讲,自定义ViewGroup的measure过程的伪代码为:
//ViewGroup开始测量本身的尺寸 viewGroup.onMeasure(); //ViewGroup为每一个child计算测量限制信息(MeasureSpec) viewGroup.getChildMeasureSpec(); //把上一步生成的限制信息,传递给每一个子View,而后子View开始measure本身的尺寸 child.measure(); //子View测量完成后,ViewGroup就能够获取每一个子View测量后的尺寸 child.getChildMeasuredSize(); //ViewGroup根据本身自身情况,好比Padding等,计算本身的尺寸 viewGroup.calculateSelfSize(); //ViewGroup保存本身的尺寸 viewGroupsetMeasuredDimension();
不少开发人员都遇到过这种需求,就是ScrollView内部嵌套ListView,而该ListView数据条数是不肯定的,因此须要设置为包裹内容,而后就会发现ListView就会显示第一行出来。而后就会百度到一条解决方案,继承ListView,覆写onMeasure方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); }
问题是解决了,可是不少开发人员并不知道为何。
下面会从ScrollView和ListView的measure过程来分析一下。
备注:截取部分问题相关代码,并非完整代码。
看看ListView的onMeasure:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final View child = obtainView(0, mIsScrap); childHeight = child.getMeasuredHeight(); if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; } }
当MeasureSpec mode为UNSPECIFIED的时候,只测量第一个item打的高度,跟问题描述相符,因此咱们猜想多是由于ScrollView传递了一个UNSPECIFIED限制给ListView。
再来看ScrollView的onMeasure代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
调用了父类的onMeasure:
看看FrameLayout的onMeasure:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); } } }
调用了measureChildWithMargins,可是由于ScrollView覆写了该方法,因此看看ScrollView的measureChildWithMargins方法:
@Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
果真,它向ListView的onMeasure传递了一个UNSPECIFIED的限制。
为何呢,想一想,由于ScrollView,原本就是能够在竖直方向滚动的布局,因此,它对它全部的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,由于我原本就是须要竖直滑动的,它的本意就是如此,因此它对子View高度不作任何限制。
看看ListView的onMeasure:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final View child = obtainView(0, mIsScrap); childHeight = child.getMeasuredHeight(); if (heightMode == MeasureSpec.UNSPECIFIED) { heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2; if (heightMode == MeasureSpec.AT_MOST) { // TODO: after first layout we should maybe start at the first visible position, not 0 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1); } setMeasuredDimension(widthSize, heightSize); mWidthMeasureSpec = widthMeasureSpec; } }
只要让heightMode == MeasureSpec.AT_MOST,它就会测量它的完整高度,因此第一个数据,限制mode的值就肯定下来了。第二个数据就是尺寸上限,若是给个200,那么当ListView数据过多的时候,该ListView最大高度就是200了,仍是不能彻底显示内容,怎么办?那么就给个最大值吧,最大值是多少呢,Integer.MAX_VALUE?
先看一下MeasureSpec的代码说明:
private static final int MODE_SHIFT = 30; public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT;
他用最高两位存储mode,用其余剩余未存储size。因此Integer.MAX_VALUE >> 2,就是限制信息所能携带的最大尺寸数据。因此最后就须要用这两个值作成一个限制信息,传递给ListView的height维度。
也就是以下代码:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); }
下面咱们本身写一个自定义的ViewGroup,让它内部的每个子View都垂直排布,而且让每个子View的左边界都距离上一个子View的左边界必定的距离。而且支持wrap_content。大概看起来以下图所示:
实际运行效果以下图所示:
代码以下:
public class VerticalOffsetLayout extends ViewGroup { private static final int OFFSET = 100; public VerticalOffsetLayout(Context context) { super(context); } public VerticalOffsetLayout(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = 0; int height = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); ViewGroup.LayoutParams lp = child.getLayoutParams(); int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width); int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height); child.measure(childWidthSpec, childHeightSpec); } switch (widthMode) { case MeasureSpec.EXACTLY: width = widthSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); int widthAddOffset = i * OFFSET + child.getMeasuredWidth(); width = Math.max(width, widthAddOffset); } break; default: break; } switch (heightMode) { case MeasureSpec.EXACTLY: height = heightSize; break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: for (int i = 0; i < childCount; i++) { View child = getChildAt(i); height = height + child.getMeasuredHeight(); } break; default: break; } setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int left = 0; int right = 0; int top = 0; int bottom = 0; int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); left = i * OFFSET; right = left + child.getMeasuredWidth(); bottom = top + child.getMeasuredHeight(); child.layout(left, top, right, bottom); top += child.getMeasuredHeight(); } } }