, ListUpdateCallback {
17 |
18 | /**
19 | * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and
20 | * return how they should be ordered.
21 | *
22 | * @param o1 The first object to compare.
23 | * @param o2 The second object to compare.
24 | * @return a negative integer, zero, or a positive integer as the
25 | * first argument is less than, equal to, or greater than the
26 | * second.
27 | */
28 | @Override
29 | abstract public int compare(T2 o1, T2 o2);
30 |
31 | /**
32 | * Called by the SortedList when the item at the given position is updated.
33 | *
34 | * @param position The position of the item which has been updated.
35 | * @param count The number of items which has changed.
36 | */
37 | abstract public void onChanged(int position, int count);
38 |
39 | @Override
40 | public void onChanged(int position, int count, Object payload) {
41 | onChanged(position, count);
42 | }
43 |
44 | /**
45 | * Called by the SortedList when it wants to check whether two items have the same data
46 | * or not. SortedList uses this information to decide whether it should call
47 | * {@link #onChanged(int, int)} or not.
48 | *
49 | * SortedList uses this method to check equality instead of {@link Object#equals(Object)}
50 | * so
51 | * that you can change its behavior depending on your UI.
52 | *
53 | * For example, if you are using SortedList with a RecyclerView.Adapter, you should
54 | * return whether the items' visual representations are the same or not.
55 | *
56 | * @param oldItem The previous representation of the object.
57 | * @param newItem The new object that replaces the previous one.
58 | * @return True if the contents of the items are the same or false if they are different.
59 | */
60 | abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem);
61 |
62 | /**
63 | * Called by the SortedList to decide whether two object represent the same Item or not.
64 | *
65 | * For example, if your items have unique ids, this method should check their equality.
66 | *
67 | * @param item1 The first item to check.
68 | * @param item2 The second item to check.
69 | * @return True if the two items represent the same object or false if they are different.
70 | */
71 | abstract public boolean areItemsTheSame(T2 item1, T2 item2);
72 |
73 | /**
74 | * A callback implementation that can batch notify events dispatched by the SortedList.
75 | *
76 | * This class can be useful if you want to do multiple operations on a SortedList but don't
77 | * want to dispatch each event one by one, which may result in a performance issue.
78 | *
79 | * For example, if you are going to add multiple items to a SortedList, BatchedCallback call
80 | * convert individual onInserted(index, 1) calls into one
81 | * onInserted(index, N) if items are added into consecutive indices. This change
82 | * can help RecyclerView resolve changes much more easily.
83 | *
84 | * If consecutive changes in the SortedList are not suitable for batching, BatchingCallback
85 | * dispatches them as soon as such case is detected. After your edits on the SortedList is
86 | * complete, you must always call {@link me.silong.snappyadapter.RxSortedListCallback.BatchedCallback#dispatchLastEvent()} to flush
87 | * all changes to the Callback.
88 | */
89 | public static final class BatchedCallback extends RxSortedListCallback {
90 |
91 | final RxSortedListCallback mWrappedCallback;
92 |
93 | final BatchingListUpdateCallback mBatchingListUpdateCallback;
94 |
95 | /**
96 | * Creates a new BatchedCallback that wraps the provided Callback.
97 | *
98 | * @param wrappedCallback The Callback which should received the data change callbacks.
99 | * Other method calls (e.g. {@link #compare(Object, Object)} from
100 | * the SortedList are directly forwarded to this Callback.
101 | */
102 | public BatchedCallback(RxSortedListCallback wrappedCallback) {
103 | mWrappedCallback = wrappedCallback;
104 | mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback);
105 | }
106 |
107 | @Override
108 | public int compare(T2 o1, T2 o2) {
109 | return mWrappedCallback.compare(o1, o2);
110 | }
111 |
112 | @Override
113 | public void onInserted(int position, int count) {
114 | mBatchingListUpdateCallback.onInserted(position, count);
115 | }
116 |
117 | @Override
118 | public void onRemoved(int position, int count) {
119 | mBatchingListUpdateCallback.onRemoved(position, count);
120 | }
121 |
122 | @Override
123 | public void onMoved(int fromPosition, int toPosition) {
124 | mBatchingListUpdateCallback.onInserted(fromPosition, toPosition);
125 | }
126 |
127 | @Override
128 | public void onChanged(int position, int count) {
129 | mBatchingListUpdateCallback.onChanged(position, count, null);
130 | }
131 |
132 | @Override
133 | public boolean areContentsTheSame(T2 oldItem, T2 newItem) {
134 | return mWrappedCallback.areContentsTheSame(oldItem, newItem);
135 | }
136 |
137 | @Override
138 | public boolean areItemsTheSame(T2 item1, T2 item2) {
139 | return mWrappedCallback.areItemsTheSame(item1, item2);
140 | }
141 |
142 | /**
143 | * This method dispatches any pending event notifications to the wrapped Callback.
144 | * You must always call this method after you are done with editing the SortedList.
145 | */
146 | public void dispatchLastEvent() {
147 | mBatchingListUpdateCallback.dispatchLastEvent();
148 | }
149 | }
150 | }
--------------------------------------------------------------------------------
/observablerm/src/test/java/me/silong/observablerm/ObservableAdapterManagerTest.java:
--------------------------------------------------------------------------------
1 | package me.silong.observablerm;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 | import java.util.concurrent.Executors;
9 | import java.util.concurrent.TimeUnit;
10 |
11 | import rx.Observable;
12 | import rx.Scheduler;
13 | import rx.Subscriber;
14 | import rx.Subscription;
15 | import rx.android.plugins.RxAndroidPlugins;
16 | import rx.android.plugins.RxAndroidSchedulersHook;
17 | import rx.schedulers.Schedulers;
18 |
19 | /**
20 | * Created by SILONG on 11/3/16.
21 | */
22 | public class ObservableAdapterManagerTest {
23 |
24 | private static List generateTestData(int count) {
25 | List testDatas = new ArrayList<>(count);
26 | for (int i = 0; i < count; i++) {
27 | testDatas.add(new TestData("id" + i, "name" + i));
28 | }
29 | return testDatas;
30 | }
31 |
32 | @Before
33 | public void setup() {
34 | RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
35 | @Override
36 | public Scheduler getMainThreadScheduler() {
37 | return Schedulers.from(Executors.newSingleThreadExecutor());
38 | }
39 | });
40 | }
41 |
42 | @Test
43 | public void testScheduler() throws Exception {
44 | Scheduler scheduler = Schedulers.newThread();
45 | Subscription subscription = Observable.interval(100, TimeUnit.MILLISECONDS, scheduler)
46 | .doOnNext(aLong -> System.out.println("onNext 1:" + aLong))
47 | .map(aLong -> {
48 | for (int i = 0; i < 100000000; i++) {
49 | aLong += i;
50 | aLong ^= i;
51 | }
52 | return aLong;
53 | })
54 | .doOnNext(aLong -> System.out.println("onNext 2:" + aLong + "-" + System.currentTimeMillis() / 1000))
55 | .subscribeOn(scheduler)
56 | .doOnTerminate(() -> System.out.println("terminated"))
57 | .doOnUnsubscribe(() -> System.out.println("unsubscribed-" + System.currentTimeMillis() / 1000))
58 | .doOnCompleted(() -> System.out.println("completed-" + System.currentTimeMillis() / 1000))
59 | .subscribe(new Subscriber() {
60 | @Override
61 | public void onCompleted() {
62 |
63 | }
64 |
65 | @Override
66 | public void onError(Throwable e) {
67 |
68 | }
69 |
70 | @Override
71 | public void onNext(Long aLong) {
72 | if (!isUnsubscribed()) {
73 | System.out.println("onNext event 1:" + aLong);
74 | }
75 | System.out.println("onNext event 2:" + aLong);
76 | }
77 | });
78 | Thread.sleep(3200);
79 | subscription.unsubscribe();
80 | Thread.sleep(2000);
81 | }
82 |
83 | @Test(timeout = 5000)
84 | public void testQueueingEvent() throws Exception {
85 | // ObservableAdapterManager observableAdapterManager = new ObservableAdapterManager(null, new ArrayList<>(),
86 | // new DataComparable() {
87 | // @Override
88 | // public boolean areContentsTheSame(TestData oldData, TestData newData) {
89 | // return oldData.name.equals(newData.name);
90 | // }
91 | //
92 | // @Override
93 | // public boolean areItemsTheSame(TestData oldData, TestData newData) {
94 | // return oldData.id.equals(newData.id);
95 | // }
96 | // });
97 | // PublishSubject> subject = PublishSubject.create();
98 | // TestSubscriber testSubscriber = new TestSubscriber<>();
99 | // subject
100 | // .flatMap(testData -> observableAdapterManager.setItems(testData))
101 | // .subscribe(testSubscriber);
102 | // subject.onNext(generateTestData(100));
103 | // Thread.sleep(5);
104 | // subject.onNext(generateTestData(0));
105 | // Thread.sleep(5);
106 | // subject.onNext(generateTestData(101));
107 | // Thread.sleep(5);
108 | // subject.onNext(generateTestData(0));
109 | // Thread.sleep(5);
110 | // subject.onNext(generateTestData(102));
111 | // Thread.sleep(5);
112 | // subject.onNext(generateTestData(0));
113 | // Thread.sleep(5);
114 | // subject.onNext(generateTestData(200));
115 | // Thread.sleep(5);
116 | // subject.onNext(generateTestData(0));
117 | // Thread.sleep(5);
118 | // subject.onNext(generateTestData(201));
119 | // subject.onNext(generateTestData(200));
120 | // Thread.sleep(5);
121 | // subject.onNext(generateTestData(0));
122 | // Thread.sleep(5);
123 | // subject.onNext(generateTestData(201));
124 | // subject.onNext(generateTestData(200));
125 | // Thread.sleep(5);
126 | // subject.onNext(generateTestData(0));
127 | // Thread.sleep(5);
128 | // subject.onNext(generateTestData(201));
129 | // Thread.sleep(2000);
130 | // // subject.onCompleted();
131 | // // testSubscriber.awaitTerminalEvent();
132 | // assertThat(testSubscriber.getOnNextEvents().size(), equalTo(15));
133 | }
134 |
135 | @Test(timeout = 5000)
136 | public void testClearData() throws Exception {
137 | // ObservableAdapterManager observableAdapterManager = new ObservableAdapterManager(null, new ArrayList<>(),
138 | // new DataComparable() {
139 | // @Override
140 | // public boolean areContentsTheSame(TestData oldData, TestData newData) {
141 | // return oldData.name.equals(newData.name);
142 | // }
143 | //
144 | // @Override
145 | // public boolean areItemsTheSame(TestData oldData, TestData newData) {
146 | // return oldData.id.equals(newData.id);
147 | // }
148 | // });
149 | // PublishSubject> subject = PublishSubject.create();
150 | // TestSubscriber testSubscriber = new TestSubscriber<>();
151 | // subject
152 | // .flatMap(testData -> observableAdapterManager.setItems(testData))
153 | // .doOnNext(aVoid -> System.out.println("onNext event"))
154 | // .doOnUnsubscribe(() -> System.out.println("onUnsubscribed"))
155 | // .subscribe(testSubscriber);
156 | // subject.onNext(generateTestData(100));
157 | // subject.onNext(generateTestData(0));
158 | // subject.onNext(generateTestData(101));
159 | // subject.onNext(generateTestData(0));
160 | // subject.onNext(generateTestData(102));
161 | // subject.onNext(generateTestData(0));
162 | // subject.onNext(generateTestData(200));
163 | // subject.onNext(generateTestData(0));
164 | // subject.onNext(generateTestData(201));
165 | // subject.onNext(generateTestData(200));
166 | // subject.onNext(generateTestData(0));
167 | // subject.onNext(generateTestData(201));
168 | // subject.onNext(generateTestData(200));
169 | // subject.onNext(generateTestData(0));
170 | // subject.onNext(generateTestData(201));
171 | // Thread.sleep(15);
172 | // observableAdapterManager.clearEvents();
173 | // Thread.sleep(2000);
174 | // System.out.println("event count:" + testSubscriber.getValueCount());
175 | }
176 |
177 | private static class TestData {
178 |
179 | String id;
180 |
181 | String name;
182 |
183 | public TestData(String id, String name) {
184 | this.id = id;
185 | this.name = name;
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/app/src/main/java/com/silong/snappyrecycleradapter/RecyclerViewActivity.java:
--------------------------------------------------------------------------------
1 | package com.silong.snappyrecycleradapter;
2 |
3 | import com.silong.snappyrecycleradapter.adapter.RegularRecyclerViewAdapter;
4 | import com.silong.snappyrecycleradapter.adapter.RxUserRecyclerViewAdapter;
5 | import com.silong.snappyrecycleradapter.adapter.SortedListRecyclerViewAdapter;
6 | import com.silong.snappyrecycleradapter.adapter.SyncList;
7 | import com.silong.snappyrecycleradapter.model.DataFactory;
8 | import com.silong.snappyrecycleradapter.model.User;
9 |
10 | import android.content.Context;
11 | import android.content.Intent;
12 | import android.os.Bundle;
13 | import android.support.annotation.Nullable;
14 | import android.support.v7.app.AppCompatActivity;
15 | import android.support.v7.widget.LinearLayoutManager;
16 | import android.support.v7.widget.RecyclerView;
17 | import android.view.Menu;
18 | import android.view.MenuItem;
19 |
20 |
21 | /**
22 | * Created by SILONG on 8/28/16.
23 | */
24 | public class RecyclerViewActivity extends AppCompatActivity {
25 |
26 | private static final String MODE = "mode";
27 |
28 | private static final int MODE_REGULAR = 2;
29 |
30 | private static final int MODE_SORTED_LIST = 1;
31 |
32 | private static final int MODE_RXSORTED_LIST = 0;
33 |
34 | private RxUserRecyclerViewAdapter mRxUserRecyclerViewAdapter;
35 |
36 | private SyncList mSyncAdapter;
37 |
38 | public static Intent newRxSortedList(Context context, String name) {
39 | Intent intent = new Intent(context, RecyclerViewActivity.class);
40 | intent.putExtra(MODE, MODE_RXSORTED_LIST);
41 | intent.putExtra("name", name);
42 | return intent;
43 | }
44 |
45 | public static Intent newSortedIntent(Context context, String name) {
46 | Intent intent = new Intent(context, RecyclerViewActivity.class);
47 | intent.putExtra(MODE, MODE_SORTED_LIST);
48 | intent.putExtra("name", name);
49 | return intent;
50 | }
51 |
52 | public static Intent newRegularIntent(Context context, String name) {
53 | Intent intent = new Intent(context, RecyclerViewActivity.class);
54 | intent.putExtra(MODE, MODE_REGULAR);
55 | intent.putExtra("name", name);
56 | return intent;
57 | }
58 |
59 | @Override
60 | protected void onCreate(@Nullable Bundle savedInstanceState) {
61 | super.onCreate(savedInstanceState);
62 | setTitle(getIntent().getStringExtra("name"));
63 | RecyclerView recyclerView = new RecyclerView(this);
64 | setContentView(recyclerView);
65 | recyclerView.setLayoutManager(new LinearLayoutManager(this));
66 | int mode = getIntent().getIntExtra(MODE, MODE_REGULAR);
67 | DataFactory.fakeUsersToSet(DataFactory.CHUNK).subscribe(users -> {
68 | switch (mode) {
69 | case MODE_REGULAR:
70 | RegularRecyclerViewAdapter regularRecyclerViewAdapter = new RegularRecyclerViewAdapter();
71 | mSyncAdapter = regularRecyclerViewAdapter;
72 | mSyncAdapter.set(users);
73 | recyclerView.setAdapter(regularRecyclerViewAdapter);
74 | break;
75 | case MODE_SORTED_LIST:
76 | SortedListRecyclerViewAdapter sortedListRecyclerViewAdapter = new SortedListRecyclerViewAdapter();
77 | mSyncAdapter = sortedListRecyclerViewAdapter;
78 | mSyncAdapter.set(users);
79 | recyclerView.setAdapter(sortedListRecyclerViewAdapter);
80 | break;
81 | case MODE_RXSORTED_LIST:
82 | mRxUserRecyclerViewAdapter = RxUserRecyclerViewAdapter.newAdapter();
83 | mRxUserRecyclerViewAdapter.getObservableAdapterManager().set(users).subscribe();
84 | recyclerView.setAdapter(mRxUserRecyclerViewAdapter);
85 | break;
86 | }
87 | });
88 | }
89 |
90 | @Override
91 | public boolean onOptionsItemSelected(MenuItem item) {
92 | switch (item.getItemId()) {
93 | case R.id.action_add_multi:
94 | if (mSyncAdapter != null) {
95 | DataFactory.fakeUsersToAddOrUpdate(mSyncAdapter.getItemCount(), DataFactory.CHUNK)
96 | .subscribe(users -> {
97 | mSyncAdapter.add(users);
98 | });
99 | } else {
100 | DataFactory.fakeUsersToAddOrUpdate(mRxUserRecyclerViewAdapter.getItemCount(), DataFactory.CHUNK)
101 | .flatMap(users -> mRxUserRecyclerViewAdapter.getObservableAdapterManager().addAll(users))
102 | .subscribe();
103 | }
104 | break;
105 | case R.id.action_add_multi_at_specific_index:
106 | if (mSyncAdapter != null) {
107 | DataFactory.fakeUsersToAddOrUpdate(mSyncAdapter.getItemCount(), DataFactory.CHUNK)
108 | .subscribe(users -> {
109 | mSyncAdapter
110 | .add(users);
111 | });
112 | } else {
113 | DataFactory.fakeUsersToAddOrUpdate(mRxUserRecyclerViewAdapter.getItemCount(), DataFactory.CHUNK)
114 | .flatMap(users -> mRxUserRecyclerViewAdapter.getObservableAdapterManager()
115 | .addAll(users))
116 | .subscribe();
117 | }
118 | break;
119 | case R.id.action_add_single:
120 | if (mSyncAdapter != null) {
121 | DataFactory.fakeUsersToAddOrUpdate(mSyncAdapter.getItemCount(), 1)
122 | .subscribe(users -> {
123 | mSyncAdapter.add(users.get(0));
124 | });
125 | } else {
126 | DataFactory.fakeUsersToAddOrUpdate(mRxUserRecyclerViewAdapter.getItemCount(), 1)
127 | .map(users -> users.get(0))
128 | .flatMap(user -> mRxUserRecyclerViewAdapter.getObservableAdapterManager()
129 | .add(user))
130 | .subscribe();
131 | }
132 | break;
133 | case R.id.action_clear:
134 | if (mSyncAdapter != null) {
135 | mSyncAdapter.clear();
136 | } else {
137 | mRxUserRecyclerViewAdapter.getObservableAdapterManager().clear().subscribe();
138 | }
139 | break;
140 | case R.id.action_remove_one_item:
141 | if (mSyncAdapter != null) {
142 | mSyncAdapter.remove((int) (Math.random() * mSyncAdapter.getItemCount() - 1));
143 | } else {
144 | mRxUserRecyclerViewAdapter.getObservableAdapterManager()
145 | .removeItemAt((int) (Math.random() * mRxUserRecyclerViewAdapter.getItemCount() - 1))
146 | .subscribe();
147 | }
148 | break;
149 | case R.id.action_set_items:
150 | if (mSyncAdapter != null) {
151 | DataFactory.fakeUsersToSet(DataFactory.CHUNK)
152 | .subscribe(users -> {
153 | mSyncAdapter.set(users);
154 | });
155 | } else {
156 | DataFactory.fakeUsersToSet(DataFactory.CHUNK)
157 | .flatMap(users -> mRxUserRecyclerViewAdapter.getObservableAdapterManager().set(users))
158 | .subscribe();
159 | }
160 | break;
161 | case R.id.action_set_one_item:
162 | int i = (int) (Math.random() * 100);
163 | if (mSyncAdapter != null) {
164 | mSyncAdapter.set(new User("User_" + i, "custom_name " + Math.random(), 100, User.Gender.male),
165 | (int) (Math.random() * mSyncAdapter.getItemCount() - 1));
166 | } else {
167 | mRxUserRecyclerViewAdapter.getObservableAdapterManager().updateItemAt(
168 | (int) (Math.random() * mRxUserRecyclerViewAdapter.getItemCount() - 1),
169 | new User("User_" + i, "custom_name" + Math.random(), 100, User.Gender.male)).subscribe();
170 | }
171 | break;
172 | }
173 | return super.onOptionsItemSelected(item);
174 | }
175 |
176 | @Override
177 | public boolean onCreateOptionsMenu(Menu menu) {
178 | getMenuInflater().inflate(R.menu.menu_action, menu);
179 | return super.onCreateOptionsMenu(menu);
180 | }
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/observablerm/src/main/java/me/silong/snappyadapter/SnappySortedList.java:
--------------------------------------------------------------------------------
1 | package me.silong.snappyadapter;
2 |
3 | import java.lang.reflect.Array;
4 | import java.util.Arrays;
5 | import java.util.Collection;
6 |
7 | import rx.Observable;
8 | import rx.android.schedulers.AndroidSchedulers;
9 |
10 | /**
11 | * Created by SILONG on 4/20/17.
12 | */
13 |
14 | class SnappySortedList {
15 |
16 | /**
17 | * Used by {@link #indexOf(Object)} when he item cannot be found in the list.
18 | */
19 | public static final int INVALID_POSITION = -1;
20 |
21 | private static final int MIN_CAPACITY = 10;
22 |
23 | private static final int CAPACITY_GROWTH = MIN_CAPACITY;
24 |
25 | private static final int INSERTION = 1;
26 |
27 | private static final int DELETION = 1 << 1;
28 |
29 | private static final int LOOKUP = 1 << 2;
30 |
31 | private final Class mTClass;
32 |
33 | T[] mData;
34 |
35 | private T[] mOldData;
36 |
37 | private int mOldDataStart;
38 |
39 | private int mOldDataSize;
40 |
41 | private int mMergedSize;
42 |
43 | private RxSortedListCallback mCallback;
44 |
45 | private RxSortedListCallback.BatchedCallback mBatchedCallback;
46 |
47 | private int mSize;
48 |
49 | public SnappySortedList(Class klass, RxSortedListCallback callback) {
50 | this(klass, callback, MIN_CAPACITY);
51 | }
52 |
53 | public SnappySortedList(Class klass, RxSortedListCallback callback, int initialCapacity) {
54 | mTClass = klass;
55 | mData = (T[]) Array.newInstance(klass, initialCapacity);
56 | mCallback = callback;
57 | mSize = 0;
58 | }
59 |
60 | public int size() {
61 | return mSize;
62 | }
63 |
64 | public int add(T item) {
65 | throwIfMerging();
66 | return add(item, true);
67 | }
68 |
69 | public void addAll(T[] items, boolean mayModifyInput) {
70 | throwIfMerging();
71 | if (items.length == 0) {
72 | return;
73 | }
74 | if (mayModifyInput) {
75 | addAllInternal(items);
76 | } else {
77 | T[] copy = (T[]) Array.newInstance(mTClass, items.length);
78 | System.arraycopy(items, 0, copy, 0, items.length);
79 | addAllInternal(copy);
80 | }
81 |
82 | }
83 |
84 | public void addAll(T... items) {
85 | addAll(items, false);
86 | }
87 |
88 | public void addAll(Collection items) {
89 | T[] copy = (T[]) Array.newInstance(mTClass, items.size());
90 | addAll(items.toArray(copy), true);
91 | }
92 |
93 | public Observable set(Collection items) {
94 | return set(items, false);
95 | }
96 |
97 | public Observable set(Collection items, boolean isSorted) {
98 | return Observable.fromCallable(() -> {
99 | T[] newItems = (T[]) Array.newInstance(mTClass, items.size());
100 | T[] oldItems = (T[]) Array.newInstance(mTClass, mData.length);
101 | System.arraycopy(mData, 0, oldItems, 0, mData.length);
102 | items.toArray(newItems);
103 | if (!isSorted) {
104 | Arrays.sort(newItems, mCallback);
105 | }
106 | return SnappyDiffCallback.calculate(mCallback, oldItems, newItems, mSize, items.size());
107 | })
108 | .observeOn(AndroidSchedulers.mainThread())
109 | .map(diffResultPair -> {
110 | mData = diffResultPair.second;
111 | mSize = diffResultPair.second.length;
112 | diffResultPair.first.dispatchUpdatesTo(mCallback);
113 | return null;
114 | });
115 | }
116 |
117 | private void addAllInternal(T[] newItems) {
118 | final boolean forceBatchedUpdates = !(mCallback instanceof RxSortedListCallback.BatchedCallback);
119 | if (forceBatchedUpdates) {
120 | beginBatchedUpdates();
121 | }
122 |
123 | mOldData = mData;
124 | mOldDataStart = 0;
125 | mOldDataSize = mSize;
126 |
127 | Arrays.sort(newItems, mCallback); // Arrays.sort is stable.
128 |
129 | final int newSize = deduplicate(newItems);
130 | if (mSize == 0) {
131 | mData = newItems;
132 | mSize = newSize;
133 | mMergedSize = newSize;
134 | mCallback.onInserted(0, newSize);
135 | } else {
136 | merge(newItems, newSize);
137 | }
138 |
139 | mOldData = null;
140 |
141 | if (forceBatchedUpdates) {
142 | endBatchedUpdates();
143 | }
144 | }
145 |
146 | private int deduplicate(T[] items) {
147 | if (items.length == 0) {
148 | throw new IllegalArgumentException("Input array must be non-empty");
149 | }
150 |
151 | // Keep track of the range of equal items at the end of the output.
152 | // Start with the range containing just the first item.
153 | int rangeStart = 0;
154 | int rangeEnd = 1;
155 |
156 | for (int i = 1; i < items.length; ++i) {
157 | T currentItem = items[i];
158 |
159 | int compare = mCallback.compare(items[rangeStart], currentItem);
160 | if (compare > 0) {
161 | throw new IllegalArgumentException("Input must be sorted in ascending order.");
162 | }
163 |
164 | if (compare == 0) {
165 | // The range of equal items continues, update it.
166 | final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd);
167 | if (sameItemPos != INVALID_POSITION) {
168 | // Replace the duplicate item.
169 | items[sameItemPos] = currentItem;
170 | } else {
171 | // Expand the range.
172 | if (rangeEnd != i) { // Avoid redundant copy.
173 | items[rangeEnd] = currentItem;
174 | }
175 | rangeEnd++;
176 | }
177 | } else {
178 | // The range has ended. Reset it to contain just the current item.
179 | if (rangeEnd != i) { // Avoid redundant copy.
180 | items[rangeEnd] = currentItem;
181 | }
182 | rangeStart = rangeEnd++;
183 | }
184 | }
185 | return rangeEnd;
186 | }
187 |
188 |
189 | private int findSameItem(T item, T[] items, int from, int to) {
190 | for (int pos = from; pos < to; pos++) {
191 | if (mCallback.areItemsTheSame(items[pos], item)) {
192 | return pos;
193 | }
194 | }
195 | return INVALID_POSITION;
196 | }
197 |
198 | /**
199 | * This method assumes that newItems are sorted and deduplicated.
200 | */
201 | private void merge(T[] newData, int newDataSize) {
202 | final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH;
203 | mData = (T[]) Array.newInstance(mTClass, mergedCapacity);
204 | mMergedSize = 0;
205 |
206 | int newDataStart = 0;
207 | while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) {
208 | if (mOldDataStart == mOldDataSize) {
209 | // No more old items, copy the remaining new items.
210 | int itemCount = newDataSize - newDataStart;
211 | System.arraycopy(newData, newDataStart, mData, mMergedSize, itemCount);
212 | mMergedSize += itemCount;
213 | mSize += itemCount;
214 | mCallback.onInserted(mMergedSize - itemCount, itemCount);
215 | break;
216 | }
217 |
218 | if (newDataStart == newDataSize) {
219 | // No more new items, copy the remaining old items.
220 | int itemCount = mOldDataSize - mOldDataStart;
221 | System.arraycopy(mOldData, mOldDataStart, mData, mMergedSize, itemCount);
222 | mMergedSize += itemCount;
223 | break;
224 | }
225 |
226 | T oldItem = mOldData[mOldDataStart];
227 | T newItem = newData[newDataStart];
228 | int compare = mCallback.compare(oldItem, newItem);
229 | if (compare > 0) {
230 | // New item is lower, output it.
231 | mData[mMergedSize++] = newItem;
232 | mSize++;
233 | newDataStart++;
234 | mCallback.onInserted(mMergedSize - 1, 1);
235 | } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) {
236 | // Items are the same. Output the new item, but consume both.
237 | mData[mMergedSize++] = newItem;
238 | newDataStart++;
239 | mOldDataStart++;
240 | if (!mCallback.areContentsTheSame(oldItem, newItem)) {
241 | mCallback.onChanged(mMergedSize - 1, 1);
242 | }
243 | } else {
244 | // Old item is lower than or equal to (but not the same as the new). Output it.
245 | // New item with the same sort order will be inserted later.
246 | mData[mMergedSize++] = oldItem;
247 | mOldDataStart++;
248 | }
249 | }
250 | }
251 |
252 | private void throwIfMerging() {
253 | if (mOldData != null) {
254 | throw new IllegalStateException("Cannot call this method from within addAll");
255 | }
256 | }
257 |
258 | public void beginBatchedUpdates() {
259 | throwIfMerging();
260 | if (mCallback instanceof RxSortedListCallback.BatchedCallback) {
261 | return;
262 | }
263 | if (mBatchedCallback == null) {
264 | mBatchedCallback = new RxSortedListCallback.BatchedCallback(mCallback);
265 | }
266 | mCallback = mBatchedCallback;
267 | }
268 |
269 | public void endBatchedUpdates() {
270 | throwIfMerging();
271 | if (mCallback instanceof RxSortedListCallback.BatchedCallback) {
272 | ((RxSortedListCallback.BatchedCallback) mCallback).dispatchLastEvent();
273 | }
274 | if (mCallback == mBatchedCallback) {
275 | mCallback = mBatchedCallback.mWrappedCallback;
276 | }
277 | }
278 |
279 | private int add(T item, boolean notify) {
280 | int index = findIndexOf(item, mData, 0, mSize, INSERTION);
281 | if (index == INVALID_POSITION) {
282 | index = 0;
283 | } else if (index < mSize) {
284 | T existing = mData[index];
285 | if (mCallback.areItemsTheSame(existing, item)) {
286 | if (mCallback.areContentsTheSame(existing, item)) {
287 | //no change but still replace the item
288 | mData[index] = item;
289 | return index;
290 | } else {
291 | mData[index] = item;
292 | mCallback.onChanged(index, 1);
293 | return index;
294 | }
295 | }
296 | }
297 | addToData(index, item);
298 | if (notify) {
299 | mCallback.onInserted(index, 1);
300 | }
301 | return index;
302 | }
303 |
304 | public boolean remove(T item) {
305 | throwIfMerging();
306 | return remove(item, true);
307 | }
308 |
309 | public T removeItemAt(int index) {
310 | throwIfMerging();
311 | T item = get(index);
312 | removeItemAtIndex(index, true);
313 | return item;
314 | }
315 |
316 | private boolean remove(T item, boolean notify) {
317 | int index = findIndexOf(item, mData, 0, mSize, DELETION);
318 | if (index == INVALID_POSITION) {
319 | return false;
320 | }
321 | removeItemAtIndex(index, notify);
322 | return true;
323 | }
324 |
325 | private void removeItemAtIndex(int index, boolean notify) {
326 | System.arraycopy(mData, index + 1, mData, index, mSize - index - 1);
327 | mSize--;
328 | mData[mSize] = null;
329 | if (notify) {
330 | mCallback.onRemoved(index, 1);
331 | }
332 | }
333 |
334 | public void updateItemAt(int index, T item) {
335 | throwIfMerging();
336 | final T existing = get(index);
337 | // assume changed if the same object is given back
338 | boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item);
339 | if (existing != item) {
340 | // different items, we can use comparison and may avoid lookup
341 | final int cmp = mCallback.compare(existing, item);
342 | if (cmp == 0) {
343 | mData[index] = item;
344 | if (contentsChanged) {
345 | mCallback.onChanged(index, 1);
346 | }
347 | return;
348 | }
349 | }
350 | if (contentsChanged) {
351 | mCallback.onChanged(index, 1);
352 | }
353 | // TODO this done in 1 pass to avoid shifting twice.
354 | removeItemAtIndex(index, false);
355 | int newIndex = add(item, false);
356 | if (index != newIndex) {
357 | mCallback.onMoved(index, newIndex);
358 | }
359 | }
360 |
361 | public void recalculatePositionOfItemAt(int index) {
362 | throwIfMerging();
363 | // TODO can be improved
364 | final T item = get(index);
365 | removeItemAtIndex(index, false);
366 | int newIndex = add(item, false);
367 | if (index != newIndex) {
368 | mCallback.onMoved(index, newIndex);
369 | }
370 | }
371 |
372 | public T get(int index) throws IndexOutOfBoundsException {
373 | if (index >= mSize || index < 0) {
374 | throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is "
375 | + mSize);
376 | }
377 | if (mOldData != null) {
378 | // The call is made from a callback during addAll execution. The data is split
379 | // between mData and mOldData.
380 | if (index >= mMergedSize) {
381 | return mOldData[index - mMergedSize + mOldDataStart];
382 | }
383 | }
384 | return mData[index];
385 | }
386 |
387 | public int indexOf(T item) {
388 | if (mOldData != null) {
389 | int index = findIndexOf(item, mData, 0, mMergedSize, LOOKUP);
390 | if (index != INVALID_POSITION) {
391 | return index;
392 | }
393 | index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP);
394 | if (index != INVALID_POSITION) {
395 | return index - mOldDataStart + mMergedSize;
396 | }
397 | return INVALID_POSITION;
398 | }
399 | return findIndexOf(item, mData, 0, mSize, LOOKUP);
400 | }
401 |
402 | private int findIndexOf(T item, T[] mData, int left, int right, int reason) {
403 | while (left < right) {
404 | final int middle = (left + right) / 2;
405 | T myItem = mData[middle];
406 | final int cmp = mCallback.compare(myItem, item);
407 | if (cmp < 0) {
408 | left = middle + 1;
409 | } else if (cmp == 0) {
410 | if (mCallback.areItemsTheSame(myItem, item)) {
411 | return middle;
412 | } else {
413 | int exact = linearEqualitySearch(item, middle, left, right);
414 | if (reason == INSERTION) {
415 | return exact == INVALID_POSITION ? middle : exact;
416 | } else {
417 | return exact;
418 | }
419 | }
420 | } else {
421 | right = middle;
422 | }
423 | }
424 | return reason == INSERTION ? left : INVALID_POSITION;
425 | }
426 |
427 | private int linearEqualitySearch(T item, int middle, int left, int right) {
428 | // go left
429 | for (int next = middle - 1; next >= left; next--) {
430 | T nextItem = mData[next];
431 | int cmp = mCallback.compare(nextItem, item);
432 | if (cmp != 0) {
433 | break;
434 | }
435 | if (mCallback.areItemsTheSame(nextItem, item)) {
436 | return next;
437 | }
438 | }
439 | for (int next = middle + 1; next < right; next++) {
440 | T nextItem = mData[next];
441 | int cmp = mCallback.compare(nextItem, item);
442 | if (cmp != 0) {
443 | break;
444 | }
445 | if (mCallback.areItemsTheSame(nextItem, item)) {
446 | return next;
447 | }
448 | }
449 | return INVALID_POSITION;
450 | }
451 |
452 | private void addToData(int index, T item) {
453 | if (index > mSize) {
454 | throw new IndexOutOfBoundsException(
455 | "cannot add item to " + index + " because size is " + mSize);
456 | }
457 | if (mSize == mData.length) {
458 | // we are at the limit enlarge
459 | T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH);
460 | System.arraycopy(mData, 0, newData, 0, index);
461 | newData[index] = item;
462 | System.arraycopy(mData, index, newData, index + 1, mSize - index);
463 | mData = newData;
464 | } else {
465 | // just shift, we fit
466 | System.arraycopy(mData, index, mData, index + 1, mSize - index);
467 | mData[index] = item;
468 | }
469 | mSize++;
470 | }
471 |
472 | /**
473 | * Removes all items from the SortedList.
474 | */
475 | public void clear() {
476 | throwIfMerging();
477 | if (mSize == 0) {
478 | return;
479 | }
480 | final int prevSize = mSize;
481 | Arrays.fill(mData, 0, prevSize, null);
482 | mSize = 0;
483 | mCallback.onRemoved(0, prevSize);
484 | }
485 |
486 | }
--------------------------------------------------------------------------------