重温View测量之MeasureSpec
需求
这个是手机QQ吃喝玩乐里面的,选择城市界面。就是一个ListView里面嵌套着不同规格的GridView/ListView,在比如电商里面的,物品分类界面,这种需求很常见,当然解决的办法也有很多。下面根据自己的工作经验介绍种很常用的方法。
理解MeasureSpec
以前刚刚接触android的时候,就感觉这还不简单,直接,在ListView的适配器中,根据gridview的行数乘以gridView的每个item,然后去设置gridView的LayoutParams,这样做不是不行,而是太浪费性能了,我们知道,在Adapter的getView方法里面不能做太多的工作,要做工作也要异步去完成,比如图片的加载。否则会出现卡顿,我们这里第一步要计算,第二部要去设置LayoutParams,在重新绘制,GridView,然而我们之前已经绘制GridView过一次了。可想而知,这不是一种很好的方法。
在介绍常规的方法之前,我们先复习下MeasureSpec,这是个很重要的知识点。因为android的View在测量的时候离不开他,也可以说,是根据他来测量View的大小的。MeasureSpec是一个32位的int值,最高的两位代表了SpecMode(测量模式),后面的低30位代表了SpecSize(规格大小)。SpecMode有三个值,分别是UNSPECIFIED、EXACTLY、AT_MOST。
/**
* Figures out the measure spec for the root view in a window based on it's
* layout params.
*
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
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;
}
这是从ViewRootImpl(顶级View)中截取的代码段,根据布局的参数生成了我们所需要的MeasureSpec。如果我们的顶级View设置成了match_parent,那么我们只能给当前窗口的大小了,如果不是match_parent和wrap_content的话,我们只能给他所需要的大小了,这些都属于精确测量,比如50dp的大小。而如果顶级View是wrap_content模式的话,我们只能用最大模式At_most了,告诉下层的View,最大的空间也只有windowsize这么大了,你看着搞把。这样,我们在绘制子View的时候就会根据父View的MeasureSpec和自身的LayoutParams来测量和绘制自身了。
/**
* 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);
}
这是ViewGroup在绘制子View时,获取子View的MeasureSpec的代码,可以看到,从父View得到的不同的测量模式分别为精确测量,最大化测量和不准确测量,都有不同的结果。
实现
按照上面的源码分析,我们可以尝试把我之前的工作简单化处理,像计算和测量都让他绘制的时候自己去处理,这样效率会好很多。那么我们重新继承GridView,重写它的onMeasure方法:
public class MyGridView extends GridView{
public MyGridView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
每次测量高度的时候,将GridView的高度改成AT_MOST模式测量,那么根据上面的分析,如果parent是AT_MOST,而子控件的高度是准确的话,那么子控件的规格就是 SpecMode :EXACTLY,SpecSize:childSize。
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;
这样我们就完美的解决了我们的需求,在也不用自己测量,然后重新绘制了…
代码
仿QQ吃喝玩乐选择城市列表