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时牵涉到其他方法,会尽量分享在以后的文章中。