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 717 private void updateEmptyStatus(boolean empty) { 718 if (isInFilterMode()) { 719 empty = false; 720 } 721 722 if (empty) { 723 if (mEmptyView != null) { 724 mEmptyView.setVisibility(View.VISIBLE); 725 setVisibility(View.GONE); 726 } else { 728 setVisibility(View.VISIBLE); 729 } 730 734 if (mDataChanged) { 735 this.onLayout(false, mLeft, mTop, mRight, mBottom); 736 } 737 } else { 738 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 739 setVisibility(View.VISIBLE); 740 } 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() 643 public void setEmptyView(View emptyView) { 644 mEmptyView = emptyView; 645 646 final T adapter = getAdapter(); 647 final boolean empty = ((adapter == null) || adapter.isEmpty()); 648 updateEmptyStatus(empty); 649 }

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/java/android/widget/AdapterView.java#717
checkFocus() 698 void checkFocus() { 699 final T adapter = getAdapter(); 700 final boolean empty = adapter == null || adapter.getCount() == 0; 701 final boolean focusable = !empty || isInFilterMode(); 705 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 706 super.setFocusable(focusable && mDesiredFocusableState); 707 if (mEmptyView != null) { 708 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 709 } 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 308 private void ensureList() { 309 if (mList != null) { 310 return; 311 } 312 setContentView(com.android.internal.R.layout.list_content_simple); 313 314 }
http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content_simple.xml 20
ちなみに 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 308 private void ensureList() { 309 if (mList != null) { 310 return; 311 } 312 setContentView(com.android.internal.R.layout.list_content); 313 314 }
http://tools.oesf.biz/android-2.3.4_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml 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 />
一方、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 190 @Override 191 public View onCreateView(LayoutInflater inflater, ViewGroup container, 192 Bundle savedInstanceState) { 193 return inflater.inflate(com.android.internal.R.layout.list_content, 194 container, false); 195 }
こっちはちょっと複雑なレイアウトになっています。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/frameworks/base/core/res/res/layout/list_content.xml 18 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 19 android:layout_width="match_parent" 20 android:layout_height="match_parent"> 21 22 <LinearLayout android:id="@+id/progressContainer" 23 android:orientation="vertical" 24 android:layout_width="match_parent" 25 android:layout_height="match_parent" 26 android:visibility="gone" 27 android:gravity="center"> 28 29 <ProgressBar style="?android:attr/progressBarStyleLarge" 30 android:layout_width="wrap_content" 31 android:layout_height="wrap_content" /> 32 <TextView android:layout_width="wrap_content" 33 android:layout_height="wrap_content" 34 android:textAppearance="?android:attr/textAppearanceSmall" 35 android:text="@string/loading" 36 android:paddingTop="4dip" 37 android:singleLine="true" /> 38 39 </LinearLayout> 40 41 <FrameLayout android:id="@+id/listContainer" 42 android:layout_width="match_parent" 43 android:layout_height="match_parent"> 44 45 <ListView android:id="@android:id/list" 46 android:layout_width="match_parent" 47 android:layout_height="match_parent" 48 android:drawSelectorOnTop="false" /> 49 <TextView android:id="@+android:id/internalEmpty" 50 android:layout_width="match_parent" 51 android:layout_height="match_parent" 52 android:gravity="center" 53 android:textAppearance="?android:attr/textAppearanceLarge" /> 54 </FrameLayout> 55 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 289 public void setEmptyText(CharSequence text) { 290 ensureList(); 291 if (mStandardEmptyView == null) { 292 throw new IllegalStateException("Can't be used with a custom content view"); 293 } 294 mStandardEmptyView.setText(text); 295 if (mEmptyText == null) { 296 mList.setEmptyView(mStandardEmptyView); 297 } 298 mEmptyText = text; 299 }
ここの mStandardEmptyView というのが上記の @+android:id/internalEmpty という ID の TextView に対応しています。


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

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

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

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

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

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

public class MainActivity extends ListActivity implements View.OnClickListener{ ArrayAdapter<String> mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1); LayoutInflater inflater = getLayoutInflater(); View header = inflater.inflate(R.layout.header, null, false); getListView().addHeaderView(header); setListAdapter(mAdapter); View emptyHeader = getListView().getEmptyView(); emptyHeader.setOnClickListener(this); header.setOnClickListener(this); } @Override public void onClick(View v) { if(mAdapter.isEmpty()) { mAdapter.add("Test"); } else { mAdapter.remove("Test"); } } } <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" android:drawSelectorOnTop="false" /> <LinearLayout android:id="@android:id/empty" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <include layout="@layout/header"/> <TextView android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:gravity="center" android:text="No data" android:textSize="30sp" /> </LinearLayout> </FrameLayout> <?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Header" /> ListFragment でも同じ感じ。

public class MainFragment extends ListFragment implements View.OnClickListener { ArrayAdapter<String> mAdapter; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.main, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1); LayoutInflater inflater = getActivity().getLayoutInflater(); View header = inflater.inflate(R.layout.header, null, false); getListView().addHeaderView(header); setListAdapter(mAdapter); View emptyHeader = getListView().getEmptyView(); emptyHeader.setOnClickListener(this); header.setOnClickListener(this); } @Override public void onClick(View v) { if(mAdapter.isEmpty()) { mAdapter.add("Test"); } else { mAdapter.remove("Test"); } } }

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


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





0 件のコメント:

コメントを投稿