Tuesday, May 13, 2008

Expandable lists

Multilevel, expandable lists are useful user interface elements and Android does support two-level expandable lists. This fact would not be worth a blog entry if there were no traps using the widget.

Click here to download the example program.

The example program implements a simple, two-level expandable list to display color shade information.



The first thing to note is that our Activity extends android.app.ExpandableListActivity. Otherwise setting up the list is pretty similar to ordinary ListActivity classes. The tricky part is the android.widget.SimpleExpandableListAdapter.

SimpleExpandableListAdapter expListAdapter =
new SimpleExpandableListAdapter(

this,

createGroupList(), // groupData describes the first-level entries

R.layout.child_row, // Layout for the first-level entries
new
String[] { "colorName" }, // Key in the groupData maps to display

new int[] { R.id.childname }, // Data under "colorName" key goes into this TextView
createChildList(), // childData describes second-level entries
R.layout.child_row, // Layout for second-level entries

new String[] { "shadeName", "rgb" }, // Keys in childData maps to display

new int[] { R.id.childname, R.id.rgb } // Data under the keys above go into these TextViews

);


First of all, this adapter requires somewhat complicated (although pretty well-documented) data structures. There are two of them, one describing the first-level group elements, the other describing the second-level string elements.

Here is the first one:
List->Map
Every map describes one first-level group element. The keys in the Map are arbitrary but are specified for SimpleExpandableListAdapter so that it can map the keys in the Map to widget IDs. In our case, the data under key name "colourName" will be set as the value of the TextView with "childname" ID.

The second one is a bit more complicated.
List->List->Map
Every entry in the first List represents a group. Entries in the second List represent entries of the group and each such entry is a Map that describes one child, similarly to the description of groups. In our case, the child Map contains two entries (keys "shadeName" and "rgb") that go to TextViews ("childname" and "rgb").

So far so good. Unfortunately, SimpleExpandableListAdapter has its tricks. First of all, the adapter takes the description of one row from a layout (child_row in our case, located under res/layout/child_row.xml). When the adapter draws the expand button onto the row, it does not consider the content of the row, it simply draws the button over the row. That's the reason child_row.xml defines generous left padding for the first TextView. While this behaviour can be considered just an oddity, the fact that SimpleExpandableListAdapter must be created with identical views for first- and second-level lines is a plain bug that existed at least since m3-rc37a. Having group- and child rows with different styles is a cool and useful feature that is supported by SimpleExpandableListAdapter but does not work.

Replace this fragment:

R.layout.child_row, // Layout for the first-level entries
new String[] { "colorName" }, // Key in the groupData maps to display

new int[] { R.id.childname }, // Data under "colorName" key goes into this TextView


with this one:

R.layout.group_row, // Layout for the first-level entries
new String[] { "colorName" }, // Key in the groupData maps to display

new int[] { R.id.groupname }, // Data under "colorName" key goes into this TextView


and you will see, how the expandable list gets confused.