views:

147

answers:

1

I have a ListActivity that was originally using an ArrayAdapter. Each item in my ListView contains a checkbox.

I changed Adapters to a CursorAdapter. Everything is working fine except checkboxes are out of control. For example: When I click the checkbox in position 1, the checkbox in position 4 gets checked. If I click the checkbox in position 1 again, it will check (Ie: When I click the checkbox in position 1, if the checkbox in position 4 is not checked, the checkbox in position 4 will get checked; else, the checkbox in position 1 is checked). Unchecking checkboxes doesn't seem to show any weird behavior. Any ideas why?

Update: It's checking the checkbox backwards based on how many items are on screen. What I mean is, if there are 5 items visible, clicking the 1st one will check the 5th, clicking the 2nd one with check the 4th, clicking the 3rd one will check the 3rd one. Also, once one has been checked, they all start behaving properly.

Apparently commenting out this makes the problem go away:

    if (holder.chkSelect != null) {
        holder.chkSelect.setOnCheckedChangeListener(this);
    }

This definitely has something to do with showMultiPanel popping up and possibly reducing the number of rows that can be seen. Though I don't understand why it worked fine with an ArrayAdapter.

Here is a copy of my CursorAdapter

public class InboxAdapter extends CursorAdapter implements CheckBox.OnCheckedChangeListener {
    private int mCheckedCount = 0;

    public InboxAdapter(Context context, Cursor c) {
        super(context, c);
    }

    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (buttonView.getId() == R.id.inbox_itemcheck) {
            if (isChecked) {
                mCheckedCount++;
            }
            else {
                mCheckedCount--;
            }
            ActivityInbox.this.showMultiPanel((mCheckedCount > 0) ? true : false);
        }
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        InboxHolder holder = (InboxHolder)view.getTag();

        if (holder.txtDate != null) {
            DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
            Date d = new Date(cursor.getInt(cursor.getColumnIndex("created_on")) * 1000L);
            holder.txtDate.setText(df.format(d));
        }
        if (holder.txtSubject != null) {
            holder.txtSubject.setText(cursor.getString(cursor.getColumnIndex("subject")));
            if (cursor.getInt(cursor.getColumnIndex("read")) == 0) {
                holder.txtSubject.setTypeface(null, Typeface.BOLD);
            }
            else {
                holder.txtSubject.setTypeface(null, Typeface.NORMAL);
            }
        }

        if(holder.txtSummary != null) {
            holder.txtSummary.setText(cursor.getString(cursor.getColumnIndex("summary")));
        }

        if (cursor.getInt(cursor.getColumnIndex("read")) == 0) {
            view.setBackgroundResource(R.drawable.selector_inbox_unread);
        }
        else {
            view.setBackgroundResource(R.drawable.selector_inbox_read);
        }

        if (holder.lMessageType != null) {
            if (cursor.getInt(cursor.getColumnIndex("read")) == 0) {
                holder.lMessageType.setVisibility(View.VISIBLE);
            }
            else {
                holder.lMessageType.setVisibility(View.INVISIBLE);
            }
        }
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.layout_inbox_item, parent, false);

        InboxHolder holder = new InboxHolder();
        holder.txtDate = (TextView) v.findViewById(R.id.inbox_item_date);
        holder.txtSubject = (TextView) v.findViewById(R.id.inbox_item_subject);
        holder.txtSummary = (TextView) v.findViewById(R.id.inbox_item_summary);
        holder.chkSelect = (CheckBox) v.findViewById(R.id.inbox_itemcheck);
        holder.lMessageType = (LinearLayout) v.findViewById(R.id.inbox_messagetype);

        v.setTag(holder);

        if (holder.chkSelect != null) {
            holder.chkSelect.setOnCheckedChangeListener(this);
        }
        return v;
    }

}

A: 

Note that a ListView can reuse a single View for more than one row of the table, and it's up to the adapter to decide whether that happens. I believe CursorAdapter is set up to do this where possible by pooling view instances and only creating enough to build the screen.

For a simple solution for a short list, just stick with an ArrayAdapter, and read all the data ahead of time. This avoids the whole issue.

If you want to stick with CursorAdapter, you need to store the checked states yourself. One way would be keeping a HashSet of the id's that are checked. In your checkbox click listener, update it by either adding or removing that row's id in the set. In bindView, set the checkbox state based on the whether the row's id is in the map. You may also want to set the listener in bindView, where you can have a final local variable set to the id, and can use that value in the code for the listener.

Walter Mundt
Thanks for your response. It seems I don't have access to the Activity's context inside getView? newView and bindView pass it in, getView doesn't. getContext() is undefined.
Andrew
I am attempting to use parent.getContext()
Andrew
I have overridden getView() and have newView() returning null. The same exact problem occurs. I think it has something to do with showMultiPanel. showMultiPanel is a function that animates a little popup at the bottom of the screen (exactly the same as the Android Mail application when you checkbox an email). When the panel shows, the number of visible items in the list decreases by 1. For some reason, I think this is throwing the Adapter a curveball. Works fine for ArrayAdapters though...
Andrew
Hmm. I think you may want to try the second approach. One easy way would be to keep a HashMap of id -> checked pairs and set the checked state in bindView. Don't override getView in this case, that was a misguided suggestion on my part.
Walter Mundt
(I've edited my answer to reflect my updated suggestions.)
Walter Mundt
Hmm, I'll give it a shot, though I don't understand why I'm needing to do anything here. The system is the one getting confused. I do not write the code to check/uncheck a checkbox. That is handled internally. I may write code that determines how my application behaves thereafter, but the drawn checkbox being checked v unchecked is intrinsic to the Android system; not my code. The only thing I'm doing is making a LinearLayout visible, which reduces the items visible in the list. If this works fine with ArrayAdapters, it seems there is some code in CursorAdapter using index instead of position.
Andrew
When implementing a CursorAdapter, `bindView` is responsible for making sure the view passed in matches the row of data you are displaying. If you neglect to set up any aspect of the view's state, it will remain as it was -- usually, as it was from having been used for some other row. So the view bound to row five was used for row 1, and then reused when you scrolled down. Since you didn't uncheck the checkbox when the view got moved, it stayed checked. Does that make sense?
Walter Mundt
I think so. As a test I placed "holder.chkSelect.setChecked(false);" inside my bindView to force the checkbox of every row to not be checked. I am still able to click checkboxes and have them display as checked (and obviously I still have my original problem).
Andrew
However, if i place "((CheckBox) view.findViewById(R.id.inbox_itemcheck)).setChecked(false);" inside of bindView, nothing shows up checked the first time I click any checkbox. Subsequent checks show up.
Andrew