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 GroupingListAdapter(Context context)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 */ addGroups(Cursor cursor)125 protected abstract void addGroups(Cursor cursor); 126 newStandAloneView(Context context, ViewGroup parent)127 protected abstract View newStandAloneView(Context context, ViewGroup parent); bindStandAloneView(View view, Context context, Cursor cursor)128 protected abstract void bindStandAloneView(View view, Context context, Cursor cursor); 129 newGroupView(Context context, ViewGroup parent)130 protected abstract View newGroupView(Context context, ViewGroup parent); bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)131 protected abstract void bindGroupView(View view, Context context, Cursor cursor, int groupSize, 132 boolean expanded); 133 newChildView(Context context, ViewGroup parent)134 protected abstract View newChildView(Context context, ViewGroup parent); bindChildView(View view, Context context, Cursor cursor)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 */ resetCache()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 onContentChanged()149 protected void onContentChanged() { 150 } 151 changeCursor(Cursor cursor)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 getCursor()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 */ findGroups()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 */ addGroup(int cursorPosition, int size, boolean expanded)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 idealLongArraySize(int need)218 private int idealLongArraySize(int need) { 219 return idealByteArraySize(need * 8) / 8; 220 } 221 222 // Copy/paste from ArrayUtils idealByteArraySize(int need)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 getCount()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 */ obtainPositionMetadata(PositionMetadata metadata, int position)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 */ isGroupHeader(int position)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 */ getGroupSize(int position)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 */ toggleGroup(int position)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 getViewTypeCount()422 public int getViewTypeCount() { 423 return 3; 424 } 425 426 @Override getItemViewType(int position)427 public int getItemViewType(int position) { 428 obtainPositionMetadata(mPositionMetadata, position); 429 return mPositionMetadata.itemType; 430 } 431 getItem(int position)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 getItemId(int position)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 getView(int position, View convertView, ViewGroup parent)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