FileDocCategorySizeDatePackage
GroupingListAdapter.javaAPI DocAndroid 5.1 API16174Thu Mar 12 22:22:48 GMT 2015com.android.common.widget

GroupingListAdapter

public abstract class GroupingListAdapter extends android.widget.BaseAdapter
Maintains a list that groups adjacent items sharing the same value of a "group-by" field. The list has three types of elements: stand-alone, group header and group child. Groups are collapsible and collapsed by default.

Fields Summary
private static final int
GROUP_METADATA_ARRAY_INITIAL_SIZE
private static final int
GROUP_METADATA_ARRAY_INCREMENT
private static final long
GROUP_OFFSET_MASK
private static final long
GROUP_SIZE_MASK
private static final long
EXPANDED_GROUP_MASK
public static final int
ITEM_TYPE_STANDALONE
public static final int
ITEM_TYPE_GROUP_HEADER
public static final int
ITEM_TYPE_IN_GROUP
private android.content.Context
mContext
private android.database.Cursor
mCursor
private int
mCount
Count of list items.
private int
mRowIdColumnIndex
private int
mGroupCount
Count of groups in the list.
private long[]
mGroupMetadata
Information about where these groups are located in the list, how large they are and whether they are expanded.
private android.util.SparseIntArray
mPositionCache
private int
mLastCachedListPosition
private int
mLastCachedCursorPosition
private int
mLastCachedGroup
private PositionMetadata
mPositionMetadata
A reusable temporary instance of PositionMetadata
protected android.database.ContentObserver
mChangeObserver
protected android.database.DataSetObserver
mDataSetObserver
Constructors Summary
public GroupingListAdapter(android.content.Context context)


       
        mContext = context;
        resetCache();
    
Methods Summary
protected voidaddGroup(int cursorPosition, int size, boolean expanded)
Records information about grouping in the list. Should be called by the overridden {@link #addGroups} method.

        if (mGroupCount >= mGroupMetadata.length) {
            int newSize = idealLongArraySize(
                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
            long[] array = new long[newSize];
            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
            mGroupMetadata = array;
        }

        long metadata = ((long)size << 32) | cursorPosition;
        if (expanded) {
            metadata |= EXPANDED_GROUP_MASK;
        }
        mGroupMetadata[mGroupCount++] = metadata;
    
protected abstract voidaddGroups(android.database.Cursor cursor)
Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them.

protected abstract voidbindChildView(android.view.View view, android.content.Context context, android.database.Cursor cursor)

protected abstract voidbindGroupView(android.view.View view, android.content.Context context, android.database.Cursor cursor, int groupSize, boolean expanded)

protected abstract voidbindStandAloneView(android.view.View view, android.content.Context context, android.database.Cursor cursor)

public voidchangeCursor(android.database.Cursor cursor)

        if (cursor == mCursor) {
            return;
        }

        if (mCursor != null) {
            mCursor.unregisterContentObserver(mChangeObserver);
            mCursor.unregisterDataSetObserver(mDataSetObserver);
            mCursor.close();
        }
        mCursor = cursor;
        resetCache();
        findGroups();

        if (cursor != null) {
            cursor.registerContentObserver(mChangeObserver);
            cursor.registerDataSetObserver(mDataSetObserver);
            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
            notifyDataSetChanged();
        } else {
            // notify the observers about the lack of a data set
            notifyDataSetInvalidated();
        }

    
private voidfindGroups()
Scans over the entire cursor looking for duplicate phone numbers that need to be collapsed.

        mGroupCount = 0;
        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];

        if (mCursor == null) {
            return;
        }

        addGroups(mCursor);
    
public intgetCount()

        if (mCursor == null) {
            return 0;
        }

        if (mCount != -1) {
            return mCount;
        }

        int cursorPosition = 0;
        int count = 0;
        for (int i = 0; i < mGroupCount; i++) {
            long metadata = mGroupMetadata[i];
            int offset = (int)(metadata & GROUP_OFFSET_MASK);
            boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
            int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);

            count += (offset - cursorPosition);

            if (expanded) {
                count += size + 1;
            } else {
                count++;
            }

            cursorPosition = offset + size;
        }

        mCount = count + mCursor.getCount() - cursorPosition;
        return mCount;
    
public android.database.CursorgetCursor()

        return mCursor;
    
public intgetGroupSize(int position)
Given a position of a groups header in the list, returns the size of the corresponding group.

        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.childCount;
    
public java.lang.ObjectgetItem(int position)

        if (mCursor == null) {
            return null;
        }

        obtainPositionMetadata(mPositionMetadata, position);
        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
            return mCursor;
        } else {
            return null;
        }
    
public longgetItemId(int position)

        Object item = getItem(position);
        if (item != null) {
            return mCursor.getLong(mRowIdColumnIndex);
        } else {
            return -1;
        }
    
public intgetItemViewType(int position)

        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.itemType;
    
public android.view.ViewgetView(int position, android.view.View convertView, android.view.ViewGroup parent)

        obtainPositionMetadata(mPositionMetadata, position);
        View view = convertView;
        if (view == null) {
            switch (mPositionMetadata.itemType) {
                case ITEM_TYPE_STANDALONE:
                    view = newStandAloneView(mContext, parent);
                    break;
                case ITEM_TYPE_GROUP_HEADER:
                    view = newGroupView(mContext, parent);
                    break;
                case ITEM_TYPE_IN_GROUP:
                    view = newChildView(mContext, parent);
                    break;
            }
        }

        mCursor.moveToPosition(mPositionMetadata.cursorPosition);
        switch (mPositionMetadata.itemType) {
            case ITEM_TYPE_STANDALONE:
                bindStandAloneView(view, mContext, mCursor);
                break;
            case ITEM_TYPE_GROUP_HEADER:
                bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount,
                        mPositionMetadata.isExpanded);
                break;
            case ITEM_TYPE_IN_GROUP:
                bindChildView(view, mContext, mCursor);
                break;

        }
        return view;
    
public intgetViewTypeCount()

        return 3;
    
private intidealByteArraySize(int need)

        for (int i = 4; i < 32; i++)
            if (need <= (1 << i) - 12)
                return (1 << i) - 12;

        return need;
    
private intidealLongArraySize(int need)

        return idealByteArraySize(need * 8) / 8;
    
public booleanisGroupHeader(int position)
Returns true if the specified position in the list corresponds to a group header.

        obtainPositionMetadata(mPositionMetadata, position);
        return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
    
protected abstract android.view.ViewnewChildView(android.content.Context context, android.view.ViewGroup parent)

protected abstract android.view.ViewnewGroupView(android.content.Context context, android.view.ViewGroup parent)

protected abstract android.view.ViewnewStandAloneView(android.content.Context context, android.view.ViewGroup parent)

public voidobtainPositionMetadata(com.android.common.widget.GroupingListAdapter$PositionMetadata metadata, int position)
Figures out whether the item at the specified position represents a stand-alone element, a group or a group child. Also computes the corresponding cursor position.


        // If the description object already contains requested information, just return
        if (metadata.listPosition == position) {
            return;
        }

        int listPosition = 0;
        int cursorPosition = 0;
        int firstGroupToCheck = 0;

        // Check cache for the supplied position.  What we are looking for is
        // the group descriptor immediately preceding the supplied position.
        // Once we have that, we will be able to tell whether the position
        // is the header of the group, a member of the group or a standalone item.
        if (mLastCachedListPosition != -1) {
            if (position <= mLastCachedListPosition) {

                // Have SparceIntArray do a binary search for us.
                int index = mPositionCache.indexOfKey(position);

                // If we get back a positive number, the position corresponds to
                // a group header.
                if (index < 0) {

                    // We had a cache miss, but we did obtain valuable information anyway.
                    // The negative number will allow us to compute the location of
                    // the group header immediately preceding the supplied position.
                    index = ~index - 1;

                    if (index >= mPositionCache.size()) {
                        index--;
                    }
                }

                // A non-negative index gives us the position of the group header
                // corresponding or preceding the position, so we can
                // search for the group information at the supplied position
                // starting with the cached group we just found
                if (index >= 0) {
                    listPosition = mPositionCache.keyAt(index);
                    firstGroupToCheck = mPositionCache.valueAt(index);
                    long descriptor = mGroupMetadata[firstGroupToCheck];
                    cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
                }
            } else {

                // If we haven't examined groups beyond the supplied position,
                // we will start where we left off previously
                firstGroupToCheck = mLastCachedGroup;
                listPosition = mLastCachedListPosition;
                cursorPosition = mLastCachedCursorPosition;
            }
        }

        for (int i = firstGroupToCheck; i < mGroupCount; i++) {
            long group = mGroupMetadata[i];
            int offset = (int)(group & GROUP_OFFSET_MASK);

            // Move pointers to the beginning of the group
            listPosition += (offset - cursorPosition);
            cursorPosition = offset;

            if (i > mLastCachedGroup) {
                mPositionCache.append(listPosition, i);
                mLastCachedListPosition = listPosition;
                mLastCachedCursorPosition = cursorPosition;
                mLastCachedGroup = i;
            }

            // Now we have several possibilities:
            // A) The requested position precedes the group
            if (position < listPosition) {
                metadata.itemType = ITEM_TYPE_STANDALONE;
                metadata.cursorPosition = cursorPosition - (listPosition - position);
                return;
            }

            boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
            int size = (int) ((group & GROUP_SIZE_MASK) >> 32);

            // B) The requested position is a group header
            if (position == listPosition) {
                metadata.itemType = ITEM_TYPE_GROUP_HEADER;
                metadata.groupPosition = i;
                metadata.isExpanded = expanded;
                metadata.childCount = size;
                metadata.cursorPosition = offset;
                return;
            }

            if (expanded) {
                // C) The requested position is an element in the expanded group
                if (position < listPosition + size + 1) {
                    metadata.itemType = ITEM_TYPE_IN_GROUP;
                    metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
                    return;
                }

                // D) The element is past the expanded group
                listPosition += size + 1;
            } else {

                // E) The element is past the collapsed group
                listPosition++;
            }

            // Move cursor past the group
            cursorPosition += size;
        }

        // The required item is past the last group
        metadata.itemType = ITEM_TYPE_STANDALONE;
        metadata.cursorPosition = cursorPosition + (position - listPosition);
    
protected voidonContentChanged()

    
private voidresetCache()
Cache should be reset whenever the cursor changes or groups are expanded or collapsed.

        mCount = -1;
        mLastCachedListPosition = -1;
        mLastCachedCursorPosition = -1;
        mLastCachedGroup = -1;
        mPositionMetadata.listPosition = -1;
        mPositionCache.clear();
    
public voidtoggleGroup(int position)
Mark group as expanded if it is collapsed and vice versa.

        obtainPositionMetadata(mPositionMetadata, position);
        if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
            throw new IllegalArgumentException("Not a group at position " + position);
        }


        if (mPositionMetadata.isExpanded) {
            mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
        } else {
            mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
        }
        resetCache();
        notifyDataSetChanged();