Android Dividing Your ListView Into Sections with Group Headers

ListView is commonly used to display a set of data in a list. For example a list of emails, messages, tasks or contacts. I’ve written an article on how to work with lists in android before, which you might want to read.

In this article we’ll discuss how to break a list of our phone contacts into sections. So instead of displaying our contacts in a random order, we’ll sort them alphabetically and group them into chunks with headers representing each alphabet as in the native “Contacts” application. Hence, we’ll have something like “A -> Adam, Anne | B -> Ben, Brad …”.

What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.

We’ll discuss 2 approaches.

First Approach: Toggle Visibility of Separator View inside rows

In this method, what we’ll do is place a View, which will be a TextView in this case inside the row’s layout file. In the getView() method that gets called by the Adapter to render our views inside the ListView, we’ll check if the current item belongs to a different group than the previous item or if the current item is the first item of the entire group or not. If yes then we can simply show up the separator by calling the setVisibility() method on the View object and pass it the View.VISIBLE argument. On the other hand, if we pass the View.GONE argument like setVisiblity(View.GONE) then it’ll simply make the view invisibile and not take up any space.

So we’ll fetch all the contacts from our phone book using the Contacts ContentProvider and section them alphabetically. Here’s how the layout will look like:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="10dp">

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/separator"
        android:textSize="14sp"
        android:textStyle="bold"
        style="?android:listSeparatorTextViewStyle" />

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/contact_name"
        android:textSize="18sp"
        android:layout_marginTop="10dip"
        android:paddingLeft="8dip"
        android:paddingRight="8dip" />

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/phone_number"
        android:textColor="#ff8a8a8c"
        android:paddingLeft="8dip"
        android:paddingRight="8dip" />
</LinearLayout>

If you notice, the first TextView is the one that’ll be used as a separator. It is visible by default but based on certain conditions (that we’ll look into in a bit) we’ll hide it from the user interface.

Here’s our entire Activity code (that contains the Adapter as an inner class):

package com.pycitup.pyc;

import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class ContactsActivity extends Activity {

    String[] mProjection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_contacts);


        mProjection = new String[] {
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
                ContactsContract.CommonDataKinds.Phone.NUMBER
        };

        final Cursor cursor = getContentResolver().query(
                ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                mProjection,
                null,
                null,
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
        );

        CustomAdapter adapter = new CustomAdapter(this, cursor);

        // Create the list view and bind the adapter
        ListView listView = (ListView) findViewById(R.id.listview);
        listView.setAdapter(adapter);
    }

    public class CustomAdapter extends BaseAdapter {

        private Context mContext;
        private Cursor mCursor;
        
        // State of the row that needs to show separator
        private static final int SECTIONED_STATE = 1;
        // State of the row that need not show separator
        private static final int REGULAR_STATE = 2;
        // Cache row states based on positions
        private int[] mRowStates;

        public CustomAdapter(Context context, Cursor cursor) {
            mContext = context;
            mCursor = cursor;
            mRowStates = new int[getCount()];
        }

        @Override
        public int getCount() {
            return mCursor.getCount();
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            View view;
            boolean showSeparator = false;

            mCursor.moveToPosition(position);

            if (convertView == null) {
                LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                view = inflater.inflate(R.layout.contact_item, null);
            }
            else {
                view = convertView;
            }

            // Set contact name and number
            TextView contactNameView = (TextView) view.findViewById(R.id.contact_name);
            TextView phoneNumberView = (TextView) view.findViewById(R.id.phone_number);

            String name = mCursor.getString( mCursor.getColumnIndex(mProjection[0]) );
            String number = mCursor.getString( mCursor.getColumnIndex(mProjection[1]) );

            contactNameView.setText( name );
            phoneNumberView.setText( number );


            // Show separator ?

            switch (mRowStates[position]) {

                case SECTIONED_STATE:
                    showSeparator = true;
                    break;

                case REGULAR_STATE:
                    showSeparator = false;
                    break;

                default:

                    if (position == 0) {
                        showSeparator = true;
                    }
                    else {
                        mCursor.moveToPosition(position - 1);

                        String previousName = mCursor.getString(mCursor.getColumnIndex(mProjection[0]));
                        char[] previousNameArray = previousName.toCharArray();
                        char[] nameArray = name.toCharArray();

                        if (nameArray[0] != previousNameArray[0]) {
                            showSeparator = true;
                        }

                        mCursor.moveToPosition(position);
                    }

                    // Cache it
                    mRowStates[position] = showSeparator ? SECTIONED_STATE : REGULAR_STATE;

                    break;
            }

            TextView separatorView = (TextView) view.findViewById(R.id.separator);

            if (showSeparator) {
                separatorView.setText(name.toCharArray(), 0, 1);
                separatorView.setVisibility(View.VISIBLE);
            }
            else {
                view.findViewById(R.id.separator).setVisibility(View.GONE);
            }

            return view;
        }
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.contacts, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

That’s quite a bit of code but we’ll only focus on the portions that are essential to us. So the part that fetches all the contacts is this one:

mProjection = new String[] {
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
};

final Cursor cursor = getContentResolver().query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        mProjection,
        null,
        null,
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
);

We use the Contacts Provider to fetch all the contacts names and phone numbers in ascending order. You can get to know more about then query syntax and you can easily relate it to SQL here.

Now on to the real portion that does the job of toggling the separator’s visiblity. It’s all in the getView() method:

public View getView(int position, View convertView, ViewGroup parent) {

    View view;
    boolean showSeparator = false;

    mCursor.moveToPosition(position);

    if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        view = inflater.inflate(R.layout.contact_item, null);
    }
    else {
        view = convertView;
    }

    // Set contact name and number
    TextView contactNameView = (TextView) view.findViewById(R.id.contact_name);
    TextView phoneNumberView = (TextView) view.findViewById(R.id.phone_number);

    String name = mCursor.getString( mCursor.getColumnIndex(mProjection[0]) );
    String number = mCursor.getString( mCursor.getColumnIndex(mProjection[1]) );

    contactNameView.setText( name );
    phoneNumberView.setText( number );


    // Show separator ?

    switch (mRowStates[position]) {

        case SECTIONED_STATE:
            showSeparator = true;
            break;

        case REGULAR_STATE:
            showSeparator = false;
            break;

        default:

            if (position == 0) {
                showSeparator = true;
            }
            else {
                mCursor.moveToPosition(position - 1);

                String previousName = mCursor.getString(mCursor.getColumnIndex(mProjection[0]));
                char[] previousNameArray = previousName.toCharArray();
                char[] nameArray = name.toCharArray();

                if (nameArray[0] != previousNameArray[0]) {
                    showSeparator = true;
                }

                mCursor.moveToPosition(position);
            }

            // Cache it
            mRowStates[position] = showSeparator ? SECTIONED_STATE : REGULAR_STATE;

            break;
    }

    TextView separatorView = (TextView) view.findViewById(R.id.separator);

    if (showSeparator) {
        separatorView.setText(name.toCharArray(), 0, 1);
        separatorView.setVisibility(View.VISIBLE);
    }
    else {
        view.findViewById(R.id.separator).setVisibility(View.GONE);
    }

    return view;
}

We start off by moving the cursor to the appropriate position by making this call mCursor.moveToPosition(position); and then based on whether the convertView is cached or not, we inflate the R.layout.contact_item layout file. Next, we set the contact name and number in the appropriate views and then hop on to the real part of checking where we show or hide the separator. The default block inside the switch block is where the real checking is done:

default:

    if (position == 0) {
        showSeparator = true;
    }
    else {
        mCursor.moveToPosition(position - 1);

        String previousName = mCursor.getString(mCursor.getColumnIndex(mProjection[0]));
        char[] previousNameArray = previousName.toCharArray();
        char[] nameArray = name.toCharArray();

        if (nameArray[0] != previousNameArray[0]) {
            showSeparator = true;
        }

        mCursor.moveToPosition(position);
    }

    // Cache it
    mRowStates[position] = showSeparator ? SECTIONED_STATE : REGULAR_STATE;

    break;

If the position is 0, that means that it’s the first item then we show the separator. If not, then we move to the previous item in the cursor and get the name of the contact. Then we convert the previous name fetched and the current name to char[] arrays to do a check on the first character, if they’re same or not. If not then we must show the separator. Finally, it is important to move back to the original position and ofcourse cache the state in mRowStates.

When we’re done with all the hard work, we finally use the showSeparator flag to hide/unhide the separator:

if (showSeparator) {
    separatorView.setText(name.toCharArray(), 0, 1);
    separatorView.setVisibility(View.VISIBLE);
}
else {
    view.findViewById(R.id.separator).setVisibility(View.GONE);
}

There’s one UX issue with this method which is separators are clickable. Atleast the default highlighting on rows on tapping can be prevented by setting android:clickable="true" on the separator view (as it intercepts touch calls to the ListView), that too only when the separator is tapped on but not the sibling views in the same row.

Also when I think about 90-95% of the rows having a hidden view (separator) I feel like there’s more memory being used that implies wastage. That said, this method has the advantage of sectioning in realtime or “on the fly”. This is especially helpful when working with cursors (querying a Content Provider) and not Arrays which has the data stored (that can also be manipulated easily). Another benefit of this approach is the direct mapping of the row’s position to the position of the data in the cursor.

Second Approach: Using a different View for Separator

The other approach to sectioning is to use an entirely different view for the separator. So let’s create a new layout file at /layout/contact_section_header.xml:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/separator"
        android:textSize="14sp"
        android:textStyle="bold"
        style="?android:listSeparatorTextViewStyle"
        />

</LinearLayout>

And this is how our new /layout/contact_item.xml will look like:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="10dp">

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/contact_name"
        android:textSize="18sp"
        android:layout_marginTop="10dip"
        android:paddingLeft="8dip"
        android:paddingRight="8dip" />

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/phone_number"
        android:textColor="#ff8a8a8c"
        android:paddingLeft="8dip"
        android:paddingRight="8dip" />


</LinearLayout>

Our onCreate() method will also change to some extent:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_contacts);


    mProjection = new String[] {
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.NUMBER
    };

    final Cursor cursor = getContentResolver().query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            mProjection,
            null,
            null,
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"
    );

    int nameIndex = cursor.getColumnIndex(mProjection[0]);
    int numberIndex = cursor.getColumnIndex(mProjection[1]);

    ArrayList contacts = new ArrayList();

    int position = 0;
    boolean isSeparator = false;
    while(cursor.moveToNext()) {
        isSeparator = false;

        String name = cursor.getString(nameIndex);
        String number = cursor.getString(numberIndex);

        char[] nameArray;

        // If it is the first item then need a separator
        if (position == 0) {
            isSeparator = true;
            nameArray = name.toCharArray();
        }
        else {
            // Move to previous
            cursor.moveToPrevious();

            // Get the previous contact's name
            String previousName = cursor.getString(nameIndex);

            // Convert the previous and current contact names
            // into char arrays
            char[] previousNameArray = previousName.toCharArray();
            nameArray = name.toCharArray();

            // Compare the first character of previous and current contact names
            if (nameArray[0] != previousNameArray[0]) {
                isSeparator = true;
            }

            // Don't forget to move to next
            // which is basically the current item
            cursor.moveToNext();
        }

        // Need a separator? Then create a Contact
        // object and save it's name as the section
        // header while pass null as the phone number
        if (isSeparator) {
            Contact contact = new Contact(String.valueOf(nameArray[0]), null, isSeparator);
            contacts.add( contact );
        }

        // Create a Contact object to store the name/number details
        Contact contact = new Contact(name, number, false);
        contacts.add( contact );

        position++;
    }

    // Creating our custom adapter
    CustomAdapter adapter = new CustomAdapter(this, contacts);

    // Create the list view and bind the adapter
    ListView listView = (ListView) findViewById(R.id.listview);
    listView.setAdapter(adapter);
}

So we get the previous contact name and compare its first character with the first character of the current contact name. If they’re different then create a Contact object to which the third argument passed is true while null is passed as the phone number. The name of the contact is set to the first character.

Regardless of the check, one Contact object is always created for the current contact. All these objects are stored in an ArrayList which is then used by our custom adapter.

Let’s quickly look at our Contact class first which is fairly straightforward:

public class Contact {
    public String mName;
    public String mNumber;
    public boolean mIsSeparator;

    public Contact(String name, String number, boolean isSeparator) {
        mName = name;
        mNumber = number;
        mIsSeparator = isSeparator;
    }

    public void setName(String name) {
        mName = name;
    }

    public void setNumber(String number) {
        mNumber = number;
    }

    public void setIsSection(boolean isSection) {
        mIsSeparator = isSection;
    }
}

The convention is to define getter methods as well, but to keep things short we’ll access the public properties directly from wherever it is required.

Finally, here’s our CustomAdapter class with umpteen new things that we haven’t covered before.

public class CustomAdapter extends BaseAdapter {

    private Context mContext;
    private ArrayList<Contact> mList;

    // View Type for Separators
    private static final int ITEM_VIEW_TYPE_SEPARATOR = 0;
    // View Type for Regular rows
    private static final int ITEM_VIEW_TYPE_REGULAR = 1;
    // Types of Views that need to be handled
    // -- Separators and Regular rows --
    private static final int ITEM_VIEW_TYPE_COUNT = 2;

    public CustomAdapter(Context context, ArrayList list) {
        mContext = context;
        mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

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

    @Override
    public int getViewTypeCount() {
        return ITEM_VIEW_TYPE_COUNT;
    }

    @Override
    public int getItemViewType(int position) {
        boolean isSection = mList.get(position).mIsSeparator;

        if (isSection) {
            return ITEM_VIEW_TYPE_SEPARATOR;
        }
        else {
            return ITEM_VIEW_TYPE_REGULAR;
        }
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view;

        Contact contact = mList.get(position);
        int itemViewType = getItemViewType(position);

        if (convertView == null) {
            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            if (itemViewType == ITEM_VIEW_TYPE_SEPARATOR) {
                // If its a section ?
                view = inflater.inflate(R.layout.contact_section_header, null);
            }
            else {
                // Regular row
                view = inflater.inflate(R.layout.contact_item, null);
            }
        }
        else {
            view = convertView;
        }


        if (itemViewType == ITEM_VIEW_TYPE_SEPARATOR) {
            // If separator

            TextView separatorView = (TextView) view.findViewById(R.id.separator);
            separatorView.setText(contact.mName);
        }
        else {
            // If regular

            // Set contact name and number
            TextView contactNameView = (TextView) view.findViewById(R.id.contact_name);
            TextView phoneNumberView = (TextView) view.findViewById(R.id.phone_number);

            contactNameView.setText( contact.mName );
            phoneNumberView.setText( contact.mNumber );
        }

        return view;
    }
}

Three new methods have been introduced (overridden) here:

  • getViewTypeCount() – This method should return the number of types of Views that will be created and returned by the getView() method.
  • getItemViewType() – This method should return an integer representing the type of View that will be created by getView() for the item at the specified position. In our case we’ve defined 2 constants ITEM_VIEW_TYPE_SEPARATOR = 0 and ITEM_VIEW_TYPE_REGULAR = 1 that is returned by this method. So basically this method must return and integer between 0 and getViewTypeCount() - 1.
  • isEnabled – If the item at the specified position is a separator (a non-selectable and non-clickable item) then this method should return true, else false.

Finally, if you go through the contents of the getView() method then you’ll notice that we’re basically inflating and setting content (texts) for different layout files and views based on the view type returned by getItemViewType() method.

The best part about this approach is that it is very easy to comprehend and at the same time can manage multiple types of items in the ListView.

Note: Let’s say that our section is something like this – “s1 -> r1, r2, r3 | s2 -> r4, r5” – where sN is a separator and rN is a row, the position of r4 will actually be 6th (5th index) in the list and not 4th (3rd index). Thankfully since we have the data for both separators and rows stored in the ArrayList, when the user taps or does some sort of interaction with the 6th item on the list (r4) the position passed to event listeners will match with that of the data structure (ArrayList).

That’s All!

That’s all folks! I hope the 2 approaches explained in order to divide your list view into groups on android proves to be helpful for your projects. If you’ve any questions then let’s discuss in the comments below.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download

Author: Rishabh

Rishabh is a full stack web and mobile developer from India. Follow me on Twitter.

6 thoughts on “Android Dividing Your ListView Into Sections with Group Headers”

  1. Hey Brother , i Like your posts.
    Thanks

    But one suggestion , try to give some snap shots for wtever you are doing.
    Like in this post you are dividing this List View so You should also give some snap shots of your App.

    Gaurav

  2. Hi,
    Thank you for this post. It is really helpful.
    But what if I add an contact? In that case there is an ArrayIndexOfBoundsException in getView() method.
    Is this code only for fetching the data from the database?

    Thanx

Leave a Reply

Your email address will not be published. Required fields are marked *