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; } }
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…
Did you ever find a solution you can use on the Google Play market?
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!!!!
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!
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
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.
android:cacheColorHint=”#00000000″
add this line of code to your listview. This will work!
Hey Baris,
I have added this android:cacheColorHint=”#00000000″ in listview. But still the header color becomes dark while scrolling.
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 !
My bad ! Don’t forget to “+ one” in your getCount() method !
How can i make the header to be custom. for example change the header color or add an image to the header.
excellent! thank you very much
Nice tutorial, but how to put differents contents in each section ?
eg :
Section1
– babababa
– babababa
Section2
– lalalalala
– lulululul
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.
Thanks =D
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.
How to set thumbnail for each list item. please help me..
How do you make the sections stick to the top?
Is it possible to change the background colour for a specific section?
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;
}
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)?
3 Pingbacks
GOOGLE
Recent Posts
Archives
Categories
Meta