1

I was surprised that I couldn't find an existing answer on Stack that I could use for this, so here I am.

I have a ListFragment with a list attached to a SimpleCursorAdapter comprised of the rows defined by the following row.xml file:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="fill_parent"
   android:layout_height="wrap_content"
   android:orientation="vertical"
   android:padding="6dip" >

   <CheckBox
       android:id="@+id/story_check_box"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_alignParentLeft="true"
       android:focusable="false"
       android:focusableInTouchMode="false" />

   <TextView
       android:id="@+id/story"
       android:layout_width="wrap_content"
       android:layout_height="24sp"
       android:lines="1"
       android:scrollHorizontally="true"
       android:singleLine="true"
       android:layout_alignBaseline="@+id/story_check_box"
       android:layout_alignBottom="@+id/story_check_box"
       android:layout_toRightOf="@+id/story_check_box" />

</RelativeLayout>

I connect the list with the adapter with the following code in my ListFragment:

adapter = new SimpleCursorAdapter(getActivity(), R.layout.row, null, new String[] { CProvider.Stories.TITLE }, new int[] { R.id.story }, 0);
setListAdapter(adapter);

I then try to use a CheckBox in my fragment to toggle all the list checkboxes as follows:

CheckBox selectAll = (CheckBox) rootView.findViewById(R.id.select_check_box);
    selectAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            final ListView listView = getListView();
            for(int i=0; i < getListAdapter().getCount(); i++){
                View view = getViewByPosition(i, listView);
                CheckBox cb = (CheckBox)view.findViewById(R.id.story_check_box);
                if (isChecked) {
                    cb.setChecked(true);
                }
                else {
                    cb.setChecked(false);
                }
            }
        }

    });

I got getViewByPositionfrom here: Get ListView children that are not in view, and that almost works, but a few of the checkboxes don't get checked (and there is a pattern to it, but I can't seem to figure it out). It also seems a bit kludgier than I would think is necessary.

I want the checkboxes on the left, so I don't want to use checkedtextviews. Maybe I need to extend CursorAdapter and override getView?

Thanks in advance.

Community
  • 1
  • 1
hBrent
  • 1,696
  • 1
  • 17
  • 38
  • Duplicate? http://stackoverflow.com/a/5239889/494879 – ataulm Aug 03 '14 at 19:47
  • with regards to checkboxes on the left, consider a [composite view](http://lucasr.org/2014/05/12/custom-layouts-on-android/). You can have that view implement `Checkable` too so that when it's checked (via `listView.setItemChecked`) you set the inside Checkbox to checked. – ataulm Aug 03 '14 at 20:10

2 Answers2

1

Maybe I'm not correctly understanding your question but what I understood was that you wanted to check and uncheck all the checkboxes thanks to one "Select All checkbox".

Then, what I would do is to put the state of the "select all checkbox" as a variable of the class (as a boolean) which is overwritten by your selectAll.setOnCheckedChangeListener and say to the adapter "Hey, my state changed!" every time the checkbox changed its state. Something like this:

class Dummy{
    boolean isAllSelected = false;
    Checkbox selectAll = (find or create your CheckBox)
    selectAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            if (isChecked) isAllSelected = true;
            else isAllSelected = false;
            listView.getAdapter().notifyDataSetChanged();
        }
    }

And then, you just have to override the getView() of this adapter (like you suggested) adding a "if (isAllSlected)" condition.

To me, it sounds the easiest to do but it's maybe not that good to call the notifyDataSetChanged() method every time the user clicks on a checkbox (it's not that efficient for so minor changes). Anyway, hope it helps (the code I wrote is maybe not with the correct syntax: I wrote it directly on the website form)!

Laurent Meyer
  • 2,766
  • 3
  • 33
  • 57
  • This looks promising. Thanks for the answer. For expediency right now I'm just using the "built-in" listview selection mechanism so I can move forward, but I should have a chance to try to implement and test your answer soon. – hBrent Jul 30 '14 at 16:49
0

Below is what I wound up doing. In addition to taking care of the "select all/ unselect all" functionality, it handles checking/unchecking a checkbox when the text of a list item is selected/unselected, and vice versa. I was concerned about getView being called frequently, but setItemChecked causes getView to be called no matter what, so there's a limit to how much calls to getView can be avoided. As ataulm mentioned in a comment, maybe a composite view would a solution with less fuss.

In onCreateView:

selectAllCheckBox = (CheckBox) rootView.findViewById(R.id.select_all_check_box);
    selectAllCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            final ListView listView = getListView();
            for(int i=0; i < getListAdapter().getCount(); i++){
                listView.setItemChecked(i, isChecked);
            }
        }
    });

I also created a custom SimpleCursorAdapter with the following code, which also uses a simple ViewHolder class. In getView I check which items in the list are selected and check the checkboxes corresponding to those items. There's also code that sets a list item as selected or not if its corresponding checkbox has been clicked (i.e., checked or unchecked).

class AvailableCursorAdapter extends SimpleCursorAdapter {
    AvailableCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {
        super(context, layout, c, from, to, flags);
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        View row = super.getView(position, convertView, parent);
        ViewHolder holder = (ViewHolder)row.getTag();       
        if (holder == null) {                         
            holder = new ViewHolder(row);
            row.setTag(holder);
        }
        holder.storyCheckBox.setChecked(false);
        holder.story.setTextColor(Color.LTGRAY);
        long [] checkedIds = getListView().getCheckedItemIds();
        if (checkedIds != null) {
            for (int i = 0; i < checkedIds.length; i++) {
                if (checkedIds[i] == getListAdapter().getItemId(position)) {
                    holder.storyCheckBox.setChecked(true);
                    holder.story.setTextColor(Color.WHITE);
                    break;
                }
            }
        }
        final boolean isChecked = holder.storyCheckBox.isChecked();
        holder.storyCheckBox.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                getListView().setItemChecked(position, !isChecked);
            }
        });
        return(row);
    }
}

.

class ViewHolder {
    CheckBox storyCheckBox;
    TextView story = null;

    ViewHolder(final View row) {
        storyCheckBox = (CheckBox) row.findViewById(R.id.story_check_box);
        story = (TextView) row.findViewById(R.id.story);
    }
}

Finally, the following code causes getView to be called when a single ListItem is clicked, so that its corresponding checkbox gets selected or unselected, as appropriate:

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
    ViewHolder holder = (ViewHolder) v.getTag();
    holder.storyCheckBox.setChecked(false);
    holder.story.setTextColor(Color.LTGRAY);
    long [] checkedIds = l.getCheckedItemIds();
    if (checkedIds != null) {
        for (int i = 0; i < checkedIds.length; i++) {
            if (checkedIds[i] == getListAdapter().getItemId(position)) {
                holder.storyCheckBox.setChecked(true);
                holder.story.setTextColor(Color.WHITE);
                break;
            }
        }
    }
}
hBrent
  • 1,696
  • 1
  • 17
  • 38
  • I haven't read the entire answer, but based on the last paragraph, there's something odd: when you select/de-select an item, you're calling `notifyDatasetChanged()` on the adapter, indicating that the data has been updated. To me, this is a code smell - a _selection_ should not modify the underlying dataset, the selection should be an attribute of the view. An action _performed on_ selected items is something that should modify the dataset. – ataulm Aug 03 '14 at 19:44
  • 1
    @ataulm I agree that there's something "smelly" here, and I refer to that in a new edit at the top of my solution. I'm calling `notifyDatasetChanged()` so that `getView` gets called, because that's where I have a handle on my checkboxes and textviews, but if could easily get to them outside of that code, it seems that it would make sense to do that. – hBrent Aug 03 '14 at 19:59
  • @ataulm The composite view seems promising, though I may leave my code as above, since I don't think that it's likely to create real performance problems. I just wouldn't necessarily recommend it. I don't think my solution should be +1 as is. I'll update it if/when I come up with something better. – hBrent Aug 03 '14 at 20:19
  • Got rid of `notifyDatasetChanged` in `onListItemClick`. – hBrent Aug 03 '14 at 22:52
  • Is the array ever going to be null (docs doesn't say it can be), and does your adapter have stable ids? -> if no (`Adapter.hasStableIds()` returns `false`) then this might not always work. – ataulm Aug 04 '14 at 12:54
  • Because the adapter is based on CursorAdapter, I'm pretty sure the IDs are stable (since they're tied to the primary key of the database). Also, the checkedIDs are used immediately, so there's little chance of a change between when they're generated and when they're used. I've tested this solution for about a week now, and haven't had problems. – hBrent Aug 11 '14 at 02:29