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

来源:互联网 发布:win10 重置网络设置 编辑:程序博客网 时间:2024/06/01 18:33

一,写在前面        

        在文章自定义控件之测量一文中,已经很清楚描述了view,viewgroup的测量过程。本篇文章是基于自定义控件之测量的一个小小的使用,不会再详细介绍源码所有流程,直接上源码,分析在Activity中调用measure方法测量宽高的原理。那目的是什么呢?就是为了在activity的onCreate方法里获取控件的宽高值。

        在onCreate()中获取控件宽高,调用view.getMeasuredWidth(),但是这个方法返回宽度值是有条件的。在测量TextView过程中,调用setMeasuredDimession(w,h)完成测量后,getMeasuredWidth()才有值。可以选择处理方式之一:先手动测量view,调用view.measure(w,h),才能正确获取宽度值。

二,示例展示

那么参数w,h应该传入些什么呢,先看下面这个例子:

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity" >    <TextView        android:id="@+id/tv"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:textColor="#fff"        android:background="#f00"        android:layout_centerInParent="true"        android:text="测量我丫" /></RelativeLayout>
MainActivity.java

public class MainActivity extends Activity {private TextView tv;private static final String TAG = "MainActivity";@Overrideprotected 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,并获取宽度值widthint 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 modeint 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);            }        }//...  codesetMeasuredDimension(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吧!






 

阅读全文
0 0
原创粉丝点击