ListView selection remains persistent after exiting choice mode
Asked Answered
E

11

42

I have a ListView subclass that I allow selections on when the context action bar (CAB) is active. The CAB is set as a callback to the onItemLongClick event:

public boolean onCreateActionMode(ActionMode mode, Menu menu) {
    // Inflate a menu resource providing context menu items
    MenuInflater inflater = mode.getMenuInflater();
    inflater.inflate(context_menu, menu);
    getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
    return true;
}

This is fine, and the ListView works as expected, with the currently selected item staying highlighted when touched.

When I close the CAB, I want the ListView to return to normal (i.e. Touch mode). The problem is that the last selected item remains highlighted indefinitely, regardless of what methods I try to clear it:

public void onDestroyActionMode(ActionMode mode) {
    //Unselect any rows
    ListView lv = getListView();
    lv.clearChoices(); // Has no effect
    lv.setChoiceMode(ListView.CHOICE_MODE_NONE); // Has no effect on the highlighted item 
    lv.setFocusable(false); // Has no effect
    lv.setSelection(0); // Has no effect
    mActionMode = null;
}

Any suggestions?

Elaterite answered 17/3, 2012 at 22:4 Comment(0)
N
35

The main reason for the problem is that once the ListView selection mode is switched to CHOICE_MODE_NONE, the framework optimizes out the clear operation as it is no longer supporting 'selections'. I have improved the above workarounds a bit by clearing the selection state manually and then setting the mode in a delayed manner so the framework will have its turn to clear the state before turning the mode to CHOICE_MODE_NONE.

final ListView lv = getListView();
lv.clearChoices();
for (int i = 0; i < lv.getCount(); i++)
    lv.setItemChecked(i, false);
lv.post(new Runnable() {
    @Override
    public void run() {
        lv.setChoiceMode(ListView.CHOICE_MODE_NONE);
    }
});
Nutt answered 31/1, 2013 at 19:20 Comment(8)
This is actually the best and least hacky way to do this.Dario
Hmmm... I get StackOverflowErrors when doing this. Calling setItemChecked() triggers onDestroyActionMode() again.Accusatory
getChildCount() is not the same as getCount(). getChildCount includes headers and footers, and includes only the views that are visible within the list (starting at 0, regardless of what position you are scrolled to in the list). setItemChecked() takes a position argument, which is not the same position that the child index represents.Pimply
Actually, I don't see lv.clearChoices() required here, the code worked for me without it too.Weatherproof
Fixed the code to use getCount() as per Joe's recommendation.Nutt
Instead of going through all items, I use SparseBooleanArray selectedItems = lv.getCheckedItemPositions(); if(selectedItems != null){ for (int i = 0; i < selectedItems.size(); i++) { lv.setItemChecked(selectedItems.keyAt(i), false); }Fullrigged
Clearing the checked items by looping triggers onDestroyActionMode again as CommonsWare discovered and outputs an error in the logs in Android M. To get around that I used lv.clearChoices to clear the choices internally and then lvAdapter.notifyDataSetChanged to redraw the items. YMMV.Isolate
You can simply replace the loop with lv.requestLayout() to avoid errors. So, in Kotlin: lv.clearChoices(); lv.requestLayout(); lv.post { lv.choiceMode = CHOICE_MODE_NONE }Cooperative
N
21

I faced the same issue and since requesting layout doesn't solve the problem for me either I implemented a little hack which works for me. Maybe this is the same issue because I'm switching between CHOICE_MODE_SINGLE and CHOICE_MODE_NONE.

When the action mode ends I'm calling this code snippet. clearChoices makes sure that all items are not checked anymore (internally). The iteration over the views makes sure that all currently visible views are reset and not checked anymore.

mListView.clearChoices();

for (int i = 0; i < mListView.getChildCount(); i++) {
    ((Checkable) mListView.getChildAt(i)).setChecked(false);
}

mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
Nath answered 10/5, 2012 at 21:47 Comment(3)
I think this is the correct answer for APIs older than 11 which don't have AbsListView.MultiChoiceModeListenerTroyes
I can't get this to work since my ListItems are RelativeLayouts. Any suggestions?Frankforter
Hi @S.D. you can try this. 'code' for (int i = 0; i < mListView.getCount(); i++) mListView.semidetached(i, false);Cathern
O
18

Looking at the ListView sourcecode, the only way to work around this is to set the ListView to CHOICE_MODE_NONE, then re-assign the ListAdapter (which clears the internal selection list regardless of choice mode)

i.e. in a ListFragment/ListActivity

getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
getListView().setAdapter(getListAdapter())
Organic answered 21/12, 2012 at 14:3 Comment(2)
THANK YOU SO MUCH! Re-setting the adapter does the trick, and I suspect is also compatible with all API versions...Roadside
Everything is great, except that ListView will be scrolled to the top-most positionHeave
A
4

I was having this issue in API Level 17 and solved it by doing:

listView.clearChoices();
listView.invalidateViews();
Apollonius answered 15/7, 2013 at 16:10 Comment(0)
S
2

For me, it seems the accepted answer is not working for invisible items, and it's no need to call

for (int i = 0; i < lv.getCount(); i++)
        lv.setItemChecked(i, false);

instead, just call

lv.requestLayout();

To completely solve my issue, I call

lv.clearChoices();
lv.requestLayout();

in onDestroyActionMode()

and call

lv.setItemChecked(position, false)

in onItemClick() when it's not in ActionMode

However, I did not confirm whether call setItemChecked() will result some performance issues

Shrubby answered 4/4, 2014 at 2:45 Comment(0)
B
1

This has been logged as an AOSP bug, but marked as obsolete for whatever reason.

Normally you would expect this to work:

getListView().clearChoices();
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);

Unfortunately it does not. Deferring setting choice mode to none in the next layout pass would work:

getListView().clearChoices();
getListView().post(new Runnable() {
    @Override
    public void run() {
        getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
    }
});
Buffet answered 2/3, 2016 at 8:20 Comment(0)
S
0

I had tried all the approaches discussed above but none of them work for me. Finally, I decide to apply the following workaround. The key idea is that,

During multimode, instead of reusing the "cached" view, we will create a completely new view. Not efficient, but at least "partially" solve my problem.

Here is the code of my customized ArrayAdapter

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    // Key to solve this problem. When we are in multimode, we will not reusing the cached view.
    View rowView = this.multimode ? null : convertView;

    if (rowView == null) {
        LayoutInflater inflater = activity.getLayoutInflater();
        rowView = inflater.inflate(R.layout.watchlist_row_layout, null);
        ViewHolder viewHolder = new ViewHolder();
        viewHolder.textView0 = (TextView) rowView.findViewById(R.id.text_view_0);
        viewHolder.textView1 = (TextView) rowView.findViewById(R.id.text_view_1);
        viewHolder.textView2 = (TextView) rowView.findViewById(R.id.text_view_2);
        rowView.setTag(viewHolder);
    }

Also, I feel safer to have the following code in ActionMode.Callback, although I'm not sure how much it helps.

    @Override
    public void onDestroyActionMode(ActionMode mode) {
        MyFragment.this.myArrayAdapter.setMultimode(false);

        // https://mcmap.net/q/383564/-listview-selection-remains-persistent-after-exiting-choice-mode
        // Using View.post is the key to solve the problem.
        final ListView listView = MyFragment.this.getListView();
        listView.clearChoices();
        for (int i = 0, ei = listView.getChildCount(); i < ei; i++) {
            listView.setItemChecked(i, false);
        }
        listView.post(new Runnable() {
            @Override
            public void run() {
                listView.setChoiceMode(ListView.CHOICE_MODE_NONE);
            }
        });
        actionMode = null;
    }

Side Note

Using MultiChoiceModeListener couple with CHOICE_MODE_MULTIPLE_MODAL will make this bug gone. However, for device below API level 11 will not able to use this solution.

Sudan answered 22/4, 2013 at 13:54 Comment(1)
Note, this will not completely eliminate the problem. It just reduce its frequency of occurrence.Sudan
D
0

I know this has been answered, but above answers still gave me problems with the cached/recycled views that ListView maintains, that didn't update it's state when scrolled back into view. So, the above solution changes slightly to:

    lv.clearChoices();  

    ArrayList<View> list = new ArrayList<View>();
    lv.reclaimViews(list);
    for (View view : list) {
        ((Checkable) view).setChecked(false);
    }

    lv.setChoiceMode(lv.CHOICE_MODE_NONE);

This is better than using getChildAt(i) because that method jusg gives you the currently visble views and does not account for the internal cached views, that are not visible.

Disfigure answered 22/11, 2013 at 19:14 Comment(0)
I
0

I have found that the only two methods that work here (API 19) are:

  • Resetting the list adapter, which is undesirable because it goes back to the top of the list;
  • Setting the choice mode to CHOICE_MODE_NONE in a new Runnable

If the choice mode is changed without using listView.post(new Runnable()), it doesn't work. Can anyone explain to me why this is?

Apologies for not commenting; I have no reputation.

Thanks.

Instancy answered 3/1, 2014 at 10:4 Comment(0)
C
0

Not sure if this is too late just wanted to share. I created an intent to the same page so that once the clicked data is captured it recreates a fresh page without any clicked persistence.

Convenient answered 24/5, 2014 at 10:39 Comment(0)
G
-2

Is not a bug. That behavior is required to support multiple HID for Android. So to show the selection state you only need set the choice mode of the listview and a background to support the selected state for the "list item layout", like:

android:background="?android:attr/activatedBackgroundIndicator"

FYI: http://android-developers.blogspot.mx/2008/12/touch-mode.html

Gers answered 19/6, 2013 at 18:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.