/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.common.widget; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; import android.util.SparseIntArray; import android.view.View; import android.view.ViewGroup; import 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. */ public abstract class GroupingListAdapter extends BaseAdapter { private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16; private static final int GROUP_METADATA_ARRAY_INCREMENT = 128; private static final long GROUP_OFFSET_MASK = 0x00000000FFFFFFFFL; private static final long GROUP_SIZE_MASK = 0x7FFFFFFF00000000L; private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L; public static final int ITEM_TYPE_STANDALONE = 0; public static final int ITEM_TYPE_GROUP_HEADER = 1; public static final int ITEM_TYPE_IN_GROUP = 2; /** * Information about a specific list item: is it a group, if so is it expanded. * Otherwise, is it a stand-alone item or a group member. */ protected static class PositionMetadata { int itemType; boolean isExpanded; int cursorPosition; int childCount; private int groupPosition; private int listPosition = -1; } private Context mContext; private Cursor mCursor; /** * Count of list items. */ private int mCount; private int mRowIdColumnIndex; /** * Count of groups in the list. */ private int mGroupCount; /** * Information about where these groups are located in the list, how large they are * and whether they are expanded. */ private long[] mGroupMetadata; private SparseIntArray mPositionCache = new SparseIntArray(); private int mLastCachedListPosition; private int mLastCachedCursorPosition; private int mLastCachedGroup; /** * A reusable temporary instance of PositionMetadata */ private PositionMetadata mPositionMetadata = new PositionMetadata(); protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) { @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { onContentChanged(); } }; protected DataSetObserver mDataSetObserver = new DataSetObserver() { @Override public void onChanged() { notifyDataSetChanged(); } @Override public void onInvalidated() { notifyDataSetInvalidated(); } }; public GroupingListAdapter(Context context) { mContext = context; resetCache(); } /** * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for * each of them. */ protected abstract void addGroups(Cursor cursor); protected abstract View newStandAloneView(Context context, ViewGroup parent); protected abstract void bindStandAloneView(View view, Context context, Cursor cursor); protected abstract View newGroupView(Context context, ViewGroup parent); protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded); protected abstract View newChildView(Context context, ViewGroup parent); protected abstract void bindChildView(View view, Context context, Cursor cursor); /** * Cache should be reset whenever the cursor changes or groups are expanded or collapsed. */ private void resetCache() { mCount = -1; mLastCachedListPosition = -1; mLastCachedCursorPosition = -1; mLastCachedGroup = -1; mPositionMetadata.listPosition = -1; mPositionCache.clear(); } protected void onContentChanged() { } public void changeCursor(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(); } } public Cursor getCursor() { return mCursor; } /** * Scans over the entire cursor looking for duplicate phone numbers that need * to be collapsed. */ private void findGroups() { mGroupCount = 0; mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE]; if (mCursor == null) { return; } addGroups(mCursor); } /** * Records information about grouping in the list. Should be called by the overridden * {@link #addGroups} method. */ protected void addGroup(int cursorPosition, int size, boolean expanded) { 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; } // Copy/paste from ArrayUtils private int idealLongArraySize(int need) { return idealByteArraySize(need * 8) / 8; } // Copy/paste from ArrayUtils private int idealByteArraySize(int need) { for (int i = 4; i < 32; i++) if (need <= (1 << i) - 12) return (1 << i) - 12; return need; } public int getCount() { 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; } /** * 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. */ public void obtainPositionMetadata(PositionMetadata metadata, int 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); } /** * Returns true if the specified position in the list corresponds to a * group header. */ public boolean isGroupHeader(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER; } /** * Given a position of a groups header in the list, returns the size of * the corresponding group. */ public int getGroupSize(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.childCount; } /** * Mark group as expanded if it is collapsed and vice versa. */ public void toggleGroup(int position) { 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(); } @Override public int getViewTypeCount() { return 3; } @Override public int getItemViewType(int position) { obtainPositionMetadata(mPositionMetadata, position); return mPositionMetadata.itemType; } public Object getItem(int position) { if (mCursor == null) { return null; } obtainPositionMetadata(mPositionMetadata, position); if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) { return mCursor; } else { return null; } } public long getItemId(int position) { Object item = getItem(position); if (item != null) { return mCursor.getLong(mRowIdColumnIndex); } else { return -1; } } public View getView(int position, View convertView, 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; } }