[AS嘗龜]Recyclerview的OnCreatViewHolder報錯:java.lang.IllegalStateException

今天在對着例子練習Fragment和Recyclerview做兼容平板和手機的新聞客戶端時,運行時一直報錯,查看日誌,發現這個錯誤:


FATAL EXCEPTION: main

 Process: com.example.tahlia.newsclient, PID: 4410
 java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.


大意就是我所指定的子View已經擁有父View了,而按照規定子View是不允許擁有兩個父View的,因此我需要將這個子View已經添加的父View去除,纔可以重新添加一個父View。看到這裏其實我都是一臉懵逼的,因爲不清楚哪一步添加了父View。

檢查了一下代碼,發現是Recyclerview中OnCreatViewHolder出現的錯誤:我將

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_title_item, parent, false);
錯寫成了:

View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_title_item, parent);



inflate方法是具有兩個參數和三個參數的。在兩個參數的語句下,默認第三個參數爲true,具體使用區別和解釋請看下文轉載部分


**************************************************************************************************

原文鏈接:https://www.bignerdranch.com/blog/understanding-androids-layoutinflater-inflate/

譯文鏈接:http://blog.chengdazhi.com/index.php/110


************

由於我們很容易習慣公式化的預置代碼,有時我們會忽略很優雅的細節。LayoutInflater以及它在Fragment的onCreateView()中填充View的方式帶給我的就是這樣的感受。這個類用於將XML文件轉換成相對應的ViewGroup和控件Widget。我嘗試在Google官方文檔與網絡上其他討論中尋找有關的說明,而後發現許多人不但不清楚LayoutInflater的inflate()方法的細節,而且甚至在誤用它。

這裏的困惑很大程度上是因爲Google上有關attachToRoot(也就是inflate()方法第三個參數)的文檔太模糊:

被填充的層是否應該附在root參數內部?如果是false,root參數只適用於爲XML根元素View創建正確的LayoutParams的子類。

其實意思就是:如果attachToRoot是true的話,那第一個參數的layout文件就會被填充並附加在第二個參數所指定的ViewGroup內。方法返回結合後的View,根元素是第二個參數ViewGroup。如果是false的話,第一個參數所指定的layout文件會被填充並作爲View返回。這個View的根元素就是layout文件的根元素。不管是true還是false,都需要ViewGroup的LayoutParams來正確的測量與放置layout文件所產生的View對象。

attachToRoot傳入true代表layout文件填充的View會被直接添加進ViewGroup,而傳入false則代表創建的View會以其他方式被添加進ViewGroup。

讓我們就兩種情況多舉一些例子來更深入的理解。

attachToRoot是True

假設我們在XML layout文件中寫了一個Button並指定了寬高爲match_parent。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/custom_button">
</Button>

現在我們想動態地把這個按鈕添加進Fragment或Activity的LinearLayout中。如果這裏LinearLayout已經是一個成員變量mLinearLayout了,我們只需要通過如下代碼達成目標:

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

我們指定了用於填充button的layout資源文件,然後我們告訴LayoutInflater我們想把button添加到mLinearLayout中。這裏Button的LayoutParams種類爲LinearLayout.LayoutParams。

下面的代碼也有同樣的效果。LayoutInflater的兩個參數的inflate()方法自動將attachToRoot設置爲true。

inflater.inflate(R.layout.custom_button, mLinearLayout);


另一種在attachToRoot中傳遞true的情況是使用自定義View。我們看一個layout文件中根元素有標籤的例子。標籤標識着這個layout文件的根ViewGroup可以有多種類型。

public class MyCustomView extends LinearLayout {
    ...
    private void init() {
    LayoutInflater inflater = LayoutInflater.from(getContext());
    inflater.inflate(R.layout.view_with_merge_tag, this);
    }
}

這就是一個很好的使用attachToRoot的例子。這個例子中layout文件沒有ViewGroup作爲根元素,所以我們指定我們自定義的LinearLayout作爲根元素。如果layout文件有一個FrameLayout作爲根元素而不是,那麼FrameLayout和它的子元素都可以正常填充,而後都會被添加到LinearLayout中,LinearLayout是根ViewGroup,包含着FrameLayout和其子元素。

attachToRoot是False

我們看一下什麼時候attachToRoot應該是false。在這種情況下,inflate()方法中的第一個參數所指定的View不會被添加到第二個參數所指定的ViewGroup中。

回憶一下剛纔的例子中的Button,我們想通過layout文件添加自定義的Button至mLinearLayout中。當attachToRoot爲false時,我們仍可以將Button添加到mLinearLayout中,但是這需要我們自己動手。

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);

這兩行代碼與剛纔attachToRoot爲true時的一行代碼等效。通過傳入false,我們告訴LayoutInflater我們不暫時還想將View添加到根元素ViewGroup中,意思是我們一會兒再添加。在這個例子中,一會兒再添加就是在inflate()後調用addView()方法。

在將attachToRoot設置爲false的例子中,由於要手動添加View進ViewGroup導致代碼變多了。將Button添加到LinearLayout中還是用一行代碼直接將attachToRoot設置爲true簡便一些。下面我們看一下什麼情況下attachToRoot必須傳入false。

每一個RecyclerView的子元素都要在attachToRoot設置爲false的情況下填充。這裏子View在onCreateViewHolder()中填充。

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    LayoutInflater inflater = LayoutInflater.from(getActivity());
    View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
    return new ViewHolder(view);
}

RecyclerView負責決定什麼時候展示它的子View,這個不由我們決定。在任何我們不負責將View添加進ViewGroup的情況下都應該將attachToRoot設置爲false。

當在Fragment的onCreateView()方法中填充並返回View時,要將attachToRoot設爲false。如果傳入true,會拋出IllegalStateException,因爲指定的子View已經有父View了。你需要指定在哪裏將Fragment的View放進Activity裏,而添加、移除或替換Fragment則是FragmentManager的事情。

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);

if (fragment == null) {
    fragment = new MainFragment();
    fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}

上面代碼中root_viewGroup就是Activity中用於放置Fragment的容器,它會作爲inflate()方法中的第二個參數被傳入onCreateView()中。它也是你在inflate()方法中傳入的ViewGroup。FragmentManager會將Fragment的View添加到ViewGroup中,你可不想添加兩次。


public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
    …
    return view;
}

問題是:如果我們不需在onCreateView()中將View添加進ViewGroup,爲什麼還要傳入ViewGroup呢?爲什麼inflate()方法必須要傳入根ViewGroup?

原因是及時不需要馬上將新填充的View添加進ViewGroup,我們還是需要這個父元素的LayoutParams來在將來添加時決定View的size和position。

你在網上一定會遇到一些不正確的建議。有些人會建議你如果將attachToRoot設置爲false的話直接將根ViewGroup傳入null。但是,如果有父元素的話,還是應該傳入的。


Lint會警告你不要講null作爲root傳入。你的App不會掛掉,但是可能會表現異常。當你的子View沒有正確的LayoutParams時,它會自己通過generateDefaultLayoutParams)計算。

你可能並不想要這些默認的LayoutParams。你在XML指定的LayoutParams會被忽略。我們可能已經指定了子View要填充父元素的寬度,但父View又wrap_content導致最終的View小很多。

下面是一種沒有ViewGroup作爲root傳入inflate()方法的情況。當爲AlertDialog創建自定義View時,還無法訪問父元素。

AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext);
View customView = inflater.inflate(R.layout.custom_alert_dialog, null);
...
dialogBuilder.setView(customView);
dialogBuilder.show();

在這種情況下,可以將null作爲root ViewGroup傳入。後來我發現AlertDialog還是會重寫LayoutParams並設置各項參數爲match_parent。但是,規則還是在有ViewGroup可以傳入時傳入它。


避開崩潰、異常表現與誤解

希望這篇文章可以幫助你在使用LayoutInflater時避開崩潰、異常表現與誤解。下面整理了文章的要點:

  • 如果可以傳入ViewGroup作爲根元素,那就傳入它。
  • 避免將null作爲根ViewGroup傳入。
  • 當我們不負責將layout文件的View添加進ViewGroup時設置attachToRoot參數爲false。
  • 不要在View已經被添加進ViewGroup時傳入true。
  • 自定義View時很適合將attachToRoot設置爲true。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章