1 /* 2 * Copyright 2018 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 androidx.recyclerview.widget; 18 19 import android.annotation.SuppressLint; 20 21 import org.jspecify.annotations.NonNull; 22 import org.jspecify.annotations.Nullable; 23 24 import java.lang.reflect.Array; 25 import java.util.Arrays; 26 import java.util.Collection; 27 import java.util.Comparator; 28 29 /** 30 * A Sorted list implementation that can keep items in order and also notify for changes in the 31 * list 32 * such that it can be bound to a {@link RecyclerView.Adapter 33 * RecyclerView.Adapter}. 34 * <p> 35 * It keeps items ordered using the {@link Callback#compare(Object, Object)} method and uses 36 * binary search to retrieve items. If the sorting criteria of your items may change, make sure you 37 * call appropriate methods while editing them to avoid data inconsistencies. 38 * <p> 39 * You can control the order of items and change notifications via the {@link Callback} parameter. 40 */ 41 @SuppressWarnings("unchecked") 42 public class SortedList<T> { 43 44 /** 45 * Used by {@link #indexOf(Object)} when the item cannot be found in the list. 46 */ 47 public static final int INVALID_POSITION = -1; 48 49 private static final int MIN_CAPACITY = 10; 50 private static final int CAPACITY_GROWTH = MIN_CAPACITY; 51 private static final int INSERTION = 1; 52 private static final int DELETION = 1 << 1; 53 private static final int LOOKUP = 1 << 2; 54 T[] mData; 55 56 /** 57 * A reference to the previous set of data that is kept during a mutation operation (addAll or 58 * replaceAll). 59 */ 60 private T[] mOldData; 61 62 /** 63 * The current index into mOldData that has not yet been processed during a mutation operation 64 * (addAll or replaceAll). 65 */ 66 private int mOldDataStart; 67 private int mOldDataSize; 68 69 /** 70 * The current index into the new data that has not yet been processed during a mutation 71 * operation (addAll or replaceAll). 72 */ 73 private int mNewDataStart; 74 75 /** 76 * The callback instance that controls the behavior of the SortedList and get notified when 77 * changes happen. 78 */ 79 private Callback mCallback; 80 81 private BatchedCallback mBatchedCallback; 82 83 private int mSize; 84 private final Class<T> mTClass; 85 86 /** 87 * Creates a new SortedList of type T. 88 * 89 * @param klass The class of the contents of the SortedList. 90 * @param callback The callback that controls the behavior of SortedList. 91 */ SortedList(@onNull Class<T> klass, @NonNull Callback<T> callback)92 public SortedList(@NonNull Class<T> klass, @NonNull Callback<T> callback) { 93 this(klass, callback, MIN_CAPACITY); 94 } 95 96 /** 97 * Creates a new SortedList of type T. 98 * 99 * @param klass The class of the contents of the SortedList. 100 * @param callback The callback that controls the behavior of SortedList. 101 * @param initialCapacity The initial capacity to hold items. 102 */ SortedList(@onNull Class<T> klass, @NonNull Callback<T> callback, int initialCapacity)103 public SortedList(@NonNull Class<T> klass, @NonNull Callback<T> callback, int initialCapacity) { 104 mTClass = klass; 105 mData = (T[]) Array.newInstance(klass, initialCapacity); 106 mCallback = callback; 107 mSize = 0; 108 } 109 110 /** 111 * The number of items in the list. 112 * 113 * @return The number of items in the list. 114 */ size()115 public int size() { 116 return mSize; 117 } 118 119 /** 120 * Adds the given item to the list. If this is a new item, SortedList calls 121 * {@link Callback#onInserted(int, int)}. 122 * <p> 123 * If the item already exists in the list and its sorting criteria is not changed, it is 124 * replaced with the existing Item. SortedList uses 125 * {@link Callback#areItemsTheSame(Object, Object)} to check if two items are the same item 126 * and uses {@link Callback#areContentsTheSame(Object, Object)} to decide whether it should 127 * call {@link Callback#onChanged(int, int)} or not. In both cases, it always removes the 128 * reference to the old item and puts the new item into the backing array even if 129 * {@link Callback#areContentsTheSame(Object, Object)} returns false. 130 * <p> 131 * If the sorting criteria of the item is changed, SortedList won't be able to find 132 * its duplicate in the list which will result in having a duplicate of the Item in the list. 133 * If you need to update sorting criteria of an item that already exists in the list, 134 * use {@link #updateItemAt(int, Object)}. You can find the index of the item using 135 * {@link #indexOf(Object)} before you update the object. 136 * 137 * @param item The item to be added into the list. 138 * 139 * @return The index of the newly added item. 140 * @see Callback#compare(Object, Object) 141 * @see Callback#areItemsTheSame(Object, Object) 142 * @see Callback#areContentsTheSame(Object, Object)} 143 */ add(T item)144 public int add(T item) { 145 throwIfInMutationOperation(); 146 return add(item, true); 147 } 148 149 /** 150 * Adds the given items to the list. Equivalent to calling {@link SortedList#add} in a loop, 151 * except the callback events may be in a different order/granularity since addAll can batch 152 * them for better performance. 153 * <p> 154 * If allowed, will reference the input array during, and possibly after, the operation to avoid 155 * extra memory allocation, in which case you should not continue to reference or modify the 156 * array yourself. 157 * <p> 158 * @param items Array of items to be added into the list. 159 * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the 160 * input array. 161 * @see SortedList#addAll(T[] items) 162 */ addAll(T @onNull [] items, boolean mayModifyInput)163 public void addAll(T @NonNull [] items, boolean mayModifyInput) { 164 throwIfInMutationOperation(); 165 if (items.length == 0) { 166 return; 167 } 168 169 if (mayModifyInput) { 170 addAllInternal(items); 171 } else { 172 addAllInternal(copyArray(items)); 173 } 174 } 175 176 /** 177 * Adds the given items to the list. Does not modify or retain the input. 178 * 179 * @see SortedList#addAll(T[] items, boolean mayModifyInput) 180 * 181 * @param items Array of items to be added into the list. 182 */ addAll(T @onNull .... items)183 public void addAll(T @NonNull ... items) { 184 addAll(items, false); 185 } 186 187 /** 188 * Adds the given items to the list. Does not modify or retain the input. 189 * 190 * @see SortedList#addAll(T[] items, boolean mayModifyInput) 191 * 192 * @param items Collection of items to be added into the list. 193 */ addAll(@onNull Collection<T> items)194 public void addAll(@NonNull Collection<T> items) { 195 T[] copy = (T[]) Array.newInstance(mTClass, items.size()); 196 addAll(items.toArray(copy), true); 197 } 198 199 /** 200 * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events 201 * for each change detected as appropriate. 202 * <p> 203 * If allowed, will reference the input array during, and possibly after, the operation to avoid 204 * extra memory allocation, in which case you should not continue to reference or modify the 205 * array yourself. 206 * <p> 207 * Note: this method does not detect moves or dispatch 208 * {@link ListUpdateCallback#onMoved(int, int)} events. It instead treats moves as a remove 209 * followed by an add and therefore dispatches {@link ListUpdateCallback#onRemoved(int, int)} 210 * and {@link ListUpdateCallback#onRemoved(int, int)} events. See {@link DiffUtil} if you want 211 * your implementation to dispatch move events. 212 * <p> 213 * @param items Array of items to replace current items. 214 * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the 215 * input array. 216 * @see #replaceAll(T[]) 217 */ replaceAll(T @onNull [] items, boolean mayModifyInput)218 public void replaceAll(T @NonNull [] items, boolean mayModifyInput) { 219 throwIfInMutationOperation(); 220 221 if (mayModifyInput) { 222 replaceAllInternal(items); 223 } else { 224 replaceAllInternal(copyArray(items)); 225 } 226 } 227 228 /** 229 * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events 230 * for each change detected as appropriate. Does not modify or retain the input. 231 * 232 * @see #replaceAll(T[], boolean) 233 * 234 * @param items Array of items to replace current items. 235 */ replaceAll(T @onNull .... items)236 public void replaceAll(T @NonNull ... items) { 237 replaceAll(items, false); 238 } 239 240 /** 241 * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events 242 * for each change detected as appropriate. Does not modify or retain the input. 243 * 244 * @see #replaceAll(T[], boolean) 245 * 246 * @param items Array of items to replace current items. 247 */ replaceAll(@onNull Collection<T> items)248 public void replaceAll(@NonNull Collection<T> items) { 249 T[] copy = (T[]) Array.newInstance(mTClass, items.size()); 250 replaceAll(items.toArray(copy), true); 251 } 252 addAllInternal(T[] newItems)253 private void addAllInternal(T[] newItems) { 254 if (newItems.length < 1) { 255 return; 256 } 257 258 final int newSize = sortAndDedup(newItems); 259 260 if (mSize == 0) { 261 mData = newItems; 262 mSize = newSize; 263 mCallback.onInserted(0, newSize); 264 } else { 265 merge(newItems, newSize); 266 } 267 } 268 replaceAllInternal(T @onNull [] newData)269 private void replaceAllInternal(T @NonNull [] newData) { 270 final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); 271 if (forceBatchedUpdates) { 272 beginBatchedUpdates(); 273 } 274 275 mOldDataStart = 0; 276 mOldDataSize = mSize; 277 mOldData = mData; 278 279 mNewDataStart = 0; 280 int newSize = sortAndDedup(newData); 281 mData = (T[]) Array.newInstance(mTClass, newSize); 282 283 while (mNewDataStart < newSize || mOldDataStart < mOldDataSize) { 284 if (mOldDataStart >= mOldDataSize) { 285 int insertIndex = mNewDataStart; 286 int itemCount = newSize - mNewDataStart; 287 System.arraycopy(newData, insertIndex, mData, insertIndex, itemCount); 288 mNewDataStart += itemCount; 289 mSize += itemCount; 290 mCallback.onInserted(insertIndex, itemCount); 291 break; 292 } 293 if (mNewDataStart >= newSize) { 294 int itemCount = mOldDataSize - mOldDataStart; 295 mSize -= itemCount; 296 mCallback.onRemoved(mNewDataStart, itemCount); 297 break; 298 } 299 300 T oldItem = mOldData[mOldDataStart]; 301 T newItem = newData[mNewDataStart]; 302 303 int result = mCallback.compare(oldItem, newItem); 304 if (result < 0) { 305 replaceAllRemove(); 306 } else if (result > 0) { 307 replaceAllInsert(newItem); 308 } else { 309 if (!mCallback.areItemsTheSame(oldItem, newItem)) { 310 // The items aren't the same even though they were supposed to occupy the same 311 // place, so both notify to remove and add an item in the current location. 312 replaceAllRemove(); 313 replaceAllInsert(newItem); 314 } else { 315 mData[mNewDataStart] = newItem; 316 mOldDataStart++; 317 mNewDataStart++; 318 if (!mCallback.areContentsTheSame(oldItem, newItem)) { 319 // The item is the same but the contents have changed, so notify that an 320 // onChanged event has occurred. 321 mCallback.onChanged(mNewDataStart - 1, 1, 322 mCallback.getChangePayload(oldItem, newItem)); 323 } 324 } 325 } 326 } 327 328 mOldData = null; 329 330 if (forceBatchedUpdates) { 331 endBatchedUpdates(); 332 } 333 } 334 replaceAllInsert(T newItem)335 private void replaceAllInsert(T newItem) { 336 mData[mNewDataStart] = newItem; 337 mNewDataStart++; 338 mSize++; 339 mCallback.onInserted(mNewDataStart - 1, 1); 340 } 341 replaceAllRemove()342 private void replaceAllRemove() { 343 mSize--; 344 mOldDataStart++; 345 mCallback.onRemoved(mNewDataStart, 1); 346 } 347 348 /** 349 * Sorts and removes duplicate items, leaving only the last item from each group of "same" 350 * items. Move the remaining items to the beginning of the array. 351 * 352 * @return Number of deduplicated items at the beginning of the array. 353 */ sortAndDedup(T @onNull [] items)354 private int sortAndDedup(T @NonNull [] items) { 355 if (items.length == 0) { 356 return 0; 357 } 358 359 // Arrays.sort is stable. 360 Arrays.sort(items, mCallback); 361 362 // Keep track of the range of equal items at the end of the output. 363 // Start with the range containing just the first item. 364 int rangeStart = 0; 365 int rangeEnd = 1; 366 367 for (int i = 1; i < items.length; ++i) { 368 T currentItem = items[i]; 369 370 int compare = mCallback.compare(items[rangeStart], currentItem); 371 372 if (compare == 0) { 373 // The range of equal items continues, update it. 374 final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); 375 if (sameItemPos != INVALID_POSITION) { 376 // Replace the duplicate item. 377 items[sameItemPos] = currentItem; 378 } else { 379 // Expand the range. 380 if (rangeEnd != i) { // Avoid redundant copy. 381 items[rangeEnd] = currentItem; 382 } 383 rangeEnd++; 384 } 385 } else { 386 // The range has ended. Reset it to contain just the current item. 387 if (rangeEnd != i) { // Avoid redundant copy. 388 items[rangeEnd] = currentItem; 389 } 390 rangeStart = rangeEnd++; 391 } 392 } 393 return rangeEnd; 394 } 395 396 findSameItem(T item, T[] items, int from, int to)397 private int findSameItem(T item, T[] items, int from, int to) { 398 for (int pos = from; pos < to; pos++) { 399 if (mCallback.areItemsTheSame(items[pos], item)) { 400 return pos; 401 } 402 } 403 return INVALID_POSITION; 404 } 405 406 /** 407 * This method assumes that newItems are sorted and deduplicated. 408 */ merge(T[] newData, int newDataSize)409 private void merge(T[] newData, int newDataSize) { 410 final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); 411 if (forceBatchedUpdates) { 412 beginBatchedUpdates(); 413 } 414 415 mOldData = mData; 416 mOldDataStart = 0; 417 mOldDataSize = mSize; 418 419 final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; 420 mData = (T[]) Array.newInstance(mTClass, mergedCapacity); 421 mNewDataStart = 0; 422 423 int newDataStart = 0; 424 while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) { 425 if (mOldDataStart == mOldDataSize) { 426 // No more old items, copy the remaining new items. 427 int itemCount = newDataSize - newDataStart; 428 System.arraycopy(newData, newDataStart, mData, mNewDataStart, itemCount); 429 mNewDataStart += itemCount; 430 mSize += itemCount; 431 mCallback.onInserted(mNewDataStart - itemCount, itemCount); 432 break; 433 } 434 435 if (newDataStart == newDataSize) { 436 // No more new items, copy the remaining old items. 437 int itemCount = mOldDataSize - mOldDataStart; 438 System.arraycopy(mOldData, mOldDataStart, mData, mNewDataStart, itemCount); 439 mNewDataStart += itemCount; 440 break; 441 } 442 443 T oldItem = mOldData[mOldDataStart]; 444 T newItem = newData[newDataStart]; 445 int compare = mCallback.compare(oldItem, newItem); 446 if (compare > 0) { 447 // New item is lower, output it. 448 mData[mNewDataStart++] = newItem; 449 mSize++; 450 newDataStart++; 451 mCallback.onInserted(mNewDataStart - 1, 1); 452 } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) { 453 // Items are the same. Output the new item, but consume both. 454 mData[mNewDataStart++] = newItem; 455 newDataStart++; 456 mOldDataStart++; 457 if (!mCallback.areContentsTheSame(oldItem, newItem)) { 458 mCallback.onChanged(mNewDataStart - 1, 1, 459 mCallback.getChangePayload(oldItem, newItem)); 460 } 461 } else { 462 // Old item is lower than or equal to (but not the same as the new). Output it. 463 // New item with the same sort order will be inserted later. 464 mData[mNewDataStart++] = oldItem; 465 mOldDataStart++; 466 } 467 } 468 469 mOldData = null; 470 471 if (forceBatchedUpdates) { 472 endBatchedUpdates(); 473 } 474 } 475 476 /** 477 * Throws an exception if called while we are in the middle of a mutation operation (addAll or 478 * replaceAll). 479 */ throwIfInMutationOperation()480 private void throwIfInMutationOperation() { 481 if (mOldData != null) { 482 throw new IllegalStateException("Data cannot be mutated in the middle of a batch " 483 + "update operation such as addAll or replaceAll."); 484 } 485 } 486 487 /** 488 * Batches adapter updates that happen after calling this method and before calling 489 * {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop 490 * and they are placed into consecutive indices, SortedList calls 491 * {@link Callback#onInserted(int, int)} only once with the proper item count. If an event 492 * cannot be merged with the previous event, the previous event is dispatched 493 * to the callback instantly. 494 * <p> 495 * After running your data updates, you <b>must</b> call {@link #endBatchedUpdates()} 496 * which will dispatch any deferred data change event to the current callback. 497 * <p> 498 * A sample implementation may look like this: 499 * <pre> 500 * mSortedList.beginBatchedUpdates(); 501 * try { 502 * mSortedList.add(item1) 503 * mSortedList.add(item2) 504 * mSortedList.remove(item3) 505 * ... 506 * } finally { 507 * mSortedList.endBatchedUpdates(); 508 * } 509 * </pre> 510 * <p> 511 * Instead of using this method to batch calls, you can use a Callback that extends 512 * {@link BatchedCallback}. In that case, you must make sure that you are manually calling 513 * {@link BatchedCallback#dispatchLastEvent()} right after you complete your data changes. 514 * Failing to do so may create data inconsistencies with the Callback. 515 * <p> 516 * If the current Callback is an instance of {@link BatchedCallback}, calling this method 517 * has no effect. 518 */ beginBatchedUpdates()519 public void beginBatchedUpdates() { 520 throwIfInMutationOperation(); 521 if (mCallback instanceof BatchedCallback) { 522 return; 523 } 524 if (mBatchedCallback == null) { 525 mBatchedCallback = new BatchedCallback(mCallback); 526 } 527 mCallback = mBatchedCallback; 528 } 529 530 /** 531 * Ends the update transaction and dispatches any remaining event to the callback. 532 */ endBatchedUpdates()533 public void endBatchedUpdates() { 534 throwIfInMutationOperation(); 535 if (mCallback instanceof BatchedCallback) { 536 ((BatchedCallback) mCallback).dispatchLastEvent(); 537 } 538 if (mCallback == mBatchedCallback) { 539 mCallback = mBatchedCallback.mWrappedCallback; 540 } 541 } 542 add(T item, boolean notify)543 private int add(T item, boolean notify) { 544 int index = findIndexOf(item, mData, 0, mSize, INSERTION); 545 if (index == INVALID_POSITION) { 546 index = 0; 547 } else if (index < mSize) { 548 T existing = mData[index]; 549 if (mCallback.areItemsTheSame(existing, item)) { 550 if (mCallback.areContentsTheSame(existing, item)) { 551 //no change but still replace the item 552 mData[index] = item; 553 return index; 554 } else { 555 mData[index] = item; 556 mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); 557 return index; 558 } 559 } 560 } 561 addToData(index, item); 562 if (notify) { 563 mCallback.onInserted(index, 1); 564 } 565 return index; 566 } 567 568 /** 569 * Removes the provided item from the list and calls {@link Callback#onRemoved(int, int)}. 570 * 571 * @param item The item to be removed from the list. 572 * 573 * @return True if item is removed, false if item cannot be found in the list. 574 */ remove(T item)575 public boolean remove(T item) { 576 throwIfInMutationOperation(); 577 return remove(item, true); 578 } 579 580 /** 581 * Removes the item at the given index and calls {@link Callback#onRemoved(int, int)}. 582 * 583 * @param index The index of the item to be removed. 584 * 585 * @return The removed item. 586 */ removeItemAt(int index)587 public T removeItemAt(int index) { 588 throwIfInMutationOperation(); 589 T item = get(index); 590 removeItemAtIndex(index, true); 591 return item; 592 } 593 remove(T item, boolean notify)594 private boolean remove(T item, boolean notify) { 595 int index = findIndexOf(item, mData, 0, mSize, DELETION); 596 if (index == INVALID_POSITION) { 597 return false; 598 } 599 removeItemAtIndex(index, notify); 600 return true; 601 } 602 removeItemAtIndex(int index, boolean notify)603 private void removeItemAtIndex(int index, boolean notify) { 604 System.arraycopy(mData, index + 1, mData, index, mSize - index - 1); 605 mSize--; 606 mData[mSize] = null; 607 if (notify) { 608 mCallback.onRemoved(index, 1); 609 } 610 } 611 612 /** 613 * Updates the item at the given index and calls {@link Callback#onChanged(int, int)} and/or 614 * {@link Callback#onMoved(int, int)} if necessary. 615 * <p> 616 * You can use this method if you need to change an existing Item such that its position in the 617 * list may change. 618 * <p> 619 * If the new object is a different object (<code>get(index) != item</code>) and 620 * {@link Callback#areContentsTheSame(Object, Object)} returns <code>true</code>, SortedList 621 * avoids calling {@link Callback#onChanged(int, int)} otherwise it calls 622 * {@link Callback#onChanged(int, int)}. 623 * <p> 624 * If the new position of the item is different than the provided <code>index</code>, 625 * SortedList 626 * calls {@link Callback#onMoved(int, int)}. 627 * 628 * @param index The index of the item to replace 629 * @param item The item to replace the item at the given Index. 630 * @see #add(Object) 631 */ updateItemAt(int index, T item)632 public void updateItemAt(int index, T item) { 633 throwIfInMutationOperation(); 634 final T existing = get(index); 635 // assume changed if the same object is given back 636 boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); 637 if (existing != item) { 638 // different items, we can use comparison and may avoid lookup 639 final int cmp = mCallback.compare(existing, item); 640 if (cmp == 0) { 641 mData[index] = item; 642 if (contentsChanged) { 643 mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); 644 } 645 return; 646 } 647 } 648 if (contentsChanged) { 649 mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); 650 } 651 // TODO this done in 1 pass to avoid shifting twice. 652 removeItemAtIndex(index, false); 653 int newIndex = add(item, false); 654 if (index != newIndex) { 655 mCallback.onMoved(index, newIndex); 656 } 657 } 658 659 /** 660 * This method can be used to recalculate the position of the item at the given index, without 661 * triggering an {@link Callback#onChanged(int, int)} callback. 662 * <p> 663 * If you are editing objects in the list such that their position in the list may change but 664 * you don't want to trigger an onChange animation, you can use this method to re-position it. 665 * If the item changes position, SortedList will call {@link Callback#onMoved(int, int)} 666 * without 667 * calling {@link Callback#onChanged(int, int)}. 668 * <p> 669 * A sample usage may look like: 670 * 671 * <pre> 672 * final int position = mSortedList.indexOf(item); 673 * item.incrementPriority(); // assume items are sorted by priority 674 * mSortedList.recalculatePositionOfItemAt(position); 675 * </pre> 676 * In the example above, because the sorting criteria of the item has been changed, 677 * mSortedList.indexOf(item) will not be able to find the item. This is why the code above 678 * first 679 * gets the position before editing the item, edits it and informs the SortedList that item 680 * should be repositioned. 681 * 682 * @param index The current index of the Item whose position should be re-calculated. 683 * @see #updateItemAt(int, Object) 684 * @see #add(Object) 685 */ recalculatePositionOfItemAt(int index)686 public void recalculatePositionOfItemAt(int index) { 687 throwIfInMutationOperation(); 688 // TODO can be improved 689 final T item = get(index); 690 removeItemAtIndex(index, false); 691 int newIndex = add(item, false); 692 if (index != newIndex) { 693 mCallback.onMoved(index, newIndex); 694 } 695 } 696 697 /** 698 * Returns the item at the given index. 699 * 700 * @param index The index of the item to retrieve. 701 * 702 * @return The item at the given index. 703 * @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the 704 * size of the list. 705 */ get(int index)706 public T get(int index) throws IndexOutOfBoundsException { 707 if (index >= mSize || index < 0) { 708 throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is " 709 + mSize); 710 } 711 if (mOldData != null) { 712 // The call is made from a callback during addAll execution. The data is split 713 // between mData and mOldData. 714 if (index >= mNewDataStart) { 715 return mOldData[index - mNewDataStart + mOldDataStart]; 716 } 717 } 718 return mData[index]; 719 } 720 721 /** 722 * Returns the position of the provided item. 723 * 724 * @param item The item to query for position. 725 * 726 * @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the 727 * list. 728 */ indexOf(T item)729 public int indexOf(T item) { 730 if (mOldData != null) { 731 int index = findIndexOf(item, mData, 0, mNewDataStart, LOOKUP); 732 if (index != INVALID_POSITION) { 733 return index; 734 } 735 index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP); 736 if (index != INVALID_POSITION) { 737 return index - mOldDataStart + mNewDataStart; 738 } 739 return INVALID_POSITION; 740 } 741 return findIndexOf(item, mData, 0, mSize, LOOKUP); 742 } 743 findIndexOf(T item, T[] mData, int left, int right, int reason)744 private int findIndexOf(T item, T[] mData, int left, int right, int reason) { 745 while (left < right) { 746 final int middle = (left + right) / 2; 747 T myItem = mData[middle]; 748 final int cmp = mCallback.compare(myItem, item); 749 if (cmp < 0) { 750 left = middle + 1; 751 } else if (cmp == 0) { 752 if (mCallback.areItemsTheSame(myItem, item)) { 753 return middle; 754 } else { 755 int exact = linearEqualitySearch(item, middle, left, right); 756 if (reason == INSERTION) { 757 return exact == INVALID_POSITION ? middle : exact; 758 } else { 759 return exact; 760 } 761 } 762 } else { 763 right = middle; 764 } 765 } 766 return reason == INSERTION ? left : INVALID_POSITION; 767 } 768 linearEqualitySearch(T item, int middle, int left, int right)769 private int linearEqualitySearch(T item, int middle, int left, int right) { 770 // go left 771 for (int next = middle - 1; next >= left; next--) { 772 T nextItem = mData[next]; 773 int cmp = mCallback.compare(nextItem, item); 774 if (cmp != 0) { 775 break; 776 } 777 if (mCallback.areItemsTheSame(nextItem, item)) { 778 return next; 779 } 780 } 781 for (int next = middle + 1; next < right; next++) { 782 T nextItem = mData[next]; 783 int cmp = mCallback.compare(nextItem, item); 784 if (cmp != 0) { 785 break; 786 } 787 if (mCallback.areItemsTheSame(nextItem, item)) { 788 return next; 789 } 790 } 791 return INVALID_POSITION; 792 } 793 addToData(int index, T item)794 private void addToData(int index, T item) { 795 if (index > mSize) { 796 throw new IndexOutOfBoundsException( 797 "cannot add item to " + index + " because size is " + mSize); 798 } 799 if (mSize == mData.length) { 800 // we are at the limit enlarge 801 T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH); 802 System.arraycopy(mData, 0, newData, 0, index); 803 newData[index] = item; 804 System.arraycopy(mData, index, newData, index + 1, mSize - index); 805 mData = newData; 806 } else { 807 // just shift, we fit 808 System.arraycopy(mData, index, mData, index + 1, mSize - index); 809 mData[index] = item; 810 } 811 mSize++; 812 } 813 copyArray(T[] items)814 private T[] copyArray(T[] items) { 815 T[] copy = (T[]) Array.newInstance(mTClass, items.length); 816 System.arraycopy(items, 0, copy, 0, items.length); 817 return copy; 818 } 819 820 /** 821 * Removes all items from the SortedList. 822 */ clear()823 public void clear() { 824 throwIfInMutationOperation(); 825 if (mSize == 0) { 826 return; 827 } 828 final int prevSize = mSize; 829 Arrays.fill(mData, 0, prevSize, null); 830 mSize = 0; 831 mCallback.onRemoved(0, prevSize); 832 } 833 834 /** 835 * The class that controls the behavior of the {@link SortedList}. 836 * <p> 837 * It defines how items should be sorted and how duplicates should be handled. 838 * <p> 839 * SortedList calls the callback methods on this class to notify changes about the underlying 840 * data. 841 */ 842 public static abstract class Callback<T2> implements Comparator<T2>, ListUpdateCallback { 843 844 /** 845 * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and 846 * return how they should be ordered. 847 * 848 * @param o1 The first object to compare. 849 * @param o2 The second object to compare. 850 * 851 * @return a negative integer, zero, or a positive integer as the 852 * first argument is less than, equal to, or greater than the 853 * second. 854 */ 855 @Override compare(T2 o1, T2 o2)856 abstract public int compare(T2 o1, T2 o2); 857 858 /** 859 * Called by the SortedList when the item at the given position is updated. 860 * 861 * @param position The position of the item which has been updated. 862 * @param count The number of items which has changed. 863 */ onChanged(int position, int count)864 abstract public void onChanged(int position, int count); 865 866 /** {@inheritDoc} */ 867 @Override 868 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly onChanged(int position, int count, Object payload)869 public void onChanged(int position, int count, Object payload) { 870 onChanged(position, count); 871 } 872 873 /** 874 * Called by the SortedList when it wants to check whether two items have the same data 875 * or not. SortedList uses this information to decide whether it should call 876 * {@link #onChanged(int, int)} or not. 877 * <p> 878 * SortedList uses this method to check equality instead of {@link Object#equals(Object)} 879 * so 880 * that you can change its behavior depending on your UI. 881 * <p> 882 * For example, if you are using SortedList with a 883 * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should 884 * return whether the items' visual representations are the same or not. 885 * 886 * @param oldItem The previous representation of the object. 887 * @param newItem The new object that replaces the previous one. 888 * 889 * @return True if the contents of the items are the same or false if they are different. 890 */ areContentsTheSame(T2 oldItem, T2 newItem)891 abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); 892 893 /** 894 * Called by the SortedList to decide whether two objects represent the same Item or not. 895 * <p> 896 * For example, if your items have unique ids, this method should check their equality. 897 * 898 * @param item1 The first item to check. 899 * @param item2 The second item to check. 900 * 901 * @return True if the two items represent the same object or false if they are different. 902 */ areItemsTheSame(T2 item1, T2 item2)903 abstract public boolean areItemsTheSame(T2 item1, T2 item2); 904 905 /** 906 * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and 907 * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this 908 * method to get a payload about the change. 909 * <p> 910 * For example, if you are using {@link Callback} with 911 * {@link RecyclerView}, you can return the particular field that 912 * changed in the item and your 913 * {@link RecyclerView.ItemAnimator ItemAnimator} can use that 914 * information to run the correct animation. 915 * <p> 916 * Default implementation returns {@code null}. 917 * 918 * @param item1 The first item to check. 919 * @param item2 The second item to check. 920 * @return A payload object that represents the changes between the two items. 921 */ getChangePayload(T2 item1, T2 item2)922 public @Nullable Object getChangePayload(T2 item1, T2 item2) { 923 return null; 924 } 925 } 926 927 /** 928 * A callback implementation that can batch notify events dispatched by the SortedList. 929 * <p> 930 * This class can be useful if you want to do multiple operations on a SortedList but don't 931 * want to dispatch each event one by one, which may result in a performance issue. 932 * <p> 933 * For example, if you are going to add multiple items to a SortedList, BatchedCallback call 934 * convert individual <code>onInserted(index, 1)</code> calls into one 935 * <code>onInserted(index, N)</code> if items are added into consecutive indices. This change 936 * can help RecyclerView resolve changes much more easily. 937 * <p> 938 * If consecutive changes in the SortedList are not suitable for batching, BatchingCallback 939 * dispatches them as soon as such case is detected. After your edits on the SortedList is 940 * complete, you <b>must</b> always call {@link BatchedCallback#dispatchLastEvent()} to flush 941 * all changes to the Callback. 942 */ 943 public static class BatchedCallback<T2> extends Callback<T2> { 944 945 final Callback<T2> mWrappedCallback; 946 private final BatchingListUpdateCallback mBatchingListUpdateCallback; 947 /** 948 * Creates a new BatchedCallback that wraps the provided Callback. 949 * 950 * @param wrappedCallback The Callback which should received the data change callbacks. 951 * Other method calls (e.g. {@link #compare(Object, Object)} from 952 * the SortedList are directly forwarded to this Callback. 953 */ 954 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly BatchedCallback(Callback<T2> wrappedCallback)955 public BatchedCallback(Callback<T2> wrappedCallback) { 956 mWrappedCallback = wrappedCallback; 957 mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback); 958 } 959 960 /** {@inheritDoc} */ 961 @Override compare(T2 o1, T2 o2)962 public int compare(T2 o1, T2 o2) { 963 return mWrappedCallback.compare(o1, o2); 964 } 965 966 /** {@inheritDoc} */ 967 @Override onInserted(int position, int count)968 public void onInserted(int position, int count) { 969 mBatchingListUpdateCallback.onInserted(position, count); 970 } 971 972 /** {@inheritDoc} */ 973 @Override onRemoved(int position, int count)974 public void onRemoved(int position, int count) { 975 mBatchingListUpdateCallback.onRemoved(position, count); 976 } 977 978 /** {@inheritDoc} */ 979 @Override onMoved(int fromPosition, int toPosition)980 public void onMoved(int fromPosition, int toPosition) { 981 mBatchingListUpdateCallback.onMoved(fromPosition, toPosition); 982 } 983 984 /** {@inheritDoc} */ 985 @Override onChanged(int position, int count)986 public void onChanged(int position, int count) { 987 mBatchingListUpdateCallback.onChanged(position, count, null); 988 } 989 990 /** {@inheritDoc} */ 991 @Override 992 @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly onChanged(int position, int count, Object payload)993 public void onChanged(int position, int count, Object payload) { 994 mBatchingListUpdateCallback.onChanged(position, count, payload); 995 } 996 997 /** {@inheritDoc} */ 998 @Override areContentsTheSame(T2 oldItem, T2 newItem)999 public boolean areContentsTheSame(T2 oldItem, T2 newItem) { 1000 return mWrappedCallback.areContentsTheSame(oldItem, newItem); 1001 } 1002 1003 /** {@inheritDoc} */ 1004 @Override areItemsTheSame(T2 item1, T2 item2)1005 public boolean areItemsTheSame(T2 item1, T2 item2) { 1006 return mWrappedCallback.areItemsTheSame(item1, item2); 1007 } 1008 1009 /** {@inheritDoc} */ 1010 @Override getChangePayload(T2 item1, T2 item2)1011 public @Nullable Object getChangePayload(T2 item1, T2 item2) { 1012 return mWrappedCallback.getChangePayload(item1, item2); 1013 } 1014 1015 /** 1016 * This method dispatches any pending event notifications to the wrapped Callback. 1017 * You <b>must</b> always call this method after you are done with editing the SortedList. 1018 */ dispatchLastEvent()1019 public void dispatchLastEvent() { 1020 mBatchingListUpdateCallback.dispatchLastEvent(); 1021 } 1022 } 1023 } 1024