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

自定义View之onMeasure()方法

2017-08-23 14:24 253 查看

前言

一个View从创建到被绘制到屏幕上,需要完成measure(测量)、layout(布置)、draw(绘制)三个步骤,分别对应View中的measure()、layout()、draw()三个方法。网上关于这三个方法的源码解析文章有很多,而且一般情况下也不会去重写它们(measure()方法还无法覆盖),因此本文不打算将其作为重点。本文以及接下来的几篇文章会详细介绍和编程人员关系更大的onMeasure()、onLayout()与onDraw()的具体实现方法,以及过程中会涉及到的一些知识。

MeasureSpec

View中的onMeasure()方法是这样的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...}


可以看到,它的参数是widthMeasureSpec与heightMeasureSpec两个int值。这两个参数实质上是由View的静态内部类MeasureSpec管理的两个特殊的“对象”(并非真正的对象),包含了父view关于子view应当如何测量自身给出的“指示”。为了提升效率,Android系统采用位运算的方式,将模式SpecMode(2位)与尺寸SpecSize(30位)拼接成了一个int值,并传递这个int值作为测量时使用的参数。

MeasureSpec类的实现基本都是依靠位运算,没什么实质性内容。直接上一些结论:

(1)SpecMode分三种:UNSPECIFIED、EXACTLY、AT_MOST。UNSPECIFIED表示不指定具体测量模式,EXACTLY表示父View希望子view的尺寸取精确值(即等于SpecSize),AT_MOST表示父View希望子view的尺寸不超过SpecSize。

(2)使用MeasureSpec.getMode(int measureSpec)与MeasureSpec.getSize(int measureSpec)获取SpecMode与SpecSize。

(3)使用MeasureSpec.makeMeasureSpec(int size,int mode)生成一个measureSpec值。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法

下面回到onMeasure()方法。顾名思义,这个方法是在该view需要测量自身时调用的。具体来说,当这个view的父view对其调用measure()方法时,onMeasure()方法会在过程中被调用。下面分别看看View与ViewGroup分别应当怎么实现这个方法。

View的onMeasure()实现

view只需要根据自身情况,计算出自己的尺寸就可以了。步骤如下:

(1)使用MeasureSpec.getMode()与MeasureSpec.getSize()获取父view要求的SpecMode与SpecSize。

(2)根据上面的参数确定自己的实际尺寸(width与height)。一般来说,如果SpecMode是EXACTLY,那么直接取尺寸值=SpecSize即可。如果SpecMode是AT_MOST,那么就需要根据自身特点计算出一个尺寸值,并保证最终尺寸值不超过SpecSize。当然了,你也可以完全无视父view的要求,自顾自地进行测量,不过这种方式显然是不推荐的。

(3)使用setMeasuredDimension(int measuredWidth, int measuredHeight)设置最终测量尺寸。这个方法被调用之后,view的getMeasuredWidth()方法与getMeasuredHeight()方法才能生效(在之前调用会返回0)。

实际上,对于不那么复杂的自定义view,View类提供的默认实现已经可以满足大部分需求了。下面看看它是怎么做的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}


getSuggestedMinimumWidth()与getSuggestedMinimumHeight()是根据view是否设置了BackgroundDrawable确定一个最小尺寸值。重点看一下getDefaultSize()方法:

public static int getDefaultSize(int size, int measureSpec) {
//size是view根据自身需求提供的一个尺寸值,measureSpec来自父view。
//最终尺寸值
int result = size;
//获取父布局要求的测量模式与测量尺寸
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
//如果模式为UNSPECIFIED则不作限制
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//AT_MOST直接当做EXACTLY处理
case MeasureSpec.AT_MOST:
//如果模式为EXACTLY,则根据父布局要求确定最终尺寸值
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}


解释见代码注释。这里需要记住的是,如果使用这个方法计算尺寸值的话,AT_MOST模式不会生效。AT_MOST一般是在view的layout_width与layout_height为WRAP_CONTENT时使用的。因此,如果想要自定义view支持WRAP_CONTENT属性,就必须自己对AT_MOST的情况作出处理。

ViewGroup的onMeasure()实现

不同于View,ViewGroup需要负责子view的测量。具体来讲,就是为子view提供合适的MeasureSpec,并调用子view的measure()(注意,不是onMeasure())方法。至于自身的尺寸,则需要结合更高一层的父view的指示以及子view的情况来确定。

为了给子view提供合适的MeasureSpec,ViewGroup中提供了一个getChildMeasureSpec()方法,下面看看它的实现:

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
b56f
;
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}


英文注释是自带的,已经很详细了。参数spec是高层view提供的MeasureSpec,padding为需要扣除的padding部分尺寸,childDimension为子view需求的尺寸(一般直接传MarginLayoutParams.width)。实质上,这个方法就是综合考虑了高层view的指示以及低层view的需求,分9种情况构建了一个合适的MeasureSpec。下面的图来自Android View系统解析(下) ,任玉刚


总结

关于onMeasure()方法的实现差不多就这些内容了。可以看出,MeasureSpec是父view与子view沟通的桥梁。实现onMeasure()方法的关键点就在于如何响应父view的MeasureSpec,以及如何为子view构建合适的MeasureSpec。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Android onMeasure