Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.common.widget;
     18 
     19 import android.content.Context;
     20 import android.database.ContentObserver;
     21 import android.database.Cursor;
     22 import android.database.DataSetObserver;
     23 import android.os.Handler;
     24 import android.util.SparseIntArray;
     25 import android.view.View;
     26 import android.view.ViewGroup;
     27 import android.widget.BaseAdapter;
     28 
     29 /**
     30  * Maintains a list that groups adjacent items sharing the same value of
     31  * a "group-by" field.  The list has three types of elements: stand-alone, group header and group
     32  * child. Groups are collapsible and collapsed by default.
     33  */
     34 public abstract class GroupingListAdapter extends BaseAdapter {
     35 
     36     private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
     37     private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
     38     private static final long GROUP_OFFSET_MASK    = 0x00000000FFFFFFFFL;
     39     private static final long GROUP_SIZE_MASK     = 0x7FFFFFFF00000000L;
     40     private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
     41 
     42     public static final int ITEM_TYPE_STANDALONE = 0;
     43     public static final int ITEM_TYPE_GROUP_HEADER = 1;
     44     public static final int ITEM_TYPE_IN_GROUP = 2;
     45 
     46     /**
     47      * Information about a specific list item: is it a group, if so is it expanded.
     48      * Otherwise, is it a stand-alone item or a group member.
     49      */
     50     protected static class PositionMetadata {
     51         int itemType;
     52         boolean isExpanded;
     53         int cursorPosition;
     54         int childCount;
     55         private int groupPosition;
     56         private int listPosition = -1;
     57     }
     58 
     59     private Context mContext;
     60     private Cursor mCursor;
     61 
     62     /**
     63      * Count of list items.
     64      */
     65     private int mCount;
     66 
     67     private int mRowIdColumnIndex;
     68 
     69     /**
     70      * Count of groups in the list.
     71      */
     72     private int mGroupCount;
     73 
     74     /**
     75      * Information about where these groups are located in the list, how large they are
     76      * and whether they are expanded.
     77      */
     78     private long[] mGroupMetadata;
     79 
     80     private SparseIntArray mPositionCache = new SparseIntArray();
     81     private int mLastCachedListPosition;
     82     private int mLastCachedCursorPosition;
     83     private int mLastCachedGroup;
     84 
     85     /**
     86      * A reusable temporary instance of PositionMetadata
     87      */
     88     private PositionMetadata mPositionMetadata = new PositionMetadata();
     89 
     90     protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
     91 
     92         @Override
     93         public boolean deliverSelfNotifications() {
     94             return true;
     95         }
     96 
     97         @Override
     98         public void onChange(boolean selfChange) {
     99             onContentChanged();
    100         }
    101     };
    102 
    103     protected DataSetObserver mDataSetObserver = new DataSetObserver() {
    104 
    105         @Override
    106         public void onChanged() {
    107             notifyDataSetChanged();
    108         }
    109 
    110         @Override
    111         public void onInvalidated() {
    112             notifyDataSetInvalidated();
    113         }
    114     };
    115 
    116     public GroupingListAdapter(Context context) {
    117         mContext = context;
    118         resetCache();
    119     }
    120 
    121     /**
    122      * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for
    123      * each of them.
    124      */
    125     protected abstract void addGroups(Cursor cursor);
    126 
    127     protected abstract View newStandAloneView(Context context, ViewGroup parent);
    128     protected abstract void bindStandAloneView(View view, Context context, Cursor cursor);
    129 
    130     protected abstract View newGroupView(Context context, ViewGroup parent);
    131     protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    132             boolean expanded);
    133 
    134     protected abstract View newChildView(Context context, ViewGroup parent);
    135     protected abstract void bindChildView(View view, Context context, Cursor cursor);
    136 
    137     /**
    138      * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
    139      */
    140     private void resetCache() {
    141         mCount = -1;
    142         mLastCachedListPosition = -1;
    143         mLastCachedCursorPosition = -1;
    144         mLastCachedGroup = -1;
    145         mPositionMetadata.listPosition = -1;
    146         mPositionCache.clear();
    147     }
    148 
    149     protected void onContentChanged() {
    150     }
    151 
    152     public void changeCursor(Cursor cursor) {
    153         if (cursor == mCursor) {
    154             return;
    155         }
    156 
    157         if (mCursor != null) {
    158             mCursor.unregisterContentObserver(mChangeObserver);
    159             mCursor.unregisterDataSetObserver(mDataSetObserver);
    160             mCursor.close();
    161         }
    162         mCursor = cursor;
    163         resetCache();
    164         findGroups();
    165 
    166         if (cursor != null) {
    167             cursor.registerContentObserver(mChangeObserver);
    168             cursor.registerDataSetObserver(mDataSetObserver);
    169             mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
    170             notifyDataSetChanged();
    171         } else {
    172             // notify the observers about the lack of a data set
    173             notifyDataSetInvalidated();
    174         }
    175 
    176     }
    177 
    178     public Cursor getCursor() {
    179         return mCursor;
    180     }
    181 
    182     /**
    183      * Scans over the entire cursor looking for duplicate phone numbers that need
    184      * to be collapsed.
    185      */
    186     private void findGroups() {
    187         mGroupCount = 0;
    188         mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
    189 
    190         if (mCursor == null) {
    191             return;
    192         }
    193 
    194         addGroups(mCursor);
    195     }
    196 
    197     /**
    198      * Records information about grouping in the list.  Should be called by the overridden
    199      * {@link #addGroups} method.
    200      */
    201     protected void addGroup(int cursorPosition, int size, boolean expanded) {
    202         if (mGroupCount >= mGroupMetadata.length) {
    203             int newSize = idealLongArraySize(
    204                     mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
    205             long[] array = new long[newSize];
    206             System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
    207             mGroupMetadata = array;
    208         }
    209 
    210         long metadata = ((long)size << 32) | cursorPosition;
    211         if (expanded) {
    212             metadata |= EXPANDED_GROUP_MASK;
    213         }
    214         mGroupMetadata[mGroupCount++] = metadata;
    215     }
    216 
    217     // Copy/paste from ArrayUtils
    218     private int idealLongArraySize(int need) {
    219         return idealByteArraySize(need * 8) / 8;
    220     }
    221 
    222     // Copy/paste from ArrayUtils
    223     private int idealByteArraySize(int need) {
    224         for (int i = 4; i < 32; i++)
    225             if (need <= (1 << i) - 12)
    226                 return (1 << i) - 12;
    227 
    228         return need;
    229     }
    230 
    231     public int getCount() {
    232         if (mCursor == null) {
    233             return 0;
    234         }
    235 
    236         if (mCount != -1) {
    237             return mCount;
    238         }
    239 
    240         int cursorPosition = 0;
    241         int count = 0;
    242         for (int i = 0; i < mGroupCount; i++) {
    243             long metadata = mGroupMetadata[i];
    244             int offset = (int)(metadata & GROUP_OFFSET_MASK);
    245             boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
    246             int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
    247 
    248             count += (offset - cursorPosition);
    249 
    250             if (expanded) {
    251                 count += size + 1;
    252             } else {
    253                 count++;
    254             }
    255 
    256             cursorPosition = offset + size;
    257         }
    258 
    259         mCount = count + mCursor.getCount() - cursorPosition;
    260         return mCount;
    261     }
    262 
    263     /**
    264      * Figures out whether the item at the specified position represents a
    265      * stand-alone element, a group or a group child. Also computes the
    266      * corresponding cursor position.
    267      */
    268     public void obtainPositionMetadata(PositionMetadata metadata, int position) {
    269 
    270         // If the description object already contains requested information, just return
    271         if (metadata.listPosition == position) {
    272             return;
    273         }
    274 
    275         int listPosition = 0;
    276         int cursorPosition = 0;
    277         int firstGroupToCheck = 0;
    278 
    279         // Check cache for the supplied position.  What we are looking for is
    280         // the group descriptor immediately preceding the supplied position.
    281         // Once we have that, we will be able to tell whether the position
    282         // is the header of the group, a member of the group or a standalone item.
    283         if (mLastCachedListPosition != -1) {
    284             if (position <= mLastCachedListPosition) {
    285 
    286                 // Have SparceIntArray do a binary search for us.
    287                 int index = mPositionCache.indexOfKey(position);
    288 
    289                 // If we get back a positive number, the position corresponds to
    290                 // a group header.
    291                 if (index < 0) {
    292 
    293                     // We had a cache miss, but we did obtain valuable information anyway.
    294                     // The negative number will allow us to compute the location of
    295                     // the group header immediately preceding the supplied position.
    296                     index = ~index - 1;
    297 
    298                     if (index >= mPositionCache.size()) {
    299                         index--;
    300                     }
    301                 }
    302 
    303                 // A non-negative index gives us the position of the group header
    304                 // corresponding or preceding the position, so we can
    305                 // search for the group information at the supplied position
    306                 // starting with the cached group we just found
    307                 if (index >= 0) {
    308                     listPosition = mPositionCache.keyAt(index);
    309                     firstGroupToCheck = mPositionCache.valueAt(index);
    310                     long descriptor = mGroupMetadata[firstGroupToCheck];
    311                     cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
    312                 }
    313             } else {
    314 
    315                 // If we haven't examined groups beyond the supplied position,
    316                 // we will start where we left off previously
    317                 firstGroupToCheck = mLastCachedGroup;
    318                 listPosition = mLastCachedListPosition;
    319                 cursorPosition = mLastCachedCursorPosition;
    320             }
    321         }
    322 
    323         for (int i = firstGroupToCheck; i < mGroupCount; i++) {
    324             long group = mGroupMetadata[i];
    325             int offset = (int)(group & GROUP_OFFSET_MASK);
    326 
    327             // Move pointers to the beginning of the group
    328             listPosition += (offset - cursorPosition);
    329             cursorPosition = offset;
    330 
    331             if (i > mLastCachedGroup) {
    332                 mPositionCache.append(listPosition, i);
    333                 mLastCachedListPosition = listPosition;
    334                 mLastCachedCursorPosition = cursorPosition;
    335                 mLastCachedGroup = i;
    336             }
    337 
    338             // Now we have several possibilities:
    339             // A) The requested position precedes the group
    340             if (position < listPosition) {
    341                 metadata.itemType = ITEM_TYPE_STANDALONE;
    342                 metadata.cursorPosition = cursorPosition - (listPosition - position);
    343                 return;
    344             }
    345 
    346             boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
    347             int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
    348 
    349             // B) The requested position is a group header
    350             if (position == listPosition) {
    351                 metadata.itemType = ITEM_TYPE_GROUP_HEADER;
    352                 metadata.groupPosition = i;
    353                 metadata.isExpanded = expanded;
    354                 metadata.childCount = size;
    355                 metadata.cursorPosition = offset;
    356                 return;
    357             }
    358 
    359             if (expanded) {
    360                 // C) The requested position is an element in the expanded group
    361                 if (position < listPosition + size + 1) {
    362                     metadata.itemType = ITEM_TYPE_IN_GROUP;
    363                     metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
    364                     return;
    365                 }
    366 
    367                 // D) The element is past the expanded group
    368                 listPosition += size + 1;
    369             } else {
    370 
    371                 // E) The element is past the collapsed group
    372                 listPosition++;
    373             }
    374 
    375             // Move cursor past the group
    376             cursorPosition += size;
    377         }
    378 
    379         // The required item is past the last group
    380         metadata.itemType = ITEM_TYPE_STANDALONE;
    381         metadata.cursorPosition = cursorPosition + (position - listPosition);
    382     }
    383 
    384     /**
    385      * Returns true if the specified position in the list corresponds to a
    386      * group header.
    387      */
    388     public boolean isGroupHeader(int position) {
    389         obtainPositionMetadata(mPositionMetadata, position);
    390         return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
    391     }
    392 
    393     /**
    394      * Given a position of a groups header in the list, returns the size of
    395      * the corresponding group.
    396      */
    397     public int getGroupSize(int position) {
    398         obtainPositionMetadata(mPositionMetadata, position);
    399         return mPositionMetadata.childCount;
    400     }
    401 
    402     /**
    403      * Mark group as expanded if it is collapsed and vice versa.
    404      */
    405     public void toggleGroup(int position) {
    406         obtainPositionMetadata(mPositionMetadata, position);
    407         if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
    408             throw new IllegalArgumentException("Not a group at position " + position);
    409         }
    410 
    411 
    412         if (mPositionMetadata.isExpanded) {
    413             mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
    414         } else {
    415             mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
    416         }
    417         resetCache();
    418         notifyDataSetChanged();
    419     }
    420 
    421     @Override
    422     public int getViewTypeCount() {
    423         return 3;
    424     }
    425 
    426     @Override
    427     public int getItemViewType(int position) {
    428         obtainPositionMetadata(mPositionMetadata, position);
    429         return mPositionMetadata.itemType;
    430     }
    431 
    432     public Object getItem(int position) {
    433         if (mCursor == null) {
    434             return null;
    435         }
    436 
    437         obtainPositionMetadata(mPositionMetadata, position);
    438         if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
    439             return mCursor;
    440         } else {
    441             return null;
    442         }
    443     }
    444 
    445     public long getItemId(int position) {
    446         Object item = getItem(position);
    447         if (item != null) {
    448             return mCursor.getLong(mRowIdColumnIndex);
    449         } else {
    450             return -1;
    451         }
    452     }
    453 
    454     public View getView(int position, View convertView, ViewGroup parent) {
    455         obtainPositionMetadata(mPositionMetadata, position);
    456         View view = convertView;
    457         if (view == null) {
    458             switch (mPositionMetadata.itemType) {
    459                 case ITEM_TYPE_STANDALONE:
    460                     view = newStandAloneView(mContext, parent);
    461                     break;
    462                 case ITEM_TYPE_GROUP_HEADER:
    463                     view = newGroupView(mContext, parent);
    464                     break;
    465                 case ITEM_TYPE_IN_GROUP:
    466                     view = newChildView(mContext, parent);
    467                     break;
    468             }
    469         }
    470 
    471         mCursor.moveToPosition(mPositionMetadata.cursorPosition);
    472         switch (mPositionMetadata.itemType) {
    473             case ITEM_TYPE_STANDALONE:
    474                 bindStandAloneView(view, mContext, mCursor);
    475                 break;
    476             case ITEM_TYPE_GROUP_HEADER:
    477                 bindGroupView(view, mContext, mCursor, mPositionMetadata.childCount,
    478                         mPositionMetadata.isExpanded);
    479                 break;
    480             case ITEM_TYPE_IN_GROUP:
    481                 bindChildView(view, mContext, mCursor);
    482                 break;
    483 
    484         }
    485         return view;
    486     }
    487 }
    488