前回の続き。今回は複数の行を選択するサンプルの説明です。

サンプルコード: https://github.com/stack3/AndroidListViewSamples

サンプルを起動してMultipleChoiceを選択します。各行にチェックボックスが表示されていて、複数選択が可能です。

01

02

サンプルコードはほとんど前回と同じなので異なる部分に絞って解説します。

multiple_choice_activity.xml

ListViewのchoiceModeはmultipleChoiceにします。これで複数選択可能になります。

<ListView
  android:id="@+id/listView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:choiceMode="multipleChoice"
/>

MultipleChoiceActivity

今回は行のレイアウトリソースは、SDKで用意されたandroid.R.layout.simple_list_item_multiple_choiceを使います。

adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_multiple_choice, items);

このレイアウトは文字列とチェックボックスの組み合わせになります。

行をクリックしたとき、その行がチェックされたか、もしくはチェックが外されたかを以下のように判定し、ログに出力します。

private OnItemClickListener listViewOnItemClickListener = new OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
        ListView listView = (ListView)adapterView;
        SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
        Log.d("", String.format("position:%d checked:%b", position, checkedItemPositions.get(position)));
    }
};

ListView#getCheckedItemPositionsはSparseBooleanArrayを返します。このクラスのget(int key)メソッドは、引数に行のインデックス(position)を渡すと、チェックされていたらtrue、されていないならfalseを返します。

SparseBooleanArrayは、きちんと理解しないと他のメソッドでハマリますが、基本はget(int key)だけ使えば問題ないと思います。SparseBooleanArrayのハマリについては後述します。

Infoボタンを押した時にすべての行のチェック状態をログ出力します。

ListView listView = (ListView)findViewById(R.id.listView);
SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
StringBuilder sb = new StringBuilder();
for (int position = 0; position < adapter.getCount(); position++) {
    sb.append(String.format("position:%d checked:%bn", position, checkedItemPositions.get(position)));
}
Log.d("", sb.toString());

行インデックス1と29をチェックすると、そこだけtrueであとはfalseとしてログ出力されます。

06-06 02:10:05.166: D/(12417): position:0 checked:true
06-06 02:10:05.166: D/(12417): position:1 checked:false
06-06 02:10:05.166: D/(12417): position:2 checked:true
06-06 02:10:05.166: D/(12417): position:3 checked:false
〜〜途中省略〜〜
06-06 02:10:05.166: D/(12417): position:27 checked:false
06-06 02:10:05.166: D/(12417): position:28 checked:false
06-06 02:10:05.166: D/(12417): position:29 checked:true

SparseBooleanArrayについて

Javaに精通している人向けに言うと、SparseBooleanArrayはMapの類で、key(行インデックス)、value(チェック状態)という組み合わせで格納されています。Mapは継承してませんが・・・これで理解できる人は以下の駄文は読む必要はないと思います(笑)

行インデックスのチェック状態は、配列やListで返してくれればわかりやすいと思うかもしれません。しかし、行数が多いとメモリを消費するので、メモリ効率のよいSparseBooleanArrayを使っているようです。

気をつけないといけないのは、SparseBooleanArray#sizeは行の総数ではないということです。初期値は0です。行をチェックした時点で、行インデックス(key)とチェック状態(value)の組み合わせを格納します。チェックを外した時は、valueがfalseになります。つまりチェック状態がfalseのものは、格納されている場合と、されていない場合があります。

いずれにせよ、sizeは現在格納している組み合わせの数でしかありません。

よって、以下のようにするとバグになります。

for (int i = 0; i < checkedItemPositions.size(); i++) {
  // iは行のインデックスではなく、あくまで格納しているチェック状態のインデックス
  // getに指定すべきなのは行のインデックス。iを渡してはいけない。
  boolean checked = checkedItemPositions.get(i);
  if (checked) {
    // チェックした時の処理
  } else {
    // チェックした時の処理
  }
}

valueAt(int index)というメソッドもありますが、このindexも行のインデックスではありません。SparseBooleanArrayのインデックス、つまり0〜size()-1の範囲を指定すべきです。

keyAt(int index)というメソッドを使うと、SparseBooleanArrayのインデックスから行のインデックスが得られます。

valueAtとkeyAtを使ってチェック状態を得ることができます。

for (int i = 0; i < checkedItemPositions.size(); i++) {
  int position = checkedItemPositions.keyAt(i);
  boolean checked = get(position);
}

しかし、先に述べたように一度チェックしたもののチェック状態しか格納されていないので、一度もチェックしておらずチェック状態がfalseのものは、上記のコードからは検出できません。

通常はgetメソッドを使うべきで、その他のメソッドは特別な事情がないと使うことは無いと思います。

その8へ続く