Everything you need to know about Android ListViews and CheckBoxes!

10:46 AM , , 2 Comments

I have recently been developing a simple Shopping list application in Android and for the list itself, I wanted to have a list of all the items i wanted to buy with a checkbox on the far right so that I could mark any item as complete - on ticking the checkbox I wanted two things to happed:

1. The item displayed becomes struck through
2. The tick is persisted (in case I navigate anway from the app and want to come back later, i dont want to re-tick things)


I searched everywhere to find a solution, and posted on StackOverflow but managed to find no solution as to how I could implement a Checkbox listener that could work out which row in the list had been ticked. Fortunately, with some help from here I have managed to solve the problem - So here is a detailed walkthrough how to do it! This covers how to do it using a custom Adapter, in this case I extended the ArrayAdapter as I like to work with JSON - but the approach should be valid for most Adapter implementations.


The first problem people encounter, that is already pretty well documented on the web is that if you put a checkbox on a listview row, then you can no longer click on the row (for the context menu for example) as all focus is on the check box - this is easily fixed! in the getView() method, simply initialise the CheckBox and call checkBox.setFocusable(false);



Now to the problem of discovering the row that has been selected:

1. First, in the getView() method whilst we are populating the row data (for example here we set the text in the textview) we will add a "Tag" to our checkbox - this tag will indicate the row position that the CheckBox belongs to:

 checkBox.setTag(new Integer(position)); //we tag every checkbox we create with the row number for later


2. Next we set the OnCheckChangedListener - this could be a seperate class, but for ease of this overview I just changed my Adapter to implement the listener and added the required onCheckChange method

 checkBox.setOnCheckedChangeListener(this); //define the listener (we will use this class but could be another)


3. Now we need to implement the listener method to use this information:

  public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
  //first we need to work out the row selected - we can do this as we have already tagged the CheckBox
  Integer posSelected = (Integer)cb.getTag();
  
  //We can now get the associated JSONObject (we might want to persist some data here..)
  JSONObject checked = getItem(posSelected);
  //persistChangeToDb(checked);
  
  //now we will retrieve the current row selected so we can change the appearance
  View row = (View) cb.getParent();
  
  //once we have the row, we are going to change the TextView field as strikethrough (or not) if it is checked
  TextView descrip = (TextView)row.findViewById(R.id.item_description);
  if (isChecked){
   descrip.setPaintFlags(descrip.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
  }
  else{
   descrip.setPaintFlags(Constants.CLEAR_FORMATTING_TEXTVIEW); 
  }
 }

As you can see, as we have the CheckBox in this listener, we can get its tag to discover the row position clicked - this allows us to access the JSONObject associated with the row and use that data as we please (here I persist the change to the checkbox so it is remembered).

We can also get the entire row, this allows us to change the text/formatting/background etc - you will see that I get the TextView (this is the textView that has my item description) and then strike it through (or remove formatting) if it is checked.




Below is the entire adapter class:

public class ShoppingListItemsAdapter extends ArrayAdapter implements OnCheckedChangeListener {
 
 public ShoppingListItemsAdapter(Activity activity, List shoppingLists) {
  super(activity, 0, shoppingLists);
 }
 
 
 @Override
 public View getView(int position, View convertView, ViewGroup parent) {

  Activity activity = (Activity) getContext();
  LayoutInflater inflater = activity.getLayoutInflater();

  // Inflate the views from XML
  View rowView = inflater.inflate(R.layout.shopping_list_item_view, null);
  JSONObject jsonCurrentRow = getItem(position);


  //////////////////////////////////////////////////////////////////////////////////////////////////////
  //The next section we update at runtime the text - as provided by the JSON being passed in
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  
  // Set the text on the TextView
  TextView textView = (TextView) rowView.findViewById(R.id.item_description);
  
  //initialise the checkbox
  CheckBox checkBox = (CheckBox)rowView.findViewById(R.id.boughtCheck);
  checkBox.setFocusable(false);  //this is so the onLongClick on the row still works!
  checkBox.setTag(new Integer(position)); //we tag every checkbox we create with the row number for later
  checkBox.setOnCheckedChangeListener(this); //define the listener (we will use this class but could be another)
  
  
  try {
   //populating the row with data from the JSONObject
   String title = (String)jsonCurrentRow.get("title");
   textView.setText(title);
  } catch (JSONException e) {
   textView.setText("JSON Exception");
  }
  return rowView;
 }


 
 /**
  * This method is our checkbox change listener - this is where
  * we handle the changes in the checkbox state
  */
 @Override
 public void onCheckedChanged(CompoundButton cb, boolean isChecked) {
  //first we need to work out the row selected - we can do this as we have already tagged the CheckBox
  Integer posSelected = (Integer)cb.getTag();
  
  //We can now get the associated JSONObject (we might want to persist some data here..)
  JSONObject checked = getItem(posSelected);
  //persistChangeToDb(checked);
  
  //now we will retrieve the current row selected so we can change the appearance
  View row = (View) cb.getParent();
  
  //once we have the row, we are going to change the TextView field as strikethrough (or not) if it is checked
  TextView descrip = (TextView)row.findViewById(R.id.item_description);
  if (isChecked){
   descrip.setPaintFlags(descrip.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
  }
  else{
   descrip.setPaintFlags(Constants.CLEAR_FORMATTING_TEXTVIEW); //this is a bit of a hack! to remove strike through the normal setting is 257 so I created a constant for this, you could just pass the int literal 257 in here to clear formatting
  }
 }
}

2 comments:

  1. Great tutorial!

    I can properly save the states of all the checkboxes as booleans in a sharedpreferences editor. However, when I want to load all the booleans back, I'm unsure of how to manually set the checkboxes with values. For example, I load the data for the checkboxes and I find out the checkbox on the third row is checked, for example. How do I change just the checkbox with tag (new Integer(3))?

    Thanks!

    ReplyDelete
  2. Hi Michael,

    glad that the tutorial was helpful - and you are right, in the code I am not setting the checked status when loading.

    Assuming you are using the same/a similar JSON based adapter it should be as follows:


    on lines 33-37 of the adpater class posted in my blog you will see:

    try {
    //populating the row with data from the JSONObject
    String title = (String)jsonCurrentRow.get("title");
    textView.setText(title);
    }


    As you can see, this is a pretty basic initialisation of every row in the list, and actually you will probably want to set other information for the row here too - such as re-loading the boolean to indicate if it hsa been checked. This would be done by adding something like:

    boolean isChecked= jsonCurrentRow.getBoolean("isChecked");
    checkBox.setChecked(isChecked);


    so our try block that initialises the list row with its data now looks like:


    try {
    //populating the row with data from the JSONObject
    String title = (String)jsonCurrentRow.get("title");
    textView.setText(title);


    boolean isChecked= jsonCurrentRow.getBoolean("isChecked");
    checkBox.setChecked(isChecked);
    }





    All you need to do is make sure that the JSON object being passed in (that currently holds the row title) also contains a boolean indicating whether it is previously checked.

    Hope that makes sense!

    ReplyDelete