Android源码解析之在Activity中调用measure方法测量宽高的原理

一,写在前面        

        在文章自定义控件之测量一文中,已经很清楚描述了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吧!






 

你可能感兴趣的:(android)