AndroidのListView.CHOICE_MODE_MULTIPLEを使うと,複数選択式のリストが簡単に実装できる.
しかし,これは要素のpositionで選択状態を管理しているらしく,単純に要素を追加・削除しようと思うとバグる.
問題(具体例)
具体的には,以下のようなActivityを考える.ある要素が選択されたあとで,その要素より上に別の要素が追加されたり,その要素より上の要素が削除されたりすると,要素の選択状態がずれて,本来チェックしているのと違う要素が選択されてしまう.
public class MainActivity extends Activity {
private List<String> animals = new ArrayList<String>(Arrays.asList(new String[]{
"bird", "cat", "dog", "elephant", "giraffe", "pig", "rabbit", "rhino", "sheep"
}));
private ArrayAdapter<String> adapter;
private ListView listView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
listView = (ListView) findViewById(R.id.list);
adapter = new ArrayAdapter<String>(
this, android.R.layout.simple_list_item_multiple_choice, animals);
listView.setAdapter(adapter);
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id) {
removeItem(position);
return true;
}
});
findViewById(R.id.add_element_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
insertItem("New Item");
}
});
}
private void insertItem(String item) {
adapter.insert(item, 0);
}
private void removeItem(int position) {
adapter.remove(adapter.getItem(position));
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="@+id/add_element_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Add new element"/>
<ListView
android:id="@+id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:choiceMode="multipleChoice"/>
</LinearLayout>
解決
これを回避するには,以下のように自分で要素の選択状態をコピーして移し替えてあげる必要がある
private void insertItem(String item) {
Set<Integer> checkedIndices = copyCheckedItemPositions();
adapter.insert(item, 0);
listView.clearChoices();
for (Integer i: checkedIndices) {
listView.setItemChecked(i + 1, true);
}
}
private void removeItem(int position) {
Set<Integer> checkedIndices = copyCheckedItemPositions();
adapter.remove(adapter.getItem(position));
listView.clearChoices();
for (Integer i: checkedIndices) {
if (i < position) {
listView.setItemChecked(i, true);
} else if (i > position) {
listView.setItemChecked(i - 1, true);
}
}
}
private Set<Integer> copyCheckedItemPositions() {
SparseBooleanArray selection = listView.getCheckedItemPositions();
Set<Integer> checkedIndices = new HashSet<Integer>();
for (int i = 0; i < selection.size(); i++) {
if (selection.get(selection.keyAt(i))) {
checkedIndices.add(selection.keyAt(i));
}
}
return checkedIndices;
}