27

How should I implement multi selection on AdapterView with ActionBarSherlock, because it does not provide MultiChoiceModeListener?

This is what it looks like

Contextual Action Mode

How can you do this?

Charles
  • 50,943
  • 13
  • 104
  • 142
Yaroslav Mytkalyk
  • 16,950
  • 10
  • 72
  • 99

2 Answers2

40

So here's what I did.

Edit: Over a year passed since I found out the previous answer had alot of useless code (woops) and the CAB thing can be achieved with much less effort and a cleaner code, so I took some time and updated it

The LibraryFragment ListView should be defined with choice mode "none"

<ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:choiceMode="none"/>

The list item should have an ?attr/activatedBackgroundIndicator foreground in order to automatically draw highlighted semitransparent overlay on list.setItemChecked(pos, true)

list_item_library.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foreground="?attr/activatedBackgroundIndicator"
    android:paddingBottom="5dp"
    android:paddingTop="5dp" >

....

The ListFragment

import android.support.v4.app.DialogFragment;
import com.actionbarsherlock.app.SherlockListFragment;
import com.actionbarsherlock.view.ActionMode;
import com.actionbarsherlock.view.Menu;

public final class LibraryFragment
        extends SherlockListFragment
{

    private MyListAdapter adapter;
    private ListView list;

    // if ActoinMode is null - assume we are in normal mode
    private ActionMode actionMode;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        View v = inflater.inflate(R.layout.fragment_library, null);
        this.list = (ListView) v.findViewById(android.R.id.list);
        this.initListView();
        return v;
    }

    @Override
    public void onPause()
    {
        super.onPause();
        if (this.actionMode != null) {
            this.actionMode.finish();
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        updateData();
    }

    // update ListView
    protected void updateData()
    {
        if (adapter == null) {
            return;
        }
        adapter.clear();
        // my kinda stuff :)
        File[] items = scan();
        if (items != null) {
            adapter.updateData(items);
            if (actionMode != null) {
                actionMode.invalidate();
            }
        }
        // if empty - finish action mode.
        if (actionMode != null && (files == null || files.length == 0)) {
            actionMode.finish();
        }
    }

    private void initListView()
    {
        this.adapter = new MyAdapter(getActivity());
        this.list.setAdapter(adapter);
        this.list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener()
        {

            @Override
            public boolean onItemLongClick(AdapterView<?> arg0,
                    View arg1, int arg2, long arg3)
            {
                if (actionMode != null) {
                    // if already in action mode - do nothing
                    return false;
                }
                // set checked selected item and enter multi selection mode
                list.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
                list.setItemChecked(arg2, true);

                getSherlockActivity().startActionMode(
                        new ActionModeCallback());
                return true;
            }
        });
        this.list.setOnItemClickListener(new AdapterView.OnItemClickListener()
        {
            @Override
            public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
                    long arg3)
            {
                if (actionMode != null) {
                    // the items are auomatically "checked" becaise we've set AbsListView.CHOICE_MODE_MULTIPLE before
                    // starting action mode, so the only thing we have to care about is invalidating the actionmode
                    actionMode.invalidate(); //invalidate title and menus.
                } else {
                    // do whatever you should on item click
                }
            }
        });
    }


    // all our ActionMode stuff here :)
    private final class ActionModeCallback
            implements ActionMode.Callback
    {

        // " selected" string resource to update ActionBar text
        private String selected = getActivity().getString(
                R.string.library_selected);

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu)
        {
            actionMode = mode;
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu)
        {
            // remove previous items
            menu.clear();
            final int checked = list.getCheckedItemCount();
            // update title with number of checked items
            mode.setTitle(checked + this.selected);
            switch (checked) {
            case 0:
                // if nothing checked - exit action mode
                mode.finish();
                return true;
            case 1:
                // all items - rename + delete
                getSherlockActivity().getSupportMenuInflater().inflate(
                        R.menu.library_context, menu);
                return true;
            default:
                getSherlockActivity().getSupportMenuInflater().inflate(
                        R.menu.library_context, menu);
                // remove rename option - because we have more than one selected
                menu.removeItem(R.id.library_context_rename);
                return true;
            }
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode,
                com.actionbarsherlock.view.MenuItem item)
        {
            SparseBooleanArray checked;
            switch (item.getItemId()) {
            case R.id.library_context_rename:
                // the rename action is present only when only one item is selected. 
                // so when the first checked item found, show the dialog and break
                checked = list.getCheckedItemPositions();
                for (int i = 0; i < checked.size(); i++) {
                    final int index = checked.keyAt(i);
                    if (checked.get(index)) {
                        final DialogFragment d = RenameDialog.instantiate(adapter.getItem(index).getFile(), LibraryFragment.this);
                        d.show(getActivity().getSupportFragmentManager(), "dialog");
                        break;
                    }
                }
                return true;

            case R.id.library_context_delete:
                // delete every checked item
                checked = list.getCheckedItemPositions();
                for (int i = 0; i < checked.size(); i++) {
                    final int index = checked.keyAt(i);
                    if (checked.get(index)) {
                        adapter.getItem(index).getFile().delete();
                    }
                }
                updateData();
                return true;
            default:
                return false;
            }
        }

        @Override
        public void onDestroyActionMode(ActionMode mode)
        {
            list.clearChoices();

            //workaround for some items not being unchecked.
            //see http://stackoverflow.com/a/10542628/1366471
            for (int i = 0; i < list.getChildCount(); i++) {
                (list.getChildAt(i).getBackground()).setState(new int[] { 0 });
            }

            list.setChoiceMode(AbsListView.CHOICE_MODE_NONE);
            actionMode = null;
        }

    }
Yaroslav Mytkalyk
  • 16,950
  • 10
  • 72
  • 99
  • Thanks for sharing. I was looking for exactly this ! – h4ck3d Feb 06 '13 at 19:57
  • thanks for this great post, but I have an issue on orientation change. Let's say, I selected a few items on the actionmode, then rotate the device, I am able to restore the actionbar, and set the selected items. But the highlight doesn't show up until I scroll the list off the screen and back, the highlight showed. Any idea? – triston Jul 28 '13 at 20:20
  • @triston how do you restore state? Make sure you call setChecked() on Adapter. In this case it should work. But if that's what you were doing and it's not working, I'm afraid I can't help you since I don't have enough free time to test these things out. – Yaroslav Mytkalyk Jul 29 '13 at 07:43
  • hey DD, thank you for getting back to me, really appreciate for your kindness. And yes, I did call setChecked() after the rotation, the adapter's items are checked (by setChecked), I can see from the LogCat. When i scroll the list to the listItems (I have a big listview header, so i have to scroll down to the list items after each rotation), the highlight do not show, then i scroll back to the top, then scroll back down to the list items, the highlight showed. – triston Jul 29 '13 at 20:18
  • This answer is the new "42". Thank you so much for providing this sample code. – Phillip Aug 24 '13 at 05:50
  • 1
    I used a rather simple way by setting `` as my style of the list item. – Sufian Mar 14 '14 at 07:50
  • @Sufian I thought about this option recently. When I wrote that answer I wasn't aware of it. I will reconsider updating it soon. – Yaroslav Mytkalyk Mar 14 '14 at 08:48
  • 2
    Updated the answer with more cleaner way. Now no custom selectors or adapters needed. – Yaroslav Mytkalyk Mar 14 '14 at 10:33
  • Your solution is perfect but the problem i am facing is that i am not able to highlight the selected image and using this line :" android:foreground="?attr/activatedBackgroundIndicator" gives me an resource not found exception and ya i have already added the actionbarsherlock library – Satyen Udeshi Apr 30 '14 at 06:48
  • @SatyenUdeshi ActionBarSherlock has this attributes in it's themes. Are you sure your app theme extends Sherlock theme? Anyway, you can try to use drawable instead @drawable/abs__activated_background_holo_dark or @drawable/abs__activated_background_holo_light. Also, there is now Google's Appcompat-v7 library with ActionBar that has less bugs that you can use instead of Sherlock. – Yaroslav Mytkalyk Apr 30 '14 at 08:31
  • Thank you for your answer. I'm supporting API 8+ which is kind of problem to do multi-select list with CAB. I'm using support library v7 instead of ActionBarSherlock and with little changes in your code it works flawlessly. – Stepan Sanda Jul 12 '14 at 08:11
  • I had problems as well in highlighting the selected item, though I'm not using ActionBarSherlock. The solution for me was to create a `selector` drawable, containg this ``. Then using it as the list item's background like so `android:background="@drawable/my_list_selector"` – Robert May 21 '15 at 21:27
  • @Robert yes, that's what the `?attr/activatedBackgroundIndicator` which I mentioned as FrameLayout foreground does, which is fine for the default theme. If you want to override with your own colors you must create your own as that has the structure you mentioned. – Yaroslav Mytkalyk May 22 '15 at 09:25
0

your solution is the best and easiest solution for this thread. But there is a tiny issue in the getView() - refer back to my comments above.

int version = android.os.Build.VERSION.SDK_INT;     
if(version < 11){
    if (checkedItems.contains(Integer.valueOf(position))) {
        convertView.getBackground().setState(
                new int[] { android.R.attr.state_checked });
    } else {
        convertView.getBackground().setState(
                new int[] { -android.R.attr.state_checked });
    }
}else{

    if (checkedItems.contains(Integer.valueOf(position))) {
        convertView.setActivated(true);
    } else {
        convertView.setActivated(false);        
    }
}

This will give you full support from API8 to API18

triston
  • 493
  • 1
  • 10
  • 24
  • setActivated is available only starting from API level 11. If you use API 11 there is no need for ActionBarSherlock and my solution is bad since there is much easier way of doing this starting API 11. http://developer.android.com/guide/topics/ui/menus.html#CAB – Yaroslav Mytkalyk Jul 30 '13 at 07:02
  • My project is set a minimum API8, so I have to use both. I will modify my suggestion above too. Thanks. – triston Aug 01 '13 at 15:12