Layout inflation在Android上下文環境下轉換XML文件成View結構對象的時候需要用到。
LayoutInflater這個對象在Android的SDK中很常見,但是你絕對沒想到竟然能夠找到一個使用誤區。說不定你的App裏就是這麼用的!如果你在寫APP的時候像如下代碼一樣使用LayoutInflater的話:
1
|
inflater.inflate(R.layout.my_layout,
null ); |
請你繼續讀完這篇文章,稍後我會解釋爲什麼這樣做不對。
認識LayoutInflater
首先看一下LayoutInflater的工作原理,有兩個重載的版本可以使用:
inflate(int resource, ViewGroup root)
和 inflate(int
resource, ViewGroup root, boolean attachToRoot)
第一個參數指出要載入的佈局文件資源,第二個參數指出視圖結構中載入的佈局將要放入的根視圖。如果有第三個參數,那麼它用來決定是否把載入後的視圖綁定到給出的根視圖中。
最後兩個參數可能會導致一些問題。如果使用兩個參數的版本,Layoutinflater會自動嘗試把載入的視圖綁定到給定的根視圖對象中。但是,如果你傳遞null
,系統就不會嘗試綁定操作了,否則應用程序就崩潰了。
很多開發者會這樣做,認爲傳遞null
作爲根視圖就可以禁用綁定操作了。很多時候很多開發者甚至不知道還有三個參數的Layoutinflater版本的存在,如果這麼做的話,也會同時禁用了根視圖的一個很重要的函數……但是之前我沒有研究過。
框架中的示例
現在我們來仔細看看Android框架關於動態載入佈局的場景。
Adapter是最常用的場景,我們經常需要使用LayoutInflater
來自定義ListView
(通過重寫getView()
方法),具體的方法簽名是這樣的:
1
|
getView( int
position, View convertView, ViewGroup parent) |
Fragment也會用到inflation操作,通過onCreateView()
方法創建view的時候會用到。這個方法的簽名是這樣的:
1
|
onCreateView(LayoutInflater
inflater, ViewGroup container, Bundle savedInstanceState) |
不知你有沒有注意到這一點,每次Framework需要你去載入一個佈局文件時,都會傳入一個ViewGroup參數(最後需要綁定到的根視圖),如果Layoutinflater設爲自動綁定到根視圖的話,會拋出一個異常。
所以你想想看,如果我做綁定操作的話,爲什麼要給你一個ViewGroup參數呢?事實證明父視圖在這個inflation操作過程中是很重要的,它會計算被載入的XML在根元素中的LayoutParams,如果傳入null
話,就等於是告訴框架“我不知道載入的View要放到哪個父視圖中”。
問題在於,android:layout_xxx屬性會在父視圖對象中被重新計算,結果就是所有你定義的LayoutParams都會被忽略掉(因爲沒有已知的父視圖對象)。然後你就納悶“爲什麼框架會忽略掉我自己定義的佈局屬性呢?還是去StackOverFlow上看看,提一個bug吧”。
如果沒有設置LayoutParams,那麼最終ViewGroup也會給你生成一個默認的屬性,幸運的話(很多時候),這些默認的設置正好和你在XML文件中定義的一樣……所以你就察覺不到其實已經出現問題了。
應用案例
你敢說你沒有在應用中碰到過這樣的場景嗎?看看下面的代碼,爲Listview
簡單地載入一個佈局文件:
R.layout.item_row
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
< LinearLayout
xmlns:android = "http://schemas.android.com/apk/res/android" android:layout_width = "match_parent" android:layout_height = "?android:attr/listPreferredItemHeight" android:gravity = "center_vertical" android:orientation = "horizontal" > < TextView android:id = "@+id/text1" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:paddingRight = "15dp" android:text = "Text1"
/> < TextView android:id = "@+id/text2" android:layout_width = "0dp" android:layout_height = "wrap_content" android:layout_weight = "1" android:text = "Text2"
/> </ LinearLayout > |
這裏我們想把高度設置爲固定高度,上面把它設爲當前主題下的推薦高度……看似很合理。
但是,當我們這樣載入佈局文件的時候,就不對了:
1
2
3
4
5
6
7
|
public
View getView( int
position, View convertView, ViewGroup parent) { if
(convertView == null )
{ convertView
= inflate(R.layout.item_row, null ); } return
convertView; } |
然後結果就變成這樣了:
爲什麼設定的固定高度不起作用?這是因爲你沒有把所有子View的高都設爲固定高度,只需要把根視圖的高設置成wrap_content就可以了。不需要知道爲什麼會這樣(你可以吐槽一下Google爲什麼這麼處理!)。
而如果這樣載入佈局的話就沒有問題:
1
2
3
4
5
6
7
|
public
View getView( int
position, View convertView, ViewGroup parent) { if
(convertView == null )
{ convertView
= inflate(R.layout.item_row, parent, false ); } return
convertView; } |
這樣我們就得到了想要的結果:
任何規則都有例外
當然,也有需要在載入佈局的時候指定null
作爲父佈局對象,但這種情況非常少。一個典型的例子就是爲AlertDialog中載入一個自定義佈局。看看下面的例子,使用和上面一樣的XML佈局文件來作爲對話框的佈局:
1
2
3
4
5
6
7
|
AlertDialog.Builder
builder = new
AlertDialog.Builder(context); View
content = LayoutInflater.from(context).inflate(R.layout.item_row, null ); builder.setTitle( "My
Dialog" ); builder.setView(content); builder.setPositiveButton( "OK" ,
null ); builder.show(); |
這裏的問題就是,AlertDialog.Builder
支持自定義佈局,但是setView()
方法不提供帶有佈局文件作爲參數的版本,所以只能先手動載入XML佈局文件。由於最終會進入到對話框裏面,不會接觸到根佈局(事實上這時候還沒有根佈局),所以我們也操作不了佈局文件的最終父視圖對象,當然也就不能用於載入使用了。事實證明,這些都是無關緊要的,因爲AlertDialog
會擦除佈局上的所有Layoutparams
然後替換爲match_parent
。
所以,下次使用inflate()
函數時,如果還想輸入null
應該停下來想一想“我真的不知道它該放到哪裏嗎?”
最後,你應該想想兩個參數的inflate()
版本作爲一個便捷的使用方式,可以忽略第三個參數(默認爲true
),但是不要想着爲了方便而傳遞一個null
卻忽略了第三個參數會默認是false
。