2012年4月12日木曜日

Android ListView でデータが空のときもヘッダー・フッターを表示する

ListView には addHeaderView()addFooterView() でヘッダーやフッターをつけることができます。

また、ListView にはリストのデータが空の時に表示させる emptyView を指定することができます。データがないときに画面が真っ黒になるとユーザーはアプリが壊れたと思ってしまうかもしれないので、空のときにはメッセージをだしましょうとよく言われます。

ただ、この emptyView を指定するとリストのデータが空のときに、ヘッダーやフッターも表示されなくなります。

emptyView を指定している状態でヘッダーやフッターを表示できるのか調べてみました。

まず、ListView で emptyView への切り替えをどこでしているかいうと AdapterView の updateEmptyStatus(boolean empty) です。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
  1. 717     private void updateEmptyStatus(boolean empty) {  
  2. 718         if (isInFilterMode()) {  
  3. 719             empty = false;  
  4. 720         }  
  5. 721   
  6. 722         if (empty) {  
  7. 723             if (mEmptyView != null) {  
  8. 724                 mEmptyView.setVisibility(View.VISIBLE);  
  9. 725                 setVisibility(View.GONE);  
  10. 726             } else {  
  11. 728                 setVisibility(View.VISIBLE);  
  12. 729             }  
  13. 730   
  14. 734             if (mDataChanged) {  
  15. 735                 this.onLayout(false, mLeft, mTop, mRight, mBottom);  
  16. 736             }  
  17. 737         } else {  
  18. 738             if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);  
  19. 739             setVisibility(View.VISIBLE);  
  20. 740         }  
  21. 741     }  
ListView には setFilterText() でフィルターをセットできるのですが、このフィルターモードの場合はリストに表示するデータがなくても、フィルターに一致するデータがないというだけで実際のデータが空というわけではないので、最初の if 文で false にしています。

その次の if else 文が本体と emptyView の表示・非表示の切り替えをしているところです。
これをみると empty が true でも emptyView があれば本体が表示されることがわかります。

では、この updateEmptyStatus() がどこから呼ばれているかというと

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#643
setEmptyView()
  1. 643     public void setEmptyView(View emptyView) {  
  2. 644         mEmptyView = emptyView;  
  3. 645   
  4. 646         final T adapter = getAdapter();  
  5. 647         final boolean empty = ((adapter == null) || adapter.isEmpty());  
  6. 648         updateEmptyStatus(empty);  
  7. 649     }  


http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
checkFocus()
  1. 698     void checkFocus() {  
  2. 699         final T adapter = getAdapter();  
  3. 700         final boolean empty = adapter == null || adapter.getCount() == 0;  
  4. 701         final boolean focusable = !empty || isInFilterMode();  
  5.   
  6. 705         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);  
  7. 706         super.setFocusable(focusable && mDesiredFocusableState);  
  8. 707         if (mEmptyView != null) {  
  9. 708             updateEmptyStatus((adapter == null) || adapter.isEmpty());  
  10. 709         }  
  11. 710     }  
の2カ所です。

いずれも adapter が null もしくは adapter.isEmpty() が true なら updateEmptyStatus() の引数として true が渡されています。

つまりまとめると

1. adapter != null && adapter.isEmpty == false → 本体が表示される
2. adapter == null or adapter.isEmpty == true
  → emptyView != null → 本体 が表示される
  → emptyView == null → emptyView が表示される

なので、結論としては、 isEmpty() で常に false を返すように Override するか、emptyView を null にすればよい。

ListView を単体で使うときは明示的に setEmptyView() するか、android/id:empty の View を XML で定義するので意識できるますが、Android で用意されている ListView 用の ListActivity と ListFragment を使うときにはちょっと注意が必要です。

Android 4.0 では、ListActivity でのデフォルトのレイアウトとして com.android.internal.R.layout.list_content_simple をセットしています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308
  1. 308     private void ensureList() {  
  2. 309         if (mList != null) {  
  3. 310             return;  
  4. 311         }  
  5. 312         setContentView(com.android.internal.R.layout.list_content_simple);  
  6. 313   
  7. 314     }  

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content_simple.xml
  1. 20 <listview xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" 21="" android:layout_width="match_parent" 22="" android:layout_height="match_parent" 23="" android:drawselectorontop="false" 24="">  
  2. tview>  

ちなみに Android 2.3.4 では、com.android.internal.R.layout.list_content をセットしていますが、なかのレイアウトは 4.0 の list_content_simple と同じです。

http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/java/android/app/ListActivity.java#308
  1. 308     private void ensureList() {  
  2. 309         if (mList != null) {  
  3. 310             return;  
  4. 311         }  
  5. 312         setContentView(com.android.internal.R.layout.list_content);  
  6. 313   
  7. 314     }  

http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml
  1. 20 <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list"  
  2. 21     android:layout_width="match_parent"   
  3. 22     android:layout_height="match_parent"  
  4. 23     android:drawSelectorOnTop="false"  
  5. 24     />  

一方、ListFragment のデフォルトのレイアウトとしては com.android.internal.R.layout.list_content がセットされています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#191
  1. 190     @Override  
  2. 191     public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  3. 192             Bundle savedInstanceState) {  
  4. 193         return inflater.inflate(com.android.internal.R.layout.list_content,  
  5. 194                 container, false);  
  6. 195     }  

こっちはちょっと複雑なレイアウトになっています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml
  1. 18 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2. 19         android:layout_width="match_parent"  
  3. 20         android:layout_height="match_parent">  
  4. 21       
  5. 22     <LinearLayout android:id="@+id/progressContainer"  
  6. 23             android:orientation="vertical"  
  7. 24             android:layout_width="match_parent"   
  8. 25             android:layout_height="match_parent"  
  9. 26             android:visibility="gone"  
  10. 27             android:gravity="center">  
  11. 28           
  12. 29         <ProgressBar style="?android:attr/progressBarStyleLarge"  
  13. 30                 android:layout_width="wrap_content"  
  14. 31                 android:layout_height="wrap_content" />  
  15. 32         <TextView android:layout_width="wrap_content"  
  16. 33                 android:layout_height="wrap_content"  
  17. 34                 android:textAppearance="?android:attr/textAppearanceSmall"  
  18. 35                 android:text="@string/loading"  
  19. 36                 android:paddingTop="4dip"  
  20. 37                 android:singleLine="true" />  
  21. 38               
  22. 39     </LinearLayout>  
  23. 40           
  24. 41     <FrameLayout android:id="@+id/listContainer"  
  25. 42             android:layout_width="match_parent"   
  26. 43             android:layout_height="match_parent">  
  27. 44               
  28. 45         <ListView android:id="@android:id/list"  
  29. 46                 android:layout_width="match_parent"   
  30. 47                 android:layout_height="match_parent"  
  31. 48                 android:drawSelectorOnTop="false" />  
  32. 49         <TextView android:id="@+android:id/internalEmpty"  
  33. 50                 android:layout_width="match_parent"  
  34. 51                 android:layout_height="match_parent"  
  35. 52                 android:gravity="center"  
  36. 53                 android:textAppearance="?android:attr/textAppearanceLarge" />  
  37. 54     </FrameLayout>  
  38. 55           
  39. 56 </FrameLayout>  


注目してほしいのが @+android:id/internalEmpty という ID の TextView です。
ListFragment には setEmptyText() という、データが空のときに表示する文字をセットするメソッドが用意されています。このメソッドが呼ばれると、次のように ListView の setEmptyView() が呼ばれます。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/app/ListFragment.java#289
  1. 289     public void setEmptyText(CharSequence text) {  
  2. 290         ensureList();  
  3. 291         if (mStandardEmptyView == null) {  
  4. 292             throw new IllegalStateException("Can't be used with a custom content view");  
  5. 293         }  
  6. 294         mStandardEmptyView.setText(text);  
  7. 295         if (mEmptyText == null) {  
  8. 296             mList.setEmptyView(mStandardEmptyView);  
  9. 297         }  
  10. 298         mEmptyText = text;  
  11. 299     }  

ここの mStandardEmptyView というのが上記の @+android:id/internalEmpty という ID の TextView に対応しています。


ということで、ListFragment でなんとなくやってた setEmptyText() をコメントアウトしたらヘッダーでるようになったー!

ただし、残念ながらこの場合も

-----
ヘッダー
empty message
フッター
-----

のようにはできないです。ヘッダー・フッターと emptyView は一緒に出すことはコードを見た限りではできないですねー

updateEmptyStatus が protected だったらいろいろできたのに。。。

やるとしたらこんな感じかな。
ヘッダーと、emptyView を同じレイアウトXMLから生成するくらいしか方法がないかな。

  1. public class MainActivity extends ListActivity implements View.OnClickListener{  
  2.   
  3.     ArrayAdapter<String> mAdapter;  
  4.       
  5.     @Override  
  6.     public void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.main);  
  9.   
  10.         mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);  
  11.           
  12.         LayoutInflater inflater = getLayoutInflater();  
  13.         View header = inflater.inflate(R.layout.header, nullfalse);  
  14.   
  15.         getListView().addHeaderView(header);  
  16.         setListAdapter(mAdapter);  
  17.           
  18.         View emptyHeader = getListView().getEmptyView();  
  19.           
  20.         emptyHeader.setOnClickListener(this);  
  21.         header.setOnClickListener(this);  
  22.     }  
  23.   
  24.     @Override  
  25.     public void onClick(View v) {  
  26.         if(mAdapter.isEmpty()) {  
  27.             mAdapter.add("Test");  
  28.         }  
  29.         else {  
  30.             mAdapter.remove("Test");  
  31.         }  
  32.     }  
  33. }  
  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.     <LinearLayout  
  13.         android:id="@android:id/empty"  
  14.         android:layout_width="match_parent"  
  15.         android:layout_height="match_parent"  
  16.         android:orientation="vertical" >  
  17.   
  18.         <include layout="@layout/header"/>  
  19.   
  20.         <TextView  
  21.             android:layout_width="match_parent"  
  22.             android:layout_height="0dip"  
  23.             android:layout_weight="1"  
  24.             android:gravity="center"  
  25.             android:text="No data"  
  26.             android:textSize="30sp" />  
  27.     </LinearLayout>  
  28.   
  29. </FrameLayout>  
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <Button xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/header"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="wrap_content"  
  6.     android:text="Header" />  
ListFragment でも同じ感じ。

  1. public class MainFragment extends ListFragment implements View.OnClickListener {  
  2.   
  3.   
  4.     ArrayAdapter<String> mAdapter;  
  5.       
  6.     @Override  
  7.     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {  
  8.         return inflater.inflate(R.layout.main, container, false);  
  9.     }  
  10.   
  11.     @Override  
  12.     public void onActivityCreated(Bundle savedInstanceState) {  
  13.         super.onActivityCreated(savedInstanceState);  
  14.   
  15.         mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1);  
  16.           
  17.         LayoutInflater inflater = getActivity().getLayoutInflater();  
  18.         View header = inflater.inflate(R.layout.header, nullfalse);  
  19.   
  20.         getListView().addHeaderView(header);  
  21.         setListAdapter(mAdapter);  
  22.           
  23.         View emptyHeader = getListView().getEmptyView();  
  24.           
  25.         emptyHeader.setOnClickListener(this);  
  26.         header.setOnClickListener(this);  
  27.     }  
  28.   
  29.     @Override  
  30.     public void onClick(View v) {  
  31.         if(mAdapter.isEmpty()) {  
  32.             mAdapter.add("Test");  
  33.         }  
  34.         else {  
  35.             mAdapter.remove("Test");  
  36.         }  
  37.     }  
  38. }  


もちろん Adapter を extends して isEmpty() で true を返すようにすれば emptyView がセットされていても本体が表示されるようになります。 具体的な用途は思いつかないですが、データのあるなしにかかわらず独自の基準で emptyView の表示・非表示を切り替えたい場合には便利だと思います。


ちなみに、ListFragment で emptyView を使う場合ははまりポイントがいっぱいなので、 このエントリを見ておくことをオススメします!
Y.A.M の 雑記帳: Android ListFragment でカスタムレイアウトを使うと setEmptyText() が使えない -





0 件のコメント:

コメントを投稿