Skip to content

Commit 14c0804

Browse files
committed
Fix bug 5399568 - ListView check states inconsistent after data set change
ListView tracks check states in two ways, by position and by ID if an adapter reports stable IDs. After a data set change there was no guarantee that the position checked mapping was consistent. Fix up the position mapping from the ID mapping after a data set change. In the future this should happen by asking the adapter where a given ID is now located, but this will require new API and not all adapters in the wild will implement it. For now make a best guess by searching in a limited window around the item's last known position. Change-Id: I70ba89eb103c438b0410c3c6d066acc3918459f9
1 parent f270a15 commit 14c0804

File tree

1 file changed

+86
-16
lines changed

1 file changed

+86
-16
lines changed

core/java/android/widget/AbsListView.java

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
237237
SparseBooleanArray mCheckStates;
238238

239239
/**
240-
* Running state of which IDs are currently checked
240+
* Running state of which IDs are currently checked.
241+
* If there is a value for a given key, the checked state for that ID is true
242+
* and the value holds the last known position in the adapter for that id.
241243
*/
242-
LongSparseArray<Boolean> mCheckedIdStates;
244+
LongSparseArray<Integer> mCheckedIdStates;
243245

244246
/**
245247
* Controls how the next layout will happen
@@ -471,6 +473,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
471473
*/
472474
static final int OVERSCROLL_LIMIT_DIVISOR = 3;
473475

476+
/**
477+
* How many positions in either direction we will search to try to
478+
* find a checked item with a stable ID that moved position across
479+
* a data set change. If the item isn't found it will be unselected.
480+
*/
481+
private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
482+
474483
/**
475484
* Used to request a layout when we changed touch mode
476485
*/
@@ -806,7 +815,7 @@ public void setAdapter(ListAdapter adapter) {
806815
if (adapter != null) {
807816
if (mChoiceMode != CHOICE_MODE_NONE && mAdapter.hasStableIds() &&
808817
mCheckedIdStates == null) {
809-
mCheckedIdStates = new LongSparseArray<Boolean>();
818+
mCheckedIdStates = new LongSparseArray<Integer>();
810819
}
811820
}
812821

@@ -901,7 +910,7 @@ public long[] getCheckedItemIds() {
901910
return new long[0];
902911
}
903912

904-
final LongSparseArray<Boolean> idStates = mCheckedIdStates;
913+
final LongSparseArray<Integer> idStates = mCheckedIdStates;
905914
final int count = idStates.size();
906915
final long[] ids = new long[count];
907916

@@ -948,7 +957,7 @@ public void setItemChecked(int position, boolean value) {
948957
mCheckStates.put(position, value);
949958
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
950959
if (value) {
951-
mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
960+
mCheckedIdStates.put(mAdapter.getItemId(position), position);
952961
} else {
953962
mCheckedIdStates.delete(mAdapter.getItemId(position));
954963
}
@@ -980,7 +989,7 @@ public void setItemChecked(int position, boolean value) {
980989
if (value) {
981990
mCheckStates.put(position, true);
982991
if (updateIds) {
983-
mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
992+
mCheckedIdStates.put(mAdapter.getItemId(position), position);
984993
}
985994
mCheckedItemCount = 1;
986995
} else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
@@ -1010,7 +1019,7 @@ public boolean performItemClick(View view, int position, long id) {
10101019
mCheckStates.put(position, newValue);
10111020
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
10121021
if (newValue) {
1013-
mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
1022+
mCheckedIdStates.put(mAdapter.getItemId(position), position);
10141023
} else {
10151024
mCheckedIdStates.delete(mAdapter.getItemId(position));
10161025
}
@@ -1032,7 +1041,7 @@ public boolean performItemClick(View view, int position, long id) {
10321041
mCheckStates.put(position, true);
10331042
if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
10341043
mCheckedIdStates.clear();
1035-
mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
1044+
mCheckedIdStates.put(mAdapter.getItemId(position), position);
10361045
}
10371046
mCheckedItemCount = 1;
10381047
} else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
@@ -1081,7 +1090,7 @@ public void setChoiceMode(int choiceMode) {
10811090
mCheckStates = new SparseBooleanArray();
10821091
}
10831092
if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
1084-
mCheckedIdStates = new LongSparseArray<Boolean>();
1093+
mCheckedIdStates = new LongSparseArray<Integer>();
10851094
}
10861095
// Modal multi-choice mode only has choices when the mode is active. Clear them.
10871096
if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
@@ -1427,7 +1436,7 @@ static class SavedState extends BaseSavedState {
14271436
boolean inActionMode;
14281437
int checkedItemCount;
14291438
SparseBooleanArray checkState;
1430-
LongSparseArray<Boolean> checkIdState;
1439+
LongSparseArray<Integer> checkIdState;
14311440

14321441
/**
14331442
* Constructor called from {@link AbsListView#onSaveInstanceState()}
@@ -1451,10 +1460,14 @@ private SavedState(Parcel in) {
14511460
checkedItemCount = in.readInt();
14521461
checkState = in.readSparseBooleanArray();
14531462
long[] idState = in.createLongArray();
1463+
int[] idPositions = in.createIntArray();
14541464

1455-
if (idState.length > 0) {
1456-
checkIdState = new LongSparseArray<Boolean>();
1457-
checkIdState.setValues(idState, Boolean.TRUE);
1465+
final int idLength = idState.length;
1466+
if (idLength > 0) {
1467+
checkIdState = new LongSparseArray<Integer>();
1468+
for (int i = 0; i < idLength; i++) {
1469+
checkIdState.put(idState[i], idPositions[i]);
1470+
}
14581471
}
14591472
}
14601473

@@ -1471,6 +1484,15 @@ public void writeToParcel(Parcel out, int flags) {
14711484
out.writeInt(checkedItemCount);
14721485
out.writeSparseBooleanArray(checkState);
14731486
out.writeLongArray(checkIdState != null ? checkIdState.getKeys() : new long[0]);
1487+
1488+
int size = checkIdState != null ? checkIdState.size() : 0;
1489+
int[] idPositions = new int[size];
1490+
if (size > 0) {
1491+
for (int i = 0; i < size; i++) {
1492+
idPositions[i] = checkIdState.valueAt(i);
1493+
}
1494+
out.writeIntArray(idPositions);
1495+
}
14741496
}
14751497

14761498
@Override
@@ -1565,7 +1587,7 @@ public Parcelable onSaveInstanceState() {
15651587
ss.checkState = mCheckStates.clone();
15661588
}
15671589
if (mCheckedIdStates != null) {
1568-
final LongSparseArray<Boolean> idState = new LongSparseArray<Boolean>();
1590+
final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
15691591
final int count = mCheckedIdStates.size();
15701592
for (int i = 0; i < count; i++) {
15711593
idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
@@ -4786,15 +4808,63 @@ boolean resurrectSelection() {
47864808
return selectedPos >= 0;
47874809
}
47884810

4811+
void confirmCheckedPositionsById() {
4812+
// Clear out the positional check states, we'll rebuild it below from IDs.
4813+
mCheckStates.clear();
4814+
4815+
boolean checkedCountChanged = false;
4816+
for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
4817+
final long id = mCheckedIdStates.keyAt(checkedIndex);
4818+
final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
4819+
4820+
final long lastPosId = mAdapter.getItemId(lastPos);
4821+
if (id != lastPosId) {
4822+
// Look around to see if the ID is nearby. If not, uncheck it.
4823+
final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
4824+
final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
4825+
boolean found = false;
4826+
for (int searchPos = start; searchPos < end; searchPos++) {
4827+
final long searchId = mAdapter.getItemId(searchPos);
4828+
if (id == searchId) {
4829+
found = true;
4830+
mCheckStates.put(searchPos, true);
4831+
mCheckedIdStates.setValueAt(checkedIndex, searchPos);
4832+
break;
4833+
}
4834+
}
4835+
4836+
if (!found) {
4837+
mCheckedIdStates.delete(id);
4838+
checkedIndex--;
4839+
mCheckedItemCount--;
4840+
checkedCountChanged = true;
4841+
if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) {
4842+
mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
4843+
lastPos, id, false);
4844+
}
4845+
}
4846+
} else {
4847+
mCheckStates.put(lastPos, true);
4848+
}
4849+
}
4850+
4851+
if (checkedCountChanged && mChoiceActionMode != null) {
4852+
mChoiceActionMode.invalidate();
4853+
}
4854+
}
4855+
47894856
@Override
47904857
protected void handleDataChanged() {
47914858
int count = mItemCount;
47924859
int lastHandledItemCount = mLastHandledItemCount;
47934860
mLastHandledItemCount = mItemCount;
4794-
if (count > 0) {
47954861

4796-
int newPos;
4862+
if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) {
4863+
confirmCheckedPositionsById();
4864+
}
47974865

4866+
if (count > 0) {
4867+
int newPos;
47984868
int selectablePos;
47994869

48004870
// Find the row we are supposed to sync to

0 commit comments

Comments
 (0)