Saturday, February 13, 2010

Expandable lists and check boxes

I got another trivial question in a comment: how to add checkboxes to an expandable list view like this one? Nothing can be simpler, you just add CheckBox control to the child view that forms the row and that's it. Except that it does not work without some tweaks.

You can download the example program from here.



The first bizarre thing you can notice in res/layout/child_row.xml that the CheckBox is made non-focusable. Why to do that when we want the checkbox to capture "click" events beside "touch" events? There is a complicated answer already presented in this post. The ViewGroup encapsulating the list row (the LinearLayout in res/layout/child_row.xml) must retain the focus otherwise we don't get onChildClick events (see ElistCBox.java).

This would solve the problem for a Button but not for a CheckBox. Actually, I don't know why the Button behaves correctly and the CheckBox does not. My experience is that even if CheckBox has the focus, clicking on the row of the CheckBox does not toggle its state. I am curious if anyone can provide an explanation, here I just record the fact. Remove the android:focusable="false" line from the CheckBox element in child_row.xml and observe, that you can click on the highlighted row as much as you like but the CheckBox does not toggle. That's why I implemented it by "hand" - I took away the focus from the CheckBox, this makes the child row deliver onChildClick events then I toggled the state of the CheckBox programmatically. If anyone has a better solution, I would be deeply interested.

Update: a discussion started in the comment field regarding the erratic behaviour of check boxes in this example program. See this blog post for further explanation.

39 comments:

Gustavo Matias said...
This comment has been removed by the author.
Gustavo Matias said...

How do you keep the check boxes state?? I keep losing its state every time I scroll the list or expand an item.

Cheers,

-gustavo

Anonymous said...

HI
I really want to know how to keep the check boxes state.
Could u tell me how to solve the problem ?

thx a lot :)

Kevin Bradshaw said...

hi thanks for the tutorial.
It seems to be working to a point. When I compile it, it looks fine, and when I click on a list item it does indeed toggle teh state for that item, however it also seems to randomly check othere boxes from other groups. Also, merely opening and closing a group can cause the checked state of random boxes to change.

Have you noticed this behaviour or am I missing something.

Thanks

Gustavo Matias said...

Hi Kevin,

I got this issue solve by overriding the getView method. you should get the check box with the findViewById method then have an object that set its state of the current views position. got it? let me give you an example just in case:

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

LinearLayout rowView;
Color color = getItem(position);

if (convertView == null) {
rowView = new LinearLayout(getContext());
LayoutInflater vi = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
vi.inflate(textViewResourceId, rowView, true);
} else {
rowView = (LinearLayout) convertView;
}

TextView name = (TextView) rowView
.findViewById(R.id.color_name_txt);
CheckBox colorCheckbox = (CheckBox) rowView
.findViewById(R.id.color_checkbox);

meaning.setText(color.getName());
colorCheckbox.setChecked(color.isChecked());

return rowView;
}

Hope it helps!

Thanks.

- Gus

Kevin Bradshaw said...

Thanks for your prompt reply gus, but Im afraid I dont follow.

Is there any way you could post the code for this project with the fix that you mention above?

Ive tried to paste your fix into the project but I cannot get it to compile. I am quite new to Android so Im probably not as quick on the uptake as I should be just yet.,

Thanks again

Gustavo Matias said...

Hi Kevin,

Don't worry I've spent two days trying to figure out how to make this damn checkbox list to work properly =P I tried to make this code as simple as possible for u, hope you can make it work:

public class ColorActivity extends Activity {

ListView myListView;
ArrayList colors;
private int colorListIndex;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.colors_layout);

colors = new ArrayList();
// filling up the list
colors = Color.getColorList();

Collections.sort(colors);

// setting its values and views
myListView = (ListView) findViewById(R.id.colors_listview);
myListView.setFastScrollEnabled(true);
final MyIndexerAdapter adapter = new MyIndexerAdapter(
getApplicationContext(), R.layout.color_row,
colors);

myListView.setAdapter(adapter);
// scrolling the list to the selected item
myListView.setSelection(colorListIndex);

// when the row is touched
myListView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView arg0, View arg1, int arg2,
long l) {

Color msg = colors.get((int) l);
msg.setChecked(!msg.isChecked());

// notifiyDataSetChanged triggers the re-draw
adapter.notifyDataSetChanged();
};
});

}

private String getSelectedColor() {
String color = null;
for (Color c : colors) {
if (c.isChecked()) {
color = c.getName();
break;
}
}
return color;
}

// the list adapter, where the magic happens
class MyIndexerAdapter extends ArrayAdapter {

ArrayList myElements;
int textViewResourceId;

public MyIndexerAdapter(Context context, int textViewResourceId,
List objects) {
super(context, textViewResourceId, objects);
this.textViewResourceId = textViewResourceId;
}

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

LinearLayout rowView;
Color msg = getItem(position);

if (convertView == null) {
rowView = new LinearLayout(getContext());
LayoutInflater vi = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
vi.inflate(textViewResourceId, rowView, true);
} else {
rowView = (LinearLayout) convertView;
}

TextView meaning = (TextView) rowView
.findViewById(R.id.color_row_txt);
RadioButton img = (RadioButton) rowView
.findViewById(R.id.color_row_checkbox);

meaning.setText(msg.getName());
img.setChecked(msg.isChecked());

return rowView;
}
}
}

- Gus

Shekhar Joshi said...

Hi Guys,

It will be quite helpful,if you could share the new updated expandable list example with checkbox control working.

Thanks,
Shekhar
shekharkec@gmail.com

Lou said...

The state of the checkbox is deterministic not random, but it is dependent on (1) the total number of all items when all the groups are open, (2) how many and which items are checked, and (3) which groups are open and which are closed. This happens because Android "recycles" the view for each item, holding onto the state of the checkbox from its former item, even though the item itself has changed.

To solve this problem, keep the state of each item in a separate data structure (perhaps the same one where you are keeping its name), and use that to set the checkbox state each time an item is displayed. In my code, I kept all item information in an array of maps, one map for each item, and used map keys for the item name, the item's icon, and the item's checkbox state.

Tushar said...

Hi. Can you tell me how can I have an expandable list with radio buttons instead of check boxes?

Arief said...

Lou, can you give me the example to your solution or source code maybe. because i'm confused about how this expandable listview with checkbox work and there isn't any tutorial that can explain it. Thank you. You can reply here or you can mail it to me to arief_r_s@yahoo.com

rsnider19 said...

How can you incorporate a TimePicker in place of a check box? Please email me at rsnider19@gmail.com

JayasriRani said...

can we develop dynamic expandable list view and the inner list should contain edittext and button

sri said...

my code is this

------------------------------------

package com.dynamiclistview;

import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

public class dynamicListView extends ListActivity implements OnClickListener {

EditText textContent;
Button submit;
ListView mainListview;
String item;
private static class ListViewAdapter extends BaseAdapter {
private static final Drawable Button = null;
private LayoutInflater mInflater;


public ListViewAdapter(dynamicListView dynamicListView) {

mInflater = LayoutInflater.from((Context) dynamicListView);



}

public int getCount() {
return ListviewContent.size();
}
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {

ListContent holder;


if (convertView == null) {
convertView = mInflater.inflate(R.layout.listviewinflate, null);

holder = new ListContent();
holder.text = (TextView) convertView.findViewById(R.id.TextView01);
holder.text2=(TextView)convertView.findViewById(R.id.TextView02);

convertView.setTag(holder);
} else {

holder = (ListContent) convertView.getTag();
}


holder.text.setText(ListviewContent.get(position));
holder.text2.setText("select");
return convertView;
}

static class ListContent {
TextView text;
TextView text2;

}
}


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


textContent=(EditText)findViewById(R.id.EditText01);
submit=(Button)findViewById(R.id.Button01);
submit.setOnClickListener(this);
setListAdapter(new ListViewAdapter(this));

}

public static final ArrayList ListviewContent = new ArrayList();
public void onClick(View v) {
if(v==submit)
item=textContent.getText().toString();
{
ListviewContent.add(item);


setListAdapter(new ListViewAdapter(this));
textContent.setText("");
}

}
public void onListItemClick(ListView l, final View v, int position, long id) {
super.onListItemClick(l, v, position, id);

final dynamicListView dlv=new dynamicListView();
Object o = this.getListAdapter().getItem(position);
final AlertDialog.Builder alert = new AlertDialog.Builder(this);
final EditText input = new EditText(this);
alert.setView(input);
alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {

}

});

alert.setNegativeButton("Cancel",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.cancel();
}
});
alert.show();
}
}


In code i'm having a problem while adding items to the list
my problem is
when we click on list item an edit box should appear.
after entering the data and press ok it should append to the list
i need the modification

sri said...

hi
I need dynamic expandable list view code in android

Nyan said...

I am having a same problem with ExpandableListView with check if anyone of you have solution, please provide the code

Gabor Paller said...

sri, can't you just reuse the same approach as for normal lists?

sri said...

thank you.
I am Having another query ..
can i have code for resizing the listview item to zero

Gabor Paller said...

sri, what about using View.setVisibility() to make the sub-view appear/disappear? (then do a requestLayout() on the ListView after the visibility update)

sri said...

thank u

droid-bird said...

Can someone please upload a working version? I could not figure out anything from the discussion.

For me, it is still randomly changing! :(

Gabor Paller said...

Droid-bird, there is an update section at the end of the post.

Ankur said...

Hi Gabor,

You have mentioned in this post that check box does not behave properly and you have provided the hack in the onChildClick method. It works as you said and the checkBox state gets toggled as soon as I click on the child row. However, when I click on the checkbox only and not the row, the checkbox state gets changed but no onClildClick event gets fired. Any idea why?
Also, I tried adding a onclick event on check box. But that only gives me the view object as the argument of method and there is no way to find out the position of the checkbox and group number it belongs to, so that i can update the right object in the group for persisting the change.

thank you .

Ankur

Unknown said...

Ankur, I fixed the persistence when the checkbox is checked by adding a setOnClickListener callback in the getChildView method:

public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
View v = null;
if( convertView != null )
v = convertView;
else
v = inflater.inflate(R.layout.child_row, parent, false);
final Color c = (Color)getChild( groupPosition, childPosition );
TextView color = (TextView)v.findViewById( R.id.childname );
if( color != null )
color.setText( c.getColor() );
TextView rgb = (TextView)v.findViewById( R.id.rgb );
if( rgb != null )
rgb.setText( c.getRgb() );
final CheckBox cb = (CheckBox)v.findViewById( R.id.check1 );
cb.setChecked( c.getState() );

cb.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
c.state = cb.isChecked();
}
});

return v;
}

In the onChildClick() add something similar:

final Color c = (Color)expListAdapter.getChild( groupPosition, childPosition );
c.state = cb.isChecked();

xuannv said...

thank for share:-)

Anonymous said...

expandablelistview works well, thank for your code !
Now , i can save infor to xml file and write my setting app !

Anonymous said...

Thanks! This tutorial is really really useful. Well done.

There is NOTHING in Google's documentation that explains this.

Before I discovered this blog article, it took me a long time to investigate this issue... without a solution. Now I have a solution. Many thanks.

Anonymous said...

Can somebody give the link to a working sample. I need an expandable list with Childs having checkbox [Same as what shown in this example]. But I have problem in maintaining checkbox states. I seen posts which says we need to override getView method. But if somebody upload a working copy, then it wud be great for new learner like me. Thanks to all you already contributed more in thread. Thanks in advance for who upload a working sample.

Gabor Paller said...

Anonymous, have you checked the update section at the end of the post?

Anonymous said...

Your code is great, but I've found an Android bug when trying to programmatically set a list position:

First enable this: listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

Then use listView.setItemChecked(listRowPosition, true);

to set a list row on "on".

But a problem happens when you do this:

1) Scroll to the end of a long list.
2) Call listView.setItemChecked(0, true);
3) Scroll to the top of the list, then scroll down a bit.

4) Now you will see that the list no longer responds to touch events when you touch the list row.

I don't know why this happens.

Can anyone explain this, or figure out how to re-enable touch events on the list?

Note: Similar problem reported here: http://stackoverflow.com/q/8149301

Anonymous said...

Response to Anonymous comment (from December 20, 2011 2:39 PM)

This seems to solve the problem (for about 95% of the time):

int position = 0;
listView.setItemChecked(position, true);
if (position == 0) {
// scroll to the top, to prevent the bug where the list view stops responding to touch events
listView.setSelectionFromTop(0, 0);
}

Source:
http://stackoverflow.com/questions/3014089/scroll-to-a-position-in-a-listview

Mukunda SreenivasaMurthy said...

Hi
very nice explanation of the Expandable listview and could you please let me know how to set an listener to child node, suppose that I want to have phone number in child node and clicking on the childnode should trigger an phone call to that number. But I am not able to set an listener for childnode. Looking forward to your reply.
thanks.

Android_Newbie said...

Is there any possibility to set the state of every single checkbox, like setting a list in the java code for the checkboxes?

Anonymous said...

Hi, can someone please post a working code example? I can´t get it to work, aargh!

Thanks!

Gabor Paller said...

Anonymous, what is the problem with the example program?

Dexter said...

Hi Gabor!

Thanks for replying.

The problem is that when i check a box i one group, it randomly gets checked in other groups.

Why is that?

What i want is to have a settingslist using expandablelist with checkboxes and then save the the settings for the application in a list, that i can save on the sd-card later on.
And at restart i want the same checkboxes to be checked (read from the list and check the boxes).

You have any ide?

Thanks!

/Dexter (Anonymous from above)

Gabor Paller said...

Dexter, there is an update at the end of the post, pointing to another post. Does that help?

Anonymous said...

Can I change check boxes to spinner using Expandable list?

Arbab Khan said...

Great post!!! Helps a whole lot!!!