Layout Inflation :Unconditional layout, inflation from view adapter

yufan發表於2016-01-07

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;
}

然後結果就變成這樣了:

Image11

為什麼設定的固定高度不起作用?這是因為你沒有把所有子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;
}

這樣我們就得到了想要的結果:

Image21

任何規則都有例外

當然,也有需要在載入佈局的時候指定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

相關文章