您的位置:首页 > 移动开发 > Android开发

Android关于View的MeasureSpec详解

2018-01-04 00:00 417 查看

前言

在开发过程中,可能会遇到需要获取View的宽高的问题。
但是View在
onMeasure
之前,通过
getHeight()
,
getWidth()
获取到的宽高为0。

其中一种解决方法如下:

int widthMeasureSpec = 0;
int heightMeasureSpec = 0;
view.measure(widthMeasureSpec, heightMeasureSpec);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();

使用这种方式获得的宽高和最终onMeasure->onLayout->onDraw后通过
getHeight()
,
getWidth()
获取到的宽高不一致。
下边介绍一下为什么会出现这个问题。

问题

为什么
view.measure(0, 0);
中的参数是0?


View.measure方法的文档如下:

/**
* <p>
* 为了找到这个view应该多大,可以调用这个方法。父视图提供宽高参数的约束信息。
* </p>
*
* <p>
* view实际的测量工作在{@link #onMeasure(int, int)}中,{@link #onMeasure(int, int)}由当前方法调用。
* 因此只有{@link #onMeasure(int, int)}可以且必须被子类重写。
* </p>
*
*
* @param widthMeasureSpec 由父视图施加的水平方向的要求。
* @param heightMeasureSpec 由父视图施加的垂直方向的要求。
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
....
}

所以,在调用
measure
方法时,传入的参数不应该是
0
, 而应该是一个约束规则MeasureSpec.
这时使用
measure
方法获取宽高和View在绘制后通过
getHeight()
,
getWidth()
获取宽高产生不一致的根本原因。


Int型的MeasureSpec能取那些值呢?

我们首先需要产看一下
MeasureSpec
的文档

MeasureSpec的源码如下(API25)

/**
* Use the old (broken) way of building MeasureSpecs.
* 使用旧的(带破坏性的)方式构建MeasureSpecs.
*/
private static boolean sUseBrokenMakeMeasureSpec = false;

/**
 * 一个MeasureSpec封装了从父视图到子子视图传递布局的要求。
     * 每个MeasureSpec代表宽度或高度的要求。
     * MeasureSpec由一个大小和一个模式组成。有三种可能的模式:
* <dl>
* <dt>UNSPECIFIED</dt>
* <dd>
* 父视图没有对子视图施加任何限制。子视图可以是它想要的任何大小。
* </dd>
*
* <dt>EXACTLY</dt>
* <dd>
     * 父视图已经确定了子视图的确切大小。无论子视图想要多大,子视图将要变成被给予的边界大小。
* </dd>
*
* <dt>AT_MOST</dt>
* <dd>
* 子视图最大可以达到指定的大小。
* </dd>
* </dl>
*
* 为了减少对象的分配(注:对象创建),MeasureSpecs 用int作为实现。
* 这个类提供一个打包和解包size和mode元组变为int的功能。
*/
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;

/**
* 根据Measure模式和大小生成一个MeasureSpec
*

3ff8
* <p><strong>Note:</strong> 在API 17或更低版​​本中,makeMeasureSpec's 的实现是这样的,参数的顺序是不重要的,
* 任何一个溢出的参数都可能影响MeasureSpec的结果。
* {@link android.widget.RelativeLayout} 受此bug影响.
* App的目标API级别大于17将得到固定的,更加严格的行为。</p>
*/
public static int makeMeasureSpec(
@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

/**
* 像 {@link #makeMeasureSpec(int, int)}, 但是任何MeasureSpec的mode是UNSPECIFIED
* 它的getSize将自动为0. 旧的app期待这样
*
* @hide 仅仅为系统控件和旧版app的兼容性内部使用(隐藏api)
*/
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}

/**
* 从MeasureSpec中获取Measure模式
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}

/**
* 从MeasureSpec中获取大小
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += 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();
}
}


根据文档,我们传入的参数MeasureSpec应该使用
MeasureSpec.makeMeasureSpec(int size, int mode)
方法生成,而不是任意的传入int值。一个MeasureSpec有mode和size组成。

在使用
measure
方法时,如何确定MeasureSpec的size和mode来生成一个MeasureSpec呢?

对于手动调用
measure
方法,获取宽高的情况,一般是想知道view展示它的内容是想要多大的宽高。所以mode应该是
UNSPECIFIED
,size应该是0。view想到多大的size由它自己计算。

但是如果父视图的宽是
match_parent
,假设运行时是屏幕宽度,这时候view
measure
时,mode是
UNSPECIFIED
可能就不太合适,mode可能要变为
AT_MOST
,size应该是screenWidth。告诉view它的最大宽度是屏幕宽度。

ps: 像TextView这时候宽度有最大值了,这时它可能会换行,计算出一个准确的高度。如果widthMeasureSpec的mode是
UNSPECIFIED
,TextView无论文本多长,永远是单行,计算出的高度肯定是单行的高度。

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 void main(String[] args) {
int spec = makeMeasureSpec(20, EXACTLY);
int mode = getMode(spec);
int size = getSize(spec);
System.out.println("MODE_MASK:   " + format(MODE_MASK));
System.out.println("UNSPECIFIED: " + format(UNSPECIFIED));
System.out.println("EXACTLY:     " + format(EXACTLY));
System.out.println("AT_MOST:     " + format(AT_MOST));
System.out.println("spec:        " + format(spec));
System.out.println("mode:        " + format(mode));
System.out.println("size:        " + format(size));
}

public static String format(int value) {
String s = String.format("%1$32s", Integer.toBinaryString(value)).replace(" ", "0");
StringBuilder builder = new StringBuilder(s);
int index = 32 - 4;
while (index > 0) {
builder.insert(index, ' ');
index = index - 4;
}
return builder.toString();
}

public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}

public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}

public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

执行Main函数的输出如下:

MODE_MASK:   1100 0000 0000 0000 0000 0000 0000 0000
UNSPECIFIED: 0000 0000 0000 0000 0000 0000 0000 0000
EXACTLY:     0100 0000 0000 0000 0000 0000 0000 0000
AT_MOST:     1000 0000 0000 0000 0000 0000 0000 0000
spec:        0100 0000 0000 0000 0000 0000 0001 0100
mode:        0100 0000 0000 0000 0000 0000 0000 0000
size:        0000 0000 0000 0000 0000 0000 0001 0100

MODE_MASK
的开头两位是11,其余都是0.
UNSPECIFIED
是00
EXACTLY
是01
AT_MOST
是10
mode保存在int的开头两个二进制位中。
size保存在除开头的两个二进制位中

对于一个MeasureSpec,

通过与
MODE_MASK
做与(&)运算获取mode

通过与
MODE_MASK
先做非(~)运算,再做与(&)运算获取size.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息