ListViewのカスタムビューを作るときにやるべきこと

ListViewにカスタムビューを適用するときのチェックポイント

一番考えておくべきことは、様々な処理をできるだけListViewやAdapterに任せること。
そのためにいくつかカスタムビューに必要なものをまとめてみる。
こんなところか。

カスタムビューのXML

ListViewの「android:id」は"@android:id/list"にする

ListActivityでもListFragmentでも当然使うListView。
これをそれぞれの管理下に置くためにすることがある。
それは 必ず ListViewの「android:id」は"@android:id/list"にする こと。

これを行うとListViewは mList として保存され、getListView() などの恩恵にあずかれる。
<!-- ListViewのidは必ず"@android:id/list" -->
<ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />
 

データが無いときに表示されるandroid:id="@android:id/empty"を書いておく

これは AdapterViewが API Level 1から提供している機能。
メソッドは setEmptyView(View)。
Adapterが空になったときに自動的に表示してくれるViewを設定できる。
「データがなくなりました。」とか自動で出る。

これを利用するためには条件があって、「android:id」が
  • com.android.internal.R.id.internalEmpty
  • android.R.id.empty
のいずれかを設定したViewを登録している必要がある。

カスタムビューの場合は、前者を消しているし定義できないので、
自前で"@android:id/empty"のViewを入れておくといい。
<!--
    リストが無かった場合に表示される。
    ちなみに AdapterView#setEmptyView() に指定するのと同じこと。
    レイアウトが android.R.layout.list_content (カスタムビューじゃない場合)は、
    ListFragment#setEmptyText() に文言だけ指定すればOK。
-->
<TextView
    android:id="@android:id/empty"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    android:gravity="center"
    android:text="@string/no_data"
    android:textAppearance="?android:attr/textAppearanceLarge" />
 

カスタムビューのXML例

これを踏まえたカスタムビュー用のレイアウト例を書いておこう。
res/layout/custom_listview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >
 
        <Button
            android:id="@+id/itemAdd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/add" />
 
        <Button
            android:id="@+id/itemDel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/del" />
 
        <Button
            android:id="@+id/toggleChoiceMode"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/toggle_choice_mode" />
    </LinearLayout>
 
    <!-- ListViewのidは必ず"@android:id/list" -->
    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
 
    <!--
        リストが無かった場合に表示される。
        ちなみに AdapterView#setEmptyView() に指定するのと同じこと。
        レイアウトが android.R.layout.list_content (カスタムビューじゃない場合)は、
        ListFragment#setEmptyText() に文言だけ指定すればOK。
    -->
    <TextView
        android:id="@android:id/empty"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:gravity="center"
        android:text="@string/no_data"
        android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
 

ListViewに入れるListAdapter

AdapterはListViewとデータを繋げるもの。

簡単にAdapterを使う要点をまとめると、
  • SimpleAdapter は データ操作が発生せず、表示を主とする場合
  • ArrayAdapter は データ操作が発生する場合
に使う。

表示は以下のような「データ ⇒ View」のメソッド郡に任せる。
  • Adapter.getView(int, View, ViewGroup)
  • BaseAdapter.getDropDownView(int, View, ViewGroup)
  • SimpleAdapter.ViewBinder.setViewValue(View, Object, String)

データ操作は以下のようなメソッド郡に任せる。
  • Adapter.getItem(int)
  • ArrayAdapter.add(T)
  • ArrayAdapter.remove(T)
  • など

項目の増減が無い場合はSimpleAdapter

カスタムビューでこだわった表示をするからといって、動作が複雑とは限らない。
表示自体は固定的である場合は、SimpleAdapterを使うべきだろう。
記述も少なくなって楽ができる。

項目の増減がある場合はArrayAdapter<T>

ArrayAdapterは、項目の増減に関するメソッドも豊富に取り揃えてあるAdapter。
操作やデータ取得で項目の増減が起きる場合はArrayAdapterを使うべきだろう。
AsyncTaskLoaderとか使う人はこれを使うことになる。

ArrayAdapterのデータクラスの作り方

ArrayAdapterに指定するデータクラスはParcelableを実装する

ArrayAdapterは mContext を覚えているため、ArrayAdapterのインスタンスを覚えるとメモリリークが発生する。
メモリリークの定義については この辺を参考に。
これは本意ではないので回避するべき。
このためには、
  1. Adapterに設定したデータクラスが復元可能にする
  2. LifeCycleが起こる場合はデータのみ引き継ぐ
といったことを意識して行えばよい。

もちろん Serializable でもいい。ファイル書き出しするなら尚更だ。
ちゃんと Intent や Bundle にも入れられる。
でもAndroidが提供しているParcelableなら、多くのAndroid提供のシステムに連携できる。
手間もそれほどないので、まずは Parcelable で実現することにしよう。

ArrayAdapterのデータクラス実装例

以下に、どこにでもあるテンプレートを掲載しておく。
/* package */class CustomListItem implements Parcelable
  1. /**
  2.  * ListViewの自作アイテム。
  3.  */
  4. // 私はカプセル化のため package private にすることが多いが、
  5. // 外部提供データクラスの場合はもちろん public にする。
  6. /* package */class CustomListItem implements Parcelable {
  7.  
  8. // getter/setter は Android だと速度低下に繋がるらしいので、
  9. // 内部でしか使わないデータは基本的に直接さわるように設計する。
  10. /* package */String thumbnail_url;
  11. /* package */String user_name;
  12. /* package */String summary;
  13.  
  14. /**
  15.   * データ設定用コンストラクタ。
  16.   *
  17.   * @param thumbnail_url ThumbnailのURL
  18.   * @param user_name ユーザ名
  19.   * @param summary コメントやら説明やら
  20.   */
  21. public CustomListItem(String thumbnail_url, String user_name, String summary) {
  22. this.thumbnail_url = thumbnail_url;
  23. this.user_name = user_name;
  24. this.summary = summary;
  25. }
  26.  
  27. // ================================================================
  28. // ここから Parcelable 用のテンプレート
  29.  
  30. // 必ず read と write は同じ順番で入れること。
  31. public CustomListItem(Parcel in) {
  32. this.thumbnail_url = in.readString();
  33. this.user_name = in.readString();
  34. this.summary = in.readString();
  35. }
  36.  
  37. public void writeToParcel(Parcel dest, int flags) {
  38. dest.writeString(thumbnail_url);
  39. dest.writeString(user_name);
  40. dest.writeString(summary);
  41. }
  42.  
  43. // FileDescriptor を使わないときは常に 0
  44. public int describeContents() {
  45. return 0;
  46. }
  47.  
  48. // これは定型文なのでそのまま使える。
  49. public static final Creator<CustomListItem> CREATOR = new Creator<CustomListItem>() {
  50. public CustomListItem createFromParcel(Parcel in) {
  51. return new CustomListItem(in);
  52. }
  53.  
  54. public CustomListItem[] newArray(int size) {
  55. return new CustomListItem[size];
  56. }
  57. };
  58. }
  59.  

ArrayAdapterの継承クラス

ArrayAdapter#getView()のsuper.getView()は“呼ばない”

これは驚きそうな話だけど本当の話。
Adapterの表示方法に手を入れる場合は必ずと言っていいほどgetVeiw()をOverrideする。
しかしArrayAdapterのgetView()はなんとなく動くようにしてある。
そのためコンストラクタに入れる textViewResourceId を TextView とみなして動作する処理が書かれている。

これ、実は不要。
なぜならカスタムビューにしたレイアウトを想定した処理ではないから。

ArrayAdapterは textViewResourceId が無いことを想定して動く設計なので、
private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource)
を準備している。
getView()はOverrideで createViewFromResource() が呼ばれなくなることが前提になっている。

ちなみに getDropDownView() を使用する場合は同じように createViewFromResource() が呼ばれる。
必要ならこちらもOverrideするように。

では、ArrayAdapter#getView()に何を書けばいいのか

では、ArrayAdapter#getView()にどう書くのが望ましいのだろうか。
それはここに書かれている。
private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource)
そう、ArrayAdapter#getView()が内部で呼んでいるメソッドだ。
模範的な getView() の実装方法が書かれているので参考にしよう。

Adapter#getView()について、いくつか知っておく必要があるのは以下。
  • convertViewは最初に画面表示に必要な数しか生成されない。
  • リソースから文字列に変えるときに必要なContextは ArrayAdapter#getContext()がある。
    • 「convertView == null」のときに LayoutInflater は 数回しか使わないので LayoutInflater.from(getContext())。
    • 「convertView != null」のときに 以前表示したViewを使いまわすように言われるので 「view = convertView;」
  • 引数の「ViewGroup parent」は ListView に他ならない。
    • ChoiceModeの取得 や 設定のチェック は (ListView) parent でやる。無駄に覚えない。
  • 引数の「int position」を使用して ArrayAdapter#getItem() から登録データを取り出す。

これを踏まえてArrayAdapterを改造しよう。

ちなみに getDropDownView() を使用する場合は同じように createViewFromResource() が呼ばれる。
必要ならこちらもOverrideするように。

ChoiceModeを使うときはチェック状態をListView準拠にする

ChoiceModeは、リストのアイテムを複数選択するときにチェックボックスを出す機能のこと。
これはとても利用価値が高いのだが、カスタムビューにすると変な動きをする。
それもそのはず。
管理をしているのが ListView だからだ。
だからこの設定はすべてListViewからいただくことにしよう。
  1. @Override
  2. public View getView(int position, View convertView, ViewGroup parent) {
  3. // XXX : super.getView() は 呼ばない
  4. // 元のgetView()は TextView があることを想定しているが、
  5. // カスタムビューにしているので必要ない。
  6. // 余計な処理は省く。
  7. View view;
  8.  
  9. ~~~ 中略 ~~~
  10.  
  11. // ================================
  12. ListView listView = (ListView) parent;
  13. CheckBox check = (CheckBox) view.findViewById(R.id.choice);
  14. if (listView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
  15. check.setVisibility(View.GONE);
  16. } else {
  17. // チェックボックスを自前で設定すると、どうしてもおかしくなる。
  18. // ここはListViewで管理している mCheckStates からもらうのが得策。
  19. check.setChecked(listView.isItemChecked(position));
  20. check.setVisibility(View.VISIBLE);
  21. }
  22. return view;
  23. }
  24.  

ArrayAdapterに拡張するべきいくつかのメソッドがある

これはデータクラスを使いやすくするために必要なもの。
だいたい必要になるのは以下の3つ。
  1. /**
  2.  * 設定されている CustomListItem の ArrayList を返す。
  3.  *
  4.  * @return CustomListItem の ArrayList
  5.  */
  6. // 縦横切替などでデータを移行するために使う。
  7. public ArrayList<CustomListItem> getItemList() {
  8.  
  1. /**
  2.  * {@link getItemList}で取得できる型のリストを追加。
  3.  *
  4.  * @param parcelableArrayList ArrayList<CustomListItem>型のデータリスト
  5.  */
  6. // Bundleから復元するときに必要になるはず。
  7. public void addAll(ArrayList<CustomListItem> parcelableArrayList) {
  8.  
  1. /**
  2.  * 値設定型add。
  3.  *
  4.  * @param thumbnail_url ThumbnailのURL
  5.  * @param user_name ユーザ名
  6.  * @param summary コメントやら説明やら
  7.  */
  8. // データクラスをカプセル化すると概ね必要になる。
  9. public void add(String thumbnail_url, String user_name, String summary) {
  10.  

あと、これは無くてもいいのだが私は欲しくなる。
  1. /**
  2.  * 位置指定remove
  3.  *
  4.  * @param index Adapterに登録している位置
  5.  */
  6. // なぜ insert() という位置指定挿入はあるのに削除は無いのか。
  7. public void remove(int index) {
  8.  

ArrayAdapterの継承クラス実装例

public class CustomListViewAdapter extends ArrayAdapter<CustomListItem>
  1. public class CustomListViewAdapter extends ArrayAdapter<CustomListItem> {
  2.  
  3. // 見易さのために定義。普段は直接 getView で指定する。
  4. private static final int resource = R.layout.custom_listview_item;
  5.  
  6. public CustomListViewAdapter(Context context) {
  7. // 不要な textViewResourceId はダメなときにすぐ落ちるであろう 0 で初期化しておく。
  8. // よくあるのは getDropDownView() を意図せず呼び出して落ちることだろう。
  9. super(context, 0);
  10. }
  11.  
  12. @Override
  13. public View getView(int position, View convertView, ViewGroup parent) {
  14. // XXX : super.getView() は 呼ばない
  15. // 元のgetView()は TextView があることを想定しているが、
  16. // カスタムビューにしているので必要ない。
  17. // 余計な処理は省く。
  18. View view;
  19.  
  20. // ================================
  21. // テンプレートな処理。mInflater を protected にしてくれればいいのに。
  22. // ちなみに convertView は最初の数回しか初期化されないので
  23. // LayoutInflater は保持が必要なほどではないと思う。
  24. if (convertView == null) {
  25. LayoutInflater inflater = LayoutInflater.from(getContext());
  26. view = inflater.inflate(resource, parent, false);
  27. } else {
  28. view = convertView;
  29. }
  30.  
  31. // 自分が持っているデータは getItem() で取る。
  32. // 上手いこと position もあるわけだし。
  33. CustomListItem item = getItem(position);
  34.  
  35. // カスタムビューの場合はViewが確実にあるだろうから try-catch は不要だろう。
  36. // ================================
  37. TextView user_name = (TextView) view.findViewById(R.id.user_name);
  38. user_name.setText(item.user_name);
  39. // ================================
  40. TextView summary = (TextView) view.findViewById(R.id.summary);
  41. summary.setText(item.summary);
  42.  
  43. // ================================
  44. // ChoiceModeが必要ない場合は消すこと
  45. ListView listView = (ListView) parent;
  46. CheckBox check = (CheckBox) view.findViewById(R.id.choice);
  47. if (listView.getChoiceMode() == ListView.CHOICE_MODE_NONE) {
  48. check.setVisibility(View.GONE);
  49. } else {
  50. // チェックボックスを自前で設定すると、どうしてもおかしくなる。
  51. // ここはListViewで管理している mCheckStates からもらうのが得策。
  52. check.setChecked(listView.isItemChecked(position));
  53. check.setVisibility(View.VISIBLE);
  54. }
  55.  
  56. return view;
  57. }
  58.  
  59. // ================================================================
  60. // だいたい欲しくなる追加機能
  61. /**
  62.   * 設定されている CustomListItem の ArrayList を返す。
  63.   *
  64.   * @return CustomListItem の ArrayList
  65.   */
  66. // 縦横切替などでデータを移行するために使う。
  67. public ArrayList<CustomListItem> getItemList() {
  68. // 今回は Bundle#putParcelableArrayList() を使うことを想定する。
  69. // 必要に応じて Bundle#putSparseParcelableArray() を使ってもいい。
  70.  
  71. int size = getCount();
  72. ArrayList<CustomListItem> itemList = new ArrayList<CustomListItem>(size);
  73. for (int index = 0; index < size; index++) {
  74. itemList.add(getItem(index));
  75. }
  76. return itemList;
  77. }
  78.  
  79. /**
  80.   * {@link getItemList}で取得できる型のリストを追加。
  81.   *
  82.   * @param parcelableArrayList ArrayList<CustomListItem>型のデータリスト
  83.   */
  84. // Bundleから復元するときに必要になるはず。
  85. public void addAll(ArrayList<CustomListItem> parcelableArrayList) {
  86. // 強制でキャスト。落ちる場合は、設計か実装が間違っている。
  87. @SuppressWarnings("unchecked")
  88. ArrayList<CustomListItem> itemList = (ArrayList<CustomListItem>) parcelableArrayList;
  89. super.addAll(itemList);
  90. }
  91.  
  92. /**
  93.   * 値設定型add。
  94.   *
  95.   * @param thumbnail_url ThumbnailのURL
  96.   * @param user_name ユーザ名
  97.   * @param summary コメントやら説明やら
  98.   */
  99. // データクラスをカプセル化すると概ね必要になる。
  100. public void add(String thumbnail_url, String user_name, String summary) {
  101. CustomListItem item = new CustomListItem(thumbnail_url, user_name, summary);
  102. super.add(item);
  103. }
  104.  
  105. /**
  106.   * 位置指定remove
  107.   *
  108.   * @param index Adapterに登録している位置
  109.   */
  110. // なぜ insert() という位置指定挿入はあるのに削除は無いのか。
  111. public void remove(int index) {
  112. if (index < 0 || index >= getCount()) {
  113. return;
  114. }
  115. remove(getItem(index));
  116. }
  117. }

呼び元の onSaveInstanceState() で ListAdapter のデータを引き継ぐ

せっかく作成したデータクラスも、何もしなければConfigurationChangeで消されてしまう。
ここは全てのデータを次のActivity/Fragmentに引き継ごう。

とりあえずこんな形で実装すると動く。
  1. @Override
  2. public void onSaveInstanceState(Bundle outState) {
  3. super.onSaveInstanceState(outState);
  4.  
  5. // 復元用データの作成
  6. ListView listView = getListView();
  7. // ChoiceMode は復元できないので別枠で保存。
  8. outState.putInt(EXTRA_CHOICEMODE, listView.getChoiceMode());
  9.  
  10. // AdapterはListViewの持ち物ではないので別途保存。
  11. CustomListViewAdapter adapter = (CustomListViewAdapter) getListAdapter();
  12. outState.putParcelableArrayList(EXTRA_LISTDATA, adapter.getItemList());
  13. }
  14.  
  15. @Override
  16. protected void onRestoreInstanceState(Bundle inState) {
  17. // Fragment に onRestoreInstanceState() は無いので、
  18. // public void onViewCreated(View, Bundle) あたりで実装する。
  19. super.onRestoreInstanceState(inState);
  20.  
  21. // 初回起動っぽいときはデータを作成する。今回はAdapter設定だけ。
  22. if (inState == null) {
  23. CustomListViewAdapter adapter = new CustomListViewAdapter(getActivity());
  24. setListAdapter(adapter);
  25. return;
  26. }
  27.  
  28. // ListViewの情報は ChoiceMode さえ別枠で保存しておけば大丈夫。
  29. ListView listView = getListView();
  30. listView.setChoiceMode(inState.getInt(EXTRA_CHOICEMODE));
  31.  
  32. // Adapter は ListView の情報ではないので別途保存が必要。
  33. CustomListViewAdapter adapter = new CustomListViewAdapter(getActivity());
  34. adapter.addAll(inState.getParcelableArrayList(EXTRA_LISTDATA));
  35. setListAdapter(adapter);
  36. }
  37.  

最後に

以上、ListViewにカスタムビューを採用する際に必要なチェックポイントをまとめた。
もちろんChoiceModeを使わないのにChoiceModeの設計を盛り込むのは速度を低下させる。
必要・不要を取捨選択してもらいたい。

関連リンク

&trackback()
タグ一覧:Android ListView View Note



最終更新:2012年11月18日 17:00