Android的API为开发者提供了很多好用的widget和control,但在开发中依然会需要自定义一些额外的控件来满足特定的需求。当自定义的视图比较复杂时,就会跟onMeasure(int, int)、onLayout(boolean, int, int, int, int)、onDraw(android.graphics.Canvas)这些方法打交道。要在新创建自定义视图类里正确重载实现这些方法,就需要对这些方法的作用,参数意义有所了解。
本文就来对onMeasure(int, int)方法涉及到的MeasureSpec来做个简单介绍,讲讲它的值的含义,每个状态量都是在什么情况下形成,又该如何使用这些值。
MeasureSpec值的组成
开发和研究Android一个好处在于可以直接查看源码来学习,所以直接来看看MeasureSpec的源码(以下源码全部来自API 22):
[codesyntax lang=”java” lines=”normal”]
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
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;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(0, UNSPECIFIED);
}
int size = getSize(measureSpec) + delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
[/codesyntax]
(代码注释部分已被删除,完成代码可查看此处)
MeasureSpec类只定义了一堆静态的方法,并没有任何的字段,所以创建其对象是没用任何意义的。MeasureSpec真正的数据类型其实只是32位整型(int),这些方法只是辅助整型值来创建和获取具体的值。使用整型则可以规避对象的创建和销毁过程,提高代码效率。MeasureSpec值分两部分:整型最高2位被用来表示其状态(mode),剩下的30位来表示具体的值。(现在常用的显示设备最高的如苹果的5K屏幕,最大尺寸也只是5120,所以即使用16位表示大小也绰绰有余。)
2个比特位可以最多表示4种状态,不过MeasureSpec实际只有三种:UNSPECIFIED, EXACTLY和AT_MOST。在下一章节就来具体介绍每个状态值的都是那种条件下出现。
MesaureSpec状态量的形成
对于MeasureSpec状态量的形成方式,可以从ViewGroup的getChildMeasureSpec(int, int, int)这个静态方法来看出端倪:
[codesyntax lang=”java” lines=”normal”]
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
[/codesyntax]
每个View的MeasureSpec状态量主要受其直接父View的MeasureSpec的状态(spec)和View自身尺寸值(childDimension)影响。但因为View结构是树形的,所以也算间接受到更高级父类的影响。每个View的尺寸也有三种,即在定义Layout的XML时会指定的MATCH_PARENT、WRAP_CONTENT以及常规的非负数值。
对于常规的非负数值来说比较简单,表明View的尺寸已经是一个明确的值,所以其MeasureSpec状态量都是EXACTLY,而不用关心父View的状态量。这种情况可能比较少出现,因为为了更好的适配各种尺寸的屏幕,总是会希望其能按比例缩放。最常用的情况可能就是用来设定一条直线,该直线的宽度被设定为某个值。
如果View的尺寸被设定为MATCH_PARENT,说明它的实际大小是由父View决定,所以父View的状态量是什么值它也就是什么值。这种情况都比较常见,因为很多时候都使View在宽度上占据全屏。
当View的尺寸是WRAP_CONTENT,虽然表示其尺寸将由自己决定,但是父View依然还是会对其附加限制,当父View有一个明确的尺寸,也就是不是UNSPECIFIED时,子View都不应该超过父View的尺寸,所以MeasureSpec状态量就被设定为AT_MOST。而对于UNSPECIFIED常出现在父View是ScrollView这样的上,因为其尺寸可以通过屏幕滚动来达到无限大,所以子View可以随意规定其尺寸。
所有上边代码以及文字描述最后可以总结为下表:
| Parent View | ||||
| EXACTLY | AT_MOST | UNSPECIFIED | ||
| Child View | Non-negative Value | EXACTLY | EXACTLY | EXACTLY |
| MATCH_PARENT | EXACTLY | AT_MOST | UNSPECIFIED | |
| WRAP_CONTENT | AT_MOST | AT_MOST | UNSPECIFIED | |
在得到View的MeasureSpec状态后,将其与尺寸值通过makeMeasureSpec(int, int)方法结合在一起,就是最终传给View的onMeasure(int, int)方法的MeasureSpec值了。
View实际尺寸的获取
onMeasure(int, int)规定在该方法内一定要调用setMeasuredDimension(int, int)来设定measured的View的尺寸值。对于这个值,每个自定义的View其实可以自行决定大小而不顾传入参数信息。有时候对于比较特殊的自定义View也确实会这样做,比如ScrollView就没有使用heightMeasureSpec参数的状态,因为它的状态始终是UNSPECIFIED。
但是为了保持代码和效果的一致性,往往还是遵循基本的惯例。首先MeasureSpec的状态和尺寸应分别通过getMode(int)和getSize(int)方法获得。
当状态是EXACTLY最简单,直接取用尺寸即可。其他两种情况都需要根据自身的布局情况来计算所需的尺寸,有时候还需要先计算子View的尺寸才能判定自身所需,所以onMeasure(int, int)有时候会被往复调用多次才能确定最后结果。如果参数的状态是AT_MOST,还需要跟尺寸参数比较取其中的较小值,对于UNSPECIFIED那计算出来的结果就是最后的值了。
本文只是简单介绍了自定义View时碰到的MeasureSpec比较常规的使用情况,对于具体的设计还需要进行针对性的实现。对于自定义View时牵涉到其他方法,会尽量分享在以后的文章中。