| /* |
| * 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; |
| } |
| } |