2011年7月5日火曜日

Android ListFragment でカスタムレイアウトを使うと setEmptyText() が使えない

もともとは adakoda さんのブログのエントリ

[Android] ListFragment の View をカスタマイズする方法 - adakoda - http://goo.gl/UBgyu

で、onCreateView() を override してカスタムレイアウトを指定すると setEmptyText() で落ちるという話があって、正しい ID を指定してないからでは〜?的な流れになったのですが、どうもそうではないようなので調べてみました。

まず、setEmptyText(CharSequence text) は API Level 11 (Android 3.0) で追加された API です。

結果をまとめると

・ListActivity
  ・setEmptyView() : 使える
  ・getEmptyView() : 使える
  ・@android:id/list, @android:id/empty を使った
   カスタムレイアウト : 使える

・ListFragment
  ・@android:id/list, @android:id/empty を使った
   カスタムレイアウト : 使える

  ・カスタムレイアウトを使わない場合
    ・getListView().setEmptyView() : なにもおこらない
    ・getListView().getEmptyView() : 使える
    ・setEmptyText() : 使える

  ・カスタムレイアウトを使った場合
    ・getListView().setEmptyView() : なにもおこらない
     (@android:id/empty がそのまま表示される)
    ・getListView().getEmptyView() : 使える
    ・setEmptyText() : 落ちる

ListFragment で、カスタムレイアウトで setEmptyText() を使いたい場合は、次のように override しておくこと
  1. @Override  
  2. public void setEmptyText(CharSequence text) {  
  3.  TextView tv = (TextView)getListView().getEmptyView();  
  4.  tv.setText(text);  
  5. }  


ちなみに、このときのカスタムレイアウトはこんな感じ
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent">  
  5.       
  6.     <ListView   
  7.         android:id="@android:id/list"  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="match_parent"  
  10.         android:drawSelectorOnTop="false" />  
  11.           
  12.     <TextView   
  13.         android:id="@android:id/empty"  
  14.         android:layout_width="match_parent"  
  15.         android:layout_height="match_parent"  
  16.         android:text="No data"   
  17.         android:textSize="30sp"/>  
  18. </FrameLayout>  


さて、結論をいったので、それまでの過程もちょっと紹介。

まずは、ListFragment の onCreateView() で生成される View のなかから empty 用っぽい View を見つけて、それの ID から resourceName とればわかるんじゃね?と思ってやってみました。

  1. @Override  
  2. public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  3.      Bundle savedInstanceState) {  
  4.   
  5.     View v = super.onCreateView(inflater, container, savedInstanceState);  
  6.     Log.d("NoteListFragment", v.getClass().getName() + ", " + v.getId());  
  7.   
  8.     if (v instanceof ViewGroup) {  
  9.         ViewGroup vg = (ViewGroup) v;  
  10.         for (int i = 0; i < vg.getChildCount(); i++) {  
  11.             View child = vg.getChildAt(i);  
  12.   
  13.             String resourcename = getResourceNameByID(android.R.id.class, child.getId());  
  14.   
  15.             Log.d("NoteListFragment", child.getClass().getName() + ", "  
  16.                           + child.getId() + ", " + resourcename);  
  17.       
  18.             if(child instanceof ViewGroup) {  
  19.                 ViewGroup vg2 = (ViewGroup) child;  
  20.                 for (int j = 0; j < vg2.getChildCount(); j++) {  
  21.                     View child2 = vg2.getChildAt(j);  
  22.   
  23.                     String resourcename2 = getResourceNameByID(android.R.id.class, child2.getId());  
  24.   
  25.                     Log.d("NoteListFragment", child2.getClass().getName() + ", "  
  26.                                  + child2.getId() + ", " + resourcename2);  
  27.                 }       
  28.             }  
  29.         }  
  30.     }    
  31.     return v;  
  32. }  
  33.   
  34. public String getResourceNameByID(Class<?> clazz, int resourceId)  
  35.     throws IllegalArgumentException {  
  36.   
  37.     Field[] fields = clazz.getFields();  
  38.     for (Field f : fields) {  
  39.         try {  
  40.             if (resourceId == f.getInt(null)) {  
  41.                 return f.getName();  
  42.             }  
  43.         } catch (Exception e) {  
  44.             e.printStackTrace();  
  45.             throw new IllegalArgumentException();  
  46.        }  
  47.     }  
  48.     return null;  
  49. }  


リフレクション。リフレクション。
結果はこんな感じ

07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.FrameLayout, -1
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.LinearLayout, 16908904, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.ProgressBar, -1, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.TextView, -1, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.FrameLayout, 16908905, null
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.ListView, 16908298, list
07-05 13:46:40.090: DEBUG/NoteListFragment(30597): android.widget.TextView, 16908906, null

ListView には list (= android.R.id.list) が割当てられているのに、その下の TextView (EmptyView だと思われる) には null が入っている。つまり、android.R.id.** に対象の ID がなかったということです。コードで動的に生成してるのかな?

念のためこの TextView が EmptyView なのかチェック

  1. @Override  
  2. public void onActivityCreated(Bundle savedInstanceState) {  
  3.  super.onActivityCreated(savedInstanceState);  
  4.  ...  
  5.    
  6.  setEmptyText(getActivity().getString(R.string.no_notes));  
  7.  Log.d("NoteListFragment", getListView().getEmptyView().getId() + "");  
  8. }  


結果は

07-05 13:56:22.510: DEBUG/NoteListFragment(30953): 16908906

ビンゴ。

ちなみに setEmptyText() する前に getListView().getEmptyView().getId() するとヌルポになります。
EmptyView が生成されてないんですね。このことからも EmptyView が動的に生成されてるのがわかります。

なので、レイアウトをカスタムしないで setEmptyText を override するときは super.setEmpty() を忘れずに最初にしてください。そうしないとその後の getListView().getEmptyView() には null が返ってきてしまいます。

  1. @Override  
  2. public void setEmptyText(CharSequence text) {  
  3.  super.setEmptyText(text);  // don't forget  
  4.  TextView tv = (TextView)getListView().getEmptyView();  
  5.  tv.setText(text);  
  6. }  



カスタムを使っていてもいなくても、カスタムレイアウトに android:empty が入っていてもいなくても動くようにするには、こんな感じかなぁ。。。

  1. @Override  
  2. public void setEmptyText(CharSequence text) {  
  3.  try {  
  4.   super.setEmptyText(text);  
  5.  }  
  6.  catch (IllegalStateException e1){  
  7.   e1.printStackTrace();  
  8.   Log.d("NoteListFragment", e1.getClass().getName() + " : " + e1.getMessage());  
  9.   
  10.   try {  
  11.    TextView tv = (TextView)getListView().getEmptyView();  
  12.    tv.setText(text);      
  13.   }  
  14.   catch(NullPointerException e2) {      
  15.    e2.printStackTrace();  
  16.    Log.d("NoteListFragment", e2.getClass().getName() + " : " + e2.getMessage());  
  17.   
  18.    TextView tv = new TextView(getActivity());  
  19.    tv.setText(text);  
  20.    tv.setId(android.R.id.empty);  
  21.      
  22.    ViewParent v = getListView().getParent();  
  23.    if(v instanceof ViewGroup) {  
  24.     ((ViewGroup)v).addView(tv);  
  25.    }  
  26.    getListView().setEmptyView(tv);  
  27.   }  
  28.  }  
  29. }  





  

1 件のコメント:

  1. setListShownも
    mProgressContainer = root.findViewById(com.android.internal.R.id.progressContainer);
    で適当なのが取得できなくて死ぬ罠…(´・ω・`) ListFragmentはデフォView以外使うべきではないってことなのかなぁ

    返信削除