在文章自定义控件之测量一文中,已经很清楚描述了view,viewgroup的测量过程。本篇文章是基于自定义控件之测量的一个小小的使用,不会再详细介绍源码所有流程,直接上源码,分析在Activity中调用measure方法测量宽高的原理。那目的是什么呢?就是为了在activity的onCreate方法里获取控件的宽高值。
在onCreate()中获取控件宽高,调用view.getMeasuredWidth(),但是这个方法返回宽度值是有条件的。在测量TextView过程中,调用setMeasuredDimession(w,h)完成测量后,getMeasuredWidth()才有值。可以选择处理方式之一:先手动测量view,调用view.measure(w,h),才能正确获取宽度值。
那么参数w,h应该传入些什么呢,先看下面这个例子:
activity_main.xml
MainActivity.java
public class MainActivity extends Activity {
private TextView tv;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
//情况1 --------------------------
int width = tv.getMeasuredWidth();
Log.e(TAG, "width(in method onCreate):" + width);
//情况2 --------------------------------
int lpWidth = tv.getLayoutParams().width; //获取textview的布局参数对象LayoutParams,并获取宽度值width
int lpHeight = tv.getLayoutParams().height;
int widthSpecSize = 0; //定义textview宽度测量值的大小
int widthSpecMode = 0; //定义textview宽度测量值的模式
int heightSpecSize = 0;
int heightSpecMode = 0;
//LayoutParams的宽度值width,分3种情况考虑
if (lpWidth == LayoutParams.WRAP_CONTENT && lpHeight == LayoutParams.WRAP_CONTENT) {
//值为wrap_content(-2)时
widthSpecSize = Integer.MAX_VALUE;
widthSpecMode = MeasureSpec.AT_MOST;
heightSpecSize = Integer.MAX_VALUE;
heightSpecMode = MeasureSpec.AT_MOST;
Log.e(TAG, "宽高布局参数为wrap_content时,测量开始啦...");
} else if (lpWidth == LayoutParams.MATCH_PARENT && lpHeight == LayoutParams.MATCH_PARENT) {
//值为match_parent(-1)时
Log.e(TAG, "宽高布局参数为match_parent时,需要知道父容器给的剩余空间,才可以得到TextView的MeasureSpec");
} else if (lpWidth > 0 && lpHeight > 0){
//值为具体dp值时
widthSpecSize = lpWidth;
widthSpecMode = MeasureSpec.EXACTLY;
heightSpecSize = lpHeight;
heightSpecMode = MeasureSpec.EXACTLY;
Log.e(TAG, "宽高布局参数为具体数值时,测量开始啦...");
}
//合成size and mode
int tv_widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSpecSize, widthSpecMode);
int tv_heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSpecSize, heightSpecMode);
//传入measureSpec,开始测量
tv.measure(tv_widthMeasureSpec, tv_heightMeasureSpec);
//获取测量后的宽度值
int measuredWidth = tv.getMeasuredWidth();
Log.e(TAG, "measuredWidth(in method onCreate):" + measuredWidth);
}
}
打印log如下:
从log可以看出:如果view还没有测量完成,宽度值为0;测量之后,宽度值为56px。
我们只分析宽度,因此在代码中并没有列出所有情况,只研究宽度,高度是类似的。接下来,从源码角度解析measure方法中参数MeasureSpec的取值问题。目前我们能知道的就是textview的布局参数:android:layout_width="wrap_content",因此根据布局宽度,分3种情况分析MeasureSpec的取值。
查看ViewGroup中getChildMeasureSpec方法源码,分析父容器是如何给textview设置MeasureSpec的值,如下:
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面有3*3=9中情况,参数spec:父容器的MeasureSpec;参数padding:textview的margin值+父容器的padding+父容器已经被使用的宽度值;参数childDimession:textview布局参数中宽度值。代码中,变量size是父容器剩余的宽度空间,当textview的childDimession有精确的dp值时,直接使用这个dp值作为测量大小resultSize;当为wrap_content,match_content时,使用size作为测量大小resultSize。
分析可知:
1,当childDimession >= 0,测量大小resultSize为childDimession,测量模式为EXACTLY;
2,当childDimession=wrap_content时,测量大小resultSize为父容器剩余宽度空间size,测量模式AT_MOST;(无需讨论specMode为UNSPECIFIED情况)
3,当childDimenssion=match_parent时,测量大小resultSize为父容器剩余宽度空间size,测量模式:specMode为EXACTLY时,为EXACTLY;specMode为AT_MOST时,为AT_MOST;
结论:那么调用textview.measure(w,h)时,
1,若布局参数为具体dp值,即childDimession >= 0,textview的measureSpec=childDimession+EXACTLY;
2,若布局参数为wrap_content,即childDimession=wrap_content时,textview的measureSpec=size+AT_MOST;
3,若布局参数为match_parent,即childDimenssion=match_parent时,textview的measureSpec=size+(EXACTLY或AT_MOST);
继续分析上面结论,第1种情况,我们可以计算出textview的MeasureSpec,调用textview.measure(w,h)完成测量;第2,3种情况需要知道size,也就是父容器留给textview宽度的剩余空间,不知道怎么办呢,于是就看看textview测量时如何使用measureSpec的,查看textview的onMeasure(w,h)源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
BoringLayout.Metrics boring = UNKNOWN_BORING;
BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
int des = -1;
boolean fromexisting = false;
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
des = desired(mLayout);
}
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
if (boring == null || boring == UNKNOWN_BORING) {
if (des < 0) {
des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
}
width = des;
} else {
width = boring.width;
}
final Drawables dr = mDrawables;
if (dr != null) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
if (mHint != null) {
int hintDes = -1;
int hintWidth;
if (mHintLayout != null && mEllipsize == null) {
hintDes = desired(mHintLayout);
}
if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) FloatMath.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
}
hintWidth = hintDes;
} else {
hintWidth = hintBoring.width;
}
if (hintWidth > width) {
width = hintWidth;
}
}
width += getCompoundPaddingLeft() + getCompoundPaddingRight();
if (mMaxWidthMode == EMS) {
width = Math.min(width, mMaxWidth * getLineHeight());
} else {
width = Math.min(width, mMaxWidth);
}
if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * getLineHeight());
} else {
width = Math.max(width, mMinWidth);
}
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
//... code
setMeasuredDimension(width, height);
}
分析可知:
1,当widthMode为EXACTLY时,width为widthSize;而widthSize就是前面提到的,父控件留给textview剩余宽度空间大小size。这个width要作为setMeasuredDimession(width,height)的参数,完成textview的测量工作。
2,当widthMode为AT_MOST时,只在执行if (widthMode == MeasureSpec.AT_MOST) {width = Math.min(widthSize, width);}中用到了widthSize这个变量,其他代码计算width值并没有用到widthSize。可以看出width最终的值与widthSize没有关系,只是用来在计算出width之后,取width和widthSize中较小的值。于是,我们可以将widthSize->前面的size->最终的测量大小,设置为Integer.MAX_VALUE,完成textview的MeasureSpec构造,测量textview理论上是合理的。这里解释了前面demo中当布局参数为wrap_content时,如此构造textview的MeasureSpec的原因。
显然,当布局参数为match_parent时,一旦textview的测量模式为EXACTLY,就需要知道size的值,不知道就测量不了textview,而确实没法搞到这个值。
结论:在调用view.measure(w,h)测量控件view时,
1,如果view的宽高布局参数为具体dp值,那么可以构造出view的MeasureSpec,示例见上面demo中代码;
2,如果view的宽高布局参数为wrap_content时,该view的specMode肯定为AT_MOST,那么specSize在view的onMeasure(w,h)中计算得出,只需在调用view.measure(w,h)时,象征性给specSize设置一个超大的int值,并不影响测量textview;
3,如果view的宽高布局参数是match_parent时,由于view的specMode可能为EXACTLY(这时,需知道父容器留给view的剩余空间大小),那么无法通过measure方法测量该view。
附:获取view的宽高,可以选择先测量该控件,再获取;也可以选择在view自己测量完成后,再调用getMeasuredWidth方法。有几种方式判断view是否自己测量完成,如,在Activity中重写onWindowFucusChanged(boolean hasFocus),在该方法里面调用getMeasuredWidth方法也可以正确获取view的宽高。方式还有很多种,但不是本篇文章研究的重点,有兴趣的哥们自己去网上s吧!