CursorAdapter with Alphabet-indexed Section Headers

Hey everyone, today I would like to share how to organize a CursorAdapter into sections alphabetically. It works as long as your queries in the Cursor are alphabetically sorted (so the sortOrder parameter of your query is ASC by the column you want to alphabetize by).

CursorAdapter with Alphabetical Section Headers

This is made easier by the AlphabetIndexer widget, which uses binary search to finds the position of the first word of each starting letter in your data. However, there are still a few subtleties that must be addressed.

Alphabetical Section Headers Transformation
We wish to go from a un-sectioned data set to a list with the alphabet headers in the correct positions, like above. Here is my solution to this problem.

There are two methods inherited from BaseAdapter that we need to consider:
getCount(): the number of items the ListView should display. Since now we wish to include one extra header in the list, for every alphabetical section, this should return num_items_in_data + num_sections
getItem(positition): returns the item in the data set associated with the position in the ListView. Note that there is an offset depending on how many section headers appear in the data. So in the picture above, to get Fig in the list with headers, since 4 headers appear (A,B,C,F), we want the data item with position 6, instead of the list index which has position 10.

And there are two methods that we implement for SectionIndexer:
getPositionForSection(section): returns the position of the beginning of each section (which in our case is alphabetical). As mentioned before, the AlphabetIndexer will give us the position of the first word starting with each letter in our data set (so it will tell us where A cooresponds to 0, B cooresponds to 1, F to 6, etc). We must offset these positions by the number of other headers that have appeared, so the new position of A is 0 + 0, new position of B is 1 + 1, new position of F is 6 + 3.
getSectionForPosition(position): returns which section each position belongs to. The AlphabetIndexer does this by linearly comparing the first letter of each word to each possible letter in the alphabet, but in our case we now have an offset to consider. I choose to not use the AlphabetIndexer at all for this, but do a similar thing.

The first issue is to find out which alphabetical headers we actually need (in the above picture we only need A, B, C, F). Since at very least the number of headers we actually use needs to be quickly on the spot (say when getCount() is called), and the only way to determine this is to scan the entire data set, we should do some pre-computation.

The second issue is to calculate the offset for each section. This isn’t very hard, because each time we have a new section, there is a new extra header in our list, so the offset increases by 1. So convenience, we can define a map from section number to offset.

Here is the set-up:

private AlphabetIndexer indexer;

//this array is for fast lookup later and will contain the just the
//alphabet sections that actually appear in the data set
private int[] usedSectionIndicies;

//map from alphabet section to the index it ought
//to appear in
private Map<Integer, Integer> sectionToPosition;

//map from alphabet section to the number of other sections
//that appear before it
private Map<Integer, Integer> sectionToOffset;

	{
			indexer = new AlphabetIndexer(c, c.getColumnIndexOrThrow(YOU_COLUMN_NAME), "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
			sectionToPosition = new TreeMap<Integer, Integer>(); //use a TreeMap because we are going to iterate over its keys in sorted order
			sectionToOffset = new HashMap<Integer, Integer>();

			final int count = super.getCount();
			
			int i;
			//temporarily have a map alphabet section to first index it appears
			//(this map is going to be doing somethine else later)
			for (i = count - 1 ; i >= 0; i--){
				sectionToPosition.put(indexer.getSectionForPosition(i), i);
			}

			i = 0;
			usedSectionNumbers = new int[sectionToPosition.keySet().size()];
			
			//note that for each section that appears before a position, we must offset our
			//indices by 1, to make room for an alphabetical header in our list
			for (Integer section : sectionToPosition.keySet()){
				sectionToOffset.put(section, i);
				usedSectionNumbers[i] = section;
				i++;
			}

			//use offset to map the alphabet sections to their actual indicies in the list
			for(Integer section: sectionToPosition.keySet()){
				sectionToPosition.put(section, sectionToPosition.get(section) + sectionToOffset.get(section));
			}
	}

Now for the four discussed methods above, there are a few more subtleties with the implementation of ListView and FastScroller that must be considered.

		@Override
		public int getCount() {
			if (super.getCount() != 0){
				//sometimes your data set gets invalidated. In this case getCount()
				//should return 0 and not our adjusted count for the headers.
				//Any easy way to know if data is invalidated is to check if
				//super.getCount() is 0.
				return super.getCount() + usedSectionNumbers.length;
			}
			
			return 0;
		}
		
		@Override
		public Object getItem(int position) {
			if (getItemViewType(position) == TYPE_NORMAL){//we define this function in the full code later
				//if the list item is not a header, then we fetch the data set item with the same position
				//off-setted by the number of headers that appear before the item in the list
				return super.getItem(position - sectionToOffset.get(getSectionForPosition(position)) - 1);
			}

			return null;
		}

		@Override
		public int getPositionForSection(int section) {
			if (! sectionToOffset.containsKey(section)){ 
				//This is only the case when the FastScroller is scrolling,
				//and so this section doesn't appear in our data set. The implementation
				//of Fastscroller requires that missing sections have the same index as the
				//beginning of the next non-missing section (or the end of the the list if 
				//if the rest of the sections are missing).
				//So, in pictorial example, the sections D and E would appear at position 9
				//and G to Z appear in position 11.
				int i = 0;
				int maxLength = usedSectionNumbers.length;
				
				//linear scan over the sections (constant number of these) that appear in the 
				//data set to find the first used section that is greater than the given section, so in the
				//example D and E correspond to F
				while (i < maxLength && section > usedSectionNumbers[i]){
					i++;
				}
				if (i == maxLength) return getCount(); //the given section is past all our data

				return indexer.getPositionForSection(usedSectionNumbers[i]) + sectionToOffset.get(usedSectionNumbers[i]);
			}

			return indexer.getPositionForSection(section) + sectionToOffset.get(section);
		}

		@Override
		public int getSectionForPosition(int position) {
			int i = 0;		
			int maxLength = usedSectionNumbers.length;
	
			//linear scan over the used alphabetical sections' positions
			//to find where the given section fits in
			while (i < maxLength && position >= sectionToPosition.get(usedSectionNumbers[i])){
				i++;
			}
			return usedSectionNumbers[i-1];
		}

Now to put it all together, first we need a layout for the header. A simple TextView will suffice:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/header"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="6dp"
    android:textSize="18sp" 
    android:textStyle="bold"
    android:gravity="center"
    android:background="@android:color/white"
    android:textColor="@android:color/black"
    />

Now we need our CursorAdapter to implement SectionIndexer, and use the previously discussed method, with a few additional methods (also I’m just extending a SimpleCursorAdapter so I don’t need to implement newView() or bindView() ).

	public class MyAlphabetizedAdapter extends SimpleCursorAdapter implements SectionIndexer{

		private static final int TYPE_HEADER = 1;
		private static final int TYPE_NORMAL = 0;

		private static final int TYPE_COUNT = 2;

		private AlphabetIndexer indexer;

		private int[] usedSectionNumbers;

		private Map<Integer, Integer> sectionToOffset;
		private Map<Integer, Integer> sectionToPosition;
	
		public MyAlphabetizedAdapter(Context context, int layout, Cursor c,
				String[] from, int[] to) {
			super(context, layout, c, from, to);
			
			indexer = new AlphabetIndexer(c, c.getColumnIndexOrThrow(YOUR_COLUMN_NAME, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
			sectionToPosition = new TreeMap<Integer, Integer>();
			sectionToOffset = new HashMap<Integer, Integer>();

			final int count = super.getCount();
			
			int i;
			for (i = count - 1 ; i >= 0; i--){
				sectionToPosition.put(indexer.getSectionForPosition(i), i);
			}

			i = 0;
			usedSectionNumbers = new int[sectionToPosition.keySet().size()];
			
			for (Integer section : sectionToPosition.keySet()){
				sectionToOffset.put(section, i);
				usedSectionNumbers[i] = section;
				i++;
			}

			for(Integer section: sectionToPosition.keySet()){
				sectionToPosition.put(section, sectionToPosition.get(section) + sectionToOffset.get(section));
			}
		}

		@Override
		public int getCount() {
			if (super.getCount() != 0){
				return super.getCount() + usedSectionNumbers.length;
			}
			
			return 0;
		}
		
		@Override
		public Object getItem(int position) {
			if (getItemViewType(position) == TYPE_NORMAL){//we define this function later
				return super.getItem(position - sectionToOffset.get(getSectionForPosition(position)) - 1);
			}

			return null;
		}

		@Override
		public int getPositionForSection(int section) {
			if (! sectionToOffset.containsKey(section)){ 
				int i = 0;
				int maxLength = usedSectionNumbers.length;
				
				while (i < maxLength && section > usedSectionNumbers[i]){
					i++;
				}
				if (i == maxLength) return getCount();

				return indexer.getPositionForSection(usedSectionNumbers[i]) + sectionToOffset.get(usedSectionNumbers[i]);
			}

			return indexer.getPositionForSection(section) + sectionToOffset.get(section);
		}

		@Override
		public int getSectionForPosition(int position) {
			int i = 0;		
			int maxLength = usedSectionNumbers.length;

			while (i < maxLength && position >= sectionToPosition.get(usedSectionNumbers[i])){
				i++;
			}
			return usedSectionNumbers[i-1];
		}

		@Override
		public Object[] getSections() {
			return indexer.getSections();
		}

		//nothing much to this: headers have positions that the sectionIndexer manages.
		@Override
		public int getItemViewType(int position) {
			if (position == getPositionForSection(getSectionForPosition(position))){
				return TYPE_HEADER;
			} return TYPE_NORMAL;
		}

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

		//return the header view, if it's in a section header position
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			final int type = getItemViewType(position);
			if (type == TYPE_HEADER){
				if (convertView == null){
					convertView = getLayoutInflater().inflate(R.layout.header, parent, false); 
				}
				((TextView)convertView.findViewById(R.id.header)).setText((String)getSections()[getSectionForPosition(position)]);
				return convertView;
			}
			return super.getView(position - sectionToOffset.get(getSectionForPosition(position)) - 1, convertView, parent); 
		}


		//these two methods just disable the headers
		@Override
		public boolean areAllItemsEnabled() {
			return false;
		}

		@Override
		public boolean isEnabled(int position) {
			if (getItemViewType(position) == TYPE_HEADER){
				return false;
			}
			return true;
		}
	}

Finally, make sure to make sure that fastScroll is enabled for your ListView, so you can take advantage of the sweet fast scrolling tab 🙂

There is one more subtle problem, which is if the size of the cursor changes (so say in the pictorial example, our data set only contains Apple, Banana, Cranberry now) and a new instance of this list adapter isn’t made, then must take care to redo all the pre-computation. If this is the case and the new cursor has non-zero count, then you might want to wrap the pre-computation in its own method and call it during onCursorChanged().

Edit: As requested by Nick, here is a full-working demo of this list adapter in action!

/**
 * ListActivity demonstrating using the AlphabetIndexer to derive section headers
 * @author Eric
 *
 */
public class DemoActivity extends ListActivity {

	private SQLiteDatabase db;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		//NOTE: you should never actually start database operations from the context of the UI thread
		final DbHelper helper = new DbHelper(this);
		db = helper.getWritableDatabase();
		
		//populate the db with our dummy data, might take a while, so in real scenarios spawn a 
		//new thread to do this
		final int result = helper.insertDummyData(db); 
		
		if (result > 0){ 
			//query the db to obtain our cursor and set the list adapter, only if the rows were 
			//successfully inserted
			
			final Cursor cursor = db.query(DbHelper.TABLE_COUNTRIES, null, null, 
					null, null, null, DbHelper.COUNTRIES_NAME + " ASC" );
			startManagingCursor(cursor);
			Toast.makeText(this, "Finished populating.", Toast.LENGTH_SHORT).show();	
			
			setListAdapter(new MyAlphabetizedAdapter(this, android.R.layout.simple_list_item_1, 
					cursor, new String[]{DbHelper.COUNTRIES_NAME}, new int[]{android.R.id.text1}));
			
			//don't ever forget to do this, either here or in your ListView layout
			getListView().setFastScrollEnabled(true);
			
		} else {
			Toast.makeText(this, "Database could not be populated. Restart the activity.", Toast.LENGTH_LONG).show();	
		}

	}

	@Override
	protected void onDestroy() {
		db.close();
		super.onDestroy();
	}

	/**
	 * CursorAdapter that uses an AlphabetIndexer widget to keep track of the section indicies.
	 * These are the positions where we want to show a section header showing the respective alphabet letter.
	 * @author Eric
	 *
	 */
	public class MyAlphabetizedAdapter extends SimpleCursorAdapter implements SectionIndexer{

		private static final int TYPE_HEADER = 1;
		private static final int TYPE_NORMAL = 0;

		private static final int TYPE_COUNT = 2;

		private AlphabetIndexer indexer;

		private int[] usedSectionNumbers;

		private Map<Integer, Integer> sectionToOffset;
		private Map<Integer, Integer> sectionToPosition;
	
		public MyAlphabetizedAdapter(Context context, int layout, Cursor c,
				String[] from, int[] to) {
			super(context, layout, c, from, to);
			
			indexer = new AlphabetIndexer(c, c.getColumnIndexOrThrow(DbHelper.COUNTRIES_NAME), "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
			sectionToPosition = new TreeMap<Integer, Integer>(); //use a TreeMap because we are going to iterate over its keys in sorted order
			sectionToOffset = new HashMap<Integer, Integer>();

			final int count = super.getCount();
			
			int i;
			//temporarily have a map alphabet section to first index it appears
			//(this map is going to be doing somethine else later)
			for (i = count - 1 ; i >= 0; i--){
				sectionToPosition.put(indexer.getSectionForPosition(i), i);
			}

			i = 0;
			usedSectionNumbers = new int[sectionToPosition.keySet().size()];
			
			//note that for each section that appears before a position, we must offset our
			//indices by 1, to make room for an alphabetical header in our list
			for (Integer section : sectionToPosition.keySet()){
				sectionToOffset.put(section, i);
				usedSectionNumbers[i] = section;
				i++;
			}

			//use offset to map the alphabet sections to their actual indicies in the list
			for(Integer section: sectionToPosition.keySet()){
				sectionToPosition.put(section, sectionToPosition.get(section) + sectionToOffset.get(section));
			}
		}

		@Override
		public int getCount() {
			if (super.getCount() != 0){
				//sometimes your data set gets invalidated. In this case getCount()
				//should return 0 and not our adjusted count for the headers.
				//The only way to know if data is invalidated is to check if
				//super.getCount() is 0.
				return super.getCount() + usedSectionNumbers.length;
			}
			
			return 0;
		}
		
		@Override
		public Object getItem(int position) {
			if (getItemViewType(position) == TYPE_NORMAL){//we define this function in the full code later
				//if the list item is not a header, then we fetch the data set item with the same position
				//off-setted by the number of headers that appear before the item in the list
				return super.getItem(position - sectionToOffset.get(getSectionForPosition(position)) - 1);
			}

			return null;
		}

		@Override
		public int getPositionForSection(int section) {
			if (! sectionToOffset.containsKey(section)){ 
				//This is only the case when the FastScroller is scrolling,
				//and so this section doesn't appear in our data set. The implementation
				//of Fastscroller requires that missing sections have the same index as the
				//beginning of the next non-missing section (or the end of the the list if 
				//if the rest of the sections are missing).
				//So, in pictorial example, the sections D and E would appear at position 9
				//and G to Z appear in position 11.
				int i = 0;
				int maxLength = usedSectionNumbers.length;
				
				//linear scan over the sections (constant number of these) that appear in the 
				//data set to find the first used section that is greater than the given section, so in the
				//example D and E correspond to F
				while (i < maxLength && section > usedSectionNumbers[i]){
					i++;
				}
				if (i == maxLength) return getCount(); //the given section is past all our data

				return indexer.getPositionForSection(usedSectionNumbers[i]) + sectionToOffset.get(usedSectionNumbers[i]);
			}

			return indexer.getPositionForSection(section) + sectionToOffset.get(section);
		}

		@Override
		public int getSectionForPosition(int position) {
			int i = 0;		
			int maxLength = usedSectionNumbers.length;
	
			//linear scan over the used alphabetical sections' positions
			//to find where the given section fits in
			while (i < maxLength && position >= sectionToPosition.get(usedSectionNumbers[i])){
				i++;
			}
			return usedSectionNumbers[i-1];
		}

		@Override
		public Object[] getSections() {
			return indexer.getSections();
		}
		//nothing much to this: headers have positions that the sectionIndexer manages.
		@Override
		public int getItemViewType(int position) {
			if (position == getPositionForSection(getSectionForPosition(position))){
				return TYPE_HEADER;
			} return TYPE_NORMAL;
		}

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

		//return the header view, if it's in a section header position
		@Override
		public View getView(int position, View convertView, ViewGroup parent) {
			final int type = getItemViewType(position);
			if (type == TYPE_HEADER){
				if (convertView == null){
					convertView = getLayoutInflater().inflate(R.layout.header, parent, false); 
				}
				((TextView)convertView.findViewById(R.id.header)).setText((String)getSections()[getSectionForPosition(position)]);
				return convertView;
			}
			return super.getView(position - sectionToOffset.get(getSectionForPosition(position)) - 1, convertView, parent); 
		}


		//these two methods just disable the headers
		@Override
		public boolean areAllItemsEnabled() {
			return false;
		}

		@Override
		public boolean isEnabled(int position) {
			if (getItemViewType(position) == TYPE_HEADER){
				return false;
			}
			return true;
		}
	}
}
/**
 * A database helper to create the db table with country names
 * @author Eric
 *
 */
public class DbHelper extends SQLiteOpenHelper {

	public static final String TABLE_COUNTRIES = "countries";
	public static final String COUNTRIES_NAME = "name";

	private static final String DATABASE_NAME = "alphabetical_tutorial.db";
	private static final int DATABASE_VERSION = 1;

	public DbHelper(Context context) {
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL("create table " + TABLE_COUNTRIES + " (" + 
				BaseColumns._ID + " integer primary key autoincrement,"
				+ COUNTRIES_NAME + " text not null,"
				+ "unique (" + COUNTRIES_NAME + ") on conflict replace)");
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		db.execSQL("drop table exists" + TABLE_COUNTRIES);
		onCreate(db);
	}

	/**
	 * Inserts the list of country names into the db.
	 * We use SQL transactions for data integrity and efficiency.
	 * @param db
	 * @return
	 */
	public int insertDummyData(SQLiteDatabase db){
		int numInserted = 0;
		db.beginTransaction();
		try {
			SQLiteStatement insert = db.compileStatement("insert into " + 
					TABLE_COUNTRIES + "(" + COUNTRIES_NAME + ")" 
					+ "values " + "(?)");
			for (String country : COUNTRIES){
				insert.bindString(1, country);
				insert.execute();
			}
			db.setTransactionSuccessful();
			numInserted = COUNTRIES.length;
		} finally {
			db.endTransaction();
		}
		return numInserted;
	}

	//borrow the list of countries from the ListView tutorial on developer.android.com/
	static final String[] COUNTRIES = new String[] {
		"Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra",
		"Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina",
		"Armenia", "Aruba", "Australia", "Austria", "Azerbaijan",
		"Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium",
		"Belize", "Benin", "Bermuda", "Bhutan", "Bolivia",
		"Bosnia and Herzegovina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
		"British Virgin Islands", "Brunei", "Bulgaria", "Burkina Faso", "Burundi",
		"Cote d'Ivoire", "Cambodia", "Cameroon", "Canada", "Cape Verde",
		"Cayman Islands", "Central African Republic", "Chad", "Chile", "China",
		"Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo",
		"Cook Islands", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic",
		"Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic",
		"East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea",
		"Estonia", "Ethiopia", "Faeroe Islands", "Falkland Islands", "Fiji", "Finland",
		"Former Yugoslav Republic of Macedonia", "France", "French Guiana", "French Polynesia",
		"French Southern Territories", "Gabon", "Georgia", "Germany", "Ghana", "Gibraltar",
		"Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau",
		"Guyana", "Haiti", "Heard Island and McDonald Islands", "Honduras", "Hong Kong", "Hungary",
		"Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Jamaica",
		"Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos",
		"Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg",
		"Macau", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
		"Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia", "Moldova",
		"Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia",
		"Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand",
		"Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "North Korea", "Northern Marianas",
		"Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru",
		"Philippines", "Pitcairn Islands", "Poland", "Portugal", "Puerto Rico", "Qatar",
		"Reunion", "Romania", "Russia", "Rwanda", "Sqo Tome and Principe", "Saint Helena",
		"Saint Kitts and Nevis", "Saint Lucia", "Saint Pierre and Miquelon",
		"Saint Vincent and the Grenadines", "Samoa", "San Marino", "Saudi Arabia", "Senegal",
		"Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands",
		"Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea",
		"Spain", "Sri Lanka", "Sudan", "Suriname", "Svalbard and Jan Mayen", "Swaziland", "Sweden",
		"Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "The Bahamas",
		"The Gambia", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey",
		"Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Virgin Islands", "Uganda",
		"Ukraine", "United Arab Emirates", "United Kingdom",
		"United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan",
		"Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Wallis and Futuna", "Western Sahara",
		"Yemen", "Yugoslavia", "Zambia", "Zimbabwe"
	};
}

This also demonstrates using SQL transactions, which my next post is about.

Advertisements
This entry was posted in Android and tagged , , , , , , , , . Bookmark the permalink.

79 Responses to CursorAdapter with Alphabet-indexed Section Headers

  1. Nick Adams says:

    hi

    Very nice tutorial. Although I was hoping you could give me a full working code sample to play with

    Thanks

  2. Josh McKinney says:

    Thank you for this solution!

    I am currently working on a media player app and have a couple of questions about your code:

    1) How can I sort entries that start with a number? I would like to push these to the end if possible, but right now they are listed under the “A”.

    2) Is it possible to make the headers clickable?

    Thanks So Much!
    Josh

  3. Josh McKinney says:

    Not sure why my original comment was removed, but I did manage to figure out how to sort the number related items.

    Can you please help me with how to make the separators clickable? I would like to add an onClick method that pops up a new view when clicking on any separator.

    Thanks,
    Josh

    • eshyu says:

      Hey Josh,

      Your comment wasn’t removed, I just never changed from the default WordPress comment moderation 🙂

      The headers are actually clickable by default, but I disabled them intentionally in my code. Just delete the overriden isEnabled() areAllItemsEnabled() (in the superclass these always return true)

      Then in your onListItemClick(ListView l, View v, int position, long id) method, you can check l.getadapter().getitemviewtype(position) to see if the view is a header, and call ((SectionIndexer)l.getadapter()).getSectionForPosition() to get the exact section.

      • Josh McKinney says:

        Thanks for your reply eshyu! I will give that a try.

        Pardon my rude-ness on the comment removal….I run two wordpress sites myself, so I know all about comment moderation troubles.

      • Josh says:

        Hi eshyu,

        I am trying to work out the onListItemClick method. It seems that if I place this method inside your MyAlphabetizedAdapter class it will not register clicks. If I place it outside the class it will register clicks, but I am unable to resolve TYPE_HEADER .

        Here is the code I am working with:

        protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        if (l.getAdapter().getItemViewType(position) == TYPE_HEADER ){
        Log.i(“Log”, “Clicked on a header”);
        }else{
        Log.i(“Log”, “Clicked on an item”);
        }
        }

        Any suggestions?

        Thanks,
        Josh

      • eshyu says:

        Hey Josh,

        I believe onListItemClick() is a a method for ListActivity, so it should indeed be called outside of MyAlphabetizedAdapter.

        I declared TYPE_HEADER to be a private field for the adapter, so I guess the easiest work-around is to just make TYPE_HEADER protected or public.

  4. Josh says:

    Thanks eshyu! That was exactly what I needed and certainly something I should have caught.

    • Josh says:

      eshyu,

      One final question (I hope)!

      My onclick will access a cursor selection based on the position. When referencing position from your code, the number returned is always (correct position+ number of headers). I see that your code fixes by the calculation at getItem(), but how do I reference this object for my onclick position?

      • eshyu says:

        Hey Josh,

        You should be able to access the correct cursor in the onListItemClick() method using

        Cursor cursor = (Cursor) l.getItemAtPosition(position)

        And then use cursor.get_ to obtain the information you need.

      • Josh says:

        Your solution worked for me eshyu! Thanks for all of the help!

  5. dirk says:

    Just ran into a nasty bug (that I caused to happen). I wanted to add a ‘#’ to the list of sections (for example, the Droid comes pre-programmed with #pmt as a contact entry). The code crashes miserably if the alphabet indexer is passed the following list “ABCDEFGHIJKLMNOPQRSTUVWXYZ#” but works fine for “#ABCDEFGHIJKLMNOPQRSTUVWXYZ”. The latter is in ASCII order. I had of course wanted to display the ‘#’ section at the end. Not a big problem, but just a heads up for other people.

    • eshyu says:

      Nice find, I’ll have to look at it more closely later.

      • dirk says:

        BTW, thanks for the code, it has definitely helped me. I did spend a little time debugging it but stopped and moved on when I found the cause (deadlines and pragmatism prevailed!).

      • Josh says:

        Any luck with this # sorting? This bug is affecting my code as well.

        Thanks,
        Josh

      • eshyu says:

        Unfortunately there doesn’t seem to be an easy solution to this. My implementation uses the AlphabetIndexer class which uses binary search to find the section positions, and so its implementation needs the alphabet given to be in ASCII order to do comparisons.

  6. Josh says:

    Thanks again for this wonderful code.

    Could you please tell me how I would jump to header position using your code and imputing an int based on alphabetical position (inputing 0 would jump to #, which is the top of my list, imputing 2 would jump to B header, etc.)

    Thanks
    Josh

    • Josh says:

      Still struggling with this. Any help you could provide would be appreciated.

      Basically I need to figure out how to set on onClick that takes a header position based int (# is 0, a is 1, etc.) and pad it by how many entriess are before it. If there are 5 A entries and I click on the b button I need it to return 8.

      I apologize, as I don’t fully understand your code, and don’t know exactly what portions I need to make public.

      Again, all your help is appreciated, and I will give full credit to you when my app is complete.

      Thanks,
      Josh

  7. Bence says:

    Hi! I tried your code, with no luck.. It gives me the following error:
    10-26 12:26:32.171: ERROR/AndroidRuntime(1249): Caused by: android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 238

    Could you tell me what did I do wrong?
    Thanks!

    • eshyu says:

      The alphabet you pass into the line

      indexer = new AlphabetIndexer(c, c.getColumnIndexOrThrow(DbHelper.NAME, “ABCDEFGHIJKLMNOPQRSTUVWXYZ”);

      must be in ASCII alphabetical order

  8. Gabriela says:

    Hi,

    Could you put the complete code for downloading, please?

    Thanks

  9. Thomas Kim says:

    It works pretty well Thanks for you sharing.

  10. Leandro says:

    Hey, thanks for the post. It has been of great help. I’m trying to figure out how to handle that subtle issue you mentioned: I’m dinamically filtering the list and when the list is updated the app crashes. I’m also not extending SimpleCursorAdapter but CursorAdapter, and I cannot see a method called onCursorChanged(). Istead I could override onContentChanged() but I believe that when it’s executed it’s too late. Can you give me any other hint on what the pre-computations you mention would be and when (or in which event) should I execute them? Thanks a lot.

  11. Ragunath Jawahar says:

    Hi, I have more than 6000 entries in my database, how do I handle such a situation efficiently? Now the Activity show an ANR dialog. Any help would be appreciated. Thanks.

    • eshyu says:

      Hey Ragunath,

      Thanks for pointing this out.

      The problem is: how to find where each alphabetical section starts?

      My solution to this runs in O(nlgn) time, because I run a binary search for each position to find its section, and the lowest position in a section is where the section begins. The binary search method implemented by the AlphabetIndexer already encapsulated iterating over the elements in the cursor.

      You can reduce this to O(n) time if you do one pass through the elements in the cursor and compare the first letter of the entry. The code becomes more messier, but it’s a big performance gain if you are dealing with that many elements.

  12. ichirohang says:

    Hi,
    nice article, however, i find there are some sorting problem lowercase letter shown in the last of the list and also show duplicate data.

    Here is data i try for listing :
    //borrow the list of countries from the ListView tutorial on developer.android.com/
    static final String[] COUNTRIES = new String[] { “1”, “2”, ” 1ABC”, “3ZFK”,
    “Afghanistan”, “Albania”, “Algeria”, “American Samoa”, “Andorra”,
    “Yemen”, “Yugoslavia”, “Zambia”, “Zimbabwe”, “Cty_zabcs”, “M_ACTBC”, “ab_temp”, “b_01234”
    };

    Thank you

    • Josh says:

      I just realized I was having the same problem (lowercase entries appear at the bottom of my list).

      Anyone figure out a fix for this? Is this a problem with AlphabetIndexer itself, or the implementation of it in this special case?

      Thanks
      Josh

      • Ilian says:

        No problems here.
        Here is my code for the AlphabetIndexer:
        indexer = new AlphabetIndexer(contacts, c.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME),
        ” ABCDEFGHIJKLMNOPQRSTUVWXYZアカサヌナハマヤラワ”);
        I’m sorting my phone book database of 3000+ contacts (In English and Japanes( and they are displayed in alphabetical order, disregarding Capital letters. I do have space before the first character as my preference.
        The reason should be somewhere else in your code/ settings.

  13. Josh says:

    I was able to fix by changing the sort from:

    final Cursor cursor = db.query(DbHelper.TABLE_COUNTRIES, null, null,
    null, null, null, DbHelper.COUNTRIES_NAME + ” ASC” );

    to

    … COUNTRIES_NAME + ” COLLATE LOCALIZED ASC”

    Hope this helps.

    • Ilian says:

      up
      please disregard my previous comment (sorry I cant delete), I too had implemented “COLLATE LOCALISED ASC” but completely forgot about it 🙂

  14. Martin says:

    Hi eshyu,

    great post – searched for exactly this two days ago and found your blog.

    As some other guy already mentioned, the implementation of a filter was somehow tricky. In my ListActivity I have something like

    adapter.setFilterQueryProvider(this);

    where adapter is a “MyAlphabetizedAdapter”.

    To avoid crashes (either by adapter or by Activity), I had to implement

    @Override
    public void changeCursor(Cursor cursor) {
    super.changeCursor(cursor);
    init(cursor);
    notifyDataSetChanged();
    }

    in the Adapter.

    The “init(cursor)” does most of your constructor’s code, namely initializing sectionToOffset, sectionToPosition, and AlphabetIndexer. That’s it, you’re done and the View will filter smoothly.

    Again, thanks for sharing, CU.

    • eshyu says:

      One more thing, in the beginning of init() you should clear the keys in the maps

      sectionToPosition.clear();
      sectionToOffset.clear();
      indexerPositionForSection.clear();

      • Martin says:

        Yes, thanks for the advice, that’s what I ment with “does most of your constructor’s code” – without clearing them out some weird things happen 🙂

      • Andres G Luque says:

        what is “indexerPositionForSection” ?? i can’t find that variable.

        I tried this approach but didn’t have luck. I’m trying to fill the list while the cursor keep increasing.

      • Giancarlo says:

        Dear all, I have used your code and it works very well. I’ve also overrided the cheangecursor method. My app has many filters on the cursor, and the indexer works very fine when I switch from a filter to another. The problem is that when the activity goes onPause and then it re-opens (onResume) I have problems with the indexer, even though I can switch from a filter to another trought many buttons, and I call changeCursor method inside each onclicklistener. Does anyone can help me ???
        Thanks 🙂

      • Giancarlo says:

        The problem is that I see more letters than necessary, but the listview is correctly displayed. the problem is on he side fast scroll that shows more letters than necessary.
        Thanks.

    • eshyu says:

      Hey Andres,

      I used indexerPositionForSection for an optimization (it’s not in the code here). This implementation of getPositionForSection() call’s AlphabetIndexer’s getPositionForSection(), but (if i remember correctly) AlphabetIndexer’s compare() method allocated a new String object each invocation, and so the garbage collector would go crazy when scrolling through a big list, and scrolling would lag. So I saved the result of indexer.getPositionForSection() into a HashMap to prevent calling the method many times.

      Whats the problem you have now? You might need to explicitly tell your cursoradapter to changeCursor(newCursor).

      • Donal says:

        Hi eshyu,

        Really interested in the optimization, but can’t figure out where you load the information into the HashMap? Currently you use getPositionForSection() within the getPositionForSection method to return the int value required. I can’t see how you can optimize the code here by adding the value into a HashMap instead? Don’ you still need to call Indexer.getPositionForSection() here twice to get the values for the Hashmap?

  15. Ilian says:

    I came across several different malfunctions. I had:
    1 misaligned section headers (they were falling behind my list progressively)
    2 the getType method was also misaligned and was returning normal type for the headers
    3 reaching the end of my listview caused exception
    In my case I used the AlphabetIndexer within a CursorAdapter for a custom ListView. The reasons were:
    1 “public int getPositionForSection(int section)” should return “return indexer.getPositionForSection(section) + sectionToOffset.get(section)-section;”
    2 Line 34 had to be changed to “sectionToOffset.put(section, i-1);”
    3 getCount needn’t include ” +usedSectionNumbers.length”
    Hope this helps to others who had similar issues.

    • eshyu says:

      Hey Ilian,

      Thank you for your comments. Your problems are most likely because you have Japanese characters in your list. This current implementation uses AlphabetIndexer’s getPositionForSection, and the binary search has problems because of the foreign characters at the bottom of the list. I wasn’t anticipating non-English characters when I was making this. But I’ve ran into the same problem too, since I now have some Chinese and Greek names.

      You actually do need to include the usedSectionNumbers.length in the getCount() method. If you don’t, then then some entries in your list end up being truncated. I don’t understand why you made your other changes. But try your modifications on data with just the English alphabet, like in the sample with the country names, and you’ll see that there now are problems.

    • Vaidehi says:

      Hey Ilian, I had the same issue and my scenario is almost similar to what users is. I am using an AlphabetIndexer within a CursorAdapter for a custom ListView.
      1. My list crashes on scrolling the list to till the end with an IllegalStateException : couldn’t move cursor to poisition 109 (109 is the count of the cursor)
      2. My cursor rows are misplaced regardless of the alphabetical sections
      It would be helpful if you could post your code.

      Thank You Eshyu, your logic helped a lot.

  16. Andres G Luque says:

    Im trying to use this inside an activitygroup, it works fine the first time but when I navigate to another view and then go back to the this view it crashes. Apparently its an ANR problem but the error displayed in the log is that the activityManager.getCurrentActivity() returns null… Im guessing the ANR isnt displayed but the delay affects the activityManager… what do you think?

    • eshyu says:

      Hey Andres,

      Is your cursor for the adapter changing between when you navigate between views? If so you need to override changeCursor() so that it re-computes the section header positions.

      Why do you think you are getting an ANR in the first place? I tried this for 5000 entries inside a TabActivity and couldn’t find the problem you’re mentioning.

      • Andres G Luque says:

        Hi, thanks for the response.

        The problem appeared only when I navigate between views inside a tabactivity using activitygroup. I solve it implementing onResume(wrote startManagingCursor()) and onPause(wrote stopManagingCursor ) inside the activity. I haven’t tried this with onChangeCursor. I will and let you know if that helps too.

      • Andres G Luque says:

        Just another little question. What would you do if you have to fill the table at the same time you get the cursor. Because I’m getting the “not closed” cursor error (Invalid statement in fillWindow())

      • eshyu says:

        Hey Andre,

        Sorry for the delay, I have been busy with school.

        I remember seeing that error in LogCat before and I think it is a benign error (I can’t reproduce it atm and it’s been a while) and you should still be able to insert into the table while getting the cursor.

        Try this http://stackoverflow.com/questions/4195089/what-does-invalid-statement-in-fillwindow-in-android-cursor-mean to get rid of the error and make sure you’re cursor is managed by the activity.

        Sorry I can’t be more helpful with this.

  17. Piettes says:

    Hi, greats tips, thanks for sharing !

    But I’m actually working with an ArrayList, and not with a cursor, so I don’t use CursorAdapater, but ArrayAdapter.
    So I can’t create any AlphabetIndexer…

    1) Do think performance are better with cursor ?
    2) Do you have any idea on how adapt your solution ?

  18. Josh says:

    Hello,

    I have run into trouble with my use of the alphabet indexed sections on my application. I have had several requests to sort items beginning with “The” to the first letter of the second word. I am able to do this by sorting my cursor, but it throws off the next header in line when I do this.

    Any idea how I could adjust your code to ignore the first word if it is “The”?

    Thank You,
    Josh

  19. Throrïn says:

    Hello,
    Thanks for your tutorial it help me 😀
    But now, i have a problem for develop other Indexer. My list contain people and they are grouped by type.

    Have you an idea to develop a custom indexer for this problem?

    Thanks

  20. sat says:

    Thanks for the code. Simple and elegant

  21. Pankaj kumar says:

    Eshyu, can we use this logic for an ArrayList? In your example you are using Cursor to displaying data from database, but my need is to implement sectioned header in a listview where data source is an ArrayList.

  22. Maurice says:

    Hi all!
    Martin mentioned a “adapter.setFilterQueryProvider(this);” in the ListActivity. “this” cannot be used by the ListActivity.

    How do you use the Override changeCursor? Currently I’m doing this:

    //the database was updated so I called the method below.
    Cursor finalCursor = mDbHelper.fetchAllContacts();
    ca.changeCursor(finalCursor);

    But i get
    java.lang.IllegalStateException: attempt to re-open an already-closed object: android.database.sqlite.SQLiteQuery (mSql = SELECT _id, contact_id, display_name FROM contacts ORDER BY display_name COLLATE LOCALIZED ASC)

    Thanks!

    • Maurice says:

      I got it to work doing this:
      finalCursor.requery();
      ca.changeCursor(finalCursor);

      But requery is deprecated. What’s the current way of doing it? Is the setFilterQueryProvider necessary? By the way very awesome piece of code!

      • Maurice says:

        Ok this sounds silly, I’m like answering myself lol. Just to share:

        Cursor test = mDbHelper.fetchAllContacts();
        ca.changeCursor(test);

        In the init(Cursor cursor), I forgot to assign the new cursor to the global cursor c,
        c = cursor;

        Now it works like a charm! Thanks!

  23. Maurice says:

    Hiyah Eshyu! I’m currently seeking advice on how to sync the phone contacts with your app contacts and then use the adapter to load the contacts from the db. Currently getting nasty errors like app failed to respond because it takes a long time to compare over 2000 contacts. Any ideas?

  24. Abhishek Mukherjee says:

    Thanks a lot. Of all articles I found in the net, this was the most helpful.

    • Abhishek Mukherjee says:

      One Question though, how do I implement an iphone style side bar instead of FastScroll? I want to jump to the section with a specific label when I slide over a vertical scroll bar in the right side corner with alphabets in it.

      Please someone help

  25. neonigma says:

    Really good!! Thanks for your excellent work!

  26. Igor says:

    Hi! Thanks for nice example.
    In my implementation, in getView() I need to access data from cursor, to supply it to custom row views. How do I do it?

  27. zen kun says:

    Hi amazing this code save my day, perhaps i want to load a lot of images so i tried implement lazy load, and seems working well perhaps i had an issue in public View getView(int position, View convertView, ViewGroup parent) method, if i dont change your code all works well perhaps i want to “get the view” and load the picture seems doing well my code perhaps it throw an exception when im in like letter R, its: java.lang.ArrayIndexOutOfBoundsException
    in my get view i have this:
    public View getView(int position, View convertView, ViewGroup parent) {

    final int type = getItemViewType(position);
    if (type == TYPE_HEADER){
    if (convertView == null){
    convertView = getLayoutInflater().inflate(R.layout.header, parent, false);
    }
    ((TextView) convertView.findViewById(R.id.header)).setText((String)getSections()[getSectionForPosition(position)]);

    return convertView;
    }

    else if(type ==TYPE_NORMAL)
    {
    View vi=convertView;
    if(convertView==null)
    {
    LayoutInflater viz = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    vi = viz.inflate(R.layout.media_select_row, null);

    }
    ImageView image=(ImageView)vi.findViewById(R.id.row_icon);
    imageLoader.DisplayImage(data[position], image,cont);

    //return super.getView(position – sectionToOffset.get(getSectionForPosition(position)) – 1, vi, parent);
    return vi;
    }
    return super.getView(position – sectionToOffset.get(getSectionForPosition(position-1)) – 1, convertView, parent);
    }
    what i need to modify im kind lost =X i wish you can help me thanks a lot and nice code, without this modification i did works like a charm but its laggy because all images thats why i want do this Thanks

  28. Sagar says:

    Hi Eshyu, awseome post on AlphabetIndexer.
    I have a quick question though regarding handling null values.
    How to handle null values when I am querying through the database? Since each time if there are null values it throws an error with NullPointerException.
    It will be great if we can catch this somewhere in our code.

  29. redlenses says:

    Your getSectionForPosition can have an array out of bounds problem if length = 0, since you are returning array[i – 1]

  30. Steven Conboy says:

    Very nice code. I would like insert an EditText box at the start of the list in order to perform an autocomplete, however, your code does not have a line like setContentView(R.layout.header), after the onCreate of AndroidSQLiteTutorialActivity. The method getView() which references R.layout.header takes care of creating the list, but i don’t know how to insert any other widgets other than the list. Any help would be appreciated.

  31. Binod singh says:

    Hi Eshyu,
    How can I edit the view not the header. I want to have custom view for the listing it displays.
    Also where can I get the values from cursor. I mean in which method.

    Very good post though, Looking for positive response.

    Thanks
    Binod Singh

  32. ram says:

    This Tutorial is Awesome but i cannot find the Download Button to download the source CODE file? or maybe im to late?? its already move out? please comment if this trends is active?

    mr/ms eshyu please email the source code… :”) thanks alot keep on sharing 🙂

    i will wait everyday…. 🙂

  33. ram says:

    PLEASE help mhie i want the list having two items ??? the (item-subitem) please help me 🙂

  34. Pingback: Custom CursorAdapter with AlphabetIndexer and alphabet sections | Technology & Programming

  35. virginiawh3 says:

    Striking pctures
    http://download.sexblog.pw/?ashtyn
    italian erotic film xxx sex erotic belly dance p or n sex erotic sports

  36. larry says:

    Is it me or do the headers always show up right after that section starts? I cant seem to get them before the section

  37. Andranik says:

    Hi. Thank you for a great post. I have a problem though. I build contacts in my app using almost all the tricks from the post and comments. But sometimes, under some conditions, it gave an ArrayIndexOutOfBoundsException: length=16; index=-1 in getSectionForPosition method. I don’t know why but the i variable there remains 0… It is happening on my clients phones. I cannot reproduce the problem to see what is the matter. Do anybody have an idea why this is happening?

  38. Pingback: How to implement searchFilter in ListView having Alphabet-indexed Section Headers

  39. med says:

    great tutorial bro, i cant find the download link to the source code can you re-post it plz, i know i’m late thanks in advance.

  40. Bashar Labadi says:

    As contribution, here is a method that returns the item position in the sectioned headed list from the position of the item in the cursor, I needed it where I need to select an item by Id not by position, so I iterated over the cursor and got it’s position then passed it to this:

    add this inside the list adapter:

    public int getItemPositionByCursorPosition(int cursorPosition) {
    return 1
    + sectionToOffset.get(indexer.getSectionForPosition(cursorPosition))
    + cursorPosition;
    }

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