I have a custom Listview using a adapter class to extend ArrayAdapter of a Item class. I have the ability to change between choice modes of NONE,Single and Multi. This all
There appears to be a bug in AbsListView that causes this issue. It can happen with any subclass of AbsListView
, including ListView
and GridView
.
In single- and multi-choice mode, the ListView
responds to a notifyDataSetChanged()
call on its adapter by verifying the set of checked items in confirmCheckedPositionsById()
. Since the selected item(s) have already been deleted from the dataset at that point, the adapter will throw an exception. Notably, this issue only occurs if the adapter's hasStableIds()
method returns true
.
Loosely speaking, this is the relevant path:
ListView
updates its list of selected itemsnotifyDataSetChanged()
on your adapter, and it notifies its observers that the dataset has changed. The ListView
is one of those observers.ListView
is redrawn, it sees the adapter's notification and calls handleDataChanged()
. At this point, the ListView
still thinks that our now-deleted items are selected and in the dataset.handleDataChanged()
method calls confirmCheckedPositionsById()
, which in turn tries to call getItemId()
on the adapter using a stale position. If the deleted item happens to be near the end of the list, this position is likely to be out of bounds for the array, and the adapter will throw IndexOutOfBoundsException
.Two possible workarounds are as follows:
Create a new adapter every time the dataset changes, as noted in other answers. This has the unfortunate effect of losing the current scroll position unless it's saved and restored manually.
Clear the selected items by calling clearChoices()
on the ListView
(or GridView
) before you call notifyDataSetChanged()
on the adapter. The selected items will be deleted anyhow, so losing the current selection state is unlikely to be a problem. This method will preserve the scroll position and should prevent flickering while the list is being updated.
The bug in confirmCheckedPositionsById
(bullet #7 in acj's answer) causes getItemId
to get called on a stale position. However, it will get called again with the correct position to refresh the layout. When I ran into this problem I updated the custom Adapter's getItemId
like so
@Override
public long getItemId(int position) {
return position < getCount() ? getItem(position).getId() : -1;
}
Edit as :
for (int i = checkedItemsCount-1; i >= 0 ; i--) {
^^^^^
INSTED OF
for (int i = checkedItemsCount-1; i >= 0 ; --i) {
This is a documented bug, please vote for it:
https://code.google.com/p/android/issues/detail?can=2&start=0&num=100&q=&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars&groupby=&sort=&id=64596
My use case is the ListView configured as ListView.CHOICE_MODE_SINGLE, I tried @Peter Tran's suggestion without any luck. Here is the workaround that is successful for me:
myAdapter.deleteRow(listView.getCheckedItemPosition());
int checkedIndex = listView.getCheckedItemPosition();
System.out.println("checkedIndex="+checkedIndex);
int count=myAdapter.getCount();
if (checkedIndex==count) listView.setItemChecked(count-1, true);
My test is to manually select the last item on the list(count-1). I omitted the handling for count==0 case but that will most likely be needed. I observed that println() always prints the index of the deleted row. myAdapter.deleteRow() notifies of data change to listeners which says to me ListView isn't properly updating it's checked indices. I've tried this code with hasStableIds() returning both true and false from the custom adapter with the same results.
Filter using AlertDialog.Builder fully customized :-
((TextView) TabBarWithCustomStack.vPublic.findViewById(R.id.tv_edit)) .setVisibility(View.GONE);
class DialogSelectionClickHandler implements
DialogInterface.OnMultiChoiceClickListener {
public void onClick(DialogInterface dialog, int clicked,
boolean selected) {
Log.i("ME", hosts.toArray()[clicked] + " selected: " + selected);
}
}
class DialogButtonClickHandler implements
DialogInterface.OnClickListener {
public void onClick(DialogInterface dialog, int clicked) {
switch (clicked) {
case DialogInterface.BUTTON_POSITIVE:
filteredList.clear();
statusesSelected.clear();
for (int i = 0; i < statusesStringArray.length; i++) {
// Log.i( "ME", statuses.toArray()[ i ] + " selected: "
// + isSelectedStatuses[i] );
if (isSelectedStatuses[i] == true) {
if (statuses.get(i).toString()
.equalsIgnoreCase("Accepted")) {
statusesSelected.add("1");
} else if (statuses.get(i).toString()
.equalsIgnoreCase("Rejected")) {
statusesSelected.add("2");
} else if (statuses.get(i).toString()
.equalsIgnoreCase("Pending")) {
statusesSelected.add("3");
}
}
isSelectedStatuses[i] = false;
}
Calendar currentCalender = Calendar.getInstance(Locale
.getDefault());
Date currentDate = new Date(
currentCalender.getTimeInMillis());
if (listSelected == 0) {
for (int j = 0; j < arr_BLID.size(); j++) {
if (Helper.stringToDate(
arr_BLID.get(j).getStart_ts().toString(),
Helper.SERVER_FORMAT).after(currentDate)) {
if (statusesSelected.contains(arr_BLID.get(j)
.getStatus())) {
filteredList.add(arr_BLID.get(j));
}
}
}
} else {
for (int j = 0; j < arr_BLID.size(); j++) {
if (currentDate.after(Helper.stringToDate(arr_BLID
.get(j).getStart_ts().toString(),
Helper.SERVER_FORMAT))) {
if (statusesSelected.contains(arr_BLID.get(j)
.getStatus())) {
filteredList.add(arr_BLID.get(j));
}
}
}
}
lvBeeps.setAdapter(new EventsAdapter(ctx));
break;
case DialogInterface.BUTTON_NEGATIVE: {
currentCalender = Calendar.getInstance(Locale.getDefault());
currentDate = new Date(currentCalender.getTimeInMillis());
if (listSelected == 0) {
filteredList.clear();
for (int i = 0; i < arr_BLID.size(); i++) {
if (Helper.stringToDate(
arr_BLID.get(i).getStart_ts().toString(),
Helper.SERVER_FORMAT).after(currentDate)) {
filteredList.add(arr_BLID.get(i));
}
if (i < isSelectedStatuses.length) {
isSelectedStatuses[i] = false;
} else {
continue;
}
}
lvBeeps.setAdapter(new EventsAdapter(ctx));
} else {
filteredList.clear();
for (int i = 0; i < arr_BLID.size(); i++) {
if (currentDate.after(Helper.stringToDate(arr_BLID
.get(i).getStart_ts().toString(),
Helper.SERVER_FORMAT))) {
filteredList.add(arr_BLID.get(i));
}
if (i < isSelectedStatuses.length) {
isSelectedStatuses[i] = false;
} else {
continue;
}
}
lvBeeps.setAdapter(new EventsAdapter(ctx));
}
}
break;
}
}
}
btnHost = (Button) view.findViewById(R.id.btnHost);
btnHost.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
// ((TextView)((RelativeLayout)getActivity().getLayoutInflater().inflate(R.layout.event_filter_title,
// null)).findViewById(R.id.tvDialogTitle)).setText("Hosts");
// Log.d( "Dialog object",
// " static made dialog in checkbox--> "+((TextView)((RelativeLayout)getActivity().getLayoutInflater().inflate(R.layout.event_filter_title,
// null)).findViewById(R.id.tvDialogTitle)).getText());
final LayoutInflater inflater123 = (LayoutInflater) getActivity()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View view123 = inflater123.inflate(
R.layout.event_filter_title, null);
// Log.d( "Dialog object",
// " static made dialog in view123--> "+view123);
//
// Log.d( "Dialog object",
// " static made dialog in checkbox--> "+((CheckBox)view123.findViewById(R.id.cbSelectAll)));
//
// Log.d( "Dialog object",
// " static made dialog in checkbox--> "+((CheckBox)inflater.inflate(R.layout.event_filter_title,
// (ViewGroup) ((AlertDialog)
// BeepsFragment.dialog).getCurrentFocus(),
// true).findViewById(R.id.cbSelectAll)));
//
// Log.d( "Dialog object",
// " static made dialog in checkbox--> "+((CheckBox)(((RelativeLayout)getActivity().getLayoutInflater().inflate(R.layout.event_filter_title,
// null)).findViewById(R.id.cbSelectAll))));
hostsDialog = new AlertDialog.Builder(getActivity())
.setCustomTitle(/*
* Html.fromHtml(
* "<b><font color=\"purple\"> Host</font></b>"
* )
*/view123)
.setIcon(
getActivity().getResources().getDrawable(
R.drawable.add_host))
.setMultiChoiceItems(hostsStringArray, isSelectedHosts,
new OnMultiChoiceClickListener() {
// android.content.DialogInterface.OnShowListener
// ocl = new
// DialogInterface.OnShowListener() {
//
// @Override
// public void onShow(DialogInterface
// dialog) {
// // TODO Auto-generated method stub
// BeepsFragment.dialog = dialog;
// }
// };
public void onClick(DialogInterface dialog,
int clicked, boolean selected) {
boolean all = true;
Log.i("ME", hosts.toArray()[clicked]
+ " selected: " + selected);
// for (int i = 0; i <
// isSelectedHosts.length; i++ ){
//
// if(isSelectedHosts[i]==true){
// all = true;
// }else{
// all = false;
// }
// }
//
// Log.i( "ME", all + " selected:--- "
// +((CheckBox)view123.findViewById(R.id.cbSelectAll)));
//
// if(all = true){
// ((CheckBox)view123.findViewById(R.id.cbSelectAll)).setChecked(true);
// }else{
// ((CheckBox)view123.findViewById(R.id.cbSelectAll)).setChecked(false);
// }
Log.d("Dialog object",
" static made dialog --> "
+ BeepsFragment.dialog
+ " from parameter dialog --> "
+ dialog);
}
})
.setPositiveButton(
Html.fromHtml("<b><font color=\"purple\">Apply Filter</font></b>"),
new DialogButtonClickHandler() {
// android.content.DialogInterface.OnShowListener
// ocl = new
// DialogInterface.OnShowListener() {
//
// @Override
// public void onShow(DialogInterface
// dialog) {
// // TODO Auto-generated method stub
// BeepsFragment.dialog = dialog;
// }
// };
public void onClick(DialogInterface dialog,
int clicked) {
switch (clicked) {
case DialogInterface.BUTTON_POSITIVE:
filteredList.clear();
hostsSelected.clear();
for (int i = 0; i < hostsStringArray.length; i++) {
Log.i("ME",
hosts.toArray()[i]
+ " selected: "
+ isSelectedHosts[i]
+ "\n\n\t hostStringArray-->"
+ hostsStringArray[i]);
if (isSelectedHosts[i] == true) {
hostsSelected.add(hosts
.get(i));
isSelectedHosts[i] = false;
}
// isSelectedHosts[i] = false;
}
Calendar currentCalender = Calendar
.getInstance(Locale
.getDefault());
Date currentDate = new Date(
currentCalender
.getTimeInMillis());
if (listSelected == 0) {
for (int j = 0; j < arr_BLID
.size(); j++) {
if (Helper
.stringToDate(
arr_BLID.get(
j)
.getStart_ts()
.toString(),
Helper.SERVER_FORMAT)
.after(currentDate)) {
if (hostsSelected
.contains(arr_BLID
.get(j)
.getHost_name())) {
filteredList
.add(arr_BLID
.get(j));
}
if (hostsSelected
.contains("Me"))
if (BeepApplication
.getSelfId() == arr_BLID
.get(j)
.getHost_id())
filteredList
.add(arr_BLID
.get(j));
}
}
} else {
for (int j = 0; j < arr_BLID
.size(); j++) {
if (currentDate.after(Helper
.stringToDate(
arr_BLID.get(
j)
.getStart_ts()
.toString(),
Helper.SERVER_FORMAT))) {
if (hostsSelected
.contains(arr_BLID
.get(j)
.getHost_name())) {
filteredList
.add(arr_BLID
.get(j));
}
if (hostsSelected
.contains("Me"))
if (BeepApplication
.getSelfId() == arr_BLID
.get(j)
.getHost_id())
filteredList
.add(arr_BLID
.get(j));
}
}
}
lvBeeps.setAdapter(new EventsAdapter(
ctx));
break;
}
}
})
.setNegativeButton(
Html.fromHtml("<b><font color=\"purple\">Remove Filter</font></b>"),
new DialogButtonClickHandler() {
public void onClick(
final DialogInterface dialog,
int clicked) {
Calendar currentCalender = Calendar
.getInstance(Locale
.getDefault());
Date currentDate = new Date(
currentCalender
.getTimeInMillis());
if (listSelected == 0) {
filteredList.clear();
for (int i = 0; i < arr_BLID.size(); i++) {
if (Helper.stringToDate(
arr_BLID.get(i)
.getStart_ts()
.toString(),
Helper.SERVER_FORMAT)
.after(currentDate)) {
filteredList.add(arr_BLID
.get(i));
if (i < isSelectedHosts.length) {
isSelectedHosts[i] = false;
} else {
continue;
}
}
}
lvBeeps.setAdapter(new EventsAdapter(
ctx));
} else {
filteredList.clear();
for (int i = 0; i < arr_BLID.size(); i++) {
if (currentDate.after(Helper
.stringToDate(
arr_BLID.get(i)
.getStart_ts()
.toString(),
Helper.SERVER_FORMAT))) {
filteredList.add(arr_BLID
.get(i));
if (i < isSelectedHosts.length) {
isSelectedHosts[i] = false;
} else {
continue;
}
}
}
lvBeeps.setAdapter(new EventsAdapter(
ctx));
}
}
});
final AlertDialog dlg = hostsDialog.create();
dlg.show();
((TextView) view123.findViewById(R.id.tvDialogTitle))
.setText("Hosts");
((CheckBox) view123.findViewById(R.id.cbSelectAll))
.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(
CompoundButton buttonView, boolean isChecked) {
// TODO Auto-generated method stub
if (isChecked == true) {
ListView list = dlg.getListView();
for (int i = 0; i < list.getCount(); i++) {
isSelectedHosts[i] = true;
list.setItemChecked(i, true);
}
}
else if (isChecked == false) {
ListView list = dlg.getListView();
for (int j = 0; j < list.getCount(); j++) {
isSelectedHosts[j] = false;
list.setItemChecked(j, false);
}
// }
}
}
});
}
});
btnStatus = (Button) view.findViewById(R.id.btnStatus);
btnStatus.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
LayoutInflater inflater123 = (LayoutInflater) getActivity()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view123 = inflater123.inflate(R.layout.event_filter_title,
null);
statusDialog = new AlertDialog.Builder(getActivity())
.setIcon(
getActivity().getResources().getDrawable(
R.drawable.add_host))
.setCustomTitle(/*
* Html.fromHtml(
* "<b><font color=\"purple\"> Status</font></b>"
* )
*/view123)
.setIcon(
getActivity().getResources().getDrawable(
R.drawable.add_host))
.setMultiChoiceItems(statusesStringArray,
isSelectedStatuses,
new DialogSelectionClickHandler())
.setPositiveButton(
Html.fromHtml("<b><font color=\"purple\">Apply Filter</font></b>"),
new DialogButtonClickHandler() {
})
.setNegativeButton(
Html.fromHtml("<b><font color=\"purple\">Remove Filter</font></b>"),
new DialogButtonClickHandler());
final AlertDialog dlg = statusDialog.create();
dlg.show();
((TextView) view123.findViewById(R.id.tvDialogTitle))
.setText("Status");
((CheckBox) view123.findViewById(R.id.cbSelectAll))
.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(
CompoundButton buttonView, boolean isChecked) {
// TODO Auto-generated method stub
if (isChecked == true) {
ListView list = dlg.getListView();
for (int i = 0; i < list.getCount(); i++) {
isSelectedStatuses[i] = true;
list.setItemChecked(i, true);
}
}
else if (isChecked == false) {
ListView list = dlg.getListView();
for (int j = 0; j < list.getCount(); j++) {
isSelectedStatuses[j] = false;
list.setItemChecked(j, false);
}
// }
}
}
});
}
});
Ok I solved the issue!!! YAY!
What I had to do to prevent the IndexOutOFBounds Exception was to reset the list view adapter so to refresh the list view contents. So the magic line was
listView.setAdapter(mMyListViewAdapter);
However I believe this is not the best practise to use when working with a list view and its better to update the content of the adapter that the list view is attached to. But I not quite sure how to go about that?
Anyway he is my updated remove method code.
private void removeItems() {
final SparseBooleanArray checkedItems = listView.getCheckedItemPositions();
if (checkedItems != null) {
final int checkedItemsCount = checkedItems.size();
// Lets get the position of the view to scroll to before the first checked
// item to restore scroll position
//
int topPosition = checkedItems.keyAt(0) - 1;
listView.setAdapter(null);
for (int i = checkedItemsCount - 1; i >= 0 ; i--) {
// This tells us the item position we are looking at
// --
final int position = checkedItems.keyAt(i);
// This tells us the item status at the above position
// --
final boolean isChecked = checkedItems.valueAt(i);
if (isChecked) {
Item item = mMyListViewAdapter.getItem(position);
mMyListViewAdapter.remove(item);
//mMyListViewAdapter.notifyDataSetChanged();
}
}
listView.setAdapter(mMyListViewAdapter);
//if topPosition is -1 then item zero is positioned by default.
listView.setSelection(topPosition);
}
}