Android中measure过程、WRAP_CONTENT详解以及xm

系统 2027 0

本文原创, 转载请注明出处 http://blog.csdn.net/qinjuning



上篇文章<< Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(上) >>中,我们

了解了View树的转换过程以及如何设置View的LayoutParams的。本文继续沿着既定轨迹继续未完成的job。

主要知识点如下:
1、MeasureSpc类说明
2、measure过程详解(揭秘其细节);
3、root View被添加至窗口时,UI框架是如何设置其LayoutParams值得。

在讲解measure过程前,我们非常有必要理解MeasureSpc类的使用,否则理解起来也只能算是囫囵吞枣。


1、MeasureSpc类说明


1.1 SDK 说明如下

A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec

represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and

a mode.

即:
MeasureSpc类封装了父View传递给子View的布局(layout)要求。每个MeasureSpc实例代表宽度或者高度

(只能 是其一)要求。 它有三种模式:

①、UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;

②、EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;

③、AT_MOST(至多),子元素至多达到指定大小的值。


常用的三个函数:

static int getMode(int measureSpec) : 根据提供的测量值(格式)提取模式(上述三个模式之一)

static int getSize(int measureSpec) : 根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)

static int makeMeasureSpec(int size,int mode) : 根据提供的大小值和模式创建一个测量值(格式)


以上摘取自: <<
MeasureSpec介绍及使用详解 >>

1.2 MeasureSpc类源码分析 其为View.java类的内部类,路径:\frameworks\base\core\java\android\view\View.java

    public class View implements ... {
	 ...
	 public static class MeasureSpec {
        private static final int MODE_SHIFT = 30; //移位位数为30
        //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        //向右移位30位,其值为00 + (30位0)  , 即 0x0000(16进制表示)
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        //向右移位30位,其值为01 + (30位0)  , 即0x1000(16进制表示)
        public static final int EXACTLY     = 1 << MODE_SHIFT;
        //向右移位30位,其值为02 + (30位0)  , 即0x2000(16进制表示)
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        //创建一个整形值,其高两位代表mode类型,其余30位代表长或宽的实际值。可以是WRAP_CONTENT、MATCH_PARENT或具体大小exactly size
        public static int makeMeasureSpec(int size, int mode) {
            return size + mode;
        }
        //获取模式  ,与运算
        public static int getMode(int measureSpec) {
            return (measureSpec & MODE_MASK);
        }
        //获取长或宽的实际值 ,与运算
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

    }
	...
}
  

MeasureSpec类的处理思路是:

①、右移运算,使int 类型的高两位表示模式的实际值,其余30位表示其余30位代表长或宽的实际值----可以是

WRAP_CONTENT、MATCH_PARENT或具体大小exactly size。


②、通过掩码MODE_MASK进行与运算 “&”,取得模式(mode)以及长或宽(value)的实际值。


2、measure过程详解


2.1 measure过程深入分析


之前的一篇博文 << Android中View绘制流程以及invalidate()等相关方法分析 >> ,我们从”二B程序员”的角度简单 解了measure过程的调用过程。过了这么多,我们也该 升级了,- - 。现在请开始从”普通程序员”角度去理解这个

程。我们重点查看measure过程中地相关方法。

我们说过,当UI框架开始绘制时,皆是从ViewRoot.java类开始绘制的。


ViewRoot类简要说明 : 任何显示在设备中的窗口,例如:Activity、Dialog等,都包含一个ViewRoot实例,该

主要用来与远端 WindowManagerService交互以及控制(开始/销毁)绘制。


Step 1 、 开始UI绘制 , 具体绘制方法则是:

    路径:\frameworks\base\core\java\android\view\ViewRoot.java
public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {
    ...
    //mView对象指添加至窗口的root View ,对Activity窗口而言,则是DecorView对象。
    View mView;    
    
    //开始View绘制流程
    private void performTraversals(){
    	...
    	//这两个值我们在后面讨论时,在回过头来看看是怎么赋值的。现在只需要记住其值MeasureSpec.makeMeasureSpec()构建的。
    	int childWidthMeasureSpec; //其值由MeasureSpec类构建 , makeMeasureSpec
        int childHeightMeasureSpec;//其值由MeasureSpec类构建 , makeMeasureSpec
        

        // Ask host how big it wants to be
        host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    	...
    }
    ...
}
  

这儿,我并没有说出childWidthMeasureSpec和childHeightMeasureSpec类的来由(为了避免额外地开销,等到
第三部分时我们在来攻克它,现在只需要记住其值MeasureSpec.makeMeasureSpec()构建的。

Step 2 、 调用measure()方法去做一些前期准备

measure()方法原型定义在View.java类中,final修饰符修饰,其不能被重载:

    public class View implements ... {
   	...
    /**
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     * @see #onMeasure(int, int)
     */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
   		//判断是否为强制布局,即带有“FORCE_LAYOUT”标记 以及 widthMeasureSpec或heightMeasureSpec发生了改变
        if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
        	//清除MEASURED_DIMENSION_SET标记   ,该标记会在onMeasure()方法后被设置
            mPrivateFlags &= ~MEASURED_DIMENSION_SET; 

            // measure ourselves, this should set the measured dimension flag back
            // 1、 测量该View本身的大小 ; 2 、 设置MEASURED_DIMENSION_SET标记,否则接写来会报异常。
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling" + " setMeasuredDimension()");
            }

            mPrivateFlags |= LAYOUT_REQUIRED;  //下一步是layout了,添加LAYOUT_REQUIRED标记
        }

        mOldWidthMeasureSpec = widthMeasureSpec;   //保存值
        mOldHeightMeasureSpec = heightMeasureSpec; //保存值
    }
   	...
}
  


参数widthMeasureSpec和heightMeasureSpec 由父View构建,表示父View给子View的测量要求。其值地构建

会在下面步骤中详解。

measure()方法显示判断是否需要重新调用设置改View大小,即调用onMeasure()方法,然后操作两个标识符:

①、重置 MEASURED_DIMENSION_SET : onMeasure()方法中,需要添加该标识符,否则,会报异常;

②、添加 LAYOUT_REQUIRED : 表示需要进行layout操作。

最后,保存当前的widthMeasureSpec和heightMeasureSpec值。


Step 3 、 调用onMeasure()方法去真正设置View的长宽值,其默认实现为:

     /**
    * Measure the view and its content to determine the measured width and the
    * measured height. This method is invoked by {@link #measure(int, int)} and
    * should be overriden by subclasses to provide accurate and efficient
    * measurement of their contents.
    * 
    * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
    *                         The requirements are encoded with
    * @param heightMeasureSpec vertical space requirements as imposed by the parent.
    *                         The requirements are encoded with
    */
   //设置该View本身地大小
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
               getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
   }
   
   /**
    * Utility to return a default size. Uses the supplied size if the
    * MeasureSpec imposed no contraints. Will get larger if allowed
    * by the MeasureSpec.
    *
    * @param size Default size for this view
    * @param measureSpec Constraints imposed by the parent
    * @return The size this view should be.
    */
   //@param size参数一般表示设置了android:minHeight属性或者该View背景图片的大小值
   public static int getDefaultSize(int size, int measureSpec) {
       int result = size;  
       int specMode = MeasureSpec.getMode(measureSpec);
       int specSize =  MeasureSpec.getSize(measureSpec);

       //根据不同的mode值,取得宽和高的实际值。
       switch (specMode) {
       case MeasureSpec.UNSPECIFIED:  //表示该View的大小父视图未定,设置为默认值
           result = size;
           break;
       case MeasureSpec.AT_MOST:      //表示该View的大小由父视图指定了
       case MeasureSpec.EXACTLY:
           result = specSize;
           break;
       }
       return result;
   }
   //获得设置了android:minHeight属性或者该View背景图片的大小值, 最为该View的参考值
   protected int getSuggestedMinimumWidth() {
       int suggestedMinWidth = mMinWidth;  //  android:minHeight

       if (mBGDrawable != null) { // 背景图片对应地Width。
           final int bgMinWidth = mBGDrawable.getMinimumWidth();
           if (suggestedMinWidth < bgMinWidth) {
               suggestedMinWidth = bgMinWidth;
           }
       }

       return suggestedMinWidth;
   }
   //设置View在measure过程中宽和高
   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
       mMeasuredWidth = measuredWidth;
       mMeasuredHeight = measuredHeight;

       mPrivateFlags |= MEASURED_DIMENSION_SET;  //设置了MEASURED_DIMENSION_SET标记
   }
  

主要功能就是根据该View属性(android:minWidth和背景图片大小)和父View对该子View的"测量要求",设置该 View mMeasuredWidth 和 mMeasuredHeight 值。


这儿只是一般的View类型地实现方法。一般来说,父View,也就是ViewGroup类型,都需要在重写onMeasure() 方法,遍历 所有子View,设置每个子View的大小。基本思想如下: 遍历所有子View,设置每个子View的大小。伪

码表示为:

       //某个ViewGroup类型的视图
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	    //必须调用super.ononMeasure()或者直接调用setMeasuredDimension()方法设置该View大小,否则会报异常。
	    super.onMeasure(widthMeasureSpec , heightMeasureSpec)
        //setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        //        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        
	    //遍历每个子View
	    for(int i = 0 ; i < getChildCount() ; i++){
	    	View child = getChildAt(i);
	    	//调用子View的onMeasure,设置他们的大小。childWidthMeasureSpec , childHeightMeasureSpec ?
	    	child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
	    }
   }
  


Step 2、 Step 3 代码也比较好理解,但问题是我们示例代码中widthMeasureSpec、 heightMeasureSpec是如何

确定的呢?父View是如何设定其值的?

要想回答这个问题,我们看是去源代码里找找答案吧。在ViewGroup.java类中,为我们提供了三个方法,去设置

个子View的大小,基本思想也如同我们 之前描述的思想:遍历所有子View,设置每个子View的大小。

主要有如下方法:

    /**
 * Ask all of the children of this view to measure themselves, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * We skip children that are in the GONE state The heavy lifting is done in
 * getChildMeasureSpec.
 */
//widthMeasureSpec 和  heightMeasureSpec 表示该父View的布局要求
//遍历每个子View,然后调用measureChild()方法去实现每个子View大小
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) { // 不处于 “GONE” 状态
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}
   
/**
 * Ask one of the children of this view to measure itself, taking into
 * account both the MeasureSpec requirements for this view and its padding.
 * The heavy lifting is done in getChildMeasureSpec.
 *
 * @param child The child to measure
 * @param parentWidthMeasureSpec The width requirements for this view
 * @param parentHeightMeasureSpec The height requirements for this view
 */
//测量每个子View高宽时,清楚了该View本身的边距大小,即android:padding属性 或android:paddingLeft等属性标记
protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams(); // LayoutParams属性
    //设置子View的childWidthMeasureSpec属性,去除了该父View的边距值  mPaddingLeft + mPaddingRight
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    //设置子View的childHeightMeasureSpec属性,去除了该父View的边距值  mPaddingTop + mPaddingBottom
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
  

measureChildren()方法:遍历所有子View,调用 measureChild()方法去设置该子View的属性值。

measureChild() 方法 : 获取特定子View的 widthMeasureSpec、 heightMeasureSpec,调用measure()方法

设置子View的实际宽高值。

getChildMeasureSpec ()就是获取 子View的 widthMeasureSpec、 heightMeasureSpec值。

    /**
 * Does the hard part of measureChildren: figuring out the MeasureSpec to
 * pass to a particular child. This method figures out the right MeasureSpec
 * for one dimension (height or width) of one child view.
 *
 * The goal is to combine information from our MeasureSpec with the
 * LayoutParams of the child to get the best possible results.
 */
// spec参数                                    表示该父View本身所占的widthMeasureSpec 或  heightMeasureSpec值
// padding参数                          表示该父View的边距大小,见于android:padding属性 或android:paddingLeft等属性标记
// childDimension参数  表示该子View内部LayoutParams属性的值,可以是wrap_content、match_parent、一个精确指(an exactly size),
//           例如:由android:width指定等。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);  //获得父View的mode
    int specSize = MeasureSpec.getSize(spec);  //获得父View的实际值

    int size = Math.max(0, specSize - padding); //父View为子View设定的大小,减去边距值,

    int resultSize = 0;    //子View对应地 size 实际值 ,由下面的逻辑条件赋值
    int resultMode = 0;    //子View对应地 mode 值 , 由下面的逻辑条件赋值

    switch (specMode) {
    // Parent has imposed an exact size on us
    //1、父View是EXACTLY的 !
    case MeasureSpec.EXACTLY: 
    	//1.1、子View的width或height是个精确值 (an exactly size)
        if (childDimension >= 0) {          
            resultSize = childDimension;         //size为精确值
            resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。
        } 
        //1.2、子View的width或height为 MATCH_PARENT/FILL_PARENT 
        else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;                   //size为父视图大小
            resultMode = MeasureSpec.EXACTLY;    //mode为 EXACTLY 。
        } 
        //1.3、子View的width或height为 WRAP_CONTENT
        else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;                   //size为父视图大小
            resultMode = MeasureSpec.AT_MOST;    //mode为AT_MOST 。
        }
        break;

    // Parent has imposed a maximum size on us
    //2、父View是AT_MOST的 !    
    case MeasureSpec.AT_MOST:
    	//2.1、子View的width或height是个精确值 (an exactly size)
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;        //size为精确值
            resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY 。
        }
        //2.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
        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;                  //size为父视图大小
            resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST
        }
        //2.3、子View的width或height为 WRAP_CONTENT
        else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;                  //size为父视图大小
            resultMode = MeasureSpec.AT_MOST;   //mode为AT_MOST
        }
        break;

    // Parent asked to see how big we want to be
    //3、父View是UNSPECIFIED的 !
    case MeasureSpec.UNSPECIFIED:
    	//3.1、子View的width或height是个精确值 (an exactly size)
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;        //size为精确值
            resultMode = MeasureSpec.EXACTLY;   //mode为 EXACTLY
        }
        //3.2、子View的width或height为 MATCH_PARENT/FILL_PARENT
        else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = 0;                        //size为0! ,其值未定
            resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED
        } 
        //3.3、子View的width或height为 WRAP_CONTENT
        else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = 0;                        //size为0! ,其值未定
            resultMode = MeasureSpec.UNSPECIFIED;  //mode为 UNSPECIFIED
        }
        break;
    }
    //根据上面逻辑条件获取的mode和size构建MeasureSpec对象。
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
  

为了便于分析,我将上面的逻辑判断语句使用列表项进行了说明.


getChildMeasureSpec ()方法的主要功能如下:


根据父View的measureSpec值(widthMeasureSpec,heightMeasureSpec)值以及子View的子View内部

LayoutParams 属性值,共同决定子View的measureSpec值的大小。主要判断条件主要为MeasureSpec的mode

类型 以及 LayoutParams 的宽高实际值(lp.width,lp.height), 见于以上所贴代码中的列表项: 1、 1.1 ; 1.2 ; 1.3 ;

2、2.1等。


例如,分析列表3:假设当父View为MeasureSpec.UNSPECIFIED类型,即未定义时,只有当子View的width

或height指定时,其mode才为 MeasureSpec.EXACTLY ,否者该View size为 0 ,mode为 MeasureSpec.UNSPECIFIED

,即处于未指定状态。

由此可以得出, 每个View大小的设定都事由其父View以及该View共同决定的。但这只是一个期望的大小,每个

View在测量时最终大小的设定是由 setMeasuredDimension()最终决定的。因此,最终确定一个View的“测量长宽“是

由以下几个方面影响:

1、父View的 MeasureSpec属性;

2、子View的 LayoutParams属性 ;

3、 setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

setMeasuredDimension()原型:

       //设置View在measure过程中宽和高
   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
       mMeasuredWidth = measuredWidth;
       mMeasuredHeight = measuredHeight;

       mPrivateFlags |= MEASURED_DIMENSION_SET;  //设置了MEASURED_DIMENSION_SET标记
   }
  


将上面列表项转换为表格为:

Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)

这张表格更能帮助我们分析View的MeasureSpec的确定条件关系。


为了帮助大家理解,下面我们分析某个窗口使用地xml布局文件,我们弄清楚该xml布局文件中每个View的

MeasureSpec值的组成。

       <?xml version="1.0" encoding="utf-8"?>
   <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
       android:id="@+id/llayout"
   	   android:orientation="vertical" 
       android:layout_width="match_parent"
   	   android:layout_height="match_parent">
   	
   	
   	<TextView android:id="@+id/tv" 
   	    android:layout_width="match_parent"
   		android:layout_height="wrap_content"
   	    android:text="@string/hello" />

   </LinearLayout>   
  

该布局文件共有两个View: ①、id为llayout的 LinearLayout 布局控件 ;

②、id为tv的 TextView 控件。


假设LinearLayout的父View对应地widthSpec和heightSpec值皆为MeasureSpec.EXACTLY类型(Activity窗口

的父View为DecorView,具体原因见第三部分说明)。


LinearLayout 而言比较简单,由于 android:layout_width="match_parent",因此其width对应地widthSpec

mode 值为MeasureSpec.EXACTLY , size由父视图大小指定 ; 由于android:layout_height = "match_parent",

此其height对应地heightSpec mode MeasureSpec.EXACTLY ,size由父视图大小指定 ;


TextView 而言 ,其父View为LinearLayout的widthSpec和heightSpec值皆为MeasureSpec.EXACTLY类型,

由于android:layout_width="match_parent" , 因此其width对应地widthSpec mode值为MeasureSpec.EXACTLY

size由父视图大小指定 ; 由于android:layout_width="wrap_content" , 因此其height对应地widthSpec mode值为

MeasureSpec.AT_MOST,size由父视图大小指定 。


我们继续窥测下LinearLayout类是如何进行measure过程的:

       public class LinearLayout extends ViewGroup {
		...
		@Override  //onMeasure方法。
		protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		    //判断是垂直方向还是水平方向,这儿我们假设是VERTICAL垂直方向,
			if (mOrientation == VERTICAL) {
		        measureVertical(widthMeasureSpec, heightMeasureSpec);
		    } else {
		        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
		    }
		}
		//垂直方向布局
	    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	        mTotalLength = 0;         //该LinearLayout测量子View时的总高度。
	    	float totalWeight = 0;    //所有子View的权重和 , android:layout_weight
	    	int maxWidth = 0;         //保存子View中最大width值
	        ...
	        final int count = getVirtualChildCount();  //子View的个数
	        
	        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            ...
	        // See how tall everyone is. Also remember max width.
	        for (int i = 0; i < count; ++i) {
	            final View child = getVirtualChildAt(i);
                ...
	            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

	            totalWeight += lp.weight;  
	            //满足该条件地View会在该LinearLayout有剩余高度时,才真正调用measure()
	            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
	                ...
	            } else {
	                int oldHeight = Integer.MIN_VALUE;
	                //如果View的hight值为0,并且设置了android:layout_weight属性,重新纠正其height值为WRAP_CONTENT
	                if (lp.height == 0 && lp.weight > 0) {
	                    oldHeight = 0;
	                    lp.height = LayoutParams.WRAP_CONTENT;
	                }
	                // Determine how big this child would like to be. If this or
	                // previous children have given a weight, then we allow it to
	                // use all available space (and we will shrink things later
	                // if needed).
	                //对每个子View调用measure()方法
	                measureChildBeforeLayout(
	                       child, i, widthMeasureSpec, 0, heightMeasureSpec,
	                       totalWeight == 0 ? mTotalLength : 0);
	                
	                //这三行代码做了如下两件事情:
	                //1、获得该View的measuredHeight值,每个View都会根据他们地属性正确设置值  > 0 ;
	                //2、更新mTotalLength值:取当前高度mTotalLength值与mTotalLength + childHeight 的最大值
	                // 于是对于android:layout_height="wrap_height"属性地LinearLayout控件也就知道了它的确切高度值了。
	                final int childHeight = child.getMeasuredHeight();
	                final int totalLength = mTotalLength;
	                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
	                       lp.bottomMargin + getNextLocationOffset(child));
	                ...
	            }
	            final int margin = lp.leftMargin + lp.rightMargin;
	            final int measuredWidth = child.getMeasuredWidth() + margin;
	            maxWidth = Math.max(maxWidth, measuredWidth);
	            ...
	        }
            //后续还有很多处理,包括继续measure()某些符合条件地子View
	        ...
	    }
	    void measureChildBeforeLayout(View child, int childIndex,
	            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
	            int totalHeight) {
	    	//调用measureChildWithMargins()方法去设置子View大小
	        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
	                heightMeasureSpec, totalHeight);
	    }
		...
	}
  

继续看看measureChildWithMargins()方法,该方法定义在ViewGroup.java内,基本流程同于measureChild()方法,但添加了对子View Margin的处理,即:android:margin属性或者android:marginLeft等属性的处理。

measureChildWithMargins@ ViewGroup.java

    	/**
	 * Ask one of the children of this view to measure itself, taking into
	 * account both the MeasureSpec requirements for this view and its padding
	 * and margins. The child must have MarginLayoutParams The heavy lifting is
	 * done in getChildMeasureSpec.
	 */
	//基本流程同于measureChild()方法,但添加了对子View Margin的处理,即:android:margin属性或者android:marginLeft等属性的处理
	//widthUsed参数  表示该父View已经使用的宽度
	//heightUsed参数  表示该父View已经使用的高度
	protected void measureChildWithMargins(View child,
	        int parentWidthMeasureSpec, int widthUsed,
	        int parentHeightMeasureSpec, int heightUsed) {
	    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

	    //获得子View的childWidthMeasureSpec和childHeightMeasureSpec值
	    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
	            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
	                    + widthUsed, lp.width);
	    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
	            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
	                    + heightUsed, lp.height);

	    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
	}
  



measure()过程时,LinearLayout类做了如下事情 :

1、遍历每个子View,对其调用measure()方法;

2、子View measure()完成后,需要取得该子View地宽高实际值,继而做处理(例如:LinearLayout属性为

android:widht="wrap_content"时, LinearLayout的实际width值则是每个子View的 width值的累加值 )。

2.2 WRAP_CONTENT、MATCH_PARENT以及measure动机揭秘


子View地宽高实际值 ,即 child.getMeasuredWidth()值得返回最终会是一个确定值? 难道 WRAP_CONTENT (

其值为-2) 、MATCH_PARENT(值为-1)或者说一个具体值(an exactly size > 0)。前面我们说过, View最终“测量”值的

确定是有三个部分组成地:

①、父View的MeasureSpec属性;

②、子View的 LayoutParams属性 ;

③、setMeasuredDimension()或者其它类似设定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

因此,一个View必须以某种合适地方法确定它地最终大小。例如,如下自定义View:

    //自定义View	
public Class MyView extends View {
	
	 //针对不同地mode值,设置本View地大小
	 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
		 //获得父View传递给我们地测量需求
		 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	     int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		 
	     int width = 0 ;
	     int height = 0 ;
	     //对UNSPECIFIED 则抛出异常
	     if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)
	         throw new RuntimeException("widthMode or heightMode cannot be UNSPECIFIED");
	    
	     //精确指定
	     if(widthMode == MeasureSpec.EXACTLY){
	    	 width = 100 ;
	     }
	     //模糊指定
	     else if(widthMode == MeasureSpec.AT_MOST )
	    	 width = 50 ; 
	     
	      //精确指定
	     if(heightMode == MeasureSpec.EXACTLY){
	    	 height = 100 ;
	     }
	     //模糊指定
	     else if(heightMode == MeasureSpec.AT_MOST )
	    	 height = 50 ;
	     
	     setMeasuredDimension(width , height) ;
	 }
}
  



该自定义View重写了onMeasure()方法,根据传递过来的widthMeasureSpec和heightMeasureSpec简单设置了

View的 mMeasuredWidth 和 mMeasuredHeight值。

对于TextView而言,如果它地mode不是Exactly类型 , 它会根据一些属性,例如: android:textStyle

android:textSize android:typeface 等去确定TextView类地需要占用地长和宽。

因此,如果你地自定义View必须手动对不同mode做出处理。否则,则是mode对你而言是无效的。

Android框架中提供地一系列View/ViewGroup都需要去进行这个measure()过程地 ,因为在layout()过程中,父

View需要调用getMeasuredWidth()或getMeasuredHeight()去为每个子View设置他们地布局坐标,只有确定布局

坐标后,才能真正地将该View 绘制 (draw) 出来,否则该View的layout大小为0,得不到期望效果。我们继续看看

LinearLayout的layout布局过程:

    public class LinearLayout extends ViewGroup {
	...
    @Override  //layout 过程
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
		//假定是垂直方向布局
        if (mOrientation == VERTICAL) {
            layoutVertical();
        } else {
            layoutHorizontal();
        }
    }
	//对每个子View调用layout过程
    void layoutVertical() {
        ...
        final int count = getVirtualChildCount();
        ...
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {  //一般为非null
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
            	//获得子View测量时的实际宽高值,
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                
                ...
                //  封装了child.layout()方法,见如下
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight); 
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }
    //width = getMeasuredWidth() ; height = childHeight(); View的大小就是测量大小
    private void setChildFrame(View child, int left, int top, int width, int height) {
    	
        child.layout(left, top, left + width, top + height);
    }
	...
}	
  

对一个View进行measure操作地主要目的就是为了确定该View地布局大小,见上面所示代码。但measure操作

通常是耗时的,因此 对自定义ViewGroup而言,我们可以自由控制measure、layout过程,如果我们知道如何layout

一个View,我们可以跳过该ViewGroup地measure操作(onMeasure()方法中measure 所有子View地 ),直接去layout


在前面一篇博客<< Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用说明 >> 中,我们自定义了 一个 ViewGroup, 并且重写了onMeasure()和onLayout()方法去分别操作每个View。就该ViewGroup而言,我们只需要

重写onLayout() 操作即可,因为我们知道如何layout每个子View。 如下代码所示:


    //自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置
public class MultiViewGroup extends ViewGroup {
	private void init() {
		// 初始化3个 LinearLayout控件
		LinearLayout oneLL = new LinearLayout(mContext);
		oneLL.setBackgroundColor(Color.RED);
        addView(oneLL);
        ...
	}
	@Override
	// 我们知晓每个子View的layout布局大小,因此我们不需要为每个子View进行measure()操作了。
//	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//		setMeasuredDimension(width, height);
//		// 设置该ViewGroup的大小
//		int width = MeasureSpec.getSize(widthMeasureSpec);
//		int height = MeasureSpec.getSize(heightMeasureSpec);
//		int childCount = getChildCount();
//		for (int i = 0; i < childCount; i++) {
//			View child = getChildAt(i);
//			// 设置每个子视图的大小 , 即全屏
//			child.measure(MultiScreenActivity.screenWidth, MultiScreenActivity.scrrenHeight);
//		}
	}

	// layout过程
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		// TODO Auto-generated method stub
		Log.i(TAG, "--- start onLayout --");
		int startLeft = 0; // 每个子视图的起始布局坐标
		int startTop = 10; // 间距设置为10px 相当于 android:marginTop= "10px"
		int childCount = getChildCount();
		Log.i(TAG, "--- onLayout childCount is -->" + childCount);
		for (int i = 0; i < childCount; i++) {
			View child = getChildAt(i);
			child.layout(startLeft, startTop, 
					startLeft + MultiScreenActivity.screenWidth, 
					startTop + MultiScreenActivity.scrrenHeight);
			startLeft = startLeft + MultiScreenActivity.screenWidth ; //校准每个子View的起始布局位置
			//三个子视图的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960]
		}
	}
}  
  

更多关于自定义ViewGroup无须重写measure动作的,可以参考 Android API :

< < Optimizing the View >>

中文翻译见于:<< Android中View绘制优化之三---- 优化View >>


3、root View被添加至窗口时,UI框架是如何设置其LayoutParams值


老子道德经有言:“道生一,一生二,二生三,三生万物。” UI绘制也就是个递归过程。理解其基本架构后,
也就“掌握了一个中心点”了。在第一节中,我们没有说明开始UI绘制时 ,没有说明mView.measure()参数地由来,
参数也就是我们本节需要弄懂的“道” --- root View的 widthMeasureSpec和heightMeasureSpec 是如何确定的。

对于如下布局文件: main.xml
      <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>

    
当使用LayoutInflater类解析成View时 ,LinearLayout对象的LayoutParams参数为null 。具体原因请参考上篇博文

任何一个View被添加至窗口时,都需要利用WindowManager类去操作。例如,如下代码:
      //显示一个悬浮窗吧 , just so so 
public void showView()
{
    //解析布局文件
	LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    //rootView对应地LayoutParams属性值为null,将会在UI绘制时设定其值
	View rootView = layoutInflater.inflate(R.layout.main, null);
	
	WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
	//设置WindowManager.LayoutParams参数值,作为该窗口的各种属性
	WindowManager.LayoutParams winparams = WindowManager.LayoutParams();
     // 以屏幕左上角为原点,设置x、y初始值
	winparams.x = 0;
	winparams.y = 0;

    //设置悬浮窗口长宽数据
	winparams.width = WindowManager.LayoutParams.WRAP_CONTENT;;
	winparams.height = WindowManager.LayoutParams.WRAP_CONTENT;;
     
	windowManager.addView(rootView, winparams);
}
    

  
关于WindowManager的使用请看如下博客 :
关于WindowManager.LayoutParams类说明请看如下博客:
下面,我们从获得WindowManager对象引用开始,一步步观察addView()做了一些什么事情。
Step 1 、获得WindowManager对象服务 ,具体实现类在ContextImpl.java内中
路径:/frameworks/base/core/java/android/app/ContextImpl.java
         @Override
   public Object getSystemService(String name) {
       if (WINDOW_SERVICE.equals(name)) {
           return WindowManagerImpl.getDefault();
       }
       ...
   }

    
WindowManager是个接口,具体返回对象则是WindowManagerImpl的单例对象。

Step 2 、获得WindowManagerImpl的单例对象,以及部分源码分析
路径:/frameworks/base/core/java/android/view/ WindowManagerImpl .j ava
      public class WindowManagerImpl implements WindowManager{
	   
   public static WindowManagerImpl getDefault()
   {
       return mWindowManager;
   }
   //以特定Window属性添加一个窗口
   public void addView(View view, ViewGroup.LayoutParams params)
   {
       addView(view, params, false);
   }
   //参数nest表示该窗口是不是一个字窗口
   private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
   {   ...
       final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
       
       ViewRoot root;
       View panelParentView = null;    //该子窗口对应地父窗口View
       
       synchronized (this) {
          
    	   ...//需要对传递过来地参数进行检测...
    	   
           //对每个窗口皆构建一个ViewRoot对象
           root = new ViewRoot(view.getContext());
           root.mAddNesting = 1;
           //设置root View 的LayoutParams为wparams,即WindowManager.LayoutParams类型
           view.setLayoutParams(wparams);
           ...//对参数检测,以及拷贝原有数组...
           
           //将窗口对应地view、root、wparams保存在属性集合中
           mViews[index] = view;
           mRoots[index] = root;
           mParams[index] = wparams;
       }
       // do this last because it fires off messages to start doing things
       // 调用ViewRoot对象去通知系统添加一个窗口
       root.setView(view, wparams, panelParentView);
   }
   ...
   //这三个数组分别保存了一个窗口对应地属性
   private View[] mViews;         //root View对象 , View类型
   private ViewRoot[] mRoots;     //ViewRoot类型 , 与WMS通信
   private WindowManager.LayoutParams[] mParams;  //窗口属性
   
   //WindowManagerImpl实现了单例模式
   private static WindowManagerImpl mWindowManager = new WindowManagerImpl();
}
    


WindowManagerImpl类的三个数组集合保存了每个窗口相关属性,这样我们可以通过这些属性去操作特定的
窗口(例如,可以根据View去更新/销毁该窗口)。当参数检查成功时,构建一个ViewRoot对象,并且设置 设置root
View 的LayoutParams为wparams,即WindowManager.LayoutParams类型 。最后调用root.setView()方法去通知
系统需要创建该窗口。我们接下来往下看看ViewRoot类相关操作。
Step 3、
      public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {
	   
	View mView;	  //所有窗口地root View   
	final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();  

	...
	 /**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs,
            View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                mWindowAttributes.copyFrom(attrs); //保存WindowManager.LayoutParams属性值
                attrs = mWindowAttributes;
                ...
                
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();   //请求UI开始绘制。
                mInputChannel = new InputChannel();  //创建一个InputChannel对象,接受消息
                try {
                    //通知WindowManagerService添加一个窗口
                	res = sWindowSession.add(mWindow, mWindowAttributes,
                            getHostVisibility(), mAttachInfo.mContentInsets,
                            mInputChannel);
                } 
                ...
                view.assignParent(this);  //将root View的父View设置为该ViewRoot对象(实现了ViewParent接口)
                ...
            }
        }
    }
}
    
说明:ViewRoot类继承了Handler,实现了ViewParent接口

setView()方法地主要功能如下:
1、保存相关属性值,例如:mView、mWindowAttributes等;
2、调用requestLayout()方法请求UI绘制,由于ViewRoot是个Handler对象,异步请求;
3、通知WindowManagerService添加一个窗口;
4、注册一个事件监听管道,用来监听:按键(KeyEvent)和触摸(MotionEvent)事件。
我们这儿重点关注 requestLayout()方法请求UI绘制地流程。

Step 4、异步调用请求UI绘制
         /**
    * {@inheritDoc}
    */
   public void requestLayout() {
       checkThread();        //检查是不是UI线程调用,如果不是UI线程,会报异常
       mLayoutRequested = true;   //置为真,表示需要进行measure和layout过程
       scheduleTraversals();  
   }
   //开始UI绘制流程
   public void scheduleTraversals() {
       if (!mTraversalScheduled) {
           mTraversalScheduled = true;       //防止多次调用
           sendEmptyMessage(DO_TRAVERSAL);   //异步请求UI绘制
       }
   }
   @Override
   public void handleMessage(Message msg) {
	   switch (msg.what) {
           case DO_TRAVERSAL:
                performTraversals();  //开始UI绘制
                break;
	   }
   }
    
由于performTraversals()方法比较复杂,我们侧重于第一次设置root View的widhtSpecSize以及
heightSpecSize值。
         private void performTraversals() {
       // cache mView since it is used so much below...
       final View host = mView;

       mTraversalScheduled = false;         
       boolean surfaceChanged = false;
       WindowManager.LayoutParams lp = mWindowAttributes;  

       int desiredWindowWidth;              //表示该窗口期望width值
       int desiredWindowHeight;             //表示该窗口期望width值
       int childWidthMeasureSpec;           //保存root View的widthMeasureSpec
       int childHeightMeasureSpec;          //保存root View的heightMeasureSpec

       final View.AttachInfo attachInfo = mAttachInfo;

       final int viewVisibility = getHostVisibility();
       boolean viewVisibilityChanged = mViewVisibility != viewVisibility
               || mNewSurfaceNeeded;

       float appScale = mAttachInfo.mApplicationScale;

       WindowManager.LayoutParams params = null;
       if (mWindowAttributesChanged) {
           mWindowAttributesChanged = false;
           surfaceChanged = true;
           params = lp;
       }
       Rect frame = mWinFrame;
       if (mFirst) {   //mFirst表示是否是第一次绘制该Window
           fullRedrawNeeded = true;
           mLayoutRequested = true;

           DisplayMetrics packageMetrics =
               mView.getContext().getResources().getDisplayMetrics();
           //第一次绘制时desiredWindowWidth,desiredWindowHeight 值大小为屏幕大小
           desiredWindowWidth = packageMetrics.widthPixels;
           desiredWindowHeight = packageMetrics.heightPixels;
           ...
       } else {   //不是第一次绘制,则desiredWindowWidth值为frame保存大小,frame值会由WMS填充
           desiredWindowWidth = frame.width();
           desiredWindowHeight = frame.height();
           ...
       }
       ...
       boolean insetsChanged = false;

       if (mLayoutRequested) {
           ...//获得root View的widthMeasureSpec 和 heightMeasureSpec值
           childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
           childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
           //开始measure过程
           host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
       }
       ...
       final boolean didLayout = mLayoutRequested;
       
       boolean triggerGlobalLayoutListener = didLayout
               || attachInfo.mRecomputeGlobalAttributes;
       if (didLayout) {
           ... //layout过程
    	   host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
           ...
       }
       ...
       if (!cancelDraw && !newSurface) {
           mFullRedrawNeeded = false;
           draw(fullRedrawNeeded);
           ...
   }
    
        /**
    * @param windowSize  The available width or height of the window
    *
    * @param rootDimension The layout params for one dimension (width or height) of the window.
   */
   private int getRootMeasureSpec(int windowSize, int rootDimension) {
       int measureSpec;
       switch (rootDimension) {
       case ViewGroup.LayoutParams.MATCH_PARENT:
           // Window can't resize. Force root view to be windowSize.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
           break;
       case ViewGroup.LayoutParams.WRAP_CONTENT:
           // Window can resize. Set max size for root view.
           measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
           break;
       default:
           // Window wants to be an exact size. Force root view to be that size.
           measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
           break;
       }
       return measureSpec;
   }       
    


调用root View的measure()方法时,其参数是由getRootMeasureSpec()设置的,基本思路同我们前面描述的
差不多。贴出来的代码只是简简单单列出了measure 、layout 、 draw 过程的调用点,里面有很多逻辑处理,
阅读起来比较费劲,我也只能算是个囫囵吞枣水平。大家有兴趣地可以看看源码,加深理解。


最后,由于小子理解水平有限,可能很多地方让大家“丈二和尚--摸不着头脑”,给大家两个小建议吧:
1、仔细钻研源码 ;
2、想认真系统性研读UI绘制原理的话,建议详细阅读<< Android内核剖析 >>第十三章 < UI绘制原理 >



Android中measure过程、WRAP_CONTENT详解以及xml布局文件解析流程浅析(下)


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论