Android – Sectioned Headers in ListViews


ListView Sectioned Headers in Android

Sectioned headers in a list are great when you want to display categorized items eg. by time/day, by product category or sales price.

In this example, we will be using Jeff Sharkey’s Sectioned Headers to display journal entries by day.

Download: TestSectionedHeaderList.zip

File: main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">

	<ListView
		android:id="@+id/add_journalentry_menuitem"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content" />
	<ListView
		android:id="@+id/list_journal"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content" />
</LinearLayout>

File: list_header.xml

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

File: list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- list_item.xml -->
<TextView
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/list_item_title"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:paddingTop="10dip"
	android:paddingBottom="10dip"
	android:paddingLeft="15dip"
	android:textAppearance="?android:attr/textAppearanceLarge"
	/>

File: list_complex.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- list_complex.xml -->
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="wrap_content"
	android:orientation="vertical"
	android:paddingTop="10dip"
	android:paddingBottom="10dip"
	android:paddingLeft="15dip"
	>
	<TextView
		android:id="@+id/list_complex_title"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textAppearance="?android:attr/textAppearanceLarge"
		/>
	<TextView
		android:id="@+id/list_complex_caption"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textAppearance="?android:attr/textAppearanceSmall"
		/>
</LinearLayout>

File: add_journalentry_menuitem.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- list_item.xml -->
<TextView
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/list_item_title"
	android:gravity="right"
	android:drawableRight="@drawable/ic_menu_add"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:paddingTop="0dip"
	android:paddingBottom="0dip"
	android:paddingLeft="10dip"
	android:textAppearance="?android:attr/textAppearanceLarge" />

File: ListSample.java

import java.util.HashMap;
import java.util.Map;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import android.widget.AdapterView.OnItemClickListener;

public class ListSample extends Activity
	{

		public final static String ITEM_TITLE = "title";
		public final static String ITEM_CAPTION = "caption";

		// SectionHeaders
		private final static String[] days = new String[]{"Mon", "Tue", "Wed", "Thur", "Fri"};

		// Section Contents
		private final static String[] notes = new String[]{"Ate Breakfast", "Ran a Marathan ...yah really", "Slept all day"};

		// MENU - ListView
		private ListView addJournalEntryItem;

		// Adapter for ListView Contents
		private SeparatedListAdapter adapter;

		// ListView Contents
		private ListView journalListView;

		public Map<String, ?> createItem(String title, String caption)
			{
				Map<String, String> item = new HashMap<String, String>();
				item.put(ITEM_TITLE, title);
				item.put(ITEM_CAPTION, caption);
				return item;
			}

		@Override
		public void onCreate(Bundle icicle)
			{
				super.onCreate(icicle);

				// Sets the View Layer
				setContentView(R.layout.main);

				// Interactive Tools
				final ArrayAdapter<String> journalEntryAdapter = new ArrayAdapter<String>(this, R.layout.add_journalentry_menuitem, new String[]{"Add Journal Entry"});

				// AddJournalEntryItem
				addJournalEntryItem = (ListView) this.findViewById(R.id.add_journalentry_menuitem);
				addJournalEntryItem.setAdapter(journalEntryAdapter);
				addJournalEntryItem.setOnItemClickListener(new OnItemClickListener()
					{
						@Override
						public void onItemClick(AdapterView<?> parent, View view, int position, long duration)
							{
								String item = journalEntryAdapter.getItem(position);
								Toast.makeText(getApplicationContext(), item, Toast.LENGTH_SHORT).show();
							}
					});

				// Create the ListView Adapter
				adapter = new SeparatedListAdapter(this);
				ArrayAdapter<String> listadapter = new ArrayAdapter<String>(this, R.layout.list_item, notes);

				// Add Sections
				for (int i = 0; i < days.length; i++)
					{
						adapter.addSection(days[i], listadapter);
					}

				// Get a reference to the ListView holder
				journalListView = (ListView) this.findViewById(R.id.list_journal);

				// Set the adapter on the ListView holder
				journalListView.setAdapter(adapter);

				// Listen for Click events
				journalListView.setOnItemClickListener(new OnItemClickListener()
					{
						@Override
						public void onItemClick(AdapterView<?> parent, View view, int position, long duration)
							{
								String item = (String) adapter.getItem(position);
								Toast.makeText(getApplicationContext(), item, Toast.LENGTH_SHORT).show();
							}
					});
			}

	}

File: SeparatedListAdapter.java

import java.util.LinkedHashMap;
import java.util.Map;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;

public class SeparatedListAdapter extends BaseAdapter
	{
		public final Map<String, Adapter> sections = new LinkedHashMap<String, Adapter>();
		public final ArrayAdapter<String> headers;
		public final static int TYPE_SECTION_HEADER = 0;

		public SeparatedListAdapter(Context context)
			{
				headers = new ArrayAdapter<String>(context, R.layout.list_header);
			}

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

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

						// check if position inside this section
						if (position == 0) return section;
						if (position < size) return adapter.getItem(position - 1);

						// otherwise jump into next section
						position -= size;
					}
				return null;
			}

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

		@Override
		public int getViewTypeCount()
			{
				// assume that headers count as one, then total all sections
				int total = 1;
				for (Adapter adapter : this.sections.values())
					total += adapter.getViewTypeCount();
				return total;
			}

		@Override
		public int getItemViewType(int position)
			{
				int type = 1;
				for (Object section : this.sections.keySet())
					{
						Adapter adapter = sections.get(section);
						int size = adapter.getCount() + 1;

						// check if position inside this section
						if (position == 0) return TYPE_SECTION_HEADER;
						if (position < size) return type + adapter.getItemViewType(position - 1);

						// otherwise jump into next section
						position -= size;
						type += adapter.getViewTypeCount();
					}
				return -1;
			}

		public boolean areAllItemsSelectable()
			{
				return false;
			}

		@Override
		public boolean isEnabled(int position)
			{
				return (getItemViewType(position) != TYPE_SECTION_HEADER);
			}

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

						// check if position inside this section
						if (position == 0) return headers.getView(sectionnum, convertView, parent);
						if (position < size) return adapter.getView(position - 1, convertView, parent);

						// otherwise jump into next section
						position -= size;
						sectionnum++;
					}
				return null;
			}

		@Override
		public long getItemId(int position)
			{
				return position;
			}

	}

25 thoughts on “Android – Sectioned Headers in ListViews

  1. Let me remind everyone that this code is GPL v3, which means you can’t use in some cases (many cases if you’re developing for Android market, or any other market for that matter).

    It works very well and is easily customized to almost any needs, afaict. Right now I’m currently trying to find another solution.

    • Let me state that I wasn’t bashing GPL… also because I use it all the time. If anything, it was the opposite: I saw this example (which is great, by the way… I used it for another project), and at first I implemented it on my app (to be sold on the Market), only to find later that I couldn’t, for license reasons.

      My alert was directed at people that could be in the same position as me, to save them time, as I believe and assume that many people who look for solutions on Google would like to implement them on their own Market apps.

      Besides, I believe there is space for all approaches (be it GPL, ASL or proprietary), and because of that I feel sad when I see this kind of stuff, so I think it’s never enough to remind us all to keep up with the spirit of OSS. Anyway, just explaining myself…

  2. Hi,
    thanks you for this article, it is very usefuf. I ‘m begenner in android and i want to know how can i put different sections content for each sections header ?
    Thanks again!!!!

  3. Hi! This was a good piece of code, but just curious, did you manage to put a section indexer to the SeparatedListAdapter? I’m having trouble getting it to work properly. Thanks!

  4. I am getting following error.

    E/ArrayAdapter(17801): You must supply a resource ID for a TextView
    W/dalvikvm(17801): threadid=3: thread exiting with uncaught exception (group=0x4001e170)
    E/AndroidRuntime(17801): Uncaught handler: thread main exiting due to uncaught exception
    E/AndroidRuntime(17801): java.lang.IllegalStateException: ArrayAdapter requires the resource ID to be a TextView
    E/AndroidRuntime(17801): at android.widget.ArrayAdapter.createViewFromResource(ArrayAdapter.java:347)
    110: E/AndroidRuntime(17801): at android.widget.ArrayAdapter.getView(ArrayAdapter.java:323)
    : E/AndroidRuntime(17801): at android.widget.AbsListView.obtainView(AbsListView.java:1385)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.ListView.measureHeightOfChildren(ListView.java:1213)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.ListView.onMeasure(ListView.java:1126)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.View.measure(View.java:7964)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:3023)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:888)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.LinearLayout.measureVertical(LinearLayout.java:350)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.LinearLayout.onMeasure(LinearLayout.java:278)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.View.measure(View.java:7964)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:3023)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.FrameLayout.onMeasure(FrameLayout.java:245)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.View.measure(View.java:7964)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.LinearLayout.measureVertical(LinearLayout.java:464)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.LinearLayout.onMeasure(LinearLayout.java:278)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.View.measure(View.java:7964)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:3023)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.FrameLayout.onMeasure(FrameLayout.java:245)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.View.measure(View.java:7964)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.ViewRoot.performTraversals(ViewRoot.java:763)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.view.ViewRoot.handleMessage(ViewRoot.java:1633)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.os.Handler.dispatchMessage(Handler.java:99)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.os.Looper.loop(Looper.java:123)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.app.ActivityThread.main(ActivityThread.java:4363)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at java.lang.reflect.Method.invokeNative(Native Method)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at java.lang.reflect.Method.invoke(Method.java:521)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    11-24 16:17:52.110: E/AndroidRuntime(17801): at dalvik.system.NativeStart.main(Native Method)
    11-24 16:17:52.110: E/AndroidRuntime(17801): Caused by: java.lang.ClassCastException: android.widget.LinearLayout
    11-24 16:17:52.110: E/AndroidRuntime(17801): at android.widget.ArrayAdapter.createViewFromResource(ArrayAdapter.java:340)
    11-24 16:17:52.110: E/AndroidRuntime(17801): … 29 more
    11-24 16:18:27.600: D/ddm-heap(17801): Got feature list request

  5. Hi
    Thanks for your code. While using your code in my project I am setting the color with 50% alpha to list header and my window background color is transparent. So while scrolling header color becomes dark. Any idea how to overcome this.

    • Hey Baris,
      I have added this android:cacheColorHint=”#00000000″ in listview. But still the header color becomes dark while scrolling.

  6. Hi ! I’ve a little problem with this : everything works fine except i can’t scroll to the bottom of the list.
    It seems that the size of the list is based on the number of header multiplied by the header’s height. So each element between the headers “eats” the space for the last letters…
    I tried to modifiy the size of the list, but when i do, i can’t scroll anymore :-/
    Anyone had a similar issue ? Or an idea of solution ?

    Thanks in advance !

    • You should be able to put different contents in different sections by using a different ArrayAdapter for each section. You’ll notice that on line 70, a section is created for each day of the week, and then each section uses the “listAdapter” built in line 67, which simply contains the contents of “notes.” If you create a new list adapter, within the for loop starting on line 70, you should be able to give each section it’s own content. Try making “notes” declared on line 22 either an array of arrays, or a map of strings to arrays, with the keys being the days of the week. Then iterate over these, using each array in the constructor of the new list adapter. Here is some pseudocode:

      Line 22: private final static String[][] notes = [[“Apple”, “Banana”], [“Cherry”, “Date”]];
      Above line 72: listadapter = new ArrayAdapter(this, R.layout.list_item, notes[i]);

      You’ll have to update the index for the for loop accordingly. Basically just create a new adapter to pass in for each section on line 72.

  7. Pingback: Sectioning Listview : Android Community - For Application Development

  8. Pingback: Scroll View relative to parent View : Android Community - For Application Development

  9. Pingback: how to create sectionised listview like instagram in android : Android Community - For Application Development

  10. I realize I am late to this party but I hope someone can help me nonetheless. I do not understand where or how the list_complex.xml code comes into play. After going through the tutorial and teasing apart what was going on, I found no reference to list_complex, list_complex_title or list_complex_caption in any other xml file or java file. Please help me understand what I am missing.

  11. That’s what I did in separateListAdapter to change the view according to the section:

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

    // check if position inside this section
    if (position == 0) {
    View lHeaderView = headers.getView(sectionnum, convertView, parent);
    String headerText = ((TextView) lHeaderView).getText().toString();
    //You can change the color depending on the header text
    lHeaderView.setBackgroundColor(Color.BLUE);
    return lHeaderView;
    } else if (position < size) {

    View lHeaderView = headers.getView(sectionnum, convertView, parent);
    String headerText = ((TextView) lHeaderView).getText().toString();
    //You can change the color depending on the header text

    View lView = adapter.getView(position – 1, convertView, parent);
    lView.setBackgroundColor(Color.RED);
    return lView;
    }

    // otherwise jump into next section
    position -= size;
    sectionnum++;
    }
    return null;
    }

  12. I do find a problem when you have a list who needs to be updated often. You can’t just call adapter.notifyDataSetChanged().
    How would you update sections without recreating the whole list (which causes it to go back to the top, leading to a poor user experience)?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s