今天继续学习 Android 自定义组件,参考文档:docs/guide/topics/ui/custom-components.html

其中有两个对界面布局影响很大的方法:onDraw() 和 onMeasure()。
onDraw() 相对比较容易理解,而 onMeasure() 往往更难一些,也更复杂。引用文档中的原话就是:
onMeasure() is a little more involved. 其实还有一个原因,就是我当时对 measure 这个单词理解得不够准确,后来查了词典才放心,确实就是“测量”的意思。要实现 onMeasure() 方法,基本需要处理好以下三个方面(最终结果就是:通过代码计算出测量值,再调用 View 的相关方法进行设置,从而告诉为你的 View 安排位置和大小的父容器,你到底需要多大的显示空间):
1. 传递进来的参数 widthMeasureSpec 和 heightMeasureSpec,是你最终测量结果必须参考的限制条件。
The overidden onMeasure() method is called with width and height measure specifications(widthMeasureSpec and heightMeasureSpec parameters,both are integer codes representing dimensions) which should be treated as requirements for the restrictions on the width and height measurements you should produce.2. 你在 onMeasure 中计算并设置的 width 和 height,会直接用于组件渲染,因此应尽量保持在传递进来的宽高约束范围之内。
虽然你也可以选择让设置的尺寸超过传递进来的限制,但这样一来父容器就有多种处理方式,比如 clipping(剪裁)、scrolling(滚动)、抛出异常,或者再次调用 onMeasure() 方法(也许会传入新的声明参数)。
3. 一旦 width 和 height 计算完成,就必须调用 View.setMeasuredDimension(int width,int height) 方法,否则会导致抛出异常。 Once the width and height are calculated,the setMeasureDimension(int width,int height) method must be called with the calculated measurements.Failure to do this will result in an exceptiion being thrown
在 Android 提供的一个自定义 View 示例中(API Demos 里的 view/LabelView),可以看到一个重写 onMeasure() 方法的
实例,整体上也比较容易理解。
/**
* @see android.view.View#measure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
/**
* Determines the width of this view
* @param measureSpec A measureSpec packed into an int
* @return The width of the view, honoring constraints from measureSpec
*/
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
+ getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
} 下面直接看 measureWidth()。
首先看到的是参数,它代表宽度或高度对应的 MeasureSpec。
在 Android 2.2 文档中,对 MeasureSpec 的说明是:
一个 MeasureSpec 封装了从父容器传递给子容器的布局要求。
每一个 MeasureSpec 都表示宽度或者高度方面的测量说明。
一个 MeasureSpec 本质上是“大小 size + 模式 mode”的组合值,一共包含三种模式。
A MeasureSpec encapsulates the layout requirements passed from parent to child Each MeasureSpec represents a requirement for either the width or the height.A MeasureSpec is compsized of a size and a mode.There are three possible modes:(1)UPSPECIFIED :父容器对子容器没有任何限制,子容器想要多大就可以多大。 UNSPECIFIED The parent has not imposed any constraint on the child.It can be whatever size it wants
(2) EXACTLY
父容器已经为子容器确定了尺寸,子容器应当遵守这些边界,不管它自己希望占用多大的空间。
EXACTLY The parent has determined and exact size for the child.The child is going to be given those bounds regardless of how big it wants to be.(3) AT_MOST
子容器可以在声明大小之内,取任意不超过该限制的尺寸。
AT_MOST The child can be as large as it wants up to the specified sizeMeasureSpec 是 View 类下的一个静态公开类。MeasureSpec 之所以使用一个 int 值来表示,是为了减少对象分配带来的开销。此类用于
把 size 和 mode 打包,或者从一个 int 中解包出来。
MeasureSpecs are implemented as ints to reduce object allocation.This class is provided to pack and unpack the size,mode tuple into the int我当时比较好奇的是:到底如何把两个值打包到一个 int 中,又是怎样再把它们解包出来的。
MeasureSpec 类的代码如下(注释已经被我删除,因为上面已经说明过了):
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) {
return size + mode;
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
} } 我还特地把它们的十进制值打印了出来:
mode_shift=30,mode_mask=-1073741824,UNSPECIFIED=0,EXACTLY=1073741824,AT_MOST=-2147483648
接着又觉得有必要把它们的二进制值也打印出来,如下:
mode_shift=11110, // 30
mode_mask=11000000000000000000000000000000,
UNSPECIFIED=0,
EXACTLY=1000000000000000000000000000000,
AT_MOST=10000000000000000000000000000000
MODE_MASK = 0x3 << MODE_SHIFT //也就是说MODE_MASK是由11左移30位得到的.因为Java用补码表示数值,最后得到的值最高位是1,所以它就是负数了 对于上面的数值,我们可以这样理解:不要把 0x3 单纯看成十进制的 3,而要把它看成二进制的 11, 而把 MODE_SHIFT 看成 30。那么为什么这里是二进制的 11 呢?
因为这里只有三种模式,所以只需要两位二进制就可以表示;如果有四种模式,那么也仍然需要两位,而不是三位,这里重点是两位已经能覆盖足够的组合。
我们这样来看:
UNSPECIFIED=00000000000000000000000000000000,
EXACTLY=01000000000000000000000000000000,
AT_MOST=10000000000000000000000000000000
也就是说,0、1、2
分别对应 00、01、10
当和 11 做按位与运算时,00 &11 仍然得到 00,11&01 得到 01,10&
写到这里,相信看到这里的朋友,对 Android 布局测量和 onMeasure 的基本思路也已经能理解了。
return (measureSpec & ~MODE_MASK); 应该理解为 return (measureSpec & (~MODE_MASK));
