29

After reading and try'n'error for days, I´m giving up and ask for help.

< edit > I am using ActionBarSherlock. < /edit >

What I want to achieve: A ListView with a custom layout for each row, where the user can select multiple list items. A selected list item should have a different background color. When there is at least one item selected, a contextual action bar (CAB) should be shown. It should look more or less like the multiple selection of emails in the GMail app. The only difference is that in the gmail app the selection is done by clicking the checkbox of a row, whereas I don´t want to have a checkbox, but a row should be selected no matter, where the user clicks. Multiple selection in GMail app

What I tried: Following this tutorial, using a Checkable row layout with some logic to change the background color when the check state was toggled, I got everything working except that I could not register a click listener like OnItemClickListener on the ListView to show the CAB. Neither providing a click listener for each row View helped because this prevented to change the background color of the selected items. I also tried adding a MultiChoiceModeListener to the ListView like that

    listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
    listView.setMultiChoiceModeListener(new MultiChoiceModeListener() { //.. });

With the same result, no background color change.

What I am looking for: A hint or a tutorial or sample code how to do this. If you need some code snippets to help, let me know.

Konsumierer
  • 1,295
  • 1
  • 15
  • 33

3 Answers3

37

See if the code helps you(it's basically a ListActivity with a custom adapter to hold the status of checked items(+ different background)):

public class CABSelection extends ListActivity {

    private ArrayList<String> mItems = new ArrayList<String>();
    private SelectionAdapter mAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        for (int i = 0; i < 24; i++) {
            mItems.add("Name" + i);
        }
        // R.layout.adapters_cabselection_row is a LinearLayout(with green
        // background(#99cc00)) that wraps an ImageView and a TextView
        mAdapter = new SelectionAdapter(this,
                R.layout.adapters_cabselection_row, R.id.the_text, mItems);
        setListAdapter(mAdapter);
        getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
        getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() {

            private int nr = 0;

            @Override
            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                MenuInflater inflater = getMenuInflater();
                inflater.inflate(R.menu.cabselection_menu, menu);
                return true;
            }

            @Override
            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                return false;
            }

            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                StringBuilder sb = new StringBuilder();
                Set<Integer> positions = mAdapter.getCurrentCheckedPosition();
                for (Integer pos : positions) {
                    sb.append(" " + pos + ",");
                }               
                switch (item.getItemId()) {
                case R.id.edit_entry:
                    Toast.makeText(CABSelection.this, "Edited entries: " + sb.toString(),
                            Toast.LENGTH_SHORT).show();
                    break;
                case R.id.delete_entry:
                    Toast.makeText(CABSelection.this, "Deleted entries : " + sb.toString(),
                            Toast.LENGTH_SHORT).show();
                    break;
                case R.id.finish_it:
                    nr = 0;
                    mAdapter.clearSelection();
                    Toast.makeText(CABSelection.this, "Finish the CAB!",
                            Toast.LENGTH_SHORT).show();
                    mode.finish();
                }
                return false;
            }

            @Override
            public void onDestroyActionMode(ActionMode mode) {
                nr = 0;
                mAdapter.clearSelection();
            }

            @Override
            public void onItemCheckedStateChanged(ActionMode mode,
                    int position, long id, boolean checked) {
                if (checked) {
                    nr++;
                    mAdapter.setNewSelection(position, checked);                    
                } else {
                    nr--;
                    mAdapter.removeSelection(position);                 
                }
                mode.setTitle(nr + " rows selected!");

            }

        });
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        l.setItemChecked(position, !mAdapter.isPositionChecked(position));
    }

    private class SelectionAdapter extends ArrayAdapter<String> {

        private HashMap<Integer, Boolean> mSelection = new HashMap<Integer, Boolean>();

        public SelectionAdapter(Context context, int resource,
                int textViewResourceId, List<String> objects) {
            super(context, resource, textViewResourceId, objects);
        }

        public void setNewSelection(int position, boolean value) {
            mSelection.put(position, value);
            notifyDataSetChanged();
        }

        public boolean isPositionChecked(int position) {
            Boolean result = mSelection.get(position);
            return result == null ? false : result;
        }

        public Set<Integer> getCurrentCheckedPosition() {
            return mSelection.keySet();
        }

        public void removeSelection(int position) {
            mSelection.remove(position);
            notifyDataSetChanged();
        }

        public void clearSelection() {
            mSelection = new HashMap<Integer, Boolean>();
            notifyDataSetChanged();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v = super.getView(position, convertView, parent);//let the adapter handle setting up the row views
            v.setBackgroundColor(Color.parseColor("#99cc00")); //default color
            if (mSelection.get(position) != null) {
                v.setBackgroundColor(Color.RED);// this is a selected position so make it red
            }
            return v;
        }

    }

}

The R.layout.adapters_cabselection_row is a custom layout for the row(a very simple one) with a green background:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#99cc00" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <TextView
        android:id="@+id/the_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#ffffff"
        android:textSize="17sp"
        android:textStyle="bold" />

</LinearLayout>

R.menu.cabselection_menu is a menu file with 3 options(edit, delete, finish the CAB) which don't do anything except pop a Toast with a message regarding the rows selected:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:id="@+id/edit_entry"
        android:icon="@android:drawable/ic_menu_edit"
        android:title="Edit!"/>
    <item
        android:id="@+id/delete_entry"
        android:icon="@android:drawable/ic_menu_delete"
        android:title="Delete!"/>
    <item
        android:id="@+id/finish_it"
        android:icon="@android:drawable/ic_menu_crop"
        android:title="Get me out!"/>

</menu>
Rafael Oliveira
  • 2,823
  • 4
  • 33
  • 50
user
  • 86,916
  • 18
  • 197
  • 190
  • I cannot verify that this is the correct answer, since I forgot to mention that I am using ActionBarSherlock. The MultiChoiceModeListener is only available since API level 11, and ABS has not yet implemented an equivalent. But your previous answer seemed to help me. – Konsumierer May 21 '12 at 08:45
  • Works very well with API = 11 and higher but doesn't support on API<11. Is there any support library available? – Apurva Jan 13 '15 at 16:23
  • @Apurva A `MultiChoiceModeListener` is an addition on top of the basic `ActionMode`. If you want to implement the functionality on versions below 11 you can simply implement your own `ActionMode` and wire it up with the `ListView` events. Konsumierer's answer below shows how you can do it. – user Jan 13 '15 at 18:19
14

I think the easiest way is to apply

android:background="android:attr/activatedBackgroundIndicator"

To which ever layout is the one you will be clicking.

This highlights the layout when selected using

listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);

worked for me anyway

M-Wajeeh
  • 17,204
  • 10
  • 66
  • 103
SeanSWatkins
  • 413
  • 4
  • 9
11

Using ActionBarSherlock the MultiChoiceModeListener used in Luksprog´s answer is not yet available if you want to support API level < 11.

A workaround is to use the onItemClickListener.

List setup:

listView = (ListView) timeline.findViewById(android.R.id.list);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
listView.setItemsCanFocus(false);
listView.setAdapter(new ListAdapter(getActivity(), R.layout.cleaning_list_item, items));

Listener of ListFragment or ListActivity:

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
    SparseBooleanArray checked = listView.getCheckedItemPositions();
    boolean hasCheckedElement = false;
    for (int i = 0; i < checked.size() && !hasCheckedElement; i++) {
        hasCheckedElement = checked.valueAt(i);
    }

    if (hasCheckedElement) {
        if (mMode == null) {
            mMode = ((SherlockFragmentActivity) getActivity()).startActionMode(new MyActionMode());
            mMode.invalidate();
        } else {
            mMode.invalidate();
        }
    } else {
        if (mMode != null) {
            mMode.finish();
        }
    }
}

Where MyActionMode is an implementation of ActionMode.Callback:

private final class MyActionMode implements ActionMode.Callback { /* ... */ }
Konsumierer
  • 1,295
  • 1
  • 15
  • 33
  • You should have mentioned you use `ActionBarSherlock` I could have given the same answer. Even in my example (with the `MultiChoceiListener` available) you could implement your own `ActionMode` instance to start the CAB. – user May 21 '12 at 09:04
  • Yes, it was my fault. I didn´t think that it was of importance when I posted the question. But I am pretty sure that your answer will help others who use API level 11 and above. – Konsumierer May 21 '12 at 09:10
  • In this code example; what is the meaning of "new ListAdapter"? ListAdapter is abstract. Is the code there just to make a point? The signature looks a bit like ArrayAdapter's. Is it a typo? – Glenn Bech May 21 '12 at 17:48
  • ListAdapter is my own class extending from ArrayAdapter. Just the regular way of handling custom lists. – Konsumierer May 22 '12 at 11:28
  • 1
    This did not work for me, so here is my workaround http://stackoverflow.com/questions/14737519/implementing-multi-selection-and-contextual-actionmode-in-actionbarsherlock/14737520#14737520 – Yaroslav Mytkalyk Feb 06 '13 at 19:55
  • In your answer, what's `mMode`? An instance of your activity? When I try to do the similar stuff, the compiler told me I need to create a new instance of the activity. Thanks for the answer it did help me! – dumbfingers Apr 03 '13 at 12:41
  • `mMode` is a member variable of type `ActionMode` – Konsumierer Apr 03 '13 at 17:26