A Filterable ArrayAdapter with Fast Scrolling in Android
I spent today at the Google Sydney offices for an Android Developer Lab. It is my experience with events like this that you pick up handful of useful bits of information from the presentations, but the real value is the people you talk to during the day and the stuff that you build during the coding labs. My time in the lab was spent chasing down an issue with ListViews, Filterable ArrayAdapters and setFastScrollEnabled.
The issue raises its head when you implement a filterable and section indexed ArrayAdapter for your ListView and then call setFastScrollEnabled to enable the thumb button fast scrolly thing on the right hand side of the list:
Enables fast scrolling by letting the user quickly scroll through lists by dragging the fast scroll thumb. The adapter attached to the list may want to implement SectionIndexer if it wishes to display alphabet preview and jump between sections of the list.
Filterable ArrayAdapter
The first part, building a filterable android.widget.ArrayAdapter
The Filter interface has a couple of methods that let you create a subset of your ListView data based on the search term that user types on the keyboard. Called in a worker thread performFiltering does the actual work adjusting the ListView content based on the CharSequence parameter. The publishResults method publishes the results of the filtering operation to the UI thread. The publishResults method will look something like this:
@Override protected void publishResults(CharSequence prefix, FilterResults results)
{
//noinspection unchecked
mItems = (ArrayList<GlossaryEntry>) results.values;
updateListIndex ();
// Let the adapter h about the updated list
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
If we have some data after the filtering operation, then tell the ListView that its data set has changed via the notifyDataSetChanged method, otherwise just tell it to invalidate its contents (so it can render a blank view).
This method is significant for what I am going to talk about next, because it is the mechanism with which I can tell the ListView that it's stuff has changed and it needs to re-draw. I'll come to that in a minute.
SectionIndexer implementation on ArrayAdapter
SectionIndexer is the interface on the ArrayAdapter that allows the ListView to find out how the items should be broken up by section and what text to display as the fast scroll UI is dragged on the right hand side of the view:
Interface that should be implemented on Adapters to enable fast scrolling in an AbsListView between sections of the list. A section is a group of list items to jump to that have something in common.
So this interface has three methods:
- getPositionForSection(int section) - Provides the starting index in the list for a given section.
- getSectionForPosition(int position) - This is a reverse mapping to fetch the section index for a given position in the list.
- Object[] getSections() - This provides the list view with an array of section objects.
So when I initilise my ListView, you need to build an index to map from items to sections, sections to items and the list of section names. In my first implimentation of this stuff I did this organically based on the actual items in the list. I assumed that when I called notifyDataSetChanged the ListView would call getSections on the SectionIndexer so it can get the section names that correspond to the new content. Alas, this is not the case.
public ListIndex (int size, Delegate d)
{
// Build an index map
m_sectionNames = new ArrayList<Object>();
m_sectionForPositionArray = new ArrayList<Integer>();
m_positionForSectionArray = new ArrayList<Integer>();
String prevSection = "";
m_Size = size;
for(int i = 0; i < size; i++)
{
//String sectionName = names[i].substring(0, 1);
String sectionName = d.sectionNameForIndex(i);
if(!prevSection.equals(sectionName))
{
m_sectionNames.add(sectionName);
m_positionForSectionArray.add(i);
prevSection = sectionName;
}
m_sectionForPositionArray.add(m_sectionNames.size() - 1);
}
}
So this implementation will build an m_sectionNames array that will be different for each subset of the ListView content. Because getSections isn't called when you publish a filtering operation to the listView, you will get index out of range errors. The solution I came up with during the lab was to build a section names array based on a fixed alphabet, rather than the variable contents of the ListView:
public ListIndex (int size, Delegate d)
{
// Build an index map
m_sectionNames = new ArrayList<Object>(26);
m_sectionForPositionArray = new ArrayList<Integer>();
m_positionForSectionArray = new ArrayList<Integer>(26) ;
for (int j = 0; j < 26; j++)
{
// populate all the position for sections with -1
m_positionForSectionArray.add (-1);
// add the section names for the alphabet
m_sectionNames.add (String.format ("%c", j + 65));
}
// now run through the index
int currentIndex = 0;
for(int i = 0; i < size; i++)
{
String sectionName = d.sectionNameForIndex(i);
char c = sectionName.charAt(0);
int sectionIndex = currentIndex;
if ((int)c >= 65 && (int)c < (66 + 26))
{
// if this term has an alpha first character, store the section Index
sectionIndex = (int)c - 65;
currentIndex = sectionIndex;
}
// set the position for section array with the first value we find
if (m_positionForSectionArray.get(sectionIndex) == -1)
m_positionForSectionArray.set(sectionIndex, i);
// add the section for position item with the last valid sectionIndex we got
m_sectionForPositionArray.add(sectionIndex);
}
// now we have to run through our position for section and make
// sure we don't have any gaps left
currentIndex = -1;
for (int i = 0; i < m_positionForSectionArray.size (); i++)
{
if (currentIndex == -1 && m_positionForSectionArray.get(i) != -1)
{
// have our first index ... go back through the array
// and set the values
currentIndex = m_positionForSectionArray.get(i);
for (int j = 0; j < i; j++)
m_positionForSectionArray.set(j, currentIndex);
continue;
}
if (m_positionForSectionArray.get(i) != -1)
{
currentIndex = m_positionForSectionArray.get(i);
continue;
}
if (currentIndex != -1 && m_positionForSectionArray.get(i) == -1)
{
// fill any gap values with the last index
m_positionForSectionArray.set(i, currentIndex);
continue;
}
}
}
So this implementation is less than ideal for a couple of reasons. It will miss-categorize items that don't start with an alpha character and it will include sections with no entries (the index is forced to the next available item). It would be much nicer if the ListView just re-grabbed the section names when you call notifyDataSetChanged.
Permalink - Tags: Development,Android,Google