CompositeCursorAdapter.java revision 60ae52719e0567fa5b1860df19807716951eca50
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 */ 16package com.android.common.widget; 17 18import android.content.Context; 19import android.database.Cursor; 20import android.view.View; 21import android.view.ViewGroup; 22import android.widget.BaseAdapter; 23 24/** 25 * A general purpose adapter that is composed of multiple cursors. It just 26 * appends them in the order they are added. 27 */ 28public abstract class CompositeCursorAdapter extends BaseAdapter { 29 30 private static final int INITIAL_CAPACITY = 2; 31 32 public static class Partition { 33 boolean showIfEmpty; 34 boolean hasHeader; 35 36 Cursor cursor; 37 int idColumnIndex; 38 int count; 39 40 public Partition(boolean showIfEmpty, boolean hasHeader) { 41 this.showIfEmpty = showIfEmpty; 42 this.hasHeader = hasHeader; 43 } 44 45 /** 46 * True if the directory should be shown even if no contacts are found. 47 */ 48 public boolean getShowIfEmpty() { 49 return showIfEmpty; 50 } 51 52 public boolean getHasHeader() { 53 return hasHeader; 54 } 55 } 56 57 private final Context mContext; 58 private Partition[] mPartitions; 59 private int mSize = 0; 60 private int mCount = 0; 61 private boolean mCacheValid = true; 62 63 public CompositeCursorAdapter(Context context) { 64 this(context, INITIAL_CAPACITY); 65 } 66 67 public CompositeCursorAdapter(Context context, int initialCapacity) { 68 mContext = context; 69 mPartitions = new Partition[INITIAL_CAPACITY]; 70 } 71 72 public Context getContext() { 73 return mContext; 74 } 75 76 /** 77 * Registers a partition. The cursor for that partition can be set later. 78 * Partitions should be added in the order they are supposed to appear in the 79 * list. 80 */ 81 public void addPartition(boolean showIfEmpty, boolean hasHeader) { 82 addPartition(new Partition(showIfEmpty, hasHeader)); 83 } 84 85 public void addPartition(Partition partition) { 86 if (mSize >= mPartitions.length) { 87 int newCapacity = mSize + 2; 88 Partition[] newAdapters = new Partition[newCapacity]; 89 System.arraycopy(mPartitions, 0, newAdapters, 0, mSize); 90 mPartitions = newAdapters; 91 } 92 mPartitions[mSize++] = partition; 93 invalidate(); 94 notifyDataSetChanged(); 95 } 96 97 public void removePartition(int partitionIndex) { 98 Cursor cursor = mPartitions[partitionIndex].cursor; 99 if (cursor != null && !cursor.isClosed()) { 100 cursor.close(); 101 } 102 103 System.arraycopy(mPartitions, partitionIndex + 1, mPartitions, partitionIndex, 104 mSize - partitionIndex - 1); 105 mSize--; 106 invalidate(); 107 notifyDataSetChanged(); 108 } 109 110 /** 111 * Removes cursors for all partitions. 112 */ 113 public void clearPartitions() { 114 for (int i = 0; i < mSize; i++) { 115 mPartitions[i].cursor = null; 116 } 117 invalidate(); 118 notifyDataSetChanged(); 119 } 120 121 public void setHasHeader(int partitionIndex, boolean flag) { 122 mPartitions[partitionIndex].hasHeader = flag; 123 invalidate(); 124 } 125 126 public void setShowIfEmpty(int partitionIndex, boolean flag) { 127 mPartitions[partitionIndex].showIfEmpty = flag; 128 invalidate(); 129 } 130 131 public Partition getPartition(int partitionIndex) { 132 if (partitionIndex >= mSize) { 133 throw new ArrayIndexOutOfBoundsException(partitionIndex); 134 } 135 return mPartitions[partitionIndex]; 136 } 137 138 protected void invalidate() { 139 mCacheValid = false; 140 } 141 142 public int getPartitionCount() { 143 return mSize; 144 } 145 146 protected void ensureCacheValid() { 147 if (mCacheValid) { 148 return; 149 } 150 151 mCount = 0; 152 for (int i = 0; i < mSize; i++) { 153 Cursor cursor = mPartitions[i].cursor; 154 int count = cursor != null ? cursor.getCount() : 0; 155 if (mPartitions[i].hasHeader) { 156 if (count != 0 || mPartitions[i].showIfEmpty) { 157 count++; 158 } 159 } 160 mPartitions[i].count = count; 161 mCount += count; 162 } 163 164 mCacheValid = true; 165 } 166 167 /** 168 * Returns true if the specified partition was configured to have a header. 169 */ 170 public boolean hasHeader(int partition) { 171 return mPartitions[partition].hasHeader; 172 } 173 174 /** 175 * Returns the total number of list items in all partitions. 176 */ 177 public int getCount() { 178 ensureCacheValid(); 179 return mCount; 180 } 181 182 /** 183 * Returns the cursor for the given partition 184 */ 185 public Cursor getCursor(int partition) { 186 return mPartitions[partition].cursor; 187 } 188 189 /** 190 * Changes the cursor for an individual partition. 191 */ 192 public void changeCursor(int partition, Cursor cursor) { 193 Cursor prevCursor = mPartitions[partition].cursor; 194 if (prevCursor != cursor) { 195 if (prevCursor != null && !prevCursor.isClosed()) { 196 prevCursor.close(); 197 } 198 mPartitions[partition].cursor = cursor; 199 if (cursor != null) { 200 mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id"); 201 } 202 invalidate(); 203 notifyDataSetChanged(); 204 } 205 } 206 207 /** 208 * Returns true if the specified partition has no cursor or an empty cursor. 209 */ 210 public boolean isPartitionEmpty(int partition) { 211 Cursor cursor = mPartitions[partition].cursor; 212 return cursor == null || cursor.getCount() == 0; 213 } 214 215 /** 216 * Given a list position, returns the index of the corresponding partition. 217 */ 218 public int getPartitionForPosition(int position) { 219 ensureCacheValid(); 220 int start = 0; 221 for (int i = 0; i < mSize; i++) { 222 int end = start + mPartitions[i].count; 223 if (position >= start && position < end) { 224 return i; 225 } 226 start = end; 227 } 228 return -1; 229 } 230 231 /** 232 * Given a list position, return the offset of the corresponding item in its 233 * partition. The header, if any, will have offset -1. 234 */ 235 public int getOffsetInPartition(int position) { 236 ensureCacheValid(); 237 int start = 0; 238 for (int i = 0; i < mSize; i++) { 239 int end = start + mPartitions[i].count; 240 if (position >= start && position < end) { 241 int offset = position - start; 242 if (mPartitions[i].hasHeader) { 243 offset--; 244 } 245 return offset; 246 } 247 start = end; 248 } 249 return -1; 250 } 251 252 /** 253 * Returns the first list position for the specified partition. 254 */ 255 public int getPositionForPartition(int partition) { 256 ensureCacheValid(); 257 int position = 0; 258 for (int i = 0; i < partition; i++) { 259 position += mPartitions[i].count; 260 } 261 return position; 262 } 263 264 @Override 265 public int getViewTypeCount() { 266 return getItemViewTypeCount() + 1; 267 } 268 269 /** 270 * Returns the overall number of item view types across all partitions. An 271 * implementation of this method needs to ensure that the returned count is 272 * consistent with the values returned by {@link #getItemViewType(int,int)}. 273 */ 274 public int getItemViewTypeCount() { 275 return 1; 276 } 277 278 /** 279 * Returns the view type for the list item at the specified position in the 280 * specified partition. 281 */ 282 protected int getItemViewType(int partition, int position) { 283 return 1; 284 } 285 286 @Override 287 public int getItemViewType(int position) { 288 ensureCacheValid(); 289 int start = 0; 290 for (int i = 0; i < mSize; i++) { 291 int end = start + mPartitions[i].count; 292 if (position >= start && position < end) { 293 int offset = position - start; 294 if (mPartitions[i].hasHeader && offset == 0) { 295 return IGNORE_ITEM_VIEW_TYPE; 296 } 297 return getItemViewType(i, position); 298 } 299 start = end; 300 } 301 302 throw new ArrayIndexOutOfBoundsException(position); 303 } 304 305 public View getView(int position, View convertView, ViewGroup parent) { 306 ensureCacheValid(); 307 int start = 0; 308 for (int i = 0; i < mSize; i++) { 309 int end = start + mPartitions[i].count; 310 if (position >= start && position < end) { 311 int offset = position - start; 312 if (mPartitions[i].hasHeader) { 313 offset--; 314 } 315 View view; 316 if (offset == -1) { 317 view = getHeaderView(i, mPartitions[i].cursor, convertView, parent); 318 } else { 319 if (!mPartitions[i].cursor.moveToPosition(offset)) { 320 throw new IllegalStateException("Couldn't move cursor to position " 321 + offset); 322 } 323 view = getView(i, mPartitions[i].cursor, offset, convertView, parent); 324 } 325 if (view == null) { 326 throw new NullPointerException("View should not be null, partition: " + i 327 + " position: " + offset); 328 } 329 return view; 330 } 331 start = end; 332 } 333 334 throw new ArrayIndexOutOfBoundsException(position); 335 } 336 337 /** 338 * Returns the header view for the specified partition, creating one if needed. 339 */ 340 protected View getHeaderView(int partition, Cursor cursor, View convertView, 341 ViewGroup parent) { 342 View view = convertView != null 343 ? convertView 344 : newHeaderView(mContext, partition, cursor, parent); 345 bindHeaderView(view, partition, cursor); 346 return view; 347 } 348 349 /** 350 * Creates the header view for the specified partition. 351 */ 352 protected View newHeaderView(Context context, int partition, Cursor cursor, 353 ViewGroup parent) { 354 return null; 355 } 356 357 /** 358 * Binds the header view for the specified partition. 359 */ 360 protected void bindHeaderView(View view, int partition, Cursor cursor) { 361 } 362 363 /** 364 * Returns an item view for the specified partition, creating one if needed. 365 */ 366 protected View getView(int partition, Cursor cursor, int position, View convertView, 367 ViewGroup parent) { 368 View view; 369 if (convertView != null) { 370 view = convertView; 371 } else { 372 view = newView(mContext, partition, cursor, position, parent); 373 } 374 bindView(view, partition, cursor, position); 375 return view; 376 } 377 378 /** 379 * Creates an item view for the specified partition and position. Position 380 * corresponds directly to the current cursor position. 381 */ 382 protected abstract View newView(Context context, int partition, Cursor cursor, int position, 383 ViewGroup parent); 384 385 /** 386 * Binds an item view for the specified partition and position. Position 387 * corresponds directly to the current cursor position. 388 */ 389 protected abstract void bindView(View v, int partition, Cursor cursor, int position); 390 391 /** 392 * Returns a pre-positioned cursor for the specified list position. 393 */ 394 public Object getItem(int position) { 395 ensureCacheValid(); 396 int start = 0; 397 for (int i = 0; i < mSize; i++) { 398 int end = start + mPartitions[i].count; 399 if (position >= start && position < end) { 400 int offset = position - start; 401 if (mPartitions[i].hasHeader) { 402 offset--; 403 } 404 if (offset == -1) { 405 return null; 406 } 407 Cursor cursor = mPartitions[i].cursor; 408 cursor.moveToPosition(offset); 409 return cursor; 410 } 411 start = end; 412 } 413 414 return null; 415 } 416 417 /** 418 * Returns the item ID for the specified list position. 419 */ 420 public long getItemId(int position) { 421 ensureCacheValid(); 422 int start = 0; 423 for (int i = 0; i < mSize; i++) { 424 int end = start + mPartitions[i].count; 425 if (position >= start && position < end) { 426 int offset = position - start; 427 if (mPartitions[i].hasHeader) { 428 offset--; 429 } 430 if (offset == -1) { 431 return 0; 432 } 433 if (mPartitions[i].idColumnIndex == -1) { 434 return 0; 435 } 436 437 Cursor cursor = mPartitions[i].cursor; 438 if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { 439 return 0; 440 } 441 return cursor.getLong(mPartitions[i].idColumnIndex); 442 } 443 start = end; 444 } 445 446 return 0; 447 } 448 449 /** 450 * Returns false if any partition has a header. 451 */ 452 @Override 453 public boolean areAllItemsEnabled() { 454 for (int i = 0; i < mSize; i++) { 455 if (mPartitions[i].hasHeader) { 456 return false; 457 } 458 } 459 return true; 460 } 461 462 /** 463 * Returns true for all items except headers. 464 */ 465 @Override 466 public boolean isEnabled(int position) { 467 ensureCacheValid(); 468 int start = 0; 469 for (int i = 0; i < mSize; i++) { 470 int end = start + mPartitions[i].count; 471 if (position >= start && position < end) { 472 int offset = position - start; 473 if (mPartitions[i].hasHeader && offset == 0) { 474 return false; 475 } else { 476 return isEnabled(i, offset); 477 } 478 } 479 start = end; 480 } 481 482 return false; 483 } 484 485 /** 486 * Returns true if the item at the specified offset of the specified 487 * partition is selectable and clickable. 488 */ 489 protected boolean isEnabled(int partition, int position) { 490 return true; 491 } 492} 493