1. 程式人生 > >Android中measure過程、WRAP_CONTENT詳解以及xml佈局檔案解析流程淺析(上)

Android中measure過程、WRAP_CONTENT詳解以及xml佈局檔案解析流程淺析(上)

  繪製流程的三個步驟,即:

                      1、  measure過程 --- 測量過程

                      2、 layout 過程     --- 佈局過程
                      3、 draw 過程      --- 繪製過程


      要想對Android 中View這塊深入理解,對這三個步驟地學習是必不可少的 。

      今天,我著重講解下如下三個內容:

            1、 measure過程

            2、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT屬性的原理說明

            3、xml佈局檔案解析成View樹的流程分析。

希望對大家能有幫助。- - 分析版本基於Android 2.3

1、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT 

       初入Android殿堂的同學們,對這三個屬性一定又愛又恨。愛的是使用起來挺爽地---照葫蘆畫瓢即可,恨的

  卻是時常混淆這幾個屬性地意義,需要三思而後行。在帶著大家重溫下這幾個屬性的用法吧(希望我沒有囉嗦)。

      這三個屬性都用來適應檢視的水平或垂直大小,一個以檢視的內容或尺寸為基礎的佈局比精確地指定檢視範圍

  更加方便。

        ①  fill_parent

                設定一個檢視的佈局為fill_parent將強制性地使檢視擴充套件至父元素大小。

        ② match_parent

               Android 中match_parent和fill_parent意思一樣,但match_parent更貼切,於是從2.2開始兩個詞都可以

          用,但2.3版本後建議使用match_parent。

       ③ wrap_content

              自適應大小,強制性地使檢視擴充套件以便顯示其全部內容。以TextView和ImageView控制元件為例,設定為

         wrap_content將完整顯示其內部的文字和影象。佈局元素將根據內容更改大小。

      當然,我們可以設定View的確切寬高,而不是由以上屬性指定。

        android:layout_weight="wrap_content"   //自適應大小
        android:layout_weight="match_parent"   //與父檢視等高
        android:layout_weight="fill_parent"	   //與父檢視等高
        android:layout_weight="100dip"         //精確設定高度值為 100dip

      接下來,我們需要轉換下視角,看看ViewGroup.LayoutParams類及其派生類。

 2、ViewGroup.LayoutParams類及其派生類

    2.1、  ViewGroup.LayoutParams類說明

            Android API中如下介紹:

  LayoutParams are used by views to tell their parents how they want to be laid out.


     意思大概是說: View通過LayoutParams類告訴其父檢視它想要地大小(即,長度和寬度)。


    因此,每個View都包含一個ViewGroup.LayoutParams類或者其派生類,View類依賴於ViewGroup.LayoutParams。

    路徑:frameworks\base\core\java\android\view\View.java

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
  ...
  /**
   * The layout parameters associated with this view and used by the parent
   * {@link android.view.ViewGroup} to determine how this view should be
   * laid out.
   * {@hide}
   */
  //該View擁有的 LayoutParams屬性,父試圖新增該View時,會為其賦值,特別注意,其型別為ViewGroup.LayoutParams。
  protected ViewGroup.LayoutParams mLayoutParams;  
  ...
}

     2.2、  ViewGroup.LayoutParams原始碼分析

  路徑位於:frameworks\base\core\java\android\view\ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	...
	 public static class LayoutParams {
        /**
         * Special value for the height or width requested by a View.
         * FILL_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. This value is deprecated
         * starting in API Level 8 and replaced by {@link #MATCH_PARENT}.
         */
        @Deprecated
        public static final int FILL_PARENT = -1;  // 注意值為-1,Android2.2版本不建議使用
        /**
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,
         * minus the parent's padding, if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1; // 注意值為-1
        /**
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content, taking its own padding into account.
         */
        public static final int WRAP_CONTENT = -2; // 注意值為-2
        /**
         * Information about how wide the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT ,
         * in API Level 8) or WRAP_CONTENT. or an exact size.
         */
        public int width;  //該View的寬度,可以為WRAP_CONTENT/MATCH_PARENT 或者一個具體值
        /**
         * Information about how tall the view wants to be. Can be one of the
         * constants FILL_PARENT (replaced by MATCH_PARENT ,
         * in API Level 8) or WRAP_CONTENT. or an exact size.
         */
        public int height; //該View的高度,可以為WRAP_CONTENT/MATCH_PARENT 或者一個具體值
        /**
         * Used to animate layouts.
         */
        public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
        /**
         * Creates a new set of layout parameters. The values are extracted from
         * the supplied attributes set and context. The XML attributes mapped
         * to this set of layout parameters are:、
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_Layout_layout_width,
                    R.styleable.ViewGroup_Layout_layout_height);
            a.recycle();
        }

        /**
         * Creates a new set of layout parameters with the specified width
         * and height.
         */
        public LayoutParams(int width, int height) {
            this.width = width;
            this.height = height;
        }
        /**
         * Copy constructor. Clones the width and height values of the source.
         *
         * @param source The layout params to copy from.
         */
        public LayoutParams(LayoutParams source) {
            this.width = source.width;
            this.height = source.height;
        }
        /**
         * Used internally by MarginLayoutParams.
         * @hide
         */
        LayoutParams() {
        }
        /**
         * Extracts the layout parameters from the supplied attributes.
         *
         * @param a the style attributes to extract the parameters from
         * @param widthAttr the identifier of the width attribute
         * @param heightAttr the identifier of the height attribute
         */
        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }
}

       我們發現FILL_PARENT/MATCH_PARENT值為 -1 ,WRAP_CONETENT值為-2,是不是有點詫異? 將值

  設定為負值的目的是為了區別View的具體值(an exact size) 總是大於0的。

       ViewGroup子類可以實現自定義LayoutParams,自定義LayoutParams提供了更好地擴充套件性,例如LinearLayout

 就有LinearLayout. LayoutParams自定義類(見下文)。整個LayoutParams類家族還是挺複雜的。

      ViewGroup.LayoutParams及其常用派生類的類圖(部分類圖)如下:

                  

                              該類圖是在太龐大了,大家有興趣的去看看Android API吧。

      前面我們說過,每個View都包含一個ViewGroup.LayoutParams類或者其派生類,下面我們的疑問是Android框架

 中時如何為View設定其LayoutParams屬性的。

有兩種方法會設定View的LayoutParams屬性:

       1、 直接新增子View時,常見於如下幾種方法:ViewGroup.java

//Adds a child view.	
void addView(View child, int index)
//Adds a child view with this ViewGroup's default layout parameters 
//and the specified width and height.
void addView(View child, int width, int height)
//Adds a child view with the specified layout parameters.		
void addView(View child, ViewGroup.LayoutParams params)

         三個過載方法的區別只是新增View時構造LayoutParams物件的方式不同而已,稍後我們探尋一下它們的原始碼。

     2、 通過xml佈局檔案指定某個View的屬性為:android:layout_heigth=””以及android:layout_weight=”” 時。

    總的來說,這兩種方式都會設定View的LayoutParams屬性值----指定的或者Default值。

  方式1流程分析

     直接新增子View時,比較容易理解,我們先來看看這種方式設定LayoutParams的過程:

路徑:\frameworks\base\core\java\android\view\ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	...
    /**
     * Adds a child view. If no layout parameters are already set on the child, the
     * default parameters for this ViewGroup are set on the child.
     *
     * @param child the child view to add
     *
     * @see #generateDefaultLayoutParams()
     */
    public void addView(View child) {
        addView(child, -1);
    }
    /**
     * Adds a child view. If no layout parameters are already set on the child, the
     * default parameters for this ViewGroup are set on the child.
     *
     * @param child the child view to add
     * @param index the position at which to add the child
     *
     * @see #generateDefaultLayoutParams()
     */
    public void addView(View child, int index) {
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
            params = generateDefaultLayoutParams(); //返回預設地LayoutParams類,作為該View的屬性值
            if (params == null) {//如果不能獲取到LayoutParams物件,則丟擲異常。
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);
    }
    /**
     * Adds a child view with this ViewGroup's default layout parameters and the
     * specified width and height.
     *
     * @param child the child view to add
     */
    public void addView(View child, int width, int height) {
    	//返回預設地LayoutParams類,作為該View的屬性值
    	final LayoutParams params = generateDefaultLayoutParams(); 
        params.width = width;   //重新設定width值
        params.height = height; //重新設定height值
        addView(child, -1, params); //這兒,我們有指定width、height的大小了。
    }
    /**
     * Adds a child view with the specified layout parameters.
     *
     * @param child the child view to add
     * @param params the layout parameters to set on the child
     */
    public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }
    /**
     * Adds a child view with the specified layout parameters.
     *
     * @param child the child view to add
     * @param index the position at which to add the child
     * @param params the layout parameters to set on the child
     */
    public void addView(View child, int index, LayoutParams params) {
        ...
        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate();
        addViewInner(child, index, params, false);
    }
    /**
     * Returns a set of default layout parameters. These parameters are requested
     * when the View passed to {@link #addView(View)} has no layout parameters
     * already set. If null is returned, an exception is thrown from addView.
     *
     * @return a set of default layout parameters or null
     */
    protected LayoutParams generateDefaultLayoutParams() {
        //width 為 WRAP_CONTENT大小 , height 為WRAP_CONTENT 
    	//ViewGroup的子類可以重寫該方法,達到其特定要求。稍後會以LinearLayout類為例說明。
    	return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {

        if (!checkLayoutParams(params)) { //params物件是否為null
            params = generateLayoutParams(params); //如果params物件是為null,重新構造個LayoutParams物件
        }
        //preventRequestLayout值為false
        if (preventRequestLayout) {  
            child.mLayoutParams = params; //為View的mLayoutParams屬性賦值
        } else {
            child.setLayoutParams(params);//為View的mLayoutParams屬性賦值,但會呼叫requestLayout()請求重新佈局
        }
        //if else 語句會設定View為mLayoutParams屬性賦值
        ...
    }
	...
}

      主要功能就是在新增子View時為其構建了一個LayoutParams物件。但更重要的是,ViewGroup的子類可以過載

 上面的幾個方法,返回特定的LayoutParams物件,例如:對於LinearLayout而言,則是LinearLayout.LayoutParams

 物件。這麼做地目的是,能在其他需要它的地方,可以將其強制轉換成LinearLayout.LayoutParams物件。

      LinearLayout重寫函式地實現為:

public class LinearLayout extends ViewGroup {
	...
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LinearLayout.LayoutParams(getContext(), attrs);
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        //該LinearLayout是水平方向還是垂直方向
    	if (mOrientation == HORIZONTAL) { 
            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        } else if (mOrientation == VERTICAL) {
            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        }
        return null;
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }
	/**
     * Per-child layout information associated with ViewLinearLayout.
     * 
     * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight
     * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity
     */ //自定義的LayoutParams類
    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        /**
         * Indicates how much of the extra space in the LinearLayout will be
         * allocated to the view associated with these LayoutParams. Specify
         * 0 if the view should not be stretched. Otherwise the extra pixels
         * will be pro-rated among all views whose weight is greater than 0.
         */
        @ViewDebug.ExportedProperty(category = "layout")
        public float weight;      //  見於屬性,android:layout_weight=""  ;
        /**
         * Gravity for the view associated with these LayoutParams.
         *
         * @see android.view.Gravity
         */
        public int gravity = -1;  // 見於屬性, android:layout_gravity=""  ; 
        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a =c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

            a.recycle();
        }
        /**
         * {@inheritDoc}
         */
        public LayoutParams(int width, int height) {
            super(width, height);
            weight = 0;
        }
        /**
         * Creates a new set of layout parameters with the specified width, height
         * and weight.
         *
         * @param width the width, either {@link #MATCH_PARENT},
         *        {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param height the height, either {@link #MATCH_PARENT},
         *        {@link #WRAP_CONTENT} or a fixed size in pixels
         * @param weight the weight
         */
        public LayoutParams(int width, int height, float weight) {
            super(width, height);
            this.weight = weight;
        }
        public LayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }
        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }
    }
	...
}

       LinearLayout.LayoutParams類繼承至ViewGroup.MarginLayoutParams類,添加了對android:layout_weight以及

   android:layout_gravity這兩個屬性的獲取和儲存。而且它的重寫函式返回的都是LinearLayout.LayoutParams

   型別。樣,我們可以再對子View進行其他操作時,可以將將其強制轉換成LinearLayout.LayoutParams物件進行

   使用。

         例如,LinearLayout進行measure過程,使用了LinearLayout.LayoutParam物件,有如下程式碼:

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);
	    }
	}
	 /**
     * Measures the children when the orientation of this LinearLayout is set
     * to {@link #VERTICAL}.
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
     * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
     *
     * @see #getOrientation()
     * @see #setOrientation(int)
     * @see #onMeasure(int, int)
     */
	  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
	        mTotalLength = 0;
	        ...
	        // See how tall everyone is. Also remember max width.
	        for (int i = 0; i < count; ++i) {
	            final View child = getVirtualChildAt(i); //獲得索引處為i的子VIew   
                ...
                //注意,我們將型別為 ViewGroup.LayoutParams的例項物件強制轉換為了LinearLayout.LayoutParams,
                //即父物件轉換為了子物件,能這樣做的原因就是LinearLayout的所有子View的LayoutParams型別都為
                //LinearLayout.LayoutParams
	            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
                ...
	    }
	...
}

        超類ViewGroup.LayoutParams強制轉換為了子類LinearLayout.LayoutParams,因為LinearLayout的每個

  ”直接“子ViewLayoutParams屬性都是LinearLayout.LayoutParams型別,因此可以安全轉換。

       PS : Android 2.3原始碼Launcher2中也實現了自定義的LayoutParams類,在IDLE介面的每個View至少包含如下

  資訊:所在X方向的單元格索引和高度、所在Y方向的單元格索引和高度等。

路徑: packages\apps\Launcher2\src\com\android\launcher2\CellLayout.java

public class CellLayout extends ViewGroup {
	... 
	public static class LayoutParams extends ViewGroup.MarginLayoutParams {
	        /**
	         * Horizontal location of the item in the grid.
	         */
	        public int cellX;   //X方向的單元格索引
	        /**
	         * Vertical location of the item in the grid.
	         */
	        public int cellY;   //Y方向的單元格索引
	        /**
	         * Number of cells spanned horizontally by the item.
	         */
	        public int cellHSpan;  //水平方向所佔高度
	        /**
	         * Number of cells spanned vertically by the item.
	         */
	        public int cellVSpan;  //垂直方向所佔高度
            ...
	        public LayoutParams(Context c, AttributeSet attrs) {
	            super(c, attrs);
	            cellHSpan = 1;  //預設為高度 1
	            cellVSpan = 1;
	        }

	        public LayoutParams(ViewGroup.LayoutParams source) {
	            super(source); //預設為高度 1
	            cellHSpan = 1;
	            cellVSpan = 1;
	        }
	        
	        public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {
	            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
	            this.cellX = cellX;
	            this.cellY = cellY;
	            this.cellHSpan = cellHSpan;
	            this.cellVSpan = cellVSpan;
	        }
	        ...
	    }
	...
}

     對該自定義CellLayout.LayoutParams類的使用可以參考LinearLayout.LayoutParams類,我也不再贅述了。

 方法2流程分析

        使用屬性android:layout_heigth=””以及android:layout_weight=”” 時,為某個View設定LayoutParams值。

       其實這種賦值方法其實也如同前面那種,只不過它需要一個前期孵化過程---需要利用XML解析將佈局檔案

  解析成一個完整的View樹,可別小看它了,所有Xxx.xml的佈局檔案都需要解析成一個完整的View樹。下面,

  我們就來仔細走這個過程,重點關注如下兩個方面

         ①、xml佈局是如何解析成View樹的 ;

         ②、android:layout_heigth=””和android:layout_weight=””的解析。

        PS: 一直以來,我都想當然android:layout_heigth以及android:layout_weight這兩個屬性的解析過程是在

   View.java內部完成的,但當我真正去找尋時,卻一直沒有在View.java類或者ViewGroup.java類找到。直到一位

   網友的一次提問,才發現它們的藏身之地。

3、佈局檔案解析流程分析

       解析佈局檔案時,使用的類為LayoutInflater。 關於該類的使用請參考如下部落格:

      主要有如下API方法:

public View inflate (XmlPullParser parser, ViewGroup root, boolean attachToRoot)

public View inflate(int resource, ViewGroup root)

public View inflate(int resource, ViewGroup root, boolean attachToRoot)

這三個類主要迷惑之處在於地三個引數attachToRoot,即是否將該View樹新增到root中去。具體可看這篇部落格:

    當然還有LayoutInflater的inflate()的其他過載方法,大家可以自行了解下。

    我利用下面的例子給大家走走這個流程 :

public class MainActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //1、該方法最終也會呼叫到 LayoutInflater的inflate()方法中去解析。
        setContentView(R.layout.main);
        
        //2、使用常見的API方法去解析xml佈局檔案,
        LayoutInflater layoutInflater = (LayoutInflater)getSystemService();
        View root = layoutInflater.inflate(R.layout.main, null);
    }
}

   Step 1、獲得LayoutInflater的引用。

 路徑:\frameworks\base\core\java\android\app\ContextImpl.java

/**
 * Common implementation of Context API, which provides the base
 * context object for Activity and other application components.
 */
class ContextImpl extends Context {
	if (WINDOW_SERVICE.equals(name)) {
        return WindowManagerImpl.getDefault();
    } else if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        synchronized (mSync) {
            LayoutInflater inflater = mLayoutInflater;
            //是否已經賦值,如果是,直接返回引用
            if (inflater != null) {
                return inflater;
            }
            //返回一個LayoutInflater物件,getOuterContext()指的是我們的Activity、Service或者Application引用
            mLayoutInflater = inflater = PolicyManager.makeNewLayoutInflater(getOuterContext());
            return inflater;
        }
    } else if (ACTIVITY_SERVICE.equals(name)) {
        return getActivityManager();
    }...
}

         繼續去PolicyManager查詢對應函式,看看內部實現。    

徑:frameworks\base\core\java\com\android\internal\policy\PolicyManager.java

public final class PolicyManager {
	private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy";
	private static final IPolicy sPolicy;   // 這可不是Binder機制額,這只是是一個介面,別想多啦
	static {
        // Pull in the actual implementation of the policy at run-time
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        }
        ...
	}
	...
	public static LayoutInflater makeNewLayoutInflater(Context context) {
        return sPolicy.makeNewLayoutInflater(context); //繼續去實現類中去查詢
    }
}
    IPolicy介面的實現對為Policy類。路徑:/frameworks/base/policy/src/com/android/internal/policy/impl/Policy.java
//Simple implementation of the policy interface that spawns the right
//set of objects
public class Policy implements IPolicy{
	...
    public PhoneLayoutInflater makeNewLayoutInflater(Context context) {
        //實際上返回的是PhoneLayoutInflater類。
		return new PhoneLayoutInflater(context);
    }
}
//PhoneLayoutInflater繼承至LayoutInflater類
public class PhoneLayoutInflater extends LayoutInflater {
	...
	/**
     * Instead of instantiating directly, you should retrieve an instance
     * through {@link Context#getSystemService}
     * 
     * @param context The Context in which in which to find resources and other
     *                application-specific things.
     * 
     * @see Context#getSystemService
     */
    public PhoneLayoutInflater(Context context) {
        super(context);
    }
	...
}

       LayoutInflater是個抽象類,實際上我們返回的是PhoneLayoutInflater類,但解析過程的操作基本上是在

  LayoutInflater中完成地。



   Step 2、呼叫inflate()方法去解析佈局檔案。
public abstract class LayoutInflater {
    ...
    public View inflate(int resource, ViewGroup root) {
    	//繼續看下個函式,注意root為null
        return inflate(resource, root, root != null); 
    }
    
    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    	//獲取一個XmlResourceParser來解析XML檔案---佈局檔案。
    	//XmlResourceParser類以及xml是如何解析的,大家自己有興趣找找。
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
}
/**
 * The XML parsing interface returned for an XML resource.  This is a standard
 * XmlPullParser interface, as well as an extended AttributeSet interface and
 * an additional close() method on this interface for the client to indicate
 * when it is done reading the resource.
 */
public interface XmlResourceParser extends XmlPullParser, AttributeSet {
    /**
     * Close this interface to the resource.  Calls on the interface are no
     * longer value after this call.
     */
    public void close();
}


      我們獲得了一個當前應用程式環境的XmlResourceParser物件,該物件的主要作用就是來解析xml佈局檔案的。

  XmlResourceParser類是個介面類,更多關於XML解析的,大家可以參考下面部落格:

 Step 3 、真正地開始解析工作 。
public abstract class LayoutInflater {
    ...
    /**
     * Inflate a new view hierarchy from the specified XML node. Throws
     * {@link InflateException} if there is an error.
     */
    //我們傳遞過來的引數如下: root 為null , attachToRoot為false 。
    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;  //該mConstructorArgs屬性最後會作為引數傳遞給View的建構函式
            View result = root;  //根View

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                ...
                final String name = parser.getName();  //節點名,即API中的控制元件或者自定義View完整限定名。
                if (TAG_MERGE.equals(name)) { // 處理<merge />標籤
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //將<merge />標籤的View樹新增至root中,該函式稍後講到。
                    rInflate(parser, root, attrs);
                } else {
                    // Temp is the root view that was found in the xml
                	//建立該xml佈局檔案所對應的根View。
                    View temp = createViewFromTag(name, attrs); 

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        // Create layout params that match root, if supplied
                    	//根據AttributeSet屬性獲得一個LayoutParams例項,記住呼叫者為root。
                        params = root.generateLayoutParams(attrs); 
                        if (!attachToRoot) { //重新設定temp的LayoutParams
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    // Inflate all children under temp
                    //新增所有其子節點,即新增所有字View
                    rInflate(parser, temp, attrs);
                    
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
            ...
            return result;
        }
    }
    
    /*
     * default visibility so the BridgeInflater can override it.
     */
    View createViewFromTag(String name, AttributeSet attrs) {
    	//節點是否為View,如果是將其重新賦值,形如 <View class="com.qin.xxxView"></View>
        if (name.equals("view")) {  
            name = attrs.getAttributeValue(null, "class");
        }
        try {
            View view = (mFactory == null) ? null : mFactory.onCreateView(name,
                    mContext, attrs);  //沒有設定工廠方法

            if (view == null) {
                //通過這個判斷是Android API的View,還是自定義View
            	if (-1 == name.indexOf('.')) {
                    view = onCreateView(name, attrs); //建立Android API的View例項
                } else {
                    view = createView(name, null, attrs);//建立一個自定義View例項
                }
            }
            return view;
        } 
        ...
    }
    //獲得具體檢視的例項物件
    public final View createView(String name, String prefix, AttributeSet attrs) {
		Constructor constructor = sConstructorMap.get(name);
		Class clazz = null;
		//以下功能主要是獲取如下三個類物件:
		//1、類載入器  ClassLoader
		//2、Class物件
		//3、類的構造方法控制代碼 Constructor
		try {
		    if (constructor == null) {
		    // Class not found in the cache, see if it's real, and try to add it
		    clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name);
		    ...
		    constructor = clazz.getConstructor(mConstructorSignature);
		    sConstructorMap.put(name, constructor);
		} else {
		    // If we have a filter, apply it to cached constructor
		    if (mFilter != null) {
		        ...   
		    }
		}
		    //傳遞引數獲得該View例項物件
		    Object[] args = mConstructorArgs;
		    args[1] = attrs;
		    return (View) constructor.newInstance(args);
		} 
		...
	}

}

  這段程式碼的作用是獲取xml佈局檔案的root View,做了如下兩件事情

          1、獲取xml佈局的View例項,通過createViewFromTag()方法獲取,該方法會判斷節點名是API 控制元件

            還是自定義控制元件,繼而呼叫合適的方法去例項化View。

          2、判斷root以及attachToRoot引數,重新設定root View值以及temp變數的LayoutParams值。

        如果仔細看著段程式碼,不知大家心裡有沒有疑惑:當root為null時,我們的temp變數的LayoutParams值是為

  null的,即它不會被賦值?有個View的LayoutParams值為空,那麼,在系統中不會報異常嗎?見下面部分

  程式碼:

    //我們傳遞過來的引數如下: root 為null , attachToRoot為false 。
    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            try {
                
                ...
                if (TAG_MERGE.equals(name)) { // 處理<merge />標籤
                    ...
                } else {
                    // Temp is the root view that was found in the xml
                	//建立該xml佈局檔案所對應的根View。
                    View temp = createViewFromTag(name, attrs); 
                    ViewGroup.LayoutParams params = null;

                    //注意!!! root為null時,temp變數的LayoutParams屬性不會被賦值的。
                    if (root != null) {
                        // Create layout params that match root, if supplied
                    	//根據AttributeSet屬性獲得一個LayoutParams例項,記住呼叫者為root。
                        params = root.generateLayoutParams(attrs); 
                        if (!attachToRoot) { //重新設定temp的LayoutParams
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    ...
                }
            } 
            ...
        }
    }

        關於這個問題的詳細答案,我會在後面講到。這兒我簡單說下,任何View樹的頂層View被新增至視窗時,

  一般呼叫WindowManager.addView()新增至視窗時,在這個方法中去做進一步處理。即使LayoutParams

  值空,UI框架每次measure()時都忽略該View的LayoutParams值,而是直接傳遞MeasureSpec值至View樹。

       接下來,我們關注另外一個函式,rInflate(),該方法會遞迴呼叫每個View下的子節點,以當前View作為根View

 形成一個View樹。

    /**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     */
    //遞迴呼叫每個位元組點
    private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)
            throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            
            if (TAG_REQUEST_FOCUS.equals(name)) { //處理<requestFocus />標籤
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) { //處理<include />標籤
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs);//解析<include />節點
            } else if (TAG_MERGE.equals(name)) { //處理<merge />標籤
                throw new InflateException("<merge /> must be the root element");
            } else {
            	//根據節點名構建一個View例項物件
                final View view = createViewFromTag(name, attrs); 
                final ViewGroup viewGroup = (ViewGroup) parent;
                //呼叫generateLayoutParams()方法返回一個LayoutParams例項物件,
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs); //繼續遞迴呼叫
                viewGroup.addView(view, params); //OK,將該View以特定LayoutParams值新增至父View中
            }
        }
        parent.onFinishInflate();  //完成了解析過程,通知....
    }

          值得注意的是,每次addView前都呼叫了viewGroup.generateLayoutParams(attrs)去構建一個LayoutParams

  例項,然後在addView()方法中為其賦值。參見如下程式碼:ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	...
	
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }
	public static class LayoutParams {
        ... //會呼叫這個建構函式
        public LayoutParams(Context c, AttributeSet attrs) {
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_Layout_layout_width,
                    R.styleable.ViewGroup_Layout_layout_height);
            a.recycle();
        }
        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
            width = a.getLayoutDimension(widthAttr, "layout_width");
            height = a.getLayoutDimension(heightAttr, "layout_height");
        }
	
}

    好吧 ~~ 我們還是探尋根底,去TypeArray類的getLayoutDimension()看看。

路徑:/frameworks/base/core/java/android/content/res/TypedArray.java

public class TypedArray {
	...
	/**
     * Special version of {@link #getDimensionPixelSize} for retrieving
     * {@link android.view.ViewGroup}'s layout_width and layout_height
     * attributes.  This is only here for performance reasons; applications
     * should use {@link #getDimensionPixelSize}.
     * 
     * @param index Index of the attribute to retrieve.
     * @param name Textual name of attribute for error reporting.
     * 
     * @return Attribute dimension value multiplied by the appropriate 
     * metric and truncated to integer pixels.
     */
    public int getLayoutDimension(int index, String name) {
        index *= AssetManager.STYLE_NUM_ENTRIES;
        final int[] data = mData;
        //獲得屬性對應的識別符號 , Identifies,目前還沒有仔細研究相關類。
        final int type = data[index+AssetManager.STYLE_TYPE];
        if (type >= TypedValue.TYPE_FIRST_INT
                && type <= TypedValue.TYPE_LAST_INT) {
            return data[index+AssetManager.STYLE_DATA];
        } else if (type == TypedValue.TYPE_DIMENSION) { //型別為dimension型別
            return TypedValue.complexToDimensionPixelSize(
                data[index+AssetManager.STYLE_DATA], mResources.mMetrics);
        }
        //沒有提供layout_weight和layout_height會來到此處 ,這兒會報異常!
        //因此佈局檔案中的View包括自定義View必須加上屬性layout_weight和layout_height。
        throw new RuntimeException(getPositionDescription()
                + ": You must supply a " + name + " attribute.");
    }
	...
}

         從上面得知,  我們將View的AttributeSet屬性傳遞給generateLayoutParams()方法,讓其構建合適地

   LayoutParams物件,並且初始化屬性值weight和height。同時我們也得知 佈局檔案中的View包括自定義View

   必須加上屬性layout_weight和layout_height,否則會報異常。

    Step 3 主要做了如下事情:
       首先,獲得了了佈局檔案地root View,即佈局檔案中最頂層的View。

       其次,通過遞迴呼叫,我們形成了整個View樹以及設定了每個View的LayoutParams物件。

    總結:通過對佈局檔案的解析流程的學習,也就是轉換為View樹的過程,我們明白瞭解析過程的箇中奧妙,以及

設定ViewLayoutParams物件的過程。但是,我們這兒只是簡單的浮光掠影,更深層次的內容希望大家能深入學習。




      本來是準備接下去往下寫的,但無奈貼出來的程式碼太多,文章有點長而且自己也有點凌亂了,因此決定做兩篇

  部落格發表吧。下篇內容包括如下方面:

        1、MeasureSpec類說明 ;

        2、measure過程中如何正確設定每個View的長寬 ;

        3、UI框架正確設定頂層View的LayoutParams物件,對Activity而言,頂層View則是DecorView,

   其他的皆是普通View了。