1. 程式人生 > >xml檔案的根節點layout_width或者layout_height設定無效果的原因分析

xml檔案的根節點layout_width或者layout_height設定無效果的原因分析

在android開發中相信大家對ListView、GridView等組建都很熟悉,在使用它們的時候需要自己配置相關的Adapter,並且配置現骨幹的xml檔案作為ListView等組建的子View,這些xml檔案在Adapter的getView方法中呼叫。例如:

public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView==null) {
            convertView = App.getLayoutInflater().inflate(R.layout.item, null);
        }
        
        return convertView;
    }
item.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="400dp"
    android:background="#008000"
     >
        <ImageView
            android:src="@drawable/ic_launcher"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
        </ImageView>
</RelativeLayout>

用上面的方法會發現無論根View也就是RelativeLayout的layout_width和layout_height設定多大,執行效果始終都是一樣的;也就是說此時你想通過RelativeLayout來改變裡面子ImageView的大小是行不通的,通常用的解決方式就是裡面在新增一個View把ImageView包裹起來,通過設定該View的大小來改變ImageView的大小(注意不一定是ImageView,也可能裡面包含了若干個view):
<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="400dp"
    android:background="#008000"
     >
    <RelativeLayout
        android:layout_width="900dp"
        android:layout_height="200dp"
        android:background="@android:color/black">
        <ImageView
            android:src="@drawable/ic_launcher"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
        </ImageView>
    </RelativeLayout>

</RelativeLayout>
雖然找到了解決方法,但是知其然還要知其所以然,為什麼好這樣呢?在分析之前要弄清一個概念性的問題:

layout_width不是width!layout_height不是height!也就是說這兩個屬性設定的並不是View的寬和高,layout是佈局的意思,也即是說這兩個屬性是View在佈局中的寬和高!既然是佈局,肯定都有個放置該View的地方,也就是說有一個媒介來放置View,並且在該媒介上劃出一個layout_width和layout_heigth的大小來放置該View。如果沒有該媒介View佈局在哪兒呢?所以說為了上面的問題的根本原因就是因為你沒有為xml檔案設定一個佈局媒介(該媒介也是個View,也即是rootView),所以為了保障你的item.xml中根View的layout_width和layout_heigth能起作用,需要設定一個這樣的媒介。程式碼inflate(int,ViewGroup root)中這個root就是這樣的一個媒介,但是通常傳遞的都是null,所以item.xml檔案的根View是沒有媒介作為佈局依據的,所以不起作用;既然問題的原因找到了那麼就可以用一個笨的方法來解決這個問題:為inflate提供一個root,在程式碼裡面我簡單的做了一下處理,驗證了自己的想法:

	public View getView(int position, View convertView, ViewGroup parent) {
		if(convertView==null) {
			convertView = App.getLayoutInflater().inflate(R.layout.item, null);
                        //手動設定一個root,此時在設定layout_width或者layout_height就會起作用了
                        convertView = App.getLayoutInflater().inflate(R.layout.item, (ViewGroup)convertView);
		}
		
		return convertView;
	}
之所以是笨方法,因為它把item做了兩次的xml解析,因為inflate呼叫了兩次。當然此時的item是:
<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="400dp"
    android:background="#008000"
     >
        <ImageView
            android:src="@drawable/ic_launcher"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
        </ImageView>
</RelativeLayout>

既然跟inflate第二個引數root有關,那麼看看root都幹了些什麼,追蹤原始碼最終解析xml的方法程式碼如下:

//切記,此時root為null,attachToRoot在原始碼中為root!=null,所以此處為false
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
        synchronized (mConstructorArgs) {  
            ......
            View result = root;  //根View ,為null
            try {  
                 ....
                final String name = parser.getName();  //節點名,如果是自定義View的話 就是全限定名
               //該if語句暫不用看
               if (TAG_MERGE.equals(name)) { // 處理<merge />標籤  
                   ...
                } else {  
                    // Temp is the root view that was found in the xml  
                    //這個是xml檔案對應的那個根View,在item.xml檔案中就是RelativeLayout  
                    View temp = createViewFromTag(name, attrs);   
  
                    ViewGroup.LayoutParams params = null;  
                    //因為root==null,if條件不成立
                    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  
                    //遍歷temp下所有的子節點,也就是xml檔案跟檔案中的所有字View 
                    rInflate(parser, temp, attrs);  
                      
                    //把xml檔案的根節點以及根節點中的子節點的view都新增到root中,當然此時root等於null的情況下此處是不執行的
                    if (root != null && attachToRoot) {  
                        root.addView(temp, params);  
                    }  
                    //如果根節點為null,就直接返回xml中根節點以及根節點的子節點組成的View
                    if (root == null || !attachToRoot) {  
                        result = temp;  
                    }  
                }  
            }   
            ...  
            return result;  
        }  
    }  

通過分析上面的程式碼我們可以發現:當root為null的時候,inflate直接返回的是xml檔案生成的View,此時返回的View根View就是xml檔案的根節點。此時該View沒有存在任何的佈局中,所以根節點的layout_height和layout_width沒有依附的媒介,導致設定這兩個屬性是無效的。但是如果root不為null的話,inflate返回的View是這麼一個View,首先把xml解析成一個View,然後把該View通過root.addView新增到root裡面去,然後直接返回root。也就是說此時xml檔案中的根節點的layout_width和layout_height就是在root中的佈局的寬和高,所以此時這兩個屬性是有效果的,同時到現在你應該能夠理解getView第三個引數parent所代表的含義了,所以上述手動新增root的程式碼也可以改成這樣:

public View getView(int position, View convertView, ViewGroup parent) {
		if(convertView==null) {
		      //parent為單獨的xml檔案,用item.xml測試也可以達到效果
<pre name="code" class="java">                       parent= App.getLayoutInflater().inflate(R.layout.parent, null);
                        //手動設定一個root,此時在設定layout_width或者layout_height就會起作用了
                        convertView = App.getLayoutInflater().inflate(R.layout.item,parent);
		}
		
		return convertView;
	}
不過寫到這裡我有個疑問的地方,getView方法的呼叫是在AbsListView裡面的obtainView方法裡面呼叫的,obtainView在GridView的父類AbsListView裡面定義的,並且傳遞的第三個引數為this:
final View child = mAdapter.getView(position, scrapView, this);
既然這樣為什麼不直接在getView方法裡面把parent直接作為引數傳進去呢?不需要parent=inflate(layoutId,null)了,但是使用的效果是程式執行會丟擲異常:java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView,


檢視原始碼可以知道GridView的父類AdapterView又把View裡的addView及其過載方法都給重寫了一遍,重寫的只是簡單的丟擲一個異常讓丟擲了一個異常: 

    public void addView(View child, LayoutParams params) {
        throw new UnsupportedOperationException("addView(View, LayoutParams) "
                + "is not supported in AdapterView");
    }

所以這就知道為什麼parent不能直接傳給inflate了,通過上面的分析,infate最終會在root不為null的情況下執行如下程式碼:
//把xml檔案的根節點以及根節點中的子節點的view都新增到root中
                    if (root != null && attachToRoot) {  
                        //此處正是丟擲異常的地方
                        root.addView(temp, params);  
                    }  
所以會丟擲異常也不會見怪了!所以說在這裡我比較懷疑,既然在AbsListView裡面把adapter.getView的引數傳遞this,但是為什麼又遮蔽了addView方法,這點是我現在比較疑惑的地方,水平有限暫不分析了!希望有高手看到此處告知一二,感激不盡!

其實還有一方法:就是在getView裡面建立一個LayoutParams物件,設定該物件的寬和高屬性,並把該param物件通過convertView.setLayoutParams(parms)來重新設定。

注意,在Activity中我們經常呼叫setContentView(int layoutResourceId)來設定Activity的View,,為什麼這些xml檔案根節點的layout_width和layout_height有效果呢?且看下面的分析,分析原始碼發現setContentView實際上也是呼叫了inflate方法來對xml檔案進行解析:

    //Activity中有一個window引用,在Activity的setContentView中會呼叫window.setContentView,該引用指向的物件是PhoneWindow,此段程式碼就是PhoneWindow類中的程式碼
@Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            //該方法中會對mContentParent進行初始化,保證它不會null
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
       //此處程式碼熟悉了吧,正式像上面的Adapter中一樣,只不過此時root不為null!!!!
       mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null) {
            cb.onContentChanged();
        }
    }

可以發現該方法中有一個mContentParent,首先判斷mContentParent是否為null,如果為null的話會在installDecor()方法中對它進行初始化,也就是說mContentParent始終可以得到初始化,緊接著會呼叫inflate(layoutResId,mContentParent)方法,可以看到此時inflate中第二個代表root的引數為mContentparent,且不為null。然後執行該方法對該xml檔案進行解析,最終還會執行上面的inlfate(XmlPullParser,root,attachToRoot)方法中去,根據上面的分析,所以可得出結論在Activity中xml的根節點設定layout_width或者layout_height是有效果的!