RSS

Search Engine

Saturday, May 22, 2010

Separating Lists with Headers in Android 0.9

Earlier today the latest Android 0.9 SDK was released, and it’s packed full of wonderful changes. As you play around, you might see ListViews split into sections using separating headers. (Example shown on the right is the browser settings list.)

There isn’t an easy way of creating these separated lists, so I’ve put together SeparatedListAdapter which does it quickly. To summarize, we’re creating a new BaseAdapter that can contain several other Adapters, each with their own section headers.

First let’s create some simple XML layouts to be used for our lists: first the header view, then two item views that we’ll use later for the individual lists. (Thanks to Romain Guy for helping me find existing styles to keep these XML layouts nice and tidy.)


  1. <TextView
  2. xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/list_header_title"
  4. android:layout_width="fill_parent"
  5. android:layout_height="wrap_content"
  6. android:paddingTop="2dip"
  7. android:paddingBottom="2dip"
  8. android:paddingLeft="5dip"
  9. style="?android:attr/listSeparatorTextViewStyle" />


  10. <TextView
  11. xmlns:android="http://schemas.android.com/apk/res/android"
  12. android:id="@+id/list_item_title"
  13. android:layout_width="fill_parent"
  14. android:layout_height="fill_parent"
  15. android:paddingTop="10dip"
  16. android:paddingBottom="10dip"
  17. android:paddingLeft="15dip"
  18. android:textAppearance="?android:attr/textAppearanceLarge"
  19. />


  20. <LinearLayout
  21. xmlns:android="http://schemas.android.com/apk/res/android"
  22. android:layout_width="fill_parent"
  23. android:layout_height="wrap_content"
  24. android:orientation="vertical"
  25. android:paddingTop="10dip"
  26. android:paddingBottom="10dip"
  27. android:paddingLeft="15dip"
  28. >
  29. <TextView
  30. android:id="@+id/list_complex_title"
  31. android:layout_width="fill_parent"
  32. android:layout_height="wrap_content"
  33. android:textAppearance="?android:attr/textAppearanceLarge"
  34. />
  35. <TextView
  36. android:id="@+id/list_complex_caption"
  37. android:layout_width="fill_parent"
  38. android:layout_height="wrap_content"
  39. android:textAppearance="?android:attr/textAppearanceSmall"
  40. />
  41. LinearLayout>

Now let’s create the actual SeparatedListAdapter class which provides a single interface to multiple sections of other Adapters. After using addSection() to construct the child sections, you can easily use ListView.setAdapter() to present the now-separated list to users.

As for the Adapter internals, to correctly find the selected item among the child Adapters, we walk through subtracting from the original position until we find either a header (position = 0) or item in the current child Adapter (position <>

Here’s the source for SeparatedListAdapter:

  1. public class SeparatedListAdapter extends BaseAdapter {

  2. public final Map sections = new LinkedHashMap();
  3. public final ArrayAdapter headers;
  4. public final static int TYPE_SECTION_HEADER = 0;

  5. public SeparatedListAdapter(Context context) {
  6. headers = new ArrayAdapter(context, R.layout.list_header);
  7. }

  8. public void addSection(String section, Adapter adapter) {
  9. this.headers.add(section);
  10. this.sections.put(section, adapter);
  11. }

  12. public Object getItem(int position) {
  13. for(Object section : this.sections.keySet()) {
  14. Adapter adapter = sections.get(section);
  15. int size = adapter.getCount() + 1;

  16. // check if position inside this section
  17. if(position == 0) return section;
  18. if(position < class="keyword">return adapter.getItem(position - 1);

  19. // otherwise jump into next section
  20. position -= size;
  21. }
  22. return null;
  23. }

  24. public int getCount() {
  25. // total together all sections, plus one for each section header
  26. int total = 0;
  27. for(Adapter adapter : this.sections.values())
  28. total += adapter.getCount() + 1;
  29. return total;
  30. }

  31. public int getViewTypeCount() {
  32. // assume that headers count as one, then total all sections
  33. int total = 1;
  34. for(Adapter adapter : this.sections.values())
  35. total += adapter.getViewTypeCount();
  36. return total;
  37. }

  38. public int getItemViewType(int position) {
  39. int type = 1;
  40. for(Object section : this.sections.keySet()) {
  41. Adapter adapter = sections.get(section);
  42. int size = adapter.getCount() + 1;

  43. // check if position inside this section
  44. if(position == 0) return TYPE_SECTION_HEADER;
  45. if(position < class="keyword">return type + adapter.getItemViewType(position - 1);

  46. // otherwise jump into next section
  47. position -= size;
  48. type += adapter.getViewTypeCount();
  49. }
  50. return -1;
  51. }

  52. public boolean areAllItemsSelectable() {
  53. return false;
  54. }

  55. public boolean isEnabled(int position) {
  56. return (getItemViewType(position) != TYPE_SECTION_HEADER);
  57. }

  58. @Override
  59. public View getView(int position, View convertView, ViewGroup parent) {
  60. int sectionnum = 0;
  61. for(Object section : this.sections.keySet()) {
  62. Adapter adapter = sections.get(section);
  63. int size = adapter.getCount() + 1;

  64. // check if position inside this section
  65. if(position == 0) return headers.getView(sectionnum, convertView, parent);
  66. if(position < class="keyword">return adapter.getView(position - 1, convertView, parent);

  67. // otherwise jump into next section
  68. position -= size;
  69. sectionnum++;
  70. }
  71. return null;
  72. }

  73. @Override
  74. public long getItemId(int position) {
  75. return position;
  76. }

  77. }

0 comments:

Post a Comment