package com.twotoasters.sectioncursoradapter; import android.content.Context; import android.database.Cursor; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.SectionIndexer; import java.util.ArrayList; import java.util.Collection; import java.util.SortedMap; import java.util.TreeMap; public abstract class SectionCursorAdapter extends CursorAdapter implements SectionIndexer { public static final int NO_CURSOR_POSITION = -99; // used when mapping section list position to cursor position protected static final int VIEW_TYPE_SECTION = 0; protected static final int VIEW_TYPE_ITEM = 1; protected SortedMap<Integer, Object> mSections = new TreeMap<Integer, Object>(); // should not be null ArrayList<Integer> mSectionList = new ArrayList<Integer>(); private Object[] mFastScrollObjects; private LayoutInflater mLayoutInflater; public SectionCursorAdapter(Context context, Cursor cursor, int flags) { super(context, cursor, flags); init(context, null); } protected SectionCursorAdapter(Context context, Cursor c, boolean autoRequery, SortedMap<Integer, Object> sections) { super(context, c, autoRequery); init(context, sections); } @Deprecated public SectionCursorAdapter(Context context, Cursor cursor) { super(context, cursor); init(context, null); } private void init(Context context, SortedMap<Integer, Object> sections) { mLayoutInflater = LayoutInflater.from(context); if (sections != null) { mSections = sections; } else { buildSections(); } } /** * @return an inflater to inflate your view with. */ protected LayoutInflater getLayoutInflater() { return mLayoutInflater; } /** * If the adapter's cursor is not null then this method will call buildSections(Cursor cursor). */ private void buildSections() { if (hasOpenCursor()) { Cursor cursor = getCursor(); cursor.moveToPosition(-1); mSections = buildSections(cursor); if (mSections == null) { mSections = new TreeMap<Integer, Object>(); } } } /** * @param cursor a non-null cursor at position -1. * @return A map whose keys are the position at which a section is and values are an object * which will be passed to newSectionView and bindSectionView */ protected SortedMap<Integer, Object> buildSections(Cursor cursor) { TreeMap<Integer, Object> sections = new TreeMap<Integer, Object>(); int cursorPosition = 0; while (hasOpenCursor() && cursor.moveToNext()) { Object section = getSectionFromCursor(cursor); if (cursor.getPosition() != cursorPosition) throw new IllegalStateException("Do no move the cursor's position in getSectionFromCursor."); if (!sections.containsValue(section)) sections.put(cursorPosition + sections.size(), section); cursorPosition++; } return sections; } /** * The object which is return will determine what section this cursor position will be in. * @param cursor * @return the section from the cursor at its current position. * This object will be passed to newSectionView and bindSectionView. */ protected abstract Object getSectionFromCursor(Cursor cursor); @Override public View getView(int position, View convertView, ViewGroup parent) { boolean isSection = isSection(position); Context context = parent.getContext(); Cursor cursor = getCursor(); View view; if (!isSection) { int newPosition = getCursorPositionWithoutSections(position); if (!hasOpenCursor()) { // This only happens when the scroll is super fast and someone backs out. return new View(parent.getContext()); } else if (!cursor.moveToPosition(newPosition)) { throw new IllegalStateException("couldn't move cursor to position " + newPosition); } } if (convertView == null) { view = isSection ? newSectionView(context, getItem(position), parent) : newItemView(context, cursor, parent); } else { view = convertView; } if (isSection) { bindSectionView(view, context, position, getItem(position)); } else { bindItemView(view, context, cursor); } return view; } @Override @Deprecated /** * This method is from the CursorAdapter and will never be called. */ public final View newView(Context context, Cursor cursor, ViewGroup parent) { throw new IllegalStateException("This method is not used by " + SectionCursorAdapter.class.getSimpleName()); } @Override @Deprecated /** * This method is from the CursorAdapter and will never be called. */ public final void bindView(View view, Context context, Cursor cursor) { throw new IllegalStateException("This method is not used by " + SectionCursorAdapter.class.getSimpleName()); } /** * Creates a new section view. * @param context Interface to application's global information * @param item is the item stored in the sorted map for the section header. * @param parent The parent to which the new view is attached. * @return */ protected abstract View newSectionView(Context context, Object item, ViewGroup parent); /** * Binds data to an existing view. * @param convertView Existing view, returned earlier by newView * @param context Interface to application's global information * @param position * @param item is the item stored in the sorted map for the section header. */ protected abstract void bindSectionView(View convertView, Context context, int position, Object item); /** * Creates a new item view to use within a section. * @param cursor The cursor from which to get the data. The cursor is already moved to the correct position. * @param parent The parent to which the new view is attached to * @return */ protected abstract View newItemView(Context context, Cursor cursor, ViewGroup parent); /** * Binds data to an item view * @param convertView Existing view, returned earlier by newView * @param context Interface to application's global information * @param cursor The cursor from which to get the data. The cursor is already moved to the correct position. */ protected abstract void bindItemView(View convertView, Context context, Cursor cursor); /** * * @param listPosition the position of the current item in the list with mSections included * @return Whether or not the listPosition points to a section. */ public boolean isSection(int listPosition) { return mSections.containsKey(listPosition); } /** * This will map a position in the list adapter (which includes mSections) to a position in * the cursor (which does not contain mSections). * * @param listPosition the position of the current item in the list with mSections included * @return the correct position to use with the cursor */ public int getCursorPositionWithoutSections(int listPosition) { if (mSections.size() == 0) { return listPosition; } else if (!isSection(listPosition)) { int sectionIndex = getIndexWithinSections(listPosition); if (isListPositionBeforeFirstSection(listPosition, sectionIndex)) { return listPosition; } else { return listPosition - (sectionIndex + 1); } } else { return NO_CURSOR_POSITION; } } /** * Finds the section index for a given list position. * * @param listPosition the position of the current item in the list with mSections included * @return an index in an ordered list of section names */ public int getIndexWithinSections(int listPosition) { boolean isSection = false; int numPrecedingSections = 0; for (Integer sectionPosition : mSections.keySet()) { if (listPosition > sectionPosition) numPrecedingSections++; else if (listPosition == sectionPosition) isSection = true; else break; } return isSection ? numPrecedingSections : Math.max(numPrecedingSections - 1, 0); } private boolean isListPositionBeforeFirstSection(int listPosition, int sectionIndex) { boolean hasSections = mSections != null && mSections.size() > 0; return sectionIndex == 0 && hasSections && listPosition < mSections.firstKey(); } /** * Clears out all section data before rebuilding it. */ @Override public void notifyDataSetChanged() { if (hasOpenCursor()) { buildSections(); mFastScrollObjects = null; mSectionList.clear(); } super.notifyDataSetChanged(); } /** * Clears out all section data before rebuilding it. */ @Override public void notifyDataSetInvalidated() { if (hasOpenCursor()) { buildSections(); mFastScrollObjects = null; mSectionList.clear(); } super.notifyDataSetInvalidated(); } /** * @param listPosition the position of the current item in the list with mSections included * @return If the position is a section it will return the value for the position from the section map. * Otherwise it will convert to the cursor position and return super. */ @Override public Object getItem(int listPosition) { if (isSection(listPosition)) return mSections.get(listPosition); else return super.getItem(getCursorPositionWithoutSections(listPosition)); } /** * @param listPosition the position of the current item in the list with mSections included * @return If the position is a section it will return the value for the position from the section map. * Otherwise it will return the _id column value. */ @Override public long getItemId(int listPosition) { if (isSection(listPosition)) return listPosition; else { int cursorPosition = getCursorPositionWithoutSections(listPosition); Cursor cursor = getCursor(); if (hasOpenCursor() && cursor.moveToPosition(cursorPosition)) { return cursor.getLong(cursor.getColumnIndex("_id")); } return NO_CURSOR_POSITION; } } /** * @return How many items are in the data set represented by this Adapter. */ @Override public int getCount() { return super.getCount() + mSections.size(); } /** * @param listPosition * @return Get the type of View that will be created by getView(int, View, ViewGroup) for the specified item. */ @Override public int getItemViewType(int listPosition) { return isSection(listPosition) ? VIEW_TYPE_SECTION : VIEW_TYPE_ITEM; } /** * @return Returns the number of types of Views that will be created by getView(int, View, ViewGroup). */ @Override public int getViewTypeCount() { return 2; } /** * @return True if cursor is not null and open. * If the cursor is closed a null cursor will be swapped out. */ protected boolean hasOpenCursor() { Cursor cursor = getCursor(); if (cursor == null || cursor.isClosed()) { swapCursor(null); return false; } return true; } //////////////////////////////////// // Methods for the SectionIndexer //////////////////////////////////// /** * Given the index of a section within the array of section objects, returns * the starting position of that section within the adapter. * * If the section's starting position is outside of the adapter bounds, the * position must be clipped to fall within the size of the adapter. * * @param sectionIndex the index of the section within the array of section * objects * @return the starting position of that section within the adapter, * constrained to fall within the adapter bounds */ @Override public int getPositionForSection(int sectionIndex) { if (mSectionList.size() == 0) { for (Integer key : mSections.keySet()) { mSectionList.add(key); } } return sectionIndex < mSectionList.size() ? mSectionList.get(sectionIndex) : getCount(); } /** * Given a position within the adapter, returns the index of the * corresponding section within the array of section objects. * * If the section index is outside of the section array bounds, the index * must be clipped to fall within the size of the section array. * * For example, consider an indexer where the section at array index 0 * starts at adapter position 100. Calling this method with position 10, * which is before the first section, must return index 0. * * @param position the position within the adapter for which to return the * corresponding section index * @return the index of the corresponding section within the array of * section objects, constrained to fall within the array bounds */ @Override public int getSectionForPosition(int position) { Object[] objects = getSections(); // the fast scroll section objects int sectionIndex = getIndexWithinSections(position); return sectionIndex < objects.length ? sectionIndex : 0; } /** * Returns an array of objects representing mSections of the list. The * returned array and its contents should be non-null. * * The list view will call toString() on the objects to get the preview text * to display while scrolling. For example, an adapter may return an array * of Strings representing letters of the alphabet. Or, it may return an * array of objects whose toString() methods return their section titles. * * @return the array of section objects */ @Override public Object[] getSections() { if (mFastScrollObjects == null) { mFastScrollObjects = getFastScrollDialogLabels(); } return mFastScrollObjects; } /** * This only affects SDK < 19. * Override this to control the amount of characters the fast scroll dialog can display. */ protected int getMaxIndexerLength() { return 3; } /** * @return The values which for the sections which will be shown in the fast scroll dialog. * As the only a max of three letters can fit in this dialog before KitKat, * the string value will be trimmed according to to length specified in getMaxIndexerLength(). */ private Object[] getFastScrollDialogLabels() { Collection<Object> sectionsCollection = mSections.values(); Object[] objects = sectionsCollection.toArray(new Object[sectionsCollection.size()]); if (VERSION.SDK_INT < VERSION_CODES.KITKAT) { int max = getMaxIndexerLength(); for (int i = 0; i < objects.length; i++) { if (objects[i].toString().length() >= max) { objects[i] = objects[i].toString().substring(0, max); } } } return objects; } }