From 65fe2d32013cbed754d787db056d9048b0b24e1c Mon Sep 17 00:00:00 2001 From: longbkiter07 Date: Mon, 24 Apr 2017 11:14:09 +0700 Subject: [PATCH] * Applied best practices for sorted list & diff callback --- .../RecyclerViewActivity.java | 2 +- .../adapter/RxUserRecyclerViewAdapter.java | 2 +- .../model/DataFactory.java | 2 +- .../silong/snappyadapter/AsyncSortedList.java | 465 ------------ .../silong/snappyadapter/RxDiffCallback.java | 52 -- .../snappyadapter/RxRecyclerViewCallback.java | 5 +- .../me/silong/snappyadapter/RxSortedList.java | 218 ++---- .../snappyadapter/RxSortedListCallback.java | 148 +++- .../snappyadapter/SnappyDiffCallback.java | 50 ++ .../snappyadapter/SnappySortedList.java | 702 ++++++++++++++++++ 10 files changed, 963 insertions(+), 683 deletions(-) delete mode 100644 observablerm/src/main/java/me/silong/snappyadapter/AsyncSortedList.java delete mode 100644 observablerm/src/main/java/me/silong/snappyadapter/RxDiffCallback.java create mode 100644 observablerm/src/main/java/me/silong/snappyadapter/SnappyDiffCallback.java create mode 100644 observablerm/src/main/java/me/silong/snappyadapter/SnappySortedList.java diff --git a/app/src/main/java/com/silong/snappyrecycleradapter/RecyclerViewActivity.java b/app/src/main/java/com/silong/snappyrecycleradapter/RecyclerViewActivity.java index 9c95cae..28afccb 100644 --- a/app/src/main/java/com/silong/snappyrecycleradapter/RecyclerViewActivity.java +++ b/app/src/main/java/com/silong/snappyrecycleradapter/RecyclerViewActivity.java @@ -142,7 +142,7 @@ public boolean onOptionsItemSelected(MenuItem item) { mSyncAdapter.remove((int) (Math.random() * mSyncAdapter.getItemCount() - 1)); } else { mRxUserRecyclerViewAdapter.getObservableAdapterManager() - .removeAt((int) (Math.random() * mRxUserRecyclerViewAdapter.getItemCount() - 1)) + .removeItemAt((int) (Math.random() * mRxUserRecyclerViewAdapter.getItemCount() - 1)) .subscribe(); } break; diff --git a/app/src/main/java/com/silong/snappyrecycleradapter/adapter/RxUserRecyclerViewAdapter.java b/app/src/main/java/com/silong/snappyrecycleradapter/adapter/RxUserRecyclerViewAdapter.java index c33b822..0cab0e0 100644 --- a/app/src/main/java/com/silong/snappyrecycleradapter/adapter/RxUserRecyclerViewAdapter.java +++ b/app/src/main/java/com/silong/snappyrecycleradapter/adapter/RxUserRecyclerViewAdapter.java @@ -60,6 +60,6 @@ public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(ItemViewHolder holder, int position) { - holder.bind(mUserRxSortedList.getItemAt(position)); + holder.bind(mUserRxSortedList.get(position)); } } diff --git a/app/src/main/java/com/silong/snappyrecycleradapter/model/DataFactory.java b/app/src/main/java/com/silong/snappyrecycleradapter/model/DataFactory.java index 40247d7..a365e70 100644 --- a/app/src/main/java/com/silong/snappyrecycleradapter/model/DataFactory.java +++ b/app/src/main/java/com/silong/snappyrecycleradapter/model/DataFactory.java @@ -12,7 +12,7 @@ */ public class DataFactory { - public static final int CHUNK = 10000; + public static final int CHUNK = 10; private DataFactory() { diff --git a/observablerm/src/main/java/me/silong/snappyadapter/AsyncSortedList.java b/observablerm/src/main/java/me/silong/snappyadapter/AsyncSortedList.java deleted file mode 100644 index 405b058..0000000 --- a/observablerm/src/main/java/me/silong/snappyadapter/AsyncSortedList.java +++ /dev/null @@ -1,465 +0,0 @@ -package me.silong.snappyadapter; - -import java.lang.reflect.Array; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; - -/** - * Created by SILONG on 4/18/17. - */ - -class AsyncSortedList { - - private static final int INVALID_POSITION = -1; - - private static final int MIN_CAPACITY = 10; - - private static final int CAPACITY_GROWTH = MIN_CAPACITY; - - private static final int INSERTION = 1; - - private static final int DELETION = 1 << 1; - - private static final int LOOKUP = 1 << 2; - - private final Class mTClass; - - T[] mData; - - private T[] mOldData; - - private int mOldDataStart; - - private int mOldDataSize; - - private int mMergedSize; - - private AsyncSortedList.Callback mCallback; - - private int mSize; - - public AsyncSortedList(Class klass, AsyncSortedList.Callback callback) { - this(klass, callback, MIN_CAPACITY); - } - - public AsyncSortedList(Class klass, AsyncSortedList.Callback callback, int initialCapacity) { - mTClass = klass; - mData = (T[]) Array.newInstance(klass, initialCapacity); - mCallback = callback; - mSize = 0; - } - - public AsyncSortedList(Class klass, AsyncSortedList.Callback callback, List initialSortedList) { - mTClass = klass; - mData = initialSortedList.toArray((T[]) Array.newInstance(mTClass, initialSortedList.size())); - mCallback = callback; - mSize = initialSortedList.size(); - } - - public int size() { - return mSize; - } - - public int add(T item) { - throwIfMerging(); - return add(item, true); - } - - public List getData() { - return Arrays.asList(mData).subList(0, mSize); - } - - public void addAll(T[] items, boolean mayModifyInput) { - throwIfMerging(); - if (items.length == 0) { - return; - } - if (mayModifyInput) { - addAllInternal(items); - } else { - T[] copy = (T[]) Array.newInstance(mTClass, items.length); - System.arraycopy(items, 0, copy, 0, items.length); - addAllInternal(copy); - } - - } - - public void addAll(T... items) { - addAll(items, false); - } - - public void addAll(Collection items) { - T[] copy = (T[]) Array.newInstance(mTClass, items.size()); - addAll(items.toArray(copy), true); - } - - private void addAllInternal(T[] newItems) { - mOldData = mData; - mOldDataStart = 0; - mOldDataSize = mSize; - Arrays.sort(newItems, mCallback); // Arrays.sort is stable. - final int newSize = deduplicate(newItems); - if (mSize == 0) { - mData = newItems; - mSize = newSize; - mMergedSize = newSize; - mCallback.onInserted(0, newItems); - } else { - merge(newItems, newSize); - } - mOldData = null; - } - - private int deduplicate(T[] items) { - if (items.length == 0) { - throw new IllegalArgumentException("Input array must be non-empty"); - } - - // Keep track of the range of equal items at the end of the output. - // Start with the range containing just the first item. - int rangeStart = 0; - int rangeEnd = 1; - - for (int i = 1; i < items.length; ++i) { - T currentItem = items[i]; - - int compare = mCallback.compare(items[rangeStart], currentItem); - if (compare > 0) { - throw new IllegalArgumentException("Input must be sorted in ascending order."); - } - - if (compare == 0) { - // The range of equal items continues, update it. - final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); - if (sameItemPos != INVALID_POSITION) { - // Replace the duplicate item. - items[sameItemPos] = currentItem; - } else { - // Expand the range. - if (rangeEnd != i) { // Avoid redundant copy. - items[rangeEnd] = currentItem; - } - rangeEnd++; - } - } else { - // The range has ended. Reset it to contain just the current item. - if (rangeEnd != i) { // Avoid redundant copy. - items[rangeEnd] = currentItem; - } - rangeStart = rangeEnd++; - } - } - return rangeEnd; - } - - - private int findSameItem(T item, T[] items, int from, int to) { - for (int pos = from; pos < to; pos++) { - if (mCallback.areItemsTheSame(items[pos], item)) { - return pos; - } - } - return INVALID_POSITION; - } - - private void merge(T[] newData, int newDataSize) { - final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; - mData = (T[]) Array.newInstance(mTClass, mergedCapacity); - mMergedSize = 0; - - int newDataStart = 0; - while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) { - if (mOldDataStart == mOldDataSize) { - // No more old items, copy the remaining new items. - int itemCount = newDataSize - newDataStart; - System.arraycopy(newData, newDataStart, mData, mMergedSize, itemCount); - T[] itemsChanged = (T[]) Array.newInstance(mTClass, itemCount); - System.arraycopy(newData, newDataStart, itemsChanged, 0, itemCount); - mMergedSize += itemCount; - mSize += itemCount; - mCallback.onInserted(mMergedSize - itemCount, itemsChanged); - break; - } - - if (newDataStart == newDataSize) { - // No more new items, copy the remaining old items. - int itemCount = mOldDataSize - mOldDataStart; - System.arraycopy(mOldData, mOldDataStart, mData, mMergedSize, itemCount); - mMergedSize += itemCount; - break; - } - - T oldItem = mOldData[mOldDataStart]; - T newItem = newData[newDataStart]; - int compare = mCallback.compare(oldItem, newItem); - T[] itemsChanged = (T[]) Array.newInstance(mTClass, 1); - if (compare > 0) { - // New item is lower, output it. - mData[mMergedSize++] = newItem; - mSize++; - newDataStart++; - itemsChanged[0] = newItem; - mCallback.onInserted(mMergedSize - 1, itemsChanged); - } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) { - // Items are the same. Output the new item, but consume both. - mData[mMergedSize++] = newItem; - itemsChanged[0] = newItem; - newDataStart++; - mOldDataStart++; - if (!mCallback.areContentsTheSame(oldItem, newItem)) { - - mCallback.onChanged(mMergedSize - 1, itemsChanged); - } - } else { - // Old item is lower than or equal to (but not the same as the new). Output it. - // New item with the same sort order will be inserted later. - mData[mMergedSize++] = oldItem; - mOldDataStart++; - } - } - } - - private void throwIfMerging() { - if (mOldData != null) { - throw new IllegalStateException("Cannot call this method from within addAll"); - } - } - - private int add(T item, boolean notify) { - int index = findIndexOf(item, mData, 0, mSize, INSERTION); - if (index == INVALID_POSITION) { - index = 0; - } else if (index < mSize) { - T existing = mData[index]; - if (mCallback.areItemsTheSame(existing, item)) { - if (mCallback.areContentsTheSame(existing, item)) { - //no change but still replace the item - mData[index] = item; - return index; - } else { - mData[index] = item; - T[] itemsChanged = (T[]) Array.newInstance(mTClass, 1); - itemsChanged[0] = item; - mCallback.onChanged(index, itemsChanged); - return index; - } - } - } - addToData(index, item); - if (notify) { - T[] itemsChanged = (T[]) Array.newInstance(mTClass, 1); - itemsChanged[0] = item; - mCallback.onInserted(index, itemsChanged); - } - return index; - } - - public boolean remove(T item) { - throwIfMerging(); - return remove(item, true); - } - - public T removeItemAt(int index) { - throwIfMerging(); - T item = get(index); - removeItemAtIndex(index, true); - return item; - } - - private boolean remove(T item, boolean notify) { - int index = findIndexOf(item, mData, 0, mSize, DELETION); - if (index == INVALID_POSITION) { - return false; - } - removeItemAtIndex(index, notify); - return true; - } - - private void removeItemAtIndex(int index, boolean notify) { - System.arraycopy(mData, index + 1, mData, index, mSize - index - 1); - mSize--; - mData[mSize] = null; - if (notify) { - mCallback.onRemoved(index, 1); - } - } - - public void updateItemAt(int index, T item) { - throwIfMerging(); - final T existing = get(index); - // assume changed if the same object is given back - boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); - if (existing != item) { - // different items, we can use comparison and may avoid lookup - final int cmp = mCallback.compare(existing, item); - if (cmp == 0) { - mData[index] = item; - if (contentsChanged) { - T[] itemsChanged = (T[]) Array.newInstance(mTClass, 1); - itemsChanged[0] = item; - mCallback.onChanged(index, itemsChanged); - } - return; - } - } - if (contentsChanged) { - T[] itemsChanged = (T[]) Array.newInstance(mTClass, 1); - itemsChanged[0] = item; - mCallback.onChanged(index, itemsChanged); - } - // TODO this done in 1 pass to avoid shifting twice. - removeItemAtIndex(index, false); - int newIndex = add(item, false); - if (index != newIndex) { - mCallback.onMoved(index, newIndex); - } - } - - public void recalculatePositionOfItemAt(int index) { - throwIfMerging(); - // TODO can be improved - final T item = get(index); - removeItemAtIndex(index, false); - int newIndex = add(item, false); - if (index != newIndex) { - mCallback.onMoved(index, newIndex); - } - } - - public T get(int index) throws IndexOutOfBoundsException { - if (index >= mSize || index < 0) { - throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is " - + mSize); - } - if (mOldData != null) { - // The call is made from a callback during addAll execution. The data is split - // between mData and mOldData. - if (index >= mMergedSize) { - return mOldData[index - mMergedSize + mOldDataStart]; - } - } - return mData[index]; - } - - public int indexOf(T item) { - if (mOldData != null) { - int index = findIndexOf(item, mData, 0, mMergedSize, LOOKUP); - if (index != INVALID_POSITION) { - return index; - } - index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP); - if (index != INVALID_POSITION) { - return index - mOldDataStart + mMergedSize; - } - return INVALID_POSITION; - } - return findIndexOf(item, mData, 0, mSize, LOOKUP); - } - - private int findIndexOf(T item, T[] mData, int left, int right, int reason) { - while (left < right) { - final int middle = (left + right) / 2; - T myItem = mData[middle]; - final int cmp = mCallback.compare(myItem, item); - if (cmp < 0) { - left = middle + 1; - } else if (cmp == 0) { - if (mCallback.areItemsTheSame(myItem, item)) { - return middle; - } else { - int exact = linearEqualitySearch(item, middle, left, right); - if (reason == INSERTION) { - return exact == INVALID_POSITION ? middle : exact; - } else { - return exact; - } - } - } else { - right = middle; - } - } - return reason == INSERTION ? left : INVALID_POSITION; - } - - private int linearEqualitySearch(T item, int middle, int left, int right) { - // go left - for (int next = middle - 1; next >= left; next--) { - T nextItem = mData[next]; - int cmp = mCallback.compare(nextItem, item); - if (cmp != 0) { - break; - } - if (mCallback.areItemsTheSame(nextItem, item)) { - return next; - } - } - for (int next = middle + 1; next < right; next++) { - T nextItem = mData[next]; - int cmp = mCallback.compare(nextItem, item); - if (cmp != 0) { - break; - } - if (mCallback.areItemsTheSame(nextItem, item)) { - return next; - } - } - return INVALID_POSITION; - } - - private void addToData(int index, T item) { - if (index > mSize) { - throw new IndexOutOfBoundsException( - "cannot add item to " + index + " because size is " + mSize); - } - if (mSize == mData.length) { - // we are at the limit enlarge - T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH); - System.arraycopy(mData, 0, newData, 0, index); - newData[index] = item; - System.arraycopy(mData, index, newData, index + 1, mSize - index); - mData = newData; - } else { - // just shift, we fit - System.arraycopy(mData, index, mData, index + 1, mSize - index); - mData[index] = item; - } - mSize++; - } - - public void clear() { - throwIfMerging(); - if (mSize == 0) { - return; - } - final int prevSize = mSize; - Arrays.fill(mData, 0, prevSize, null); - mSize = 0; - mCallback.onRemoved(0, prevSize); - } - - //TODO refactor this - public void set(List items) { - mData = items.toArray((T[]) Array.newInstance(mTClass, items.size())); - mSize = items.size(); - } - - public static abstract class Callback implements Comparator { - - abstract public int compare(T2 o1, T2 o2); - - abstract public void onInserted(int position, T2[] ts); - - abstract public void onRemoved(int position, int count); - - abstract public void onMoved(int fromPosition, int toPosition); - - abstract public void onChanged(int position, T2[] ts); - - abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); - - abstract public boolean areItemsTheSame(T2 item1, T2 item2); - } - -} diff --git a/observablerm/src/main/java/me/silong/snappyadapter/RxDiffCallback.java b/observablerm/src/main/java/me/silong/snappyadapter/RxDiffCallback.java deleted file mode 100644 index f3e547b..0000000 --- a/observablerm/src/main/java/me/silong/snappyadapter/RxDiffCallback.java +++ /dev/null @@ -1,52 +0,0 @@ -package me.silong.snappyadapter; - -import android.support.v4.util.Pair; -import android.support.v7.util.DiffUtil; - -import java.util.List; - -import rx.Observable; - -class RxDiffCallback extends DiffUtil.Callback { - - private final RxSortedListCallback mDataComparable; - - private final List mNewData; - - private final List mOldData; - - RxDiffCallback(RxSortedListCallback dataComparable, List oldData, List newData) { - mOldData = oldData; - mNewData = newData; - mDataComparable = dataComparable; - } - - public static Observable>> calculate(RxSortedListCallback dataComparable, - List oldData, - List newData) { - return Observable.defer(() -> { - DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new RxDiffCallback<>(dataComparable, oldData, newData)); - return Observable.just(new Pair<>(diffResult, newData)); - }); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return mDataComparable.areContentsTheSame(mOldData.get(oldItemPosition), mNewData.get(newItemPosition)); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return mDataComparable.areItemsTheSame(mOldData.get(oldItemPosition), mNewData.get(newItemPosition)); - } - - @Override - public int getNewListSize() { - return mNewData.size(); - } - - @Override - public int getOldListSize() { - return mOldData.size(); - } -} diff --git a/observablerm/src/main/java/me/silong/snappyadapter/RxRecyclerViewCallback.java b/observablerm/src/main/java/me/silong/snappyadapter/RxRecyclerViewCallback.java index abcd358..3dbeda5 100644 --- a/observablerm/src/main/java/me/silong/snappyadapter/RxRecyclerViewCallback.java +++ b/observablerm/src/main/java/me/silong/snappyadapter/RxRecyclerViewCallback.java @@ -3,7 +3,7 @@ import android.support.v7.widget.RecyclerView; /** - * Created by SILONG on 4/19/17. + * Created by SILONG on 4/24/17. */ public abstract class RxRecyclerViewCallback extends RxSortedListCallback { @@ -14,9 +14,10 @@ public RxRecyclerViewCallback(RecyclerView.Adapter adapter) { mAdapter = adapter; } + @Override public void onChanged(int position, int count) { - mAdapter.notifyItemRangeChanged(position, count); + mAdapter.notifyItemChanged(position, count); } @Override diff --git a/observablerm/src/main/java/me/silong/snappyadapter/RxSortedList.java b/observablerm/src/main/java/me/silong/snappyadapter/RxSortedList.java index 2e21d08..c3f7fd0 100644 --- a/observablerm/src/main/java/me/silong/snappyadapter/RxSortedList.java +++ b/observablerm/src/main/java/me/silong/snappyadapter/RxSortedList.java @@ -1,10 +1,6 @@ package me.silong.snappyadapter; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -13,208 +9,116 @@ import rx.schedulers.Schedulers; /** - * Created by SILONG on 4/18/17. + * Created by SILONG on 4/21/17. */ public class RxSortedList { - private static final int MIN_CAPACITY = 10; + private static Executor sExecutor = Executors.newSingleThreadExecutor(); - private static final Executor sExecutor = Executors.newSingleThreadExecutor(); + private SnappySortedList mSnappySortedList; - private final AsyncSortedList mSortedList; - - private final RxSortedListCallback mRxSortedListCallback; - - private List mSyncList; - - public RxSortedList(Class klass, RxSortedListCallback callback) { - this(klass, callback, MIN_CAPACITY); - } - - public RxSortedList(Class klass, RxSortedListCallback rxSortedListCallback, int initialCapacity) { - mSyncList = new ArrayList(initialCapacity); - mSortedList = new AsyncSortedList(klass, new AsyncSortedListCallback(rxSortedListCallback), initialCapacity); - mRxSortedListCallback = rxSortedListCallback; + public RxSortedList(Class klass, RxSortedListCallback callback) { + mSnappySortedList = new SnappySortedList(klass, callback); } - public RxSortedList(Class klass, RxSortedListCallback rxSortedListCallback, List initialList) { - mSyncList = new ArrayList(initialList); - Collections.sort(mSyncList, rxSortedListCallback); - mSyncList = new ArrayList(initialList); - mSortedList = new AsyncSortedList(klass, new AsyncSortedListCallback(rxSortedListCallback), mSyncList); - mRxSortedListCallback = rxSortedListCallback; - } - - public Observable clear() { - return Observable.fromCallable(() -> { - mSortedList.clear(); - return null; - }).subscribeOn(Schedulers.from(sExecutor)); + public RxSortedList(Class klass, RxSortedListCallback callback, int initialCapacity) { + mSnappySortedList = new SnappySortedList(klass, callback); } - public Observable set(List items) { - return Observable.defer(() -> { - Collections.sort(items, mRxSortedListCallback); - return RxDiffCallback.calculate(mRxSortedListCallback, mSortedList.getData(), items); - }) - .doOnNext(diffResultListPair -> { - mSortedList.set(items); - }) + private Observable.Transformer queueEvent() { + return dObservable -> Observable.just(null) .subscribeOn(Schedulers.from(sExecutor)) .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(diffResultListPair -> { - mSyncList.clear(); - mSyncList.addAll(diffResultListPair.second); - diffResultListPair.first.dispatchUpdatesTo(mRxSortedListCallback); - }) - .map(diffResult -> null); + .flatMap(o -> dObservable); + } + + public int size() { + return mSnappySortedList.size(); } public Observable add(T item) { - return Observable.fromCallable(() -> mSortedList.add(item)) - .subscribeOn(Schedulers.from(sExecutor)); + return Observable.fromCallable(() -> mSnappySortedList.add(item)) + .compose(queueEvent()); } public Observable addAll(T[] items, boolean mayModifyInput) { return Observable.fromCallable(() -> { - mSortedList.addAll(items, mayModifyInput); + mSnappySortedList.addAll(items, mayModifyInput); return null; - }).subscribeOn(Schedulers.from(sExecutor)); + }) + .compose(queueEvent()); } - public Observable addAll(T... items) { + public Observable addAll(T[] items) { return Observable.fromCallable(() -> { - mSortedList.addAll(items); + mSnappySortedList.addAll(items); return null; - }).subscribeOn(Schedulers.from(sExecutor)); + }) + .compose(queueEvent()); } public Observable addAll(Collection items) { return Observable.fromCallable(() -> { - mSortedList.addAll(items); + mSnappySortedList.addAll(items); return null; - }).subscribeOn(Schedulers.from(sExecutor)); + }).compose(queueEvent()); } - public Observable remove(T item) { - return Observable.fromCallable(() -> { - mSortedList.remove(item); - return null; - }).subscribeOn(Schedulers.from(sExecutor)); + public void beginBatchedUpdates() { + mSnappySortedList.beginBatchedUpdates(); } - public Observable removeAt(int index) { - return Observable.fromCallable(() -> { - mSortedList.removeItemAt(index); - return null; - }).subscribeOn(Schedulers.from(sExecutor)); + public void endBatchedUpdates() { + mSnappySortedList.endBatchedUpdates(); } - public Observable remove(int index, int count) { - return Observable.fromCallable(() -> { - for (int i = 0; i < count; i++) { - mSortedList.removeItemAt(i + index); - } - return null; - }).subscribeOn(Schedulers.from(sExecutor)); + public Observable remove(T item) { + return Observable.fromCallable(() -> mSnappySortedList.remove(item)).compose(queueEvent()); + } + + public Observable removeItemAt(int index) { + return Observable.fromCallable(() -> mSnappySortedList.removeItemAt(index)).compose(queueEvent()); } - public Observable remove(Collection collection) { + public Observable updateItemAt(int index, T item) { return Observable.fromCallable(() -> { - for (T t : collection) { - mSortedList.remove(t); - } + mSnappySortedList.updateItemAt(index, item); return null; - }).subscribeOn(Schedulers.from(sExecutor)); + }).compose(queueEvent()); } - public Observable updateItemAt(int index, T item) { + + public Observable recalculatePositionOfItemAt(int index) { return Observable.fromCallable(() -> { - mSortedList.updateItemAt(index, item); + mSnappySortedList.recalculatePositionOfItemAt(index); return null; - }).subscribeOn(Schedulers.from(sExecutor)); + }).compose(queueEvent()); + } + + + public T get(int index) throws IndexOutOfBoundsException { + return mSnappySortedList.get(index); } public Observable indexOf(T item) { - return Observable.fromCallable(() -> mSortedList.indexOf(item)) - .subscribeOn(Schedulers.from(sExecutor)); + return Observable.fromCallable(() -> mSnappySortedList.indexOf(item)).compose(queueEvent()); + } + + public Observable clear() { + return Observable.fromCallable(() -> { + mSnappySortedList.clear(); + return null; + }).compose(queueEvent()); } - public T getItemAt(int index) throws IndexOutOfBoundsException { - return mSyncList.get(index); + public Observable set(Collection items, boolean isSorted) { + return mSnappySortedList.set(items, isSorted) + .subscribeOn(Schedulers.from(sExecutor)); } - public int size() { - return mSyncList.size(); - } - - private class AsyncSortedListCallback extends AsyncSortedList.Callback { - - private final RxSortedListCallback mRxSortedListCallback; - - - AsyncSortedListCallback(RxSortedListCallback rxSortedListCallback) { - mRxSortedListCallback = rxSortedListCallback; - } - - @Override - public int compare(T o1, T o2) { - return mRxSortedListCallback.compare(o1, o2); - } - - @Override - public void onInserted(int position, T[] ts) { - Observable.fromCallable(() -> Arrays.asList(ts)) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(tList -> { - mSyncList.addAll(position, tList); - mRxSortedListCallback.onInserted(position, ts.length); - }); - } - - @Override - public void onRemoved(int position, int count) { - Observable.just(null) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(objects -> { - //remove range http://stackoverflow.com/questions/2289183/why-is-javas-abstractlists-removerange-method-protected - mSyncList.subList(position, position + count).clear(); - mRxSortedListCallback.onRemoved(position, count); - }); - } - - @Override - public void onMoved(int fromPosition, int toPosition) { - Observable.just(null) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(objects -> { - T t = mSyncList.remove(fromPosition); - mSyncList.add(toPosition, t); - mRxSortedListCallback.onMoved(fromPosition, toPosition); - }); - } - - @Override - public void onChanged(int position, T[] ts) { - Observable.just(null) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(tList -> { - for (int i = 0; i < ts.length; i++) { - mSyncList.set(position + i, ts[i]); - } - mRxSortedListCallback.onChanged(position, ts.length); - }); - } - - @Override - public boolean areContentsTheSame(T oldItem, T newItem) { - return mRxSortedListCallback.areContentsTheSame(oldItem, newItem); - } - - @Override - public boolean areItemsTheSame(T item1, T item2) { - return mRxSortedListCallback.areItemsTheSame(item1, item2); - } + public Observable set(Collection items) { + return mSnappySortedList.set(items) + .subscribeOn(Schedulers.from(sExecutor)); } } diff --git a/observablerm/src/main/java/me/silong/snappyadapter/RxSortedListCallback.java b/observablerm/src/main/java/me/silong/snappyadapter/RxSortedListCallback.java index 6ceceee..b2f9fcd 100644 --- a/observablerm/src/main/java/me/silong/snappyadapter/RxSortedListCallback.java +++ b/observablerm/src/main/java/me/silong/snappyadapter/RxSortedListCallback.java @@ -1,11 +1,151 @@ package me.silong.snappyadapter; -import android.support.v7.util.SortedList; +import android.support.v7.util.BatchingListUpdateCallback; +import android.support.v7.util.ListUpdateCallback; + +import java.util.Comparator; /** - * Created by SILONG on 4/19/17. + * The class that controls the behavior of the {@link android.support.v7.util.SortedList}. + *

+ * It defines how items should be sorted and how duplicates should be handled. + *

+ * SortedList calls the callback methods on this class to notify changes about the underlying + * data. */ +public abstract class RxSortedListCallback implements Comparator, ListUpdateCallback { + + /** + * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and + * return how they should be ordered. + * + * @param o1 The first object to compare. + * @param o2 The second object to compare. + * @return a negative integer, zero, or a positive integer as the + * first argument is less than, equal to, or greater than the + * second. + */ + @Override + abstract public int compare(T2 o1, T2 o2); + + /** + * Called by the SortedList when the item at the given position is updated. + * + * @param position The position of the item which has been updated. + * @param count The number of items which has changed. + */ + abstract public void onChanged(int position, int count); + + @Override + public void onChanged(int position, int count, Object payload) { + onChanged(position, count); + } + + /** + * Called by the SortedList when it wants to check whether two items have the same data + * or not. SortedList uses this information to decide whether it should call + * {@link #onChanged(int, int)} or not. + *

+ * SortedList uses this method to check equality instead of {@link Object#equals(Object)} + * so + * that you can change its behavior depending on your UI. + *

+ * For example, if you are using SortedList with a {@link android.support.v7.widget.RecyclerView.Adapter + * RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same or not. + * + * @param oldItem The previous representation of the object. + * @param newItem The new object that replaces the previous one. + * @return True if the contents of the items are the same or false if they are different. + */ + abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); + + /** + * Called by the SortedList to decide whether two object represent the same Item or not. + *

+ * For example, if your items have unique ids, this method should check their equality. + * + * @param item1 The first item to check. + * @param item2 The second item to check. + * @return True if the two items represent the same object or false if they are different. + */ + abstract public boolean areItemsTheSame(T2 item1, T2 item2); + + /** + * A callback implementation that can batch notify events dispatched by the SortedList. + *

+ * This class can be useful if you want to do multiple operations on a SortedList but don't + * want to dispatch each event one by one, which may result in a performance issue. + *

+ * For example, if you are going to add multiple items to a SortedList, BatchedCallback call + * convert individual onInserted(index, 1) calls into one + * onInserted(index, N) if items are added into consecutive indices. This change + * can help RecyclerView resolve changes much more easily. + *

+ * If consecutive changes in the SortedList are not suitable for batching, BatchingCallback + * dispatches them as soon as such case is detected. After your edits on the SortedList is + * complete, you must always call {@link android.support.v7.util.SortedList.BatchedCallback#dispatchLastEvent()} to flush + * all changes to the Callback. + */ + public static class BatchedCallback extends RxSortedListCallback { + + final RxSortedListCallback mWrappedCallback; + + final BatchingListUpdateCallback mBatchingListUpdateCallback; + + /** + * Creates a new BatchedCallback that wraps the provided Callback. + * + * @param wrappedCallback The Callback which should received the data change callbacks. + * Other method calls (e.g. {@link #compare(Object, Object)} from + * the SortedList are directly forwarded to this Callback. + */ + public BatchedCallback(RxSortedListCallback wrappedCallback) { + mWrappedCallback = wrappedCallback; + mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback); + } + + @Override + public int compare(T2 o1, T2 o2) { + return mWrappedCallback.compare(o1, o2); + } + + @Override + public void onInserted(int position, int count) { + mBatchingListUpdateCallback.onInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + mBatchingListUpdateCallback.onRemoved(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + mBatchingListUpdateCallback.onInserted(fromPosition, toPosition); + } + + @Override + public void onChanged(int position, int count) { + mBatchingListUpdateCallback.onChanged(position, count, null); + } + + @Override + public boolean areContentsTheSame(T2 oldItem, T2 newItem) { + return mWrappedCallback.areContentsTheSame(oldItem, newItem); + } -public abstract class RxSortedListCallback extends SortedList.Callback { + @Override + public boolean areItemsTheSame(T2 item1, T2 item2) { + return mWrappedCallback.areItemsTheSame(item1, item2); + } -} + /** + * This method dispatches any pending event notifications to the wrapped Callback. + * You must always call this method after you are done with editing the SortedList. + */ + public void dispatchLastEvent() { + mBatchingListUpdateCallback.dispatchLastEvent(); + } + } +} \ No newline at end of file diff --git a/observablerm/src/main/java/me/silong/snappyadapter/SnappyDiffCallback.java b/observablerm/src/main/java/me/silong/snappyadapter/SnappyDiffCallback.java new file mode 100644 index 0000000..6794063 --- /dev/null +++ b/observablerm/src/main/java/me/silong/snappyadapter/SnappyDiffCallback.java @@ -0,0 +1,50 @@ +package me.silong.snappyadapter; + +import android.support.v4.util.Pair; +import android.support.v7.util.DiffUtil; + +class SnappyDiffCallback extends DiffUtil.Callback { + + private final RxSortedListCallback mCallback; + + private final D[] mNewData; + + private final D[] mOldData; + + private final int mOldDataLength; + + private final int mNewDataLength; + + SnappyDiffCallback(RxSortedListCallback callback, D[] oldData, D[] newData, int oldDataLength, int newDataLength) { + mOldData = oldData; + mNewData = newData; + mCallback = callback; + mOldDataLength = oldDataLength; + mNewDataLength = newDataLength; + } + + public static Pair calculate(RxSortedListCallback callback, D[] oldData, D[] newData, + int oldDataLength, int newDataLength) { + return Pair.create(DiffUtil.calculateDiff(new SnappyDiffCallback<>(callback, oldData, newData, oldDataLength, newDataLength)), newData); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return mCallback.areContentsTheSame(mOldData[oldItemPosition], mNewData[newItemPosition]); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mCallback.areItemsTheSame(mOldData[oldItemPosition], mNewData[newItemPosition]); + } + + @Override + public int getNewListSize() { + return mNewDataLength; + } + + @Override + public int getOldListSize() { + return mOldDataLength; + } +} diff --git a/observablerm/src/main/java/me/silong/snappyadapter/SnappySortedList.java b/observablerm/src/main/java/me/silong/snappyadapter/SnappySortedList.java new file mode 100644 index 0000000..efdced9 --- /dev/null +++ b/observablerm/src/main/java/me/silong/snappyadapter/SnappySortedList.java @@ -0,0 +1,702 @@ +package me.silong.snappyadapter; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; + +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; + +/** + * Created by SILONG on 4/20/17. + */ + +/** + * A Sorted list implementation that can keep items in order and also notify for changes in the + * list + * such that it can be bound to a {@link android.support.v7.widget.RecyclerView.Adapter + * RecyclerView.Adapter}. + *

+ * It keeps items ordered using the {@link android.support.v7.util.SortedList.Callback#compare(Object, Object)} method and uses + * binary search to retrieve items. If the sorting criteria of your items may change, make sure you + * call appropriate methods while editing them to avoid data inconsistencies. + *

+ * You can control the order of items and change notifications via the {@link android.support.v7.util.SortedList.Callback} parameter. + */ +class SnappySortedList { + + + /** + * Used by {@link #indexOf(Object)} when he item cannot be found in the list. + */ + public static final int INVALID_POSITION = -1; + + private static final int MIN_CAPACITY = 10; + + private static final int CAPACITY_GROWTH = MIN_CAPACITY; + + private static final int INSERTION = 1; + + private static final int DELETION = 1 << 1; + + private static final int LOOKUP = 1 << 2; + + private final Class mTClass; + + T[] mData; + + /** + * A copy of the previous list contents used during the merge phase of addAll. + */ + private T[] mOldData; + + private int mOldDataStart; + + private int mOldDataSize; + + /** + * The size of the valid portion of mData during the merge phase of addAll. + */ + private int mMergedSize; + + /** + * The callback instance that controls the behavior of the SortedList and get notified when + * changes happen. + */ + private RxSortedListCallback mCallback; + + private RxSortedListCallback.BatchedCallback mBatchedCallback; + + private int mSize; + + /** + * Creates a new SortedList of type T. + * + * @param klass The class of the contents of the SortedList. + * @param callback The callback that controls the behavior of SortedList. + */ + public SnappySortedList(Class klass, RxSortedListCallback callback) { + this(klass, callback, MIN_CAPACITY); + } + + /** + * Creates a new SortedList of type T. + * + * @param klass The class of the contents of the SortedList. + * @param callback The callback that controls the behavior of SortedList. + * @param initialCapacity The initial capacity to hold items. + */ + public SnappySortedList(Class klass, RxSortedListCallback callback, int initialCapacity) { + mTClass = klass; + mData = (T[]) Array.newInstance(klass, initialCapacity); + mCallback = callback; + mSize = 0; + } + + /** + * The number of items in the list. + * + * @return The number of items in the list. + */ + public int size() { + return mSize; + } + + /** + * Adds the given item to the list. If this is a new item, SortedList calls + * {@link android.support.v7.util.SortedList.Callback#onInserted(int, int)}. + *

+ * If the item already exists in the list and its sorting criteria is not changed, it is + * replaced with the existing Item. SortedList uses + * {@link android.support.v7.util.SortedList.Callback#areItemsTheSame(Object, Object)} to check if two items are the same item + * and uses {@link android.support.v7.util.SortedList.Callback#areContentsTheSame(Object, Object)} to decide whether it should + * call {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)} or not. In both cases, it always removes the + * reference to the old item and puts the new item into the backing array even if + * {@link android.support.v7.util.SortedList.Callback#areContentsTheSame(Object, Object)} returns false. + *

+ * If the sorting criteria of the item is changed, SortedList won't be able to find + * its duplicate in the list which will result in having a duplicate of the Item in the list. + * If you need to update sorting criteria of an item that already exists in the list, + * use {@link #updateItemAt(int, Object)}. You can find the index of the item using + * {@link #indexOf(Object)} before you update the object. + * + * @param item The item to be added into the list. + * @return The index of the newly added item. + * @see {@link android.support.v7.util.SortedList.Callback#compare(Object, Object)} + * @see {@link android.support.v7.util.SortedList.Callback#areItemsTheSame(Object, Object)} + * @see {@link android.support.v7.util.SortedList.Callback#areContentsTheSame(Object, Object)}} + */ + public int add(T item) { + throwIfMerging(); + return add(item, true); + } + + /** + * Adds the given items to the list. Equivalent to calling {@link android.support.v7.util.SortedList#add} in a loop, + * except the callback events may be in a different order/granularity since addAll can batch + * them for better performance. + *

+ * If allowed, may modify the input array and even take the ownership over it in order + * to avoid extra memory allocation during sorting and deduplication. + *

+ * + * @param items Array of items to be added into the list. + * @param mayModifyInput If true, SortedList is allowed to modify the input. + * @see {@link android.support.v7.util.SortedList#addAll(Object[] items)}. + */ + public void addAll(T[] items, boolean mayModifyInput) { + throwIfMerging(); + if (items.length == 0) { + return; + } + if (mayModifyInput) { + addAllInternal(items); + } else { + T[] copy = (T[]) Array.newInstance(mTClass, items.length); + System.arraycopy(items, 0, copy, 0, items.length); + addAllInternal(copy); + } + + } + + /** + * Adds the given items to the list. Does not modify the input. + * + * @param items Array of items to be added into the list. + * @see {@link android.support.v7.util.SortedList#addAll(T[] items, boolean mayModifyInput)} + */ + public void addAll(T... items) { + addAll(items, false); + } + + /** + * Adds the given items to the list. Does not modify the input. + * + * @param items Collection of items to be added into the list. + * @see {@link android.support.v7.util.SortedList#addAll(T[] items, boolean mayModifyInput)} + */ + public void addAll(Collection items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.size()); + addAll(items.toArray(copy), true); + } + + /** + * Set items using DiffUtil + * + * @param items Collection of items to be set into the list. + */ + public Observable set(Collection items) { + return set(items, false); + } + + /** + * Set items using DiffUtil + * + * @param items Collection of items to be set into the list. + * @param isSorted whether items is sorted or not + */ + public Observable set(Collection items, boolean isSorted) { + return Observable.fromCallable(() -> { + T[] newItems = (T[]) Array.newInstance(mTClass, items.size()); + T[] oldItems = (T[]) Array.newInstance(mTClass, mData.length); + System.arraycopy(mData, 0, oldItems, 0, mData.length); + items.toArray(newItems); + if (!isSorted) { + Arrays.sort(newItems, mCallback); + } + return SnappyDiffCallback.calculate(mCallback, oldItems, newItems, mSize, items.size()); + }) + .observeOn(AndroidSchedulers.mainThread()) + .map(diffResultPair -> { + mData = diffResultPair.second; + mSize = diffResultPair.second.length; + diffResultPair.first.dispatchUpdatesTo(mCallback); + return null; + }); + } + + private void addAllInternal(T[] newItems) { + final boolean forceBatchedUpdates = !(mCallback instanceof RxSortedListCallback.BatchedCallback); + if (forceBatchedUpdates) { + beginBatchedUpdates(); + } + + mOldData = mData; + mOldDataStart = 0; + mOldDataSize = mSize; + + Arrays.sort(newItems, mCallback); // Arrays.sort is stable. + + final int newSize = deduplicate(newItems); + if (mSize == 0) { + mData = newItems; + mSize = newSize; + mMergedSize = newSize; + mCallback.onInserted(0, newSize); + } else { + merge(newItems, newSize); + } + + mOldData = null; + + if (forceBatchedUpdates) { + endBatchedUpdates(); + } + } + + /** + * Remove duplicate items, leaving only the last item from each group of "same" items. + * Move the remaining items to the beginning of the array. + * + * @return Number of deduplicated items at the beginning of the array. + */ + private int deduplicate(T[] items) { + if (items.length == 0) { + throw new IllegalArgumentException("Input array must be non-empty"); + } + + // Keep track of the range of equal items at the end of the output. + // Start with the range containing just the first item. + int rangeStart = 0; + int rangeEnd = 1; + + for (int i = 1; i < items.length; ++i) { + T currentItem = items[i]; + + int compare = mCallback.compare(items[rangeStart], currentItem); + if (compare > 0) { + throw new IllegalArgumentException("Input must be sorted in ascending order."); + } + + if (compare == 0) { + // The range of equal items continues, update it. + final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); + if (sameItemPos != INVALID_POSITION) { + // Replace the duplicate item. + items[sameItemPos] = currentItem; + } else { + // Expand the range. + if (rangeEnd != i) { // Avoid redundant copy. + items[rangeEnd] = currentItem; + } + rangeEnd++; + } + } else { + // The range has ended. Reset it to contain just the current item. + if (rangeEnd != i) { // Avoid redundant copy. + items[rangeEnd] = currentItem; + } + rangeStart = rangeEnd++; + } + } + return rangeEnd; + } + + + private int findSameItem(T item, T[] items, int from, int to) { + for (int pos = from; pos < to; pos++) { + if (mCallback.areItemsTheSame(items[pos], item)) { + return pos; + } + } + return INVALID_POSITION; + } + + /** + * This method assumes that newItems are sorted and deduplicated. + */ + private void merge(T[] newData, int newDataSize) { + final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; + mData = (T[]) Array.newInstance(mTClass, mergedCapacity); + mMergedSize = 0; + + int newDataStart = 0; + while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) { + if (mOldDataStart == mOldDataSize) { + // No more old items, copy the remaining new items. + int itemCount = newDataSize - newDataStart; + System.arraycopy(newData, newDataStart, mData, mMergedSize, itemCount); + mMergedSize += itemCount; + mSize += itemCount; + mCallback.onInserted(mMergedSize - itemCount, itemCount); + break; + } + + if (newDataStart == newDataSize) { + // No more new items, copy the remaining old items. + int itemCount = mOldDataSize - mOldDataStart; + System.arraycopy(mOldData, mOldDataStart, mData, mMergedSize, itemCount); + mMergedSize += itemCount; + break; + } + + T oldItem = mOldData[mOldDataStart]; + T newItem = newData[newDataStart]; + int compare = mCallback.compare(oldItem, newItem); + if (compare > 0) { + // New item is lower, output it. + mData[mMergedSize++] = newItem; + mSize++; + newDataStart++; + mCallback.onInserted(mMergedSize - 1, 1); + } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) { + // Items are the same. Output the new item, but consume both. + mData[mMergedSize++] = newItem; + newDataStart++; + mOldDataStart++; + if (!mCallback.areContentsTheSame(oldItem, newItem)) { + mCallback.onChanged(mMergedSize - 1, 1); + } + } else { + // Old item is lower than or equal to (but not the same as the new). Output it. + // New item with the same sort order will be inserted later. + mData[mMergedSize++] = oldItem; + mOldDataStart++; + } + } + } + + private void throwIfMerging() { + if (mOldData != null) { + throw new IllegalStateException("Cannot call this method from within addAll"); + } + } + + /** + * Batches adapter updates that happen between calling this method until calling + * {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop + * and they are placed into consecutive indices, SortedList calls + * {@link android.support.v7.util.SortedList.Callback#onInserted(int, int)} only once with the proper item count. If an event + * cannot be merged with the previous event, the previous event is dispatched + * to the callback instantly. + *

+ * After running your data updates, you must call {@link #endBatchedUpdates()} + * which will dispatch any deferred data change event to the current callback. + *

+ * A sample implementation may look like this: + *

+   *     mSortedList.beginBatchedUpdates();
+   *     try {
+   *         mSortedList.add(item1)
+   *         mSortedList.add(item2)
+   *         mSortedList.remove(item3)
+   *         ...
+   *     } finally {
+   *         mSortedList.endBatchedUpdates();
+   *     }
+   * 
+ *

+ * Instead of using this method to batch calls, you can use a Callback that extends + * {@link android.support.v7.util.SortedList.BatchedCallback}. In that case, you must make sure that you are manually calling + * {@link android.support.v7.util.SortedList.BatchedCallback#dispatchLastEvent()} right after you complete your data changes. + * Failing to do so may create data inconsistencies with the Callback. + *

+ * If the current Callback in an instance of {@link android.support.v7.util.SortedList.BatchedCallback}, calling this method + * has no effect. + */ + public void beginBatchedUpdates() { + throwIfMerging(); + if (mCallback instanceof RxSortedListCallback.BatchedCallback) { + return; + } + if (mBatchedCallback == null) { + mBatchedCallback = new RxSortedListCallback.BatchedCallback(mCallback); + } + mCallback = mBatchedCallback; + } + + /** + * Ends the update transaction and dispatches any remaining event to the callback. + */ + public void endBatchedUpdates() { + throwIfMerging(); + if (mCallback instanceof RxSortedListCallback.BatchedCallback) { + ((RxSortedListCallback.BatchedCallback) mCallback).dispatchLastEvent(); + } + if (mCallback == mBatchedCallback) { + mCallback = mBatchedCallback.mWrappedCallback; + } + } + + private int add(T item, boolean notify) { + int index = findIndexOf(item, mData, 0, mSize, INSERTION); + if (index == INVALID_POSITION) { + index = 0; + } else if (index < mSize) { + T existing = mData[index]; + if (mCallback.areItemsTheSame(existing, item)) { + if (mCallback.areContentsTheSame(existing, item)) { + //no change but still replace the item + mData[index] = item; + return index; + } else { + mData[index] = item; + mCallback.onChanged(index, 1); + return index; + } + } + } + addToData(index, item); + if (notify) { + mCallback.onInserted(index, 1); + } + return index; + } + + /** + * Removes the provided item from the list and calls {@link android.support.v7.util.SortedList.Callback#onRemoved(int, int)}. + * + * @param item The item to be removed from the list. + * @return True if item is removed, false if item cannot be found in the list. + */ + public boolean remove(T item) { + throwIfMerging(); + return remove(item, true); + } + + /** + * Removes the item at the given index and calls {@link android.support.v7.util.SortedList.Callback#onRemoved(int, int)}. + * + * @param index The index of the item to be removed. + * @return The removed item. + */ + public T removeItemAt(int index) { + throwIfMerging(); + T item = get(index); + removeItemAtIndex(index, true); + return item; + } + + private boolean remove(T item, boolean notify) { + int index = findIndexOf(item, mData, 0, mSize, DELETION); + if (index == INVALID_POSITION) { + return false; + } + removeItemAtIndex(index, notify); + return true; + } + + private void removeItemAtIndex(int index, boolean notify) { + System.arraycopy(mData, index + 1, mData, index, mSize - index - 1); + mSize--; + mData[mSize] = null; + if (notify) { + mCallback.onRemoved(index, 1); + } + } + + /** + * Updates the item at the given index and calls {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)} and/or + * {@link android.support.v7.util.SortedList.Callback#onMoved(int, int)} if necessary. + *

+ * You can use this method if you need to change an existing Item such that its position in the + * list may change. + *

+ * If the new object is a different object (get(index) != item) and + * {@link android.support.v7.util.SortedList.Callback#areContentsTheSame(Object, Object)} returns true, SortedList + * avoids calling {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)} otherwise it calls + * {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)}. + *

+ * If the new position of the item is different than the provided index, + * SortedList + * calls {@link android.support.v7.util.SortedList.Callback#onMoved(int, int)}. + * + * @param index The index of the item to replace + * @param item The item to replace the item at the given Index. + * @see #add(Object) + */ + public void updateItemAt(int index, T item) { + throwIfMerging(); + final T existing = get(index); + // assume changed if the same object is given back + boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); + if (existing != item) { + // different items, we can use comparison and may avoid lookup + final int cmp = mCallback.compare(existing, item); + if (cmp == 0) { + mData[index] = item; + if (contentsChanged) { + mCallback.onChanged(index, 1); + } + return; + } + } + if (contentsChanged) { + mCallback.onChanged(index, 1); + } + // TODO this done in 1 pass to avoid shifting twice. + removeItemAtIndex(index, false); + int newIndex = add(item, false); + if (index != newIndex) { + mCallback.onMoved(index, newIndex); + } + } + + /** + * This method can be used to recalculate the position of the item at the given index, without + * triggering an {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)} callback. + *

+ * If you are editing objects in the list such that their position in the list may change but + * you don't want to trigger an onChange animation, you can use this method to re-position it. + * If the item changes position, SortedList will call {@link android.support.v7.util.SortedList.Callback#onMoved(int, int)} + * without + * calling {@link android.support.v7.util.SortedList.Callback#onChanged(int, int)}. + *

+ * A sample usage may look like: + * + *

+   *     final int position = mSortedList.indexOf(item);
+   *     item.incrementPriority(); // assume items are sorted by priority
+   *     mSortedList.recalculatePositionOfItemAt(position);
+   * 
+ * In the example above, because the sorting criteria of the item has been changed, + * mSortedList.indexOf(item) will not be able to find the item. This is why the code above + * first + * gets the position before editing the item, edits it and informs the SortedList that item + * should be repositioned. + * + * @param index The current index of the Item whose position should be re-calculated. + * @see #updateItemAt(int, Object) + * @see #add(Object) + */ + public void recalculatePositionOfItemAt(int index) { + throwIfMerging(); + // TODO can be improved + final T item = get(index); + removeItemAtIndex(index, false); + int newIndex = add(item, false); + if (index != newIndex) { + mCallback.onMoved(index, newIndex); + } + } + + /** + * Returns the item at the given index. + * + * @param index The index of the item to retrieve. + * @return The item at the given index. + * @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the + * size of the list. + */ + public T get(int index) throws IndexOutOfBoundsException { + if (index >= mSize || index < 0) { + throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is " + + mSize); + } + if (mOldData != null) { + // The call is made from a callback during addAll execution. The data is split + // between mData and mOldData. + if (index >= mMergedSize) { + return mOldData[index - mMergedSize + mOldDataStart]; + } + } + return mData[index]; + } + + /** + * Returns the position of the provided item. + * + * @param item The item to query for position. + * @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the + * list. + */ + public int indexOf(T item) { + if (mOldData != null) { + int index = findIndexOf(item, mData, 0, mMergedSize, LOOKUP); + if (index != INVALID_POSITION) { + return index; + } + index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP); + if (index != INVALID_POSITION) { + return index - mOldDataStart + mMergedSize; + } + return INVALID_POSITION; + } + return findIndexOf(item, mData, 0, mSize, LOOKUP); + } + + private int findIndexOf(T item, T[] mData, int left, int right, int reason) { + while (left < right) { + final int middle = (left + right) / 2; + T myItem = mData[middle]; + final int cmp = mCallback.compare(myItem, item); + if (cmp < 0) { + left = middle + 1; + } else if (cmp == 0) { + if (mCallback.areItemsTheSame(myItem, item)) { + return middle; + } else { + int exact = linearEqualitySearch(item, middle, left, right); + if (reason == INSERTION) { + return exact == INVALID_POSITION ? middle : exact; + } else { + return exact; + } + } + } else { + right = middle; + } + } + return reason == INSERTION ? left : INVALID_POSITION; + } + + private int linearEqualitySearch(T item, int middle, int left, int right) { + // go left + for (int next = middle - 1; next >= left; next--) { + T nextItem = mData[next]; + int cmp = mCallback.compare(nextItem, item); + if (cmp != 0) { + break; + } + if (mCallback.areItemsTheSame(nextItem, item)) { + return next; + } + } + for (int next = middle + 1; next < right; next++) { + T nextItem = mData[next]; + int cmp = mCallback.compare(nextItem, item); + if (cmp != 0) { + break; + } + if (mCallback.areItemsTheSame(nextItem, item)) { + return next; + } + } + return INVALID_POSITION; + } + + private void addToData(int index, T item) { + if (index > mSize) { + throw new IndexOutOfBoundsException( + "cannot add item to " + index + " because size is " + mSize); + } + if (mSize == mData.length) { + // we are at the limit enlarge + T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH); + System.arraycopy(mData, 0, newData, 0, index); + newData[index] = item; + System.arraycopy(mData, index, newData, index + 1, mSize - index); + mData = newData; + } else { + // just shift, we fit + System.arraycopy(mData, index, mData, index + 1, mSize - index); + mData[index] = item; + } + mSize++; + } + + /** + * Removes all items from the SortedList. + */ + public void clear() { + throwIfMerging(); + if (mSize == 0) { + return; + } + final int prevSize = mSize; + Arrays.fill(mData, 0, prevSize, null); + mSize = 0; + mCallback.onRemoved(0, prevSize); + } + +} \ No newline at end of file