13

I'm going to begin by saying that I have read in detail almost every question on SO that I can find related to custom checkable list items and selectors. Many of them have similar issues, but none of the answers solve my problem.

At a point in my app I present a custom list activity. When created, it retrieves a set of static data from the intent that called it and passes that data to it's custom array adapter. Each list item is a simple RelativeLayout that implements the Checkable interface. By default, if you click on one of the items, a new activity is shown which displays detailed information about the selected contact. However, if an item in the list is long-clicked, an ActionMode is started. Clicking on an item in the list at this point does not display the detail activity, it just sets the item to checked. Then, if the user chooses one of the action mode items, it performs the action on the checked item(s).

An important thing to understand is that in both selection 'modes', clicking on a list item sets it to checked.

All of what I described above works perfectly. My only problem has to do with the backgrounds of the list items not being highlighted when they are set to checked, even using the default selector.

What I want to do, is have two selectors: one for each selection mode. In the first, the background doesn't change when an item is checked, and in the second it does. I have tried implementing custom selectors, but even in those state_checked is ignored! Other parts of the selector work fine, but not state_checked.

My implementation of CheckableListItem incorporates ideas from a lot of different examples, so if I'm doing something wrong, or if there is a better way let me know!

Note: An interesting point is that if I set the background of the list items in results_list_item.xml to my selector, instead of the listSelector property of the ListView, the backgrounds do change when an item is checked. However, doing this causes the long-press transition in my selector not to work.

ResultsActivity.java:

public class ResultsActivity extends ListActivity implements OnItemLongClickListener {

    private ListView listView;          // Reference to the list belonging to this activity
    private ActionMode mActionMode;     // Reference to the action mode that can be started
    private boolean selectionMode;      // Detail mode or check mode

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_results);

        // When the home icon is pressed, go back
        ActionBar actionBar = getActionBar();
        actionBar.setDisplayHomeAsUpEnabled(true);

        // Get a reference to the list
        listView = getListView();

        // Initially in detail mode
        selectionMode = true;

        // Get the contacts from the intent data and pass them to the contact adapter
        @SuppressWarnings("unchecked")
        ArrayList<Contact> results = ((ArrayList<Contact>)getIntent().getSerializableExtra("results"));
        Contact[] contacts = new Contact[results.size()];
        ContactArrayAdapter adapter = new ContactArrayAdapter(this, results.toArray(contacts));
        setListAdapter(adapter);

        // We will decide what happens when an item is long-clicked
        listView.setOnItemLongClickListener(this);
    }

    /**
     * If we are in detail mode, when an item in the list is clicked
     * create an instance of the detail activity and pass it the
     * chosen contact
     */
    public void onListItemClick(ListView l, View v, int position, long id) {
        if (selectionMode) {
            Intent displayContact = new Intent(this, ContactActivity.class);
            displayContact.putExtra("contact", (Contact)l.getAdapter().getItem(position));
            startActivity(displayContact);
        }
    }

    public boolean onCreateOptionsMenu(Menu menu) {
        return super.onCreateOptionsMenu(menu);
    }

    /**
     * If the home button is pressed, go back to the
     * search activity
     */
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                Intent intent = new Intent(this, SearchActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(intent);
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    /**
     * When an item is long-pressed, switch selection modes 
     * and start the action mode 
     */
    public boolean onItemLongClick(AdapterView<?> adapter, View view, int position, long i) {
        if (mActionMode != null)
            return false;

        if (selectionMode) {
            toggleSelectionMode();
            listView.startActionMode(new ListActionMode(this, getListView()));
            return true;
        }
        return false;
    }

    /**
     * Clear the list's checked items and switch selection modes
     */
    public void toggleSelectionMode() {
        listView.clearChoices();
        ((ContactArrayAdapter)listView.getAdapter()).notifyDataSetChanged();
        if (selectionMode) {
            selectionMode = false;
        } else {
            selectionMode = true;
        }
    }
}

activity_results.xml:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:choiceMode="multipleChoice" 
    android:listSelector="@drawable/list_selector" />

list_selector.xml:

<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item android:state_pressed="true" android:drawable="@drawable/blue_transition" />
    <item android:state_checked="true" android:drawable="@drawable/blue" />
</selector>

TwoLineArrayAdapter:

public abstract class TwoLineArrayAdapter extends ArrayAdapter<Contact> {

    private int mListItemLayoutResId;

    public TwoLineArrayAdapter(Context context, Contact[] results) {
        this(context, R.layout.results_list_item, results);
    }

    public TwoLineArrayAdapter(Context context, int listItemLayoutResourceId, Contact[] results) {
        super(context, listItemLayoutResourceId, results);
        mListItemLayoutResId = listItemLayoutResourceId;
    }

    public View getView(int position, View convertView, ViewGroup parent) {

        LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View listItemView = convertView;
        if (convertView == null) {
            listItemView = inflater.inflate(mListItemLayoutResId, parent, false);
        }

        // Get the text views within the layout
        TextView lineOneView = (TextView)listItemView.findViewById(R.id.results_list_item_textview1);
        TextView lineTwoView = (TextView)listItemView.findViewById(R.id.results_list_item_textview2);

        Contact c = (Contact)getItem(position);

        lineOneView.setText(lineOneText(c));
        lineTwoView.setText(lineTwoText(c));

        return listItemView;
    }

    public abstract String lineOneText(Contact c);

    public abstract String lineTwoText(Contact c);

}

ContactArrayAdapter:

public class ContactArrayAdapter extends TwoLineArrayAdapter {

    public ContactArrayAdapter(Context context, Contact[] contacts) {
        super(context, contacts);
    }

    public String lineOneText(Contact c) {
        return (c.getLastName() + ", " + c.getFirstName());
    }

    public String lineTwoText(Contact c) {
        return c.getDepartment();
    }

}

CheckableListItem.java:

public class CheckableListItem extends RelativeLayout implements Checkable {

    private boolean isChecked;
    private List<Checkable> checkableViews;

    public CheckableListItem(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
        initialise(attrs);
    }

    public CheckableListItem(Context context, AttributeSet attrs) {
        super(context, attrs);
        initialise(attrs);
    }

    public CheckableListItem(Context context, int checkableId) {
        super(context);
        initialise(null);
    }

    private void initialise(AttributeSet attrs) {
        this.isChecked = false;
        this.checkableViews = new ArrayList<Checkable>(5);
    }

    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean check) {
        isChecked = check;
        for (Checkable c : checkableViews) {
            c.setChecked(check);
        }
        refreshDrawableState();
    }

    public void toggle() {
        isChecked = !isChecked;
        for (Checkable c : checkableViews) {
            c.toggle();
        }
    }

    private static final int[] CheckedStateSet = {
        android.R.attr.state_checked
    };

    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CheckedStateSet);
        }
        return drawableState;
    }

    protected void onFinishInflate() {
        super.onFinishInflate();
        final int childCount = this.getChildCount();
        for (int i = 0; i < childCount; i++) {
            findCheckableChildren(this.getChildAt(i));
        }
    }

    private void findCheckableChildren(View v) {
        if (v instanceof Checkable) {
            this.checkableViews.add((Checkable) v);
        }
        if (v instanceof ViewGroup) {
            final ViewGroup vg = (ViewGroup) v;
            final int childCount = vg.getChildCount();
            for (int i = 0; i < childCount; i++) {
                findCheckableChildren(vg.getChildAt(i));
            }
        }
    }
}

results_list_item.xml:

<com.test.mycompany.Widgets.CheckableListItem
    android:id="@+id/results_list_item"
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content" 
    android:paddingLeft="10dp"
    android:paddingRight="10dp"
    android:paddingTop="5dp"
    android:paddingBottom="5dp" >

    <TextView android:id="@+id/results_list_item_textview1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:textSize="20sp"
        android:textColor="#000000"
        android:focusable="false" />

    <TextView android:id="@+id/results_list_item_textview2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_below="@id/results_list_item_textview1"
        android:textSize="16sp"
        android:textColor="@android:color/darker_gray" 
        android:focusable="false" />

</com.test.mycompany.Widgets.CheckableListItem>
Groppe
  • 3,819
  • 12
  • 44
  • 68
  • Where are you checking and unchecking your items? You can also try working with android:state_activited. – DroidBender Dec 04 '12 at 08:41
  • I don't do it myself. They are getting checked, though. When I put log statements in the items' setChecked method, I see that it is being called. – Groppe Dec 04 '12 at 12:52
  • Try `android:longClickable=true` on the `CheckableListItem` when u set the bg directly..(as specified in your Note) – Ron Dec 04 '12 at 16:26
  • No dice. I'm pretty sure they are already long clickable, because the list activity is implementing OnItemLongClickListener, and it is being called. – Groppe Dec 05 '12 at 13:17
  • 'R.attr.state_checked' is referencing your own 'R' class (i.e. the generated one) or is it referencing the one from android API ? – ben75 Dec 10 '12 at 14:09
  • @ben75 It's actually android.R.attr.state_checked in my code. Let me update it here... – Groppe Dec 10 '12 at 14:23
  • OP, can you tell me how the tag "css-selectors" is relevant here, as far as i know android has to do nothing with CSS as it is only Java-based, and i cannot see where this tag would make sense. – Clemens Himmer Dec 22 '15 at 13:34
  • @Tarekis that must have been a typo. – Groppe Dec 23 '15 at 15:14

2 Answers2

0

I changed and added these methods in CheckedListItem and it's working for me:

@Override
public boolean onTouchEvent( MotionEvent event ) {

    int action = event.getAction() & MotionEvent.ACTION_MASK;
    if ( action == MotionEvent.ACTION_UP ) {
        toggle();
    }

    return true;
}

public void toggle() {

    setChecked( !isChecked() );
}

private static final int[] CheckedStateSet = { android.R.attr.state_checked };

It seems the problem was that on a click, you never handled toggling the checked state of the view.

Jason Robinson
  • 31,005
  • 19
  • 77
  • 131
  • It's not working for me... now nothing happens when I click an item. Are those the only things you changed? Did you remove any of the other methods? – Groppe Dec 10 '12 at 13:27
  • No, I didn't, but I also didn't put it in a `ListView`. You have a listener on your `ListView` to receive all clicks, so your view will never receive touch events. What I suggest is to set the choice mode to `CHOICE_MODE_SINGLE` or `CHOICE_MODE_MULTIPLE` (whichever you need), and this will enable list items to be selectable. So then you would use `state_selected` rather than `state_checked` in your `CheckedListItem` class. – Jason Robinson Dec 10 '12 at 16:13
0

If this is not solved yet, try giving your "checkable layout" a background drawable with a selector and states and colour definition in it. Otherwise, drawableStateChanged wouldn't do anything because mBackground is null. (eg. android:background="@drawable/list_selector")

Then make sure you use listview.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE) to allow checking of multiple items.

You do not have to implement setOnItemClickListener for it to check the items as setting choice mode automatically does it already. (DO NOT set THE Checkable layout to be clickable either)

Well, at least this is how I solved mine.

Trung Nguyen
  • 7,442
  • 2
  • 45
  • 87
Pui Ho Lam
  • 232
  • 1
  • 2
  • 11