Custom ListView with ability to check items

Android’s ListView is a useful component that can allow the user to check items natively. Two SDK samples (List 10 and List 11) show you how to accomplish this using simple CheckedTextView widgets to represent the rows. However, when you want to represent the rows using a custom layout, you’ll need some time to figure out why you items do not get checked.Well, by the end of this tutorial you should know how to do it.

The activity

There is not much inside the activity code : we override the onCreate method in order to setup our custom adapter and an important ListView property.

// Create the adapter to render our data
adapter = new ItemListAdapter(this, data);setListAdapter(adapter);    
// Get some views for later use
listView = getListView();listView.setItemsCanFocus(false);

The method showSelectedItems() will show you how to actually retrieve the selected items. It is worth noting that at least in Android 1.6, the values returned by the ListView#getCheckItemIds() method are incorrect (as notified in that bug report). As a consequence, always prefer the use of ListView#getCheckedItemPositions() as shown below.

final SparseBooleanArray checkedItems = listView.getCheckedItemPositions();
if (checkedItems == null) {    
  // That means our list is not able to handle selection    
  // (choiceMode is CHOICE_MODE_NONE for example)    
  return;
}

// For each element in the status array
final int checkedItemsCount = checkedItems.size();
for (int i = 0; i < checkedItemsCount; ++i) {    
  // This tells us the item position we are looking at    
  final int position = checkedItems.keyAt(i);    

  // And this tells us the item status at the above position    
  final boolean isChecked = checkedItems.valueAt(i);    

  // And we can get our data from the adapter like that    
  final Item currentItem = adapter.getItem(position);
}

The activity layout

Here again, nothing too fancy. We declare our ListView having the ID id/android:list so that our ListActivity can find it easily. Apart from that, you can also set the choice mode here if you want by using the attribute android:choiceMode which can be set to “singleChoice” (only one item can be selected at a time), “multipleChoice” (several items can be selected at the same time) and “none” (no selection is possible).

The adapter

Our adapter is implemented in order to produce the custom row views from an XML file. This is a very common task that is presented in many tutorials (Here is one). Basically, the idea is to override the Adapter#getView(int, View, ViewGroup) method, inflate the View from our XML file describing a row’s layout and set some properties of the widgets in that layout (such as the ID or the caption or the text color …).

The row layout

For the purpose of this article, we have setup a RelativeLayout to show the information in 3 colums: the first one in bold show the ID, the second one shows a caption and the third one contains a small checkbox to show the user whether this row is selected or not. Until now, nothing fancy either.

Now if you look closer at the layout declaration, you will see that we did not exactly use a standard RelativeLayout:

<fr.marvinlabs.widget.CheckableRelativeLayout  
  xmlns:android="http://schemas.android.com/apk/res/android"  
  xmlns:marvinlabs="http://schemas.android.com/apk/res/fr.marvinlabs.selectablelisttutorial"  
  android:layout_width="fill_parent"  
  android:layout_height="fill_parent">

We are using here a custom layout that we will detail in the next paragraph. And this is where the magic happens. Note that all the widgets of the layout have their android:focusable property set to false. If not, the parent view (our custom RelativeLayout) will not be able to receive the ListView events to indicate that the user want to check/uncheck the row.

The CheckableRelativeLayout widget

If you change the row layout and use the standard RelativeLayout, nothing will happen. Indeed, when the user clicks the row, the ListView will look for a widget implementing the Checkable interface. The RelativeLayout does not.

This is where we create a widget that can act just like our initial RelativeLayout but which also implements the Checkable interface:

public class CheckableRelativeLayout extends RelativeLayout implements Checkable {  
  private boolean isChecked;  
  private List checkableViews;

Our custom widget will inherit from RelativeLayout. You can also implement the same kind of custom widget for the other layout classes such as LinearLayout. Our custom class will hold two additional properties:

  • isChecked is used to keep the status of the layout (checked or not)
  • checkableViews is used to hold a reference to each of the checkable views located in our layout. This will allow to pass the checked value to those views and have them updated automatically.

Then comes the implementation of the Checkable interface. Nothing complicated, but you will notice that we also tell our Checkable children when our isChecked status is changed.

// @see android.widget.Checkable#isChecked()    
public boolean isChecked() {     
  return isChecked;    
}    

// @see android.widget.Checkable#setChecked(boolean)    
public void setChecked(boolean isChecked) {      
  this.isChecked = isChecked;      
  for (Checkable c : checkableViews) {       
    // Pass the information to all the child Checkable widgets       
    c.setChecked(isChecked);      
  }    
}    

// @see android.widget.Checkable#toggle()    
public void toggle() {      
  this.isChecked = !this.isChecked;      
  for (Checkable c : checkableViews) {         
    // Pass the information to all the child Checkable widgets       
    c.toggle();      
  }    
}

Finally, we would like to discover automatically the child widgets that implement the Checkable interface. The best place to do this is when we have finished to inflate the view.

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

/**    
 * Add to our checkable list all the children of the view that implement the    
 * interface Checkable   
 */    
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 &lt; childCount; ++i) {         
      findCheckableChildren(vg.getChildAt(i));       
    }     
  }    
}

Limitations and conclusion

There is a single limitation I know of, and is a real pain: if the user clicks the CheckBox, the event does not get captured by our customised layout. If anybody has an idea, feel free to post it here…


Edit January 20th, 2011: I had a suggestion from a reader to overcome the above limitation. Here is his way to address the problem which I think is quite clever and painless to implement.

Hi,

Thanks for your custom check list view. It helps me to make mine working.To make it working when the user click on the check box just override the CheckBox class and add the following function:

@Override
public boolean onTouchEvent(MotionEvent event) {  
  //super.onTouchEvent(event);  
  return false;
}

Best regards,

Cédric Caron

In short, the idea is to make the Checkbox not respond to touch events and make it say it did not capture any event. This will pass the event to the container (our custom layout) which will then simply select the list item. For completeness, we might also want to override the other event handling functions (onTrackballEvent, onKey*, etc.). The source code provided in this tutorial now includes those changes. Thanks again to Cédric for providing this solution.

I hope this tutorial was useful and clear enough. Do not hesitate to post comments here or on our forum. The full source code to this tutorial is available just below.

Download

As always, the source code of this tutorial is available on our GitHub account.

8 comments

  1. Hi.

    I find this really useful. However, I have a problem when the ListView has many items and you scroll down and then scroll up again, as the checked items become unchecked. I’m using this solution to solve the problem.

    I basically do the same in my BaseAdapter, except in the Holder. Instead of saving the CheckBox I save the whole CheckableRelativeLayout (as it’s who manages the value of its CheckBox childs). So, I have this in my BaseAdapter getView method:

    viewHolder.ckRelativeLayout.setChecked(itemState[position]);

    and then set an OnClickListener to ckRelativeLayout as he does with the CheckBox. I debugged the setChecked call and it actually sets the appropiate value to the CheckableRelativeLayout and its CheckBox child, but it still appears unchecked in my list :( I don’t know where can be the problem. I hope you can help me. Thanks!

    1. Hi,

      The ListView recycle its views, and thus, you loose the state when it gets re-used.

      The idea is, as in the tutorial you point out, to keep track of the state of the checkboxes in an array.

      First, you will need to add a way to get notified of the state change to CheckableRelativeLayout. For example you could add the same listener mechanism:

      public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
      
          /** 
           *  Interface definition for a callback to be invoked when the checked state of a CheckableRelativeLayout 
           *  changed. 
           */
          public static interface OnCheckedChangeListener {
              public void onCheckedChanged(CheckableRelativeLayout layout, boolean isChecked);
          }
      
          // @see android.widget.Checkable#setChecked(boolean)    
          public void setChecked(boolean isChecked) {      
            this.isChecked = isChecked;      
            for (Checkable c : checkableViews) {       
              // Pass the information to all the child Checkable widgets       
              c.setChecked(isChecked);      
            }    
            if (checkChangeListener!=null) checkChangeListener.onCheckedChanged(this, isChecked);
          }    
      
          private OnCheckedChangeListener checkChangeListener;
      }
      

      Then, in the adapter, we’ll keep track of the states. The efficient structure to do that is a SparseBooleanArray.

      public class MyAdapter extends ... implements CheckableRelativeLayout.OnCheckedChangeListener {
      
          private SparseBooleanArray checkStates;
      
          @Override
          public void onCheckedChanged(CheckableRelativeLayout layout, boolean isChecked) {
              // The layout's position is stored in its tag
              checkStates.put( (Integer) layout.getTag(), isChecked );
          }
      
          @Override
          public View getView(int position, View convertView, ViewGroup parent) {
              ViewHolder holder;
              if (convertView == null) {
                  convertView = vi.inflate(R.layout.list_row, null);
                  holder = new ViewHolder();
                  holder.layout = (CheckableRelativeLayout) convertView.findViewById(R.id.layout);
                  convertView.setTag(holder);
      
                  // We will watch for the checked state and remember it
                  holder.layout.setOnCheckedChangeListener(this);             
              } else {
                  holder = (ViewHolder) convertView.getTag();
              }
      
              holder.layout.setTag(position);
              holder.layout.setChecked(checkStates.get(position, false)); 
      
              return convertView;
          }
      }
      

      That’s it, this should do the trick nicely.

    2. That said, there is a more simple solution, because the ListView keeps track of those checked states for you. You simply need to get it within the adapter’s getView method:

      // Restore the checked state properly
      final ListView lv = (ListView) parent;
      holder.layout.setChecked(lv.isItemChecked(position));
      

      I have updated the GitHub repository to implement the ViewHolder pattern.

  2. The above is working fine for me.
    I have one more problem here, when I checked the check box by some condition I need to select the check box or deselect it.

    How I can select and deselect the check box when I click on any row?

  3. Hi,
    this is great example / tutorial, but i am newbie in Android / Java and need some help.
    If i have some (country) IDs (not items positions) stored in DB, how can i mark this rows / check-boxes as checked on Activity start ? Thanks in advance!

Comments are closed.