xml檔案的根節點layout_width或者layout_height設定無效果的原因分析
在android開發中相信大家對ListView、GridView等組建都很熟悉,在使用它們的時候需要自己配置相關的Adapter,並且配置現骨幹的xml檔案作為ListView等組建的子View,這些xml檔案在Adapter的getView方法中呼叫。例如:
item.xml檔案如下:public View getView(int position, View convertView, ViewGroup parent) { if(convertView==null) { convertView = App.getLayoutInflater().inflate(R.layout.item, null); } return convertView; }
<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是有效果的!