summaryrefslogtreecommitdiffstats
path: root/common/java/com
diff options
context:
space:
mode:
authorDmitri Plotnikov <dplotnikov@google.com>2010-10-06 17:08:22 -0700
committerDmitri Plotnikov <dplotnikov@google.com>2010-10-06 17:08:22 -0700
commit7b8b23f9c04fa33b79a8949752a90a134a11df8a (patch)
tree2b92db63c6fb3e11bb06b214a6584b8dac079acc /common/java/com
parent9f0c6badc3e8f928c6f2658a3c85ea50319424d4 (diff)
downloadandroid_frameworks_ex-7b8b23f9c04fa33b79a8949752a90a134a11df8a.tar.gz
android_frameworks_ex-7b8b23f9c04fa33b79a8949752a90a134a11df8a.tar.bz2
android_frameworks_ex-7b8b23f9c04fa33b79a8949752a90a134a11df8a.zip
Moving GroupingListAdapter to frameworks/ex
Moving from packages/apps/Contacts adb shell am instrument -e class com.android.common.widget.GroupingListAdapterTests -w com.android.common.tests/android.test.InstrumentationTestRunner com.android.common.widget.GroupingListAdapterTests:............. Test results for InstrumentationTestRunner=............. Time: 0.03 OK (13 tests) Change-Id: Ia58fe362afdc5c64e3740c89049be29f5b329a5b
Diffstat (limited to 'common/java/com')
-rw-r--r--common/java/com/android/common/widget/GroupingListAdapter.java487
1 files changed, 487 insertions, 0 deletions
diff --git a/common/java/com/android/common/widget/GroupingListAdapter.java b/common/java/com/android/common/widget/GroupingListAdapter.java
new file mode 100644
index 0000000..e346d1d
--- /dev/null
+++ b/common/java/com/android/common/widget/GroupingListAdapter.java
@@ -0,0 +1,487 @@
+/*
+ * 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;
+ }
+}