Tuesday, February 22, 2011

3-level expandable lists

I got a question in a comment whether 3-level expandable list would be possible. I responded that the ExpandableListView implementation is wired to have two levels but then another comment mentioned that it may be possible by embedding one ExpandableListView into another. I was busy then but now I found time to take the challenge and to implement a test program.

Our three-level list is a variation of an earlier example program but I inserted another level (somewhat without reason :-)).

The principle of the implementation is simple. The first-level list is backed by an ExpandableListAdapter. The child views generated by this adapter are ExpandableListAdapters themselves that provide the second-level groups and the eventual child elements. Listeners are set for the group events (expand/collapse) of the second-level lists. When a group event occurs on a second-level list, the layout of the first-level list is also recalculated to accomodate for the size changes of second-level expandable lists.

This sounds simple but there are some tricky points that made me spent more time on this prototype than I expected. The first issue is with the individual rows of the second-level lists and the view recycler. Check out getChildView() in ColorExpListAdapter.java. When the layout of the list is recalculated, the old views are offered to the list adapter for reuse. The getChildView() method gets the old view instance in the convertView parameter. If convertView is not null, the adapter has the option of reusing the old view instance. In this case the adapter does not instantiate a new view but sets the old view instance appropriately, decreasing garbage (Click here if you want to read more about the importance of less garbage collection in mobile environments). Now our problem is that we cannot just set up an ExpandableListView instance without destroying its internal state, i.e. the expanded/collapsed state of the groups. For this reason, it is extremely important to preserve the views and to prevent the first-level ExpandableListView of rearranging the second-level lists (handing out a second-level ExpandableListView in a certain position as a convertView at a different position). For this reason I implemented a cache in ColorExpListAdapter that makes sure that only one second-level ExpandableListView is generated for each child position in the first-level list and that second-level ExpandableListView instance is consistently returned for the same position. This guarantees that the collapsed/expanded state of the second-level views are preserved when the layout of the first-level list is recalculated.

The other tricky issue is the size of the second-level lists. The layout recalculation of the first-level list is triggered by a group event of a second-level list. Due to the way expandable lists are implemented, when this event occurs, the items of the second-level list which was clicked are not yet added/removed according to the expand/collapse action. This means that the size of the list will be incorrect when the first-level list layout is recalculated.

To solve this problem, observe the row count calculation in ColorExpListAdapter's calculateRowCount method and the way the row count is used in DebugExpandableListView's onMeasure method. DebugExpandableListView originally started to exist so that I can observe the layout of the second-level lists then it turned out that it has an important function: override the onMeasure method of the original ExpandableListView (actually inherited from ListView). Lazily, I kept the class name and the debug messages in it so that you can observe the rather complex operation of the layout process if you feel like.

53 comments:

Anonymous
said...

The code doesnt seem complete. When checking there is a reference to the method "getExpandableListView()" when creating a new adapter in OnCreate() but I cant find that method. Also cant find the method setListAdapter.

TitanSky, those methods are inherited from ExpList3's base class, ExpandableListActivity (fully qualified name is android.app.ExpandableListActivity). Does the code compile correctly? (I have just tried, it does for me)

Well, i have situation like this i hope you are definitely going to help me..i want to replace all the strings in 2nd levels's content i.e #D3D3D3, #696969, #EAEAEA,#1C86EE...etc with check boxes and implement on-click listener for those contents for checking whether corresponding check box is checked or not..hope you got me..

How can i add list view as child?? i would like to add n number of expandablelistview depending on the requirements..any suggestions on how to do this.In your example you have specified the size of the array ...i dont want to hardcode that value but would rather change depending on the tree size.

Good tutorial, but I'm having some trouble I hoped you might have some thoughts on. My array of values isn't static, it's based of off some html parsing. I can't figure out how to populate the contents[][][][] string array with my data. In my content, h1's and h2's are my groups (respectively), and the rest of the stuff is the 'colors'.

Thank you very much. I've been struggling very much the whole day. I got everything hooked up but the children were not getting aligned properly. I stumbled upon this and that one magic line which has been evading me all day:

Thank you very much for the post. I have question how customize group indicator at second level? I wan to disable group indicator at second level. I am able to add image there but could disable default indicator at second level.

Thank you very much for the post. I have a question how to customize group indicator at second level? I want to disable group indicator at second level. I am able to add image there but i am not able to disable default indicator at second level.

I want to create multi level list view for my Product category. But i seem that your example fix level 1 and 2. There isn't any way to do it more general,is it? I get stuck now. I hope you will reply soon

Linh, I am not sure 3-level lists are such a great idea. For me, even 2-level lists are somewhat confusing. But you can use the same concept as used in this example program: embed expandable lists as child views of upper-level expandable lists. Then you can create expandable lists of any depth.

I have a question regarding the child node. Lets say that I want to start a new activity from the child row selection. Im aware of the onChildClick() method but Im having trouble to adapt it to your code. Could you throw me an example?

can we implement checkbox with each group subgroup and child.. please help.. because i am developing tree structure with checkbox on the right side of each element.. and if checkbox can be implemented how to implement oncheckedchange listener on each checkbox..

Hi I am trying to implement a expandable list with edit text and text views as child elements.In short I want form elements as child elements of group like edit text ,spinner etc .Please help me to implement this kind of code

I would like to adapt this to dynamically calculate the height of the "embedded" ExpandableListView because all the rows within it could have different height... So i can't count the number of rows and set "height = count * row_height".

I tried to getHeight() from each getChildView() and getGroupView() of my adapter but 0 is always returned...

I think it's because :

"Be aware not to call getWidth() or getHeight() too early before the view have been laid out on the screen, otherwise "0" will be returned."

Hi,First of all thanks for this nice project. It helped me in my project.Now I am facing one issue.I am creating 3 level expandable list with swipe functionality at 3rd level. For swiping, I am using "47deg/android-swipelistview" https://github.com/47deg/android-swipelistview/ library. Based on your code, I have used second level expandablelistview and inside that I have added swipelistview (inherited from listview) as child.I have used caching concept at getview function of swipelistview.With caching on, I get very good scrolling performance but at that time, swiping functionality is not working at third level swipelistview. If caching is disabled, swiping feature works but while scrolling, performance is down.If I use caching at 1st level expandable listview, swiping feature works properly.Can you please suggest the reason for the same.

Hi Gabor. This is a great tutorial. But I've found a bug, it happens when you have opened all the 2 level groups, and if you'd start randomly closing and opening groups, at some point(usually takes 30~ taps) 2 level groups would not respond. Not all of the 2nd level groups, but again randomly. Sometimes simply closing parent group would make it go away, but sometimes you'd need close and open other sub groups. I was trying to pin that bug down, but was unable to. It all seems to be random. One thing that is not changing is that it only happens to 2nd level groups. I also have noticed when a 2nd group becomes unresponsive on press an orange highlight will appear. I've tested with jellybean 4.1.2 Do you have any idea what this could be related to? Anyway great tutorial :) Cheers.

Hey @GaborPaller, I must say, this is a genius implementation,as far as I am concerned. I'm still struggling with adding a Spinner inside an ExpandableListView. Would you mind helping me with it? I'm confused as to where do I write the code for setting the Spinner adapter, and what to write in getChild() and other such methods of BaseExpandableAdapter. A sample program would be a much needed help! You can email me at handsomejack436@yahoo.com.

This seems to be nice blog to follow. But I have been trying to solve one problem, I wish if readers can help on that.I am populating ExpandableListView dynamically and on every child item I have a spinner (which is again dynamic). The problem here is, I am not able to differentiate between which child item is clicked with respective spinner. Does anyone have their views here?

About the blog

This blog is a personal diary about my adventures with the Google Android platform. I write it in the hope that others may find my experiences useful but please, beware. The blog is created as I gain experience about the platform myself so errors, omissions, etc. may be found in the entries.