流程
-
解释为什么会出现上一节出现的问题?
-
为什么需要测量?
-
measure机制是怎样的?
-
实现对 view 的正确测量。
上节中的 View 和预期不一样?
上节中我们将mian_layout 布局中的自定义控件的高度设置为 wrap_content ,但是最终呈现的效果并不是我们所预期的那样,为什么呢?
这是因为 Android View 的 measure 机制 造成的。
View 从加载到呈现到屏幕上的过程需要经过三个阶段:
-
measure 机制测量每个 View 的大小,保证 View 的大小显示正常。
-
layout 机制(ViewGroup特有,一般 View 没有)将每个子view放在合适的位置。
-
draw 机制,精准的绘制每个 View 的内容。
上一节的问题就出现在 measure 机制中,当时没有并没有处理 view 的测量。
measure 过程大概
View 的最终的大小不仅仅决定与 View 自身的测量,与其父 View 有着直接的关系。
这里涉及源码分析,暂时没写,只讲大概过程。具体看这里,比较简单可以看
-
父 View 获取子 View 的高宽(就是laytout_width与layout_height中设置的值),然后结合父 View 的测量模式决定子 View 的测量模式与初始的宽高(因为在onMeasure中可以更改)。
三种宽高模式,最终给子view的模式由父view和子View结合得出。
- EXACTLY : 精准模式,给定了 View 指定的大小,对应。
- AT_MOST : 最大值为父View的尺寸,然后尽可能小的测量吱=自身的值。
- UNSPECIFIED :当前View的父View不对View的尺寸作限制。
-
父 View 调用子view的
measure()方法,然后转调用onMeasure()方法执行测量。 -
如果自定View没有重写
onMeasure方法,则会使用基类View的onMeasure()方法处理测量。上一节中我们没有重写
onMeasure方法,而系统就直接使用基类View进行处理了,这里给出 ViewGroup 根据自身测量模式以及子view的高宽决定子view测量模式的源码,明白为什么会出现上节出现的问题。19年6月 更新,重点!!!!不同的 ViewGroup 实现这里有差异,比如可滚动的 ViewGroup 不会去限制子 View 模式为 WRAP_CONTENT 时滚动方向的尺寸(可以参考 RecyclerView)。这里有洋神的问答贴,这个对这个模式做了详细的解答。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { // 父 View 获取自身的测量模式和尺寸 int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; // 父 View 根据自身的测量模式和子view的测量模式决定子view最终的测量模式和尺寸 switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: // 父view是精准模式,对应准确数值和match_parent 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. // 这里,上节我们将自定义view的高度设置为 wrap_content 模式了,父view再这里直接将子view的最终尺寸设置为自身的尺寸。而在自定义view中我们没有处理 onMeasure ,因此自view的最终高度为父view的高度。 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; 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); }下面正式处理 measure 过程。
measure 处理很简单
一般的 View 测量过程其实是一套模板代码,如无特殊测量需求基本代码是一致的,过程也比较简单。
1. 覆写 onMeasure 方法
覆写 onMeasure 方法,不用调用超类的方法,整个测量过程我们要精准控制。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
}
2. 分别处理高度和宽度
高度和宽度的处理逻辑一致,代码细微不同而已。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureWidth(widthMeasureSpec);
}
private int measureWidth(int widthMeasureSpec) {
// 测量宽度
// 获取宽度模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 获取父View建议宽度
int recommendWidth = MeasureSpec.getSize(widthMeasureSpec);
// 最终的宽度
int finalWidth = recommendWidth;
switch (widthMode) {
case MeasureSpec.EXACTLY:
// 已经准确的给了值,直接使用即可
break;
case MeasureSpec.AT_MOST:
// 这里对应 wrap_content,但是父view将尺寸设为了自身对应的尺寸,需要我们自行处理
// 处理逻辑是我们自身设定的最小需要尺寸+对应尺寸的内边距,外边距不用考虑
finalWidth = radius*2 + getPaddingLeft() + getPaddingRight();
break;
case MeasureSpec.UNSPECIFIED:
// 没有限制尺寸,保持父view大小即可
break;
}
return finalWidth;
}
3. 调用设置尺寸函数,完成测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec),measureHieght(heightMeasureSpec));
}
效果如下: