Android控件测量-MesaureSpec详解
2021-02-27 本文已影响0人
Timmy_zzh
1.View的测量
- 如果我们想要在界面上画一个图形,那么必须要先知道控件的大小和位置。所以系统在绘制View之前,必须要先对View进行测量,这样才能知道要绘制一个多大的View。这个过程在onMeasure()方法中进行。
MeasureSpec
-
MeasureSpec类是系统提供的,该类封装了View测量使用到的数据,包括测量模式和测量大小
- 他使用一个32位的int值,其中高2位表示测量的模式,低30位表示测量的大小。
-
测量模式分为3种
- EXACTLY:表示在xml布局文件中宽高使用match_parent或者固定大小的宽高
- AT_MOST:表示在xml布局文件中宽高使用wrap_content
- UNSPECIFIED:父容器没有对当前View有任何限制,当前View可以取人意尺寸,比如ListView中的item
-
View类默认的onMeasure方法只支持EXACTLY模式,所以如果在自定义控件的时候不重写onMeasure方法的话,就只能使用EXACTLY模式。
- 控件可以响应xml布局中设置具体的宽高值或者match_parent属性。
- 而如果要让自定义View支持wrap_content属性,就必须要重写onMeasure方法来指定wrap_content的大小
-
View的默认onMeasure方法
public class View implements ... {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
}
2.ViewGroup的测量
- ViewGroup需要管理其子View,包括子View的显示大小。
- 当ViewGroup的大小设置为match_parent或固定dp值时,可以通过具体的指定值来设置ViewGroup自身的大小
- 当ViewGroup的大小为wrap_content,ViewGroup就需要对子View进行遍历,从而来决定自己的大小。
- ViewGroup在测量时会通过遍历所有子View,从而调用子View的measure方法来获得每个子view的测量结果,View的测量逻辑就是在这里进行的。
ViewGroup的onMeasure
- 在ViewGroup测量方法中需要对子view进行测量,可以通过如下方法实现:
- measureChild
- measureChildren
- measureChildWithMargins
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
- 其中measureChildren接着会调用measureChild方法,最终都会调用子view的measure方法,并传入参数childWidthMeasureSpec和childHeightMeasureSpec,那这两个值是如何得到的呢?
- 其是通过父布局的MeasureSpec和自身的布局参数LayoutParams共同决定,在ViewGroup的getChildMeasureSpec方法中实现并返回。
其中LayoutParams源码如下:
public static class LayoutParams {
@Deprecated
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public int width;
public int height;
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
}
- 解析:所以LayoutParams的宽高值有两种来源
- 通过new LayoutParams() 设置
- 通过xml不仅设置
- 且取值只有三中情况:
- -2(WRAP_CONTENT)
- -1 (MATCH_PARENT)
- 固定大小相素值
getChildMeasureSpec
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
//参数一:spec 为父控件的MeasureSpec
//参数二:padding 父空间已消耗的大小
//参数三:childDimension 子View的LayoutParams中的宽高值
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父控件的测量模式和大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//size为留给子view剩余的最大空间
int size = Math.max(0, specSize - padding);
//最终需要计算确定的子view的大小和模式,并组合成MeasureSpec返回
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父控件测量模式是 EXACTLY,表示父控件大小为固定值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//表示子view的大小设置为固定值--》则传递给子类的大小为固定值和EXACTLY模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 表示子view大小设置为MATCH_PARENT,子view使用父控件剩余的可用空间
//--》传递给子view的大小为固定值就是父控件的剩余可用空间和EXACTLY模式
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 表示子view的大小设置为WRAP_CONTENT包裹类型,
// --》传递给子view的大小为父控件的剩余可用空间和AT_MOST模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父控件测量模式是 AT_MOST,表示父控件大小为包裹大小
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//表示子view的大小设置为固定值--》则传递给子类的大小为子view的固定值和EXACTLY模式
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 表示子view大小设置为MATCH_PARENT,子view想使用父控件剩余的可用空间,
// 但是父控件大小不固定,所以子控件也是固定不了,模式也转换成AT_MOST包裹类型
//--》传递给子view的大小为固定值就是父控件的剩余可用空间和 AT_MOST 模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 父控件和子控件都使用WRAP_CONTENT 包裹模式
// 传递给子view的大小为父控件的剩余可用空间和 AT_MOST 模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父view为UNSPECIFIED,所以MATCH_PARENT的情况下,子类大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以 WRAP_CONTENT 的情况下,子类大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//组装
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}
- 归纳整理如下:
子view布局参数(LayoutParams)\ 父控件测量模式 | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体数值(dp/px) | EXACTLY + childSize | EXACTLY + childSize | EXACTLY + childSize |
match_parent | EXACTLY + parentSize(父容器剩余空间) | AT_MOST + parentSize(大小不能超过父容器的剩余空间) | UNSPECIFIED + 0 |
wrap_content | AT_MOST + parentSize(大小不能超过父容器的剩余空间) | AT_MOST + parentSize(大小不能超过父容器的剩余空间) | UNSPECIFIED + 0 |
规律总结
-
当子view采用具体数值(dp/px)时,子View的MeasureSpec的值为:
- 测量模式 = EXACTLY
- 测量大小 = 子view自身设置的具体大小值
-
当子view采用match_parent时,子View的MeasureSpec的值为:
- 测量模式 = 父容器的测量模式
- 测量大小:
- 如果父容器的测量模式为EXACTLY,那么测量大小 = 父容器的剩余空间
- 如果父容器的测量模式为AT_MOST,那么测量大小 = 不超过父容器的剩余空间
-
当子view采用wrap_contentt时,子View的MeasureSpec的值为:
- 测量模式 = AT_MOST
- 测量大小:不超过父容器的剩余空间