Utility class to calculate difference between two lists.
Change-Id: I8a5b661f4ad6dc0851f72fae63a2187f91fc8cb8
diff --git a/api/current.txt b/api/current.txt
index c7303c6..d72ea41 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8807,6 +8807,41 @@
field public static final int HINT_SCROLL_NONE = 0; // 0x0
}
+ public class BatchingListUpdateCallback implements android.support.v7.util.ListUpdateCallback {
+ ctor public BatchingListUpdateCallback(android.support.v7.util.ListUpdateCallback);
+ method public void dispatchLastEvent();
+ method public void onChanged(int, int, java.lang.Object);
+ method public void onInserted(int, int);
+ method public void onMoved(int, int);
+ method public void onRemoved(int, int);
+ }
+
+ public class DiffUtil {
+ method public static android.support.v7.util.DiffUtil.DiffResult calculateDiff(android.support.v7.util.DiffUtil.Callback);
+ method public static android.support.v7.util.DiffUtil.DiffResult calculateDiff(android.support.v7.util.DiffUtil.Callback, boolean);
+ }
+
+ public static abstract class DiffUtil.Callback {
+ ctor public DiffUtil.Callback();
+ method public abstract boolean areContentsTheSame(int, int);
+ method public abstract boolean areItemsTheSame(int, int);
+ method public java.lang.Object getChangePayload(int, int);
+ method public abstract int getNewListSize();
+ method public abstract int getOldListSize();
+ }
+
+ public static class DiffUtil.DiffResult {
+ method public void dispatchUpdatesTo(android.support.v7.widget.RecyclerView.Adapter);
+ method public void dispatchUpdatesTo(android.support.v7.util.ListUpdateCallback);
+ }
+
+ public abstract interface ListUpdateCallback {
+ method public abstract void onChanged(int, int, java.lang.Object);
+ method public abstract void onInserted(int, int);
+ method public abstract void onMoved(int, int);
+ method public abstract void onRemoved(int, int);
+ }
+
public class SortedList {
ctor public SortedList(java.lang.Class<T>, android.support.v7.util.SortedList.Callback<T>);
ctor public SortedList(java.lang.Class<T>, android.support.v7.util.SortedList.Callback<T>, int);
@@ -8839,15 +8874,13 @@
method public void onRemoved(int, int);
}
- public static abstract class SortedList.Callback implements java.util.Comparator {
+ public static abstract class SortedList.Callback implements java.util.Comparator android.support.v7.util.ListUpdateCallback {
ctor public SortedList.Callback();
method public abstract boolean areContentsTheSame(T2, T2);
method public abstract boolean areItemsTheSame(T2, T2);
method public abstract int compare(T2, T2);
method public abstract void onChanged(int, int);
- method public abstract void onInserted(int, int);
- method public abstract void onMoved(int, int);
- method public abstract void onRemoved(int, int);
+ method public void onChanged(int, int, java.lang.Object);
}
}
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index a75879e..066afc3 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -407,6 +407,15 @@
</intent-filter>
</activity>
+ <activity android:name=".util.DiffUtilActivity"
+ android:label="@string/diff_util_activity"
+ android:theme="@style/Theme.AppCompat">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+ </intent-filter>
+ </activity>
+
<activity android:name=".widget.GridLayoutManagerActivity"
android:label="@string/grid_layout_manager"
android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index edafbd1..4bf046e 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -167,6 +167,7 @@
<string name="palette_all_colors">Full color palette</string>
<string name="search_hint">Search...</string>
<string name="sorted_list_activity">RecyclerView/Sorted List</string>
+ <string name="diff_util_activity">RecyclerView/Diff Util</string>
<string name="add_new_item">Add New Item</string>
<string name="start_action_mode">Start Action Mode</string>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java
new file mode 100644
index 0000000..304f4af
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/util/DiffUtilActivity.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.android.supportv7.util;
+
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.widget.adapter.SimpleStringAdapter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A sample activity that demonstrates usage if {@link android.support.v7.util.DiffUtil} with
+ * a RecyclerView.
+ */
+public class DiffUtilActivity extends AppCompatActivity {
+ private Random mRandom = new Random(System.nanoTime());
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ LinearLayout ll = new LinearLayout(this);
+ RecyclerView rv = new RecyclerView(this);
+ Button shuffle = new Button(this);
+ shuffle.setText("Shuffle");
+ ll.addView(shuffle);
+ ll.addView(rv);
+ rv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ rv.setLayoutManager(new LinearLayoutManager(this));
+ List<String> cheeseList = createRandomCheeseList(Collections.<String>emptyList(), 50);
+ final SimpleStringAdapter adapter =
+ new SimpleStringAdapter(this, cheeseList.toArray(new String[cheeseList.size()]));
+ rv.setAdapter(adapter);
+ final AtomicBoolean refreshingList = new AtomicBoolean(false);
+ shuffle.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (refreshingList.getAndSet(true)) {
+ // already refreshing, do not allow modifications
+ return;
+ }
+ //noinspection unchecked
+ new AsyncTask<List<String>, Void, Pair<List<String>, DiffUtil.DiffResult>>() {
+
+ @Override
+ protected Pair<List<String>, DiffUtil.DiffResult> doInBackground(
+ List<String>... lists) {
+ List<String> oldList = lists[0];
+ List<String> newList = createRandomCheeseList(oldList, 5);
+ DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
+ new MyCallback(oldList, newList));
+ //noinspection unchecked
+ return new Pair(newList, diffResult);
+ }
+
+ @Override
+ protected void onPostExecute(
+ Pair<List<String>, DiffUtil.DiffResult> resultPair) {
+ refreshingList.set(false);
+ adapter.setValues(resultPair.first);
+ resultPair.second.dispatchUpdatesTo(adapter);
+ Toast.makeText(DiffUtilActivity.this, "new list size "
+ + resultPair.first.size(), Toast.LENGTH_SHORT).show();
+ }
+ }.execute(adapter.getValues());
+
+ }
+ });
+ setContentView(ll);
+ }
+
+ private static class MyCallback extends DiffUtil.Callback {
+ private final List<String> mOld;
+ private final List<String> mNew;
+
+ public MyCallback(List<String> old, List<String> aNew) {
+ mOld = old;
+ mNew = aNew;
+ }
+
+ @Override
+ public int getOldListSize() {
+ return mOld.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return mNew.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
+ // for strings, content equality is the same as identitiy equality since we don't have
+ // duplicates in this sample.
+ return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
+ return mOld.get(oldItemPosition).equals(mNew.get(newItemPosition));
+ }
+ }
+
+ private List<String> createRandomCheeseList(List<String> seed, int iterations) {
+ List<String> output = new ArrayList<>();
+ output.addAll(seed);
+ for (int i = 0; i < iterations; i++) {
+ switch (mRandom.nextInt(3)) {
+ case 0: //add
+ output.add(mRandom.nextInt(1 + output.size()), getRandomCheese(output));
+ break;
+ case 1: // remove
+ if (output.size() > 0) {
+ output.remove(mRandom.nextInt(output.size()));
+ }
+ break;
+ case 2: // move
+ if (output.size() > 0) {
+ int from = mRandom.nextInt(output.size());
+ int to = mRandom.nextInt(output.size());
+ output.add(to, output.remove(from));
+ }
+ break;
+ }
+ }
+ return output;
+ }
+
+ private String getRandomCheese(List<String> excludes) {
+ String chosen = Cheeses.sCheeseStrings[mRandom.nextInt(Cheeses.sCheeseStrings.length)];
+ while (excludes.contains(chosen)) {
+ chosen = Cheeses.sCheeseStrings[mRandom.nextInt(Cheeses.sCheeseStrings.length)];
+ }
+ return chosen;
+ }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
index 49f1e196..04161ea 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
@@ -25,12 +25,13 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
public class SimpleStringAdapter extends RecyclerView.Adapter<SimpleStringAdapter.ViewHolder> {
private int mBackground;
- private ArrayList<String> mValues;
+ private List<String> mValues;
public static class ViewHolder extends RecyclerView.ViewHolder {
public String mBoundString;
@@ -110,4 +111,12 @@
public int getItemCount() {
return mValues.size();
}
+
+ public List<String> getValues() {
+ return mValues;
+ }
+
+ public void setValues(List<String> values) {
+ mValues = values;
+ }
}
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index 7a6391e..52ec972 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -13,6 +13,7 @@
exclude module: 'support-annotations'
}
testCompile 'junit:junit:4.12'
+ testCompile "org.mockito:mockito-core:1.9.5"
androidTestCompile "org.mockito:mockito-core:1.9.5"
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"
@@ -32,7 +33,7 @@
main.res.srcDirs 'res', 'res-public'
androidTest.setRoot('tests')
- test.java.srcDir 'jvm-tests'
+ test.java.srcDir 'jvm-tests/src'
androidTest.java.srcDir 'tests/src'
androidTest.res.srcDir 'tests/res'
androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java
new file mode 100644
index 0000000..851bad6
--- /dev/null
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/util/BatchingListUpdateCallbackTest.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import static org.mockito.Mockito.*;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class BatchingListUpdateCallbackTest {
+ BatchingListUpdateCallback mBatching;
+ ListUpdateCallback mCallback;
+
+ @Before
+ public void setup() {
+ mCallback = mock(ListUpdateCallback.class);
+ mBatching = new BatchingListUpdateCallback(mCallback);
+ }
+
+ @Test
+ public void addSimple() {
+ mBatching.onInserted(3, 2);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onInserted(3, 2);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void addToSamePos() {
+ mBatching.onInserted(3, 2);
+ mBatching.onInserted(3, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onInserted(3, 3);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void addInsidePrevious() {
+ mBatching.onInserted(3, 5);
+ mBatching.onInserted(5, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onInserted(3, 6);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void addBefore() {
+ mBatching.onInserted(3, 5);
+ mBatching.onInserted(2, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onInserted(3, 5);
+ verify(mCallback).onInserted(2, 1);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeSimple() {
+ mBatching.onRemoved(3, 2);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(3, 2);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeSamePosition() {
+ mBatching.onRemoved(3, 2);
+ mBatching.onRemoved(3, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(3, 3);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeInside() {
+ mBatching.onRemoved(3, 5);
+ mBatching.onRemoved(4, 2);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(3, 5);
+ verify(mCallback).onRemoved(4, 2);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeBefore() {
+ mBatching.onRemoved(3, 2);
+ mBatching.onRemoved(2, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(2, 3);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeBefore2() {
+ mBatching.onRemoved(3, 2);
+ mBatching.onRemoved(2, 4);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(2, 6);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void removeBefore3() {
+ mBatching.onRemoved(3, 2);
+ mBatching.onRemoved(1, 1);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onRemoved(3, 2);
+ verify(mCallback).onRemoved(1, 1);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void moveSimple() {
+ mBatching.onMoved(3, 2);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onMoved(3, 2);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void moveTwice() {
+ mBatching.onMoved(3, 2);
+ mBatching.onMoved(5, 6);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onMoved(3, 2);
+ verify(mCallback).onMoved(5, 6);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeSimple() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeConsecutive() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(5, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 4, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeTheSame() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(4, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 3, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeTheSame2() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(3, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeBefore() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(2, 1, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(2, 3, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeBeforeOverlap() {
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(2, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(2, 3, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeSimpleWithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, payload);
+ }
+
+ @Test
+ public void changeConsecutiveWithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(5, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 4, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeTheSameWithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(4, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 3, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeTheSame2WithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(3, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeBeforeWithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(2, 1, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(2, 3, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeBeforeOverlapWithPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(2, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(2, 3, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeWithNewPayload() {
+ Object payload1 = new Object();
+ Object payload2 = new Object();
+ mBatching.onChanged(3, 2, payload1);
+ mBatching.onChanged(2, 2, payload2);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, payload1);
+ verify(mCallback).onChanged(2, 2, payload2);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeWithEmptyPayload() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, payload);
+ mBatching.onChanged(2, 2, null);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, payload);
+ verify(mCallback).onChanged(2, 2, null);
+ verifyNoMoreInteractions(mCallback);
+ }
+
+ @Test
+ public void changeWithEmptyPayload2() {
+ Object payload = new Object();
+ mBatching.onChanged(3, 2, null);
+ mBatching.onChanged(2, 2, payload);
+ mBatching.dispatchLastEvent();
+ verify(mCallback).onChanged(3, 2, null);
+ verify(mCallback).onChanged(2, 2, payload);
+ verifyNoMoreInteractions(mCallback);
+ }
+}
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java
new file mode 100644
index 0000000..bf9c7b8
--- /dev/null
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/util/DiffUtilTest.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.annotation.Nullable;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class DiffUtilTest {
+ private static Random sRand = new Random(System.nanoTime());
+ private List<Item> mBefore = new ArrayList<>();
+ private List<Item> mAfter = new ArrayList<>();
+ private StringBuilder mLog = new StringBuilder();
+
+ private DiffUtil.Callback mCallback = new DiffUtil.Callback() {
+ @Override
+ public int getOldListSize() {
+ return mBefore.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return mAfter.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldItemIndex, int newItemIndex) {
+ return mBefore.get(oldItemIndex).id == mAfter.get(newItemIndex).id;
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldItemIndex, int newItemIndex) {
+ assertThat(mBefore.get(oldItemIndex).id,
+ CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
+ return mBefore.get(oldItemIndex).data.equals(mAfter.get(newItemIndex).data);
+ }
+
+ @Nullable
+ @Override
+ public Object getChangePayload(int oldItemIndex, int newItemIndex) {
+ assertThat(mBefore.get(oldItemIndex).id,
+ CoreMatchers.equalTo(mAfter.get(newItemIndex).id));
+ assertThat(mBefore.get(oldItemIndex).data,
+ not(CoreMatchers.equalTo(mAfter.get(newItemIndex).data)));
+ return mAfter.get(newItemIndex).payload;
+ }
+ };
+
+ @Rule
+ public TestWatcher mLogOnExceptionWatcher = new TestWatcher() {
+ @Override
+ protected void failed(Throwable e, Description description) {
+ System.err.println(mLog.toString());
+ }
+ };
+
+
+ @Test
+ public void testNoChange() {
+ initWithSize(5);
+ check();
+ }
+
+ @Test
+ public void testAddItems() {
+ initWithSize(2);
+ add(1);
+ check();
+ }
+
+ @Test
+ @LargeTest
+ public void testRandom() {
+ for (int x = 0; x < 100; x++) {
+ for (int i = 0; i < 100; i++) {
+ for (int j = 2; j < 40; j++) {
+ testRandom(i, j);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGen2() {
+ initWithSize(5);
+ add(5);
+ delete(3);
+ delete(1);
+ check();
+ }
+
+ @Test
+ public void testGen3() {
+ initWithSize(5);
+ add(0);
+ delete(1);
+ delete(3);
+ check();
+ }
+
+ @Test
+ public void testGen4() {
+ initWithSize(5);
+ add(5);
+ add(1);
+ add(4);
+ add(4);
+ check();
+ }
+
+ @Test
+ public void testGen5() {
+ initWithSize(5);
+ delete(0);
+ delete(2);
+ add(0);
+ add(2);
+ check();
+ }
+
+ @Test
+ public void testGen6() {
+ initWithSize(2);
+ delete(0);
+ delete(0);
+ check();
+ }
+
+ @Test
+ public void testGen7() {
+ initWithSize(3);
+ move(2, 0);
+ delete(2);
+ add(2);
+ check();
+ }
+
+ @Test
+ public void testGen8() {
+ initWithSize(3);
+ delete(1);
+ add(0);
+ move(2, 0);
+ check();
+ }
+
+ @Test
+ public void testGen9() {
+ initWithSize(2);
+ add(2);
+ move(0, 2);
+ check();
+ }
+
+ @Test
+ public void testGen10() {
+ initWithSize(3);
+ move(0, 1);
+ move(1, 2);
+ add(0);
+ check();
+ }
+
+ @Test
+ public void testGen11() {
+ initWithSize(4);
+ move(2, 0);
+ move(2, 3);
+ check();
+ }
+
+ @Test
+ public void testGen12() {
+ initWithSize(4);
+ move(3, 0);
+ move(2, 1);
+ check();
+ }
+
+ @Test
+ public void testGen13() {
+ initWithSize(4);
+ move(3, 2);
+ move(0, 3);
+ check();
+ }
+
+ @Test
+ public void testGen14() {
+ initWithSize(4);
+ move(3, 2);
+ add(4);
+ move(0, 4);
+ check();
+ }
+
+ @Test
+ public void testAdd1() {
+ initWithSize(1);
+ add(1);
+ check();
+ }
+
+ @Test
+ public void testMove1() {
+ initWithSize(3);
+ move(0, 2);
+ check();
+ }
+
+ @Test
+ public void tmp() {
+ initWithSize(4);
+ move(0, 2);
+ check();
+ }
+
+ @Test
+ public void testUpdate1() {
+ initWithSize(3);
+ update(2);
+ check();
+ }
+
+ @Test
+ public void testUpdate2() {
+ initWithSize(2);
+ add(1);
+ update(1);
+ update(2);
+ check();
+ }
+
+ @Test
+ public void testDisableMoveDetection() {
+ initWithSize(5);
+ move(0, 4);
+ List<Item> applied = applyUpdates(mBefore, DiffUtil.calculateDiff(mCallback, false));
+ assertThat(applied.size(), is(5));
+ assertThat(applied.get(4).newItem, is(true));
+ assertThat(applied.contains(mBefore.get(0)), is(false));
+ }
+
+ private void testRandom(int initialSize, int operationCount) {
+ mLog.setLength(0);
+ initWithSize(initialSize);
+ for (int i = 0; i < operationCount; i++) {
+ int op = sRand.nextInt(5);
+ switch (op) {
+ case 0:
+ add(sRand.nextInt(mAfter.size() + 1));
+ break;
+ case 1:
+ if (!mAfter.isEmpty()) {
+ delete(sRand.nextInt(mAfter.size()));
+ }
+ break;
+ case 2:
+ // move
+ if (mAfter.size() > 0) {
+ move(sRand.nextInt(mAfter.size()), sRand.nextInt(mAfter.size()));
+ }
+ break;
+ case 3:
+ // update
+ if (mAfter.size() > 0) {
+ update(sRand.nextInt(mAfter.size()));
+ }
+ break;
+ case 4:
+ // update with payload
+ if (mAfter.size() > 0) {
+ updateWithPayload(sRand.nextInt(mAfter.size()));
+ }
+ break;
+ }
+ }
+ check();
+ }
+
+ private void check() {
+ DiffUtil.DiffResult result = DiffUtil.calculateDiff(mCallback);
+ log("before", mBefore);
+ log("after", mAfter);
+ log("snakes", result.getSnakes());
+
+ List<Item> applied = applyUpdates(mBefore, result);
+ assertEquals(applied, mAfter);
+ }
+
+ private void initWithSize(int size) {
+ mBefore.clear();
+ mAfter.clear();
+ for (int i = 0; i < size; i++) {
+ mBefore.add(new Item(false));
+ }
+ mAfter.addAll(mBefore);
+ mLog.append("initWithSize(" + size + ");\n");
+ }
+
+ private void log(String title, List<?> items) {
+ mLog.append(title).append(":").append(items.size()).append("\n");
+ for (Object item : items) {
+ mLog.append(" ").append(item).append("\n");
+ }
+ }
+
+ private void assertEquals(List<Item> applied, List<Item> after) {
+ log("applied", applied);
+
+ String report = mLog.toString();
+ assertThat(report, applied.size(), is(after.size()));
+ for (int i = 0; i < after.size(); i++) {
+ Item item = applied.get(i);
+ if (after.get(i).newItem) {
+ assertThat(report, item.newItem, is(true));
+ } else if (after.get(i).changed) {
+ assertThat(report, item.newItem, is(false));
+ assertThat(report, item.changed, is(true));
+ assertThat(report, item.id, is(after.get(i).id));
+ assertThat(report, item.payload, is(after.get(i).payload));
+ } else {
+ assertThat(report, item, equalTo(after.get(i)));
+ }
+ }
+ }
+
+ private List<Item> applyUpdates(List<Item> before, DiffUtil.DiffResult result) {
+ final List<Item> target = new ArrayList<>();
+ target.addAll(before);
+ result.dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(int position, int count) {
+ for (int i = 0; i < count; i++) {
+ target.add(i + position, new Item(true));
+ }
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ for (int i = 0; i < count; i++) {
+ target.remove(position);
+ }
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ Item item = target.remove(fromPosition);
+ target.add(toPosition, item);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ for (int i = 0; i < count; i++) {
+ int positionInList = position + i;
+ Item existing = target.get(positionInList);
+ // make sure we don't update same item twice in callbacks
+ assertThat(existing.changed, is(false));
+ assertThat(existing.newItem, is(false));
+ assertThat(existing.payload, is(nullValue()));
+ Item replica = new Item(existing);
+ replica.payload = (String) payload;
+ replica.changed = true;
+ target.remove(positionInList);
+ target.add(positionInList, replica);
+ }
+ }
+ });
+ return target;
+ }
+
+ private void add(int index) {
+ mAfter.add(index, new Item(true));
+ mLog.append("add(").append(index).append(");\n");
+ }
+
+ private void delete(int index) {
+ mAfter.remove(index);
+ mLog.append("delete(").append(index).append(");\n");
+ }
+
+ private void update(int index) {
+ Item existing = mAfter.get(index);
+ if (existing.newItem) {
+ return;//new item cannot be changed
+ }
+ Item replica = new Item(existing);
+ replica.changed = true;
+ // clean the payload since this might be after an updateWithPayload call
+ replica.payload = null;
+ replica.data = UUID.randomUUID().toString();
+ mAfter.remove(index);
+ mAfter.add(index, replica);
+ mLog.append("update(").append(index).append(");\n");
+ }
+
+ private void updateWithPayload(int index) {
+ Item existing = mAfter.get(index);
+ if (existing.newItem) {
+ return;//new item cannot be changed
+ }
+ Item replica = new Item(existing);
+ replica.changed = true;
+ replica.data = UUID.randomUUID().toString();
+ replica.payload = UUID.randomUUID().toString();
+ mAfter.remove(index);
+ mAfter.add(index, replica);
+ mLog.append("update(").append(index).append(");\n");
+ }
+
+ private void move(int from, int to) {
+ Item removed = mAfter.remove(from);
+ mAfter.add(to, removed);
+ mLog.append("move(").append(from).append(",").append(to).append(");\n");
+ }
+
+ static class Item {
+ static long idCounter = 0;
+ final long id;
+ final boolean newItem;
+ boolean changed = false;
+ String payload;
+
+ String data = UUID.randomUUID().toString();
+
+ public Item(boolean newItem) {
+ id = idCounter++;
+ this.newItem = newItem;
+ }
+
+ public Item(Item other) {
+ id = other.id;
+ newItem = other.newItem;
+ changed = other.changed;
+ payload = other.payload;
+ data = other.data;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Item item = (Item) o;
+
+ if (id != item.id) return false;
+ if (newItem != item.newItem) return false;
+ if (changed != item.changed) return false;
+ if (payload != null ? !payload.equals(item.payload) : item.payload != null) {
+ return false;
+ }
+ return data.equals(item.data);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (id ^ (id >>> 32));
+ result = 31 * result + (newItem ? 1 : 0);
+ result = 31 * result + (changed ? 1 : 0);
+ result = 31 * result + (payload != null ? payload.hashCode() : 0);
+ result = 31 * result + data.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Item{" +
+ "id=" + id +
+ ", newItem=" + newItem +
+ ", changed=" + changed +
+ ", payload='" + payload + '\'' +
+ ", data='" + data + '\'' +
+ '}';
+ }
+ }
+}
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java
deleted file mode 100644
index e803d2a..0000000
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/util/SortedListAdapterCallbackWrapperTest.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.v7.util;
-
-import junit.framework.TestCase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import android.test.suitebuilder.annotation.SmallTest;
-
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_NONE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_ADD;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_REMOVE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_CHANGE;
-import static android.support.v7.util.SortedList.BatchedCallback.TYPE_MOVE;
-
-@RunWith(JUnit4.class)
-@SmallTest
-public class SortedListAdapterCallbackWrapperTest extends TestCase {
-
- private int lastReceivedType = TYPE_NONE;
- private int lastReceivedPosition = -1;
- private int lastReceivedCount = -1;
-
- private SortedList.Callback<Object> mCallback = new SortedList.Callback<Object>() {
- @Override
- public int compare(Object o1, Object o2) {
- return 0;
- }
-
- @Override
- public void onInserted(int position, int count) {
- lastReceivedType = TYPE_ADD;
- lastReceivedPosition = position;
- lastReceivedCount = count;
- }
-
- @Override
- public void onRemoved(int position, int count) {
- lastReceivedType = TYPE_REMOVE;
- lastReceivedPosition = position;
- lastReceivedCount = count;
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- lastReceivedType = TYPE_MOVE;
- lastReceivedPosition = fromPosition;
- lastReceivedCount = toPosition;
- }
-
- @Override
- public void onChanged(int position, int count) {
- lastReceivedType = TYPE_CHANGE;
- lastReceivedPosition = position;
- lastReceivedCount = count;
- }
-
- @Override
- public boolean areContentsTheSame(Object oldItem, Object newItem) {
- return false;
- }
-
- @Override
- public boolean areItemsTheSame(Object item1, Object item2) {
- return false;
- }
- };
-
- private SortedList.BatchedCallback<Object> mBatched =
- new SortedList.BatchedCallback<Object>(mCallback);
-
- @Test
- public void testAdd() throws Throwable {
- mBatched.onInserted(0, 3);
- assertPending(TYPE_ADD, 0, 3);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 0, 3);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testRemove() throws Throwable {
- mBatched.onRemoved(0, 3);
- assertPending(TYPE_REMOVE, 0, 3);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_REMOVE, 0, 3);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testChange() throws Throwable {
- mBatched.onChanged(0, 3);
- assertPending(TYPE_CHANGE, 0, 3);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_CHANGE, 0, 3);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testMove() throws Throwable {
- mBatched.onMoved(0, 3);
- assertLast(TYPE_MOVE, 0, 3);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd1() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(3, 2);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 3, 7);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd2() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(1, 2);
- assertLast(TYPE_ADD, 3, 5);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 1, 2);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd3() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(8, 2);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 3, 7);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd4() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(9, 2);
- assertLast(TYPE_ADD, 3, 5);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 9, 2);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd5() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(4, 1);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 3, 6);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAdd6() throws Throwable {
- mBatched.onInserted(3, 5);
- mBatched.onInserted(4, 1);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.onInserted(4, 1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 3, 7);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchAddLoop() throws Throwable {
- for (int i = 0; i < 10; i++) {
- mBatched.onInserted(4 + i, 1);
- assertLast(TYPE_NONE, -1, -1);
- assertPending(TYPE_ADD, 4, i + 1);
- }
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 4, 10);
- }
-
- @Test
- public void testBatchAddReverseLoop() throws Throwable {
- for (int i = 10; i >= 0; i--) {
- mBatched.onInserted(4, 1);
- assertLast(TYPE_NONE, -1, -1);
- assertPending(TYPE_ADD, 4, 10 - i + 1);
- }
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 4, 11);
- }
-
- @Test
- public void testBadBatchAddReverseLoop() throws Throwable {
- for (int i = 10; i >= 0; i--) {
- mBatched.onInserted(4 + i, 1);
- if (i < 10) {
- assertLast(TYPE_ADD, 4 + i + 1, 1);
- }
-
- }
- mBatched.dispatchLastEvent();
- assertLast(TYPE_ADD, 4, 1);
- }
-
- @Test
- public void testBatchRemove1() throws Throwable {
- mBatched.onRemoved(3, 5);
- mBatched.onRemoved(3, 1);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_REMOVE, 3, 6);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchRemove2() throws Throwable {
- mBatched.onRemoved(3, 5);
- mBatched.onRemoved(4, 1);
- assertLast(TYPE_REMOVE, 3, 5);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_REMOVE, 4, 1);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchRemove3() throws Throwable {
- mBatched.onRemoved(3, 5);
- mBatched.onRemoved(2, 3);
- assertLast(TYPE_REMOVE, 3, 5);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_REMOVE, 2, 3);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchChange1() throws Throwable {
- mBatched.onChanged(3, 5);
- mBatched.onChanged(3, 1);
- assertPending(TYPE_CHANGE, 3, 5);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_CHANGE, 3, 5);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchChange2() throws Throwable {
- mBatched.onChanged(3, 5);
- mBatched.onChanged(2, 7);
- assertPending(TYPE_CHANGE, 2, 7);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.dispatchLastEvent();
- assertLast(TYPE_CHANGE, 2, 7);
- assertPending(TYPE_NONE, -1, -1);
- }
-
- @Test
- public void testBatchChange3() throws Throwable {
- mBatched.onChanged(3, 5);
- mBatched.onChanged(2, 1);
- assertLast(TYPE_NONE, -1, -1);
- mBatched.onChanged(8, 2);
- assertLast(TYPE_NONE, -1, -1);
- assertPending(TYPE_CHANGE, 2, 8);
- }
-
- @Test
- public void testBatchChange4() throws Throwable {
- mBatched.onChanged(3, 5);
- mBatched.onChanged(1, 1);
- assertLast(TYPE_CHANGE, 3, 5);
- assertPending(TYPE_CHANGE, 1, 1);
- }
-
- @Test
- public void testBatchChange5() throws Throwable {
- mBatched.onChanged(3, 5);
- mBatched.onChanged(9, 1);
- assertLast(TYPE_CHANGE, 3, 5);
- assertPending(TYPE_CHANGE, 9, 1);
- }
-
- private void assertLast(int type, int position, int count) throws Throwable {
- try {
- assertEquals(lastReceivedType, type);
- if (position >= 0) {
- assertEquals(lastReceivedPosition, position);
- }
- if (count >= 0) {
- assertEquals(lastReceivedCount, count);
- }
- } catch (Throwable t) {
- throw new Throwable("last event: expected=" + log(type, position, count)
- + " found=" + log(lastReceivedType, lastReceivedPosition,
- lastReceivedCount), t);
- }
- }
-
- private void assertPending(int type, int position, int count) throws Throwable {
- try {
- assertEquals(mBatched.mLastEventType, type);
- if (position >= 0) {
- assertEquals(mBatched.mLastEventPosition, position);
- }
- if (count >= 0) {
- assertEquals(mBatched.mLastEventCount, count);
- }
- } catch (Throwable t) {
- throw new Throwable("pending event: expected=" + log(type, position, count)
- + " found=" + log(mBatched.mLastEventType, mBatched.mLastEventPosition,
- mBatched.mLastEventCount), t);
- }
- }
-
- private String log(int type, int position, int count) {
- return TYPES_NAMES[type]
- + ", p:" + position
- + ", c:" + count;
- }
-
- private static final String[] TYPES_NAMES = new String[]{"none", "add", "remove", "change",
- "move"};
-}
diff --git a/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java b/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java
new file mode 100644
index 0000000..c8bc1a4
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/BatchingListUpdateCallback.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+/**
+ * Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
+ * <p>
+ * For instance, when 2 add operations comes that adds 2 consecutive elements,
+ * BatchingListUpdateCallback merges them and calls the wrapped callback only once.
+ * <p>
+ * This is a general purpose class and is also used by
+ * {@link android.support.v7.util.DiffUtil.DiffResult DiffResult} and
+ * {@link SortedList} to minimize the number of updates that are dispatched.
+ * <p>
+ * If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
+ * stream of update events drain.
+ */
+public class BatchingListUpdateCallback implements ListUpdateCallback {
+ private static final int TYPE_NONE = 0;
+ private static final int TYPE_ADD = 1;
+ private static final int TYPE_REMOVE = 2;
+ private static final int TYPE_CHANGE = 3;
+
+ final ListUpdateCallback mWrapped;
+
+ int mLastEventType = TYPE_NONE;
+ int mLastEventPosition = -1;
+ int mLastEventCount = -1;
+ Object mLastEventPayload = null;
+
+ public BatchingListUpdateCallback(ListUpdateCallback callback) {
+ mWrapped = callback;
+ }
+
+ /**
+ * BatchingListUpdateCallback holds onto the last event to see if it can be merged with the
+ * next one. When stream of events finish, you should call this method to dispatch the last
+ * event.
+ */
+ public void dispatchLastEvent() {
+ if (mLastEventType == TYPE_NONE) {
+ return;
+ }
+ switch (mLastEventType) {
+ case TYPE_ADD:
+ mWrapped.onInserted(mLastEventPosition, mLastEventCount);
+ break;
+ case TYPE_REMOVE:
+ mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
+ break;
+ case TYPE_CHANGE:
+ mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
+ break;
+ }
+ mLastEventPayload = null;
+ mLastEventType = TYPE_NONE;
+ }
+
+ @Override
+ public void onInserted(int position, int count) {
+ if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
+ && position <= mLastEventPosition + mLastEventCount) {
+ mLastEventCount += count;
+ mLastEventPosition = Math.min(position, mLastEventPosition);
+ return;
+ }
+ dispatchLastEvent();
+ mLastEventPosition = position;
+ mLastEventCount = count;
+ mLastEventType = TYPE_ADD;
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position &&
+ mLastEventPosition <= position + count) {
+ mLastEventCount += count;
+ mLastEventPosition = position;
+ return;
+ }
+ dispatchLastEvent();
+ mLastEventPosition = position;
+ mLastEventCount = count;
+ mLastEventType = TYPE_REMOVE;
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ dispatchLastEvent(); // moves are not merged
+ mWrapped.onMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ if (mLastEventType == TYPE_CHANGE &&
+ !(position > mLastEventPosition + mLastEventCount
+ || position + count < mLastEventPosition || mLastEventPayload != payload)) {
+ // take potential overlap into account
+ int previousEnd = mLastEventPosition + mLastEventCount;
+ mLastEventPosition = Math.min(position, mLastEventPosition);
+ mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
+ return;
+ }
+ dispatchLastEvent();
+ mLastEventPosition = position;
+ mLastEventCount = count;
+ mLastEventPayload = payload;
+ mLastEventType = TYPE_CHANGE;
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/DiffUtil.java b/v7/recyclerview/src/android/support/v7/util/DiffUtil.java
new file mode 100644
index 0000000..6f0a078
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/DiffUtil.java
@@ -0,0 +1,856 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.util;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * DiffUtil is a utility class that can calculate the difference between two lists and output a
+ * list of update operations that converts the first list into the second one.
+ * <p>
+ * It can be used to calculate updates for a RecyclerView Adapter.
+ * <p>
+ * DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates
+ * to convert one list into another. Myers's algorithm does not handle items that are moved so
+ * DiffUtil runs a second pass on the result to detect items that were moved.
+ * <p>
+ * If the lists are large, this operation may take significant time so you are advised to run this
+ * on a background thread, get the {@link DiffResult} then apply it on the RecyclerView on the main
+ * thread.
+ * <p>
+ * This algorithm is optimized for space and uses O(N) space to find the minimal
+ * number of addition and removal operations between the two lists. It has O(N + D^2) expected time
+ * performance where D is the length of the edit script.
+ * <p>
+ * If move detection is enabled, it takes an additional O(N^2) time where N is the total number of
+ * added and removed items. If your lists are already sorted by the same constraint (e.g. a created
+ * timestamp for a list of posts), you can disable move detection to improve performance.
+ * <p>
+ * The actual runtime of the algorithm significantly depends on the number of changes in the list
+ * and the cost of your comparison methods. Below are some average run times for reference:
+ * (The test list is composed of random UUID Strings and the tests are run on Nexus 5X with M)
+ * <ul>
+ * <li>100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
+ * <li>100 items and 100 modifications: 3.82 ms, median: 3.75 ms
+ * <li>100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
+ * <li>1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
+ * <li>1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
+ * <li>1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
+ * <li>1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
+ * </ul>
+ * <p>
+ * Due to implementation constraints, the max size of the list can be 2^26.
+ */
+public class DiffUtil {
+
+ private DiffUtil() {
+ // utility class, no instance.
+ }
+
+ private static final Comparator<Snake> SNAKE_COMPARATOR = new Comparator<Snake>() {
+ @Override
+ public int compare(Snake o1, Snake o2) {
+ int cmpX = o1.x - o2.x;
+ return cmpX == 0 ? o1.y - o2.y : cmpX;
+ }
+ };
+
+ // Myers' algorithm uses two lists as axis labels. In DiffUtil's implementation, `x` axis is
+ // used for old list and `y` axis is used for new list.
+
+ /**
+ * Calculates the list of update operations that can covert one list into the other one.
+ *
+ * @param cb The callback that acts as a gateway to the backing list data
+ *
+ * @return A DiffResult that contains the information about the edit sequence to convert the
+ * old list into the new list.
+ */
+ public static DiffResult calculateDiff(Callback cb) {
+ return calculateDiff(cb, true);
+ }
+
+ /**
+ * Calculates the list of update operations that can covert one list into the other one.
+ * <p>
+ * If your old and new lists are sorted by the same constraint and items never move (swap
+ * positions), you can disable move detection which takes <code>O(N^2)</code> time where
+ * N is the number of added, moved, removed items.
+ *
+ * @param cb The callback that acts as a gateway to the backing list data
+ * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
+ *
+ * @return A DiffResult that contains the information about the edit sequence to convert the
+ * old list into the new list.
+ */
+ public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
+ final int oldSize = cb.getOldListSize();
+ final int newSize = cb.getNewListSize();
+
+ final List<Snake> snakes = new ArrayList<>();
+
+ // instead of a recursive implementation, we keep our own stack to avoid potential stack
+ // overflow exceptions
+ final List<Range> stack = new ArrayList<>();
+
+ stack.add(new Range(0, oldSize, 0, newSize));
+
+ final int max = oldSize + newSize + Math.abs(oldSize - newSize);
+ // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
+ // paper for details)
+ // These arrays lines keep the max reachable position for each k-line.
+ final int[] forward = new int[max * 2];
+ final int[] backward = new int[max * 2];
+
+ // We pool the ranges to avoid allocations for each recursive call.
+ final List<Range> rangePool = new ArrayList<>();
+ while (!stack.isEmpty()) {
+ final Range range = stack.remove(stack.size() - 1);
+ final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
+ range.newListStart, range.newListEnd, forward, backward, max);
+ if (snake != null) {
+ if (snake.size > 0) {
+ snakes.add(snake);
+ }
+ // offset the snake to convert its coordinates from the Range's area to global
+ snake.x += range.oldListStart;
+ snake.y += range.newListStart;
+
+ // add new ranges for left and right
+ final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove(
+ rangePool.size() - 1);
+ left.oldListStart = range.oldListStart;
+ left.newListStart = range.newListStart;
+ if (snake.reverse) {
+ left.oldListEnd = snake.x;
+ left.newListEnd = snake.y;
+ } else {
+ if (snake.removal) {
+ left.oldListEnd = snake.x - 1;
+ left.newListEnd = snake.y;
+ } else {
+ left.oldListEnd = snake.x;
+ left.newListEnd = snake.y - 1;
+ }
+ }
+ stack.add(left);
+
+ // re-use range for right
+ //noinspection UnnecessaryLocalVariable
+ final Range right = range;
+ if (snake.reverse) {
+ if (snake.removal) {
+ right.oldListStart = snake.x + snake.size + 1;
+ right.newListStart = snake.y + snake.size;
+ } else {
+ right.oldListStart = snake.x + snake.size;
+ right.newListStart = snake.y + snake.size + 1;
+ }
+ } else {
+ right.oldListStart = snake.x + snake.size;
+ right.newListStart = snake.y + snake.size;
+ }
+ stack.add(right);
+ } else {
+ rangePool.add(range);
+ }
+
+ }
+ // sort snakes
+ Collections.sort(snakes, SNAKE_COMPARATOR);
+
+ return new DiffResult(cb, snakes, forward, backward, detectMoves);
+
+ }
+
+ private static Snake diffPartial(Callback cb, int startOld, int endOld,
+ int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
+ final int oldSize = endOld - startOld;
+ final int newSize = endNew - startNew;
+
+ if (endOld - startOld < 1 || endNew - startNew < 1) {
+ return null;
+ }
+
+ final int delta = oldSize - newSize;
+ final int dLimit = (oldSize + newSize + 1) / 2;
+ Arrays.fill(forward, kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
+ Arrays.fill(backward, kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
+ final boolean checkInFwd = delta % 2 != 0;
+ for (int d = 0; d <= dLimit; d++) {
+ for (int k = -d; k <= d; k += 2) {
+ // find forward path
+ // we can reach k from k - 1 or k + 1. Check which one is further in the graph
+ int x;
+ final boolean removal;
+ if (k == -d || k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1]) {
+ x = forward[kOffset + k + 1];
+ removal = false;
+ } else {
+ x = forward[kOffset + k - 1] + 1;
+ removal = true;
+ }
+ // set y based on x
+ int y = x - k;
+ // move diagonal as long as items match
+ while (x < oldSize && y < newSize
+ && cb.areItemsTheSame(startOld + x, startNew + y)) {
+ x++;
+ y++;
+ }
+ forward[kOffset + k] = x;
+ if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
+ if (forward[kOffset + k] >= backward[kOffset + k]) {
+ Snake outSnake = new Snake();
+ outSnake.x = backward[kOffset + k];
+ outSnake.y = outSnake.x - k;
+ outSnake.size = forward[kOffset + k] - backward[kOffset + k];
+ outSnake.removal = removal;
+ outSnake.reverse = false;
+ return outSnake;
+ }
+ }
+ }
+ for (int k = -d; k <= d; k += 2) {
+ // find reverse path at k + delta, in reverse
+ final int backwardK = k + delta;
+ int x;
+ final boolean removal;
+ if (backwardK == d + delta || backwardK != -d + delta
+ && backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1]) {
+ x = backward[kOffset + backwardK - 1];
+ removal = false;
+ } else {
+ x = backward[kOffset + backwardK + 1] - 1;
+ removal = true;
+ }
+
+ // set y based on x
+ int y = x - backwardK;
+ // move diagonal as long as items match
+ while (x > 0 && y > 0
+ && cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
+ x--;
+ y--;
+ }
+ backward[kOffset + backwardK] = x;
+ if (!checkInFwd && k + delta >= -d && k + delta <= d) {
+ if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
+ Snake outSnake = new Snake();
+ outSnake.x = backward[kOffset + backwardK];
+ outSnake.y = outSnake.x - backwardK;
+ outSnake.size =
+ forward[kOffset + backwardK] - backward[kOffset + backwardK];
+ outSnake.removal = removal;
+ outSnake.reverse = true;
+ return outSnake;
+ }
+ }
+ }
+ }
+ throw new IllegalStateException("DiffUtil hit an unexpected case while trying to calculate"
+ + " the optimal path. Please make sure your data is not changing during the"
+ + " diff calculation.");
+ }
+
+ /**
+ * A Callback class used by DiffUtil while calculating the diff between two lists.
+ */
+ public abstract static class Callback {
+ /**
+ * Returns the size of the old list.
+ *
+ * @return The size of the old list.
+ */
+ public abstract int getOldListSize();
+
+ /**
+ * Returns the size of the new list.
+ *
+ * @return The size of the new list.
+ */
+ public abstract int getNewListSize();
+
+ /**
+ * Called by the DiffUtil to decide whether two object represent the same Item.
+ * <p>
+ * For example, if your items have unique ids, this method should check their id equality.
+ *
+ * @param oldItemPosition The position of the item in the old list
+ * @param newItemPosition The position of the item in the new list
+ * @return True if the two items represent the same object or false if they are different.
+ */
+ public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
+
+ /**
+ * Called by the DiffUtil when it wants to check whether two items have the same data.
+ * DiffUtil uses this information to detect if the contents of an item has changed.
+ * <p>
+ * DiffUtil 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 DiffUtil with a
+ * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should
+ * return whether the items' visual representations are the same.
+ * <p>
+ * This method is called only if {@link #areItemsTheSame(int, int)} returns
+ * {@code true} for these items.
+ *
+ * @param oldItemPosition The position of the item in the old list
+ * @param newItemPosition The position of the item in the new list which replaces the
+ * oldItem
+ * @return True if the contents of the items are the same or false if they are different.
+ */
+ public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
+
+ /**
+ * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
+ * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
+ * calls this method to get a payload about the change.
+ * <p>
+ * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the
+ * particular field that changed in the item and your
+ * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that
+ * information to run the correct animation.
+ * <p>
+ * Default implementation returns {@code null}.
+ *
+ * @param oldItemPosition The position of the item in the old list
+ * @param newItemPosition The position of the item in the new list
+ *
+ * @return A payload object that represents the change between the two items.
+ */
+ @Nullable
+ public Object getChangePayload(int oldItemPosition, int newItemPosition) {
+ return null;
+ }
+ }
+
+ /**
+ * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
+ * add or remove operation. See the Myers' paper for details.
+ */
+ static class Snake {
+ /**
+ * Position in the old list
+ */
+ int x;
+
+ /**
+ * Position in the new list
+ */
+ int y;
+
+ /**
+ * Number of matches. Might be 0.
+ */
+ int size;
+
+ /**
+ * If true, this is a removal from the original list followed by {@code size} matches.
+ * If false, this is an addition from the new list followed by {@code size} matches.
+ */
+ boolean removal;
+
+ /**
+ * If true, the addition or removal is at the end of the snake.
+ * If false, the addition or removal is at the beginning of the snake.
+ */
+ boolean reverse;
+ }
+
+ /**
+ * Represents a range in two lists that needs to be solved.
+ * <p>
+ * This internal class is used when running Myers' algorithm without recursion.
+ */
+ static class Range {
+
+ int oldListStart, oldListEnd;
+
+ int newListStart, newListEnd;
+
+ public Range() {
+ }
+
+ public Range(int oldListStart, int oldListEnd, int newListStart, int newListEnd) {
+ this.oldListStart = oldListStart;
+ this.oldListEnd = oldListEnd;
+ this.newListStart = newListStart;
+ this.newListEnd = newListEnd;
+ }
+ }
+
+ /**
+ * This class holds the information about the result of a
+ * {@link DiffUtil#calculateDiff(Callback, boolean)} call.
+ * <p>
+ * You can consume the updates in a DiffResult via
+ * {@link #dispatchUpdatesTo(ListUpdateCallback)} or directly stream the results into a
+ * {@link RecyclerView.Adapter} via {@link #dispatchUpdatesTo(RecyclerView.Adapter)}.
+ */
+ public static class DiffResult {
+ /**
+ * While reading the flags below, keep in mind that when multiple items move in a list,
+ * Myers's may pick any of them as the anchor item and consider that one NOT_CHANGED while
+ * picking others as additions and removals. This is completely fine as we later detect
+ * all moves.
+ * <p>
+ * Below, when an item is mentioned to stay in the same "location", it means we won't
+ * dispatch a move/add/remove for it, it DOES NOT mean the item is still in the same
+ * position.
+ */
+ // item stayed the same.
+ private static final int FLAG_NOT_CHANGED = 1;
+ // item stayed in the same location but changed.
+ private static final int FLAG_CHANGED = FLAG_NOT_CHANGED << 1;
+ // Item has moved and also changed.
+ private static final int FLAG_MOVED_CHANGED = FLAG_CHANGED << 1;
+ // Item has moved but did not change.
+ private static final int FLAG_MOVED_NOT_CHANGED = FLAG_MOVED_CHANGED << 1;
+ // Ignore this update.
+ // If this is an addition from the new list, it means the item is actually removed from an
+ // earlier position and its move will be dispatched when we process the matching removal
+ // from the old list.
+ // If this is a removal from the old list, it means the item is actually added back to an
+ // earlier index in the new list and we'll dispatch its move when we are processing that
+ // addition.
+ private static final int FLAG_IGNORE = FLAG_MOVED_NOT_CHANGED << 1;
+
+ // since we are re-using the int arrays that were created in the Myers' step, we mask
+ // change flags
+ private static final int FLAG_OFFSET = 5;
+
+ private static final int FLAG_MASK = (1 << FLAG_OFFSET) - 1;
+
+ // The Myers' snakes. At this point, we only care about their diagonal sections.
+ private final List<Snake> mSnakes;
+
+ // The list to keep oldItemStatuses. As we traverse old items, we assign flags to them
+ // which also includes whether they were a real removal or a move (and its new index).
+ private final int[] mOldItemStatuses;
+ // The list to keep newItemStatuses. As we traverse new items, we assign flags to them
+ // which also includes whether they were a real addition or a move(and its old index).
+ private final int[] mNewItemStatuses;
+ // The callback that was given to calcualte diff method.
+ private final Callback mCallback;
+
+ private final int mOldListSize;
+
+ private final int mNewListSize;
+
+ private final boolean mDetectMoves;
+
+ /**
+ * @param callback The callback that was used to calculate the diff
+ * @param snakes The list of Myers' snakes
+ * @param oldItemStatuses An int[] that can be re-purposed to keep metadata
+ * @param newItemStatuses An int[] that can be re-purposed to keep metadata
+ * @param detectMoves True if this DiffResult will try to detect moved items
+ */
+ DiffResult(Callback callback, List<Snake> snakes, int[] oldItemStatuses,
+ int[] newItemStatuses, boolean detectMoves) {
+ mSnakes = snakes;
+ mOldItemStatuses = oldItemStatuses;
+ mNewItemStatuses = newItemStatuses;
+ Arrays.fill(mOldItemStatuses, 0);
+ Arrays.fill(mNewItemStatuses, 0);
+ mCallback = callback;
+ mOldListSize = callback.getOldListSize();
+ mNewListSize = callback.getNewListSize();
+ mDetectMoves = detectMoves;
+ addRootSnake();
+ findMatchingItems();
+ }
+
+ /**
+ * We always add a Snake to 0/0 so that we can run loops from end to beginning and be done
+ * when we run out of snakes.
+ */
+ private void addRootSnake() {
+ Snake firstSnake = mSnakes.isEmpty() ? null : mSnakes.get(0);
+ if (firstSnake == null || firstSnake.x != 0 || firstSnake.y != 0) {
+ Snake root = new Snake();
+ root.x = 0;
+ root.y = 0;
+ root.removal = false;
+ root.size = 0;
+ root.reverse = false;
+ mSnakes.add(0, root);
+ }
+ }
+
+ /**
+ * This method traverses each addition / removal and tries to match it to a previous
+ * removal / addition. This is how we detect move operations.
+ * <p>
+ * This class also flags whether an item has been changed or not.
+ * <p>
+ * DiffUtil does this pre-processing so that if it is running on a big list, it can be moved
+ * to background thread where most of the expensive stuff will be calculated and kept in
+ * the statuses maps. DiffResult uses this pre-calculated information while dispatching
+ * the updates (which is probably being called on the main thread).
+ */
+ private void findMatchingItems() {
+ int posOld = mOldListSize;
+ int posNew = mNewListSize;
+ // traverse the matrix from right bottom to 0,0.
+ for (int i = mSnakes.size() - 1; i >= 0; i--) {
+ final Snake snake = mSnakes.get(i);
+ final int endX = snake.x + snake.size;
+ final int endY = snake.y + snake.size;
+ if (mDetectMoves) {
+ while (posOld > endX) {
+ // this is a removal. Check remaining snakes to see if this was added before
+ findAddition(posOld, posNew, i);
+ posOld--;
+ }
+ while (posNew > endY) {
+ // this is an addition. Check remaining snakes to see if this was removed
+ // before
+ findRemoval(posOld, posNew, i);
+ posNew--;
+ }
+ }
+ for (int j = 0; j < snake.size; j++) {
+ // matching items. Check if it is changed or not
+ final int oldItemPos = snake.x + j;
+ final int newItemPos = snake.y + j;
+ final boolean theSame = mCallback
+ .areContentsTheSame(oldItemPos, newItemPos);
+ final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED;
+ mOldItemStatuses[oldItemPos] = (newItemPos << FLAG_OFFSET) | changeFlag;
+ mNewItemStatuses[newItemPos] = (oldItemPos << FLAG_OFFSET) | changeFlag;
+ }
+ posOld = snake.x;
+ posNew = snake.y;
+ }
+ }
+
+ private void findAddition(int x, int y, int snakeIndex) {
+ if (mOldItemStatuses[x - 1] != 0) {
+ return; // already set by a latter item
+ }
+ findMatchingItem(x, y, snakeIndex, false);
+ }
+
+ private void findRemoval(int x, int y, int snakeIndex) {
+ if (mNewItemStatuses[y - 1] != 0) {
+ return; // already set by a latter item
+ }
+ findMatchingItem(x, y, snakeIndex, true);
+ }
+
+ /**
+ * Finds a matching item that is before the given coordinates in the matrix
+ * (before : left and above).
+ *
+ * @param x The x position in the matrix (position in the old list)
+ * @param y The y position in the matrix (position in the new list)
+ * @param snakeIndex The current snake index
+ * @param removal True if we are looking for a removal, false otherwise
+ *
+ * @return True if such item is found.
+ */
+ private boolean findMatchingItem(final int x, final int y, final int snakeIndex,
+ final boolean removal) {
+ final int myItemPos;
+ int curX;
+ int curY;
+ if (removal) {
+ myItemPos = y - 1;
+ curX = x;
+ curY = y - 1;
+ } else {
+ myItemPos = x - 1;
+ curX = x - 1;
+ curY = y;
+ }
+ for (int i = snakeIndex; i >= 0; i--) {
+ final Snake snake = mSnakes.get(i);
+ final int endX = snake.x + snake.size;
+ final int endY = snake.y + snake.size;
+ if (removal) {
+ // check removals for a match
+ for (int pos = curX - 1; pos >= endX; pos--) {
+ if (mCallback.areItemsTheSame(pos, myItemPos)) {
+ // found!
+ final boolean theSame = mCallback.areContentsTheSame(pos, myItemPos);
+ final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
+ : FLAG_MOVED_CHANGED;
+ mNewItemStatuses[myItemPos] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
+ mOldItemStatuses[pos] = (myItemPos << FLAG_OFFSET) | changeFlag;
+ return true;
+ }
+ }
+ } else {
+ // check for additions for a match
+ for (int pos = curY - 1; pos >= endY; pos--) {
+ if (mCallback.areItemsTheSame(myItemPos, pos)) {
+ // found
+ final boolean theSame = mCallback.areContentsTheSame(myItemPos, pos);
+ final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
+ : FLAG_MOVED_CHANGED;
+ mOldItemStatuses[x - 1] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
+ mNewItemStatuses[pos] = ((x - 1) << FLAG_OFFSET) | changeFlag;
+ return true;
+ }
+ }
+ }
+ curX = snake.x;
+ curY = snake.y;
+ }
+ return false;
+ }
+
+ /**
+ * Dispatches the update events to the given adapter.
+ * <p>
+ * For example, if you have an {@link android.support.v7.widget.RecyclerView.Adapter Adapter}
+ * that is backed by a {@link List}, you can swap the list with the new one then call this
+ * method to dispatch all updates to the RecyclerView.
+ * <pre>
+ * List oldList = mAdapter.getData();
+ * DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
+ * mAdapter.setData(newList);
+ * result.dispatchUpdatesTo(mAdapter);
+ * </pre>
+ * <p>
+ * Note that the RecyclerView requires you to dispatch adapter updates immediately when you
+ * change the data (you cannot defer {@code notify*} calls). The usage above adheres to this
+ * rule because updates are sent to the adapter right after the backing data is changed,
+ * before RecyclerView tries to read it.
+ * <p>
+ * On the other hand, if you have another
+ * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver AdapterDataObserver}
+ * that tries to process events synchronously, this may confuse that observer because the
+ * list is instantly moved to its final state while the adapter updates are dispatched later
+ * on, one by one. If you have such an
+ * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver AdapterDataObserver},
+ * you can use
+ * {@link #dispatchUpdatesTo(ListUpdateCallback)} to handle each modification
+ * manually.
+ *
+ * @param adapter A RecyclerView adapter which was displaying the old list and will start
+ * displaying the new list.
+ */
+ public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
+ dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(int position, int count) {
+ adapter.notifyItemRangeInserted(position, count);
+ }
+
+ @Override
+ public void onRemoved(int position, int count) {
+ adapter.notifyItemRangeRemoved(position, count);
+ }
+
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ adapter.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ adapter.notifyItemRangeChanged(position, count, payload);
+ }
+ });
+ }
+
+ /**
+ * Dispatches update operations to the given Callback.
+ * <p>
+ * These updates are atomic such that the first update call effects every update call that
+ * comes after it (the same as RecyclerView).
+ *
+ * @param updateCallback The callback to receive the update operations.
+ * @see #dispatchUpdatesTo(RecyclerView.Adapter)
+ */
+ public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
+ final BatchingListUpdateCallback batchingCallback;
+ if (updateCallback instanceof BatchingListUpdateCallback) {
+ batchingCallback = (BatchingListUpdateCallback) updateCallback;
+ } else {
+ batchingCallback = new BatchingListUpdateCallback(updateCallback);
+ // replace updateCallback with a batching callback and override references to
+ // updateCallback so that we don't call it directly by mistake
+ //noinspection UnusedAssignment
+ updateCallback = batchingCallback;
+ }
+ // These are add/remove ops that are converted to moves. We track their positions until
+ // their respective update operations are processed.
+ final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
+ int posOld = mOldListSize;
+ int posNew = mNewListSize;
+ for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
+ final Snake snake = mSnakes.get(snakeIndex);
+ final int snakeSize = snake.size;
+ final int endX = snake.x + snakeSize;
+ final int endY = snake.y + snakeSize;
+ if (endX < posOld) {
+ dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
+ }
+
+ if (endY < posNew) {
+ dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
+ endY);
+ }
+ for (int i = snakeSize - 1; i >= 0; i--) {
+ if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
+ batchingCallback.onChanged(snake.x + i, 1,
+ mCallback.getChangePayload(snake.x + i, snake.y + i));
+ }
+ }
+ posOld = snake.x;
+ posNew = snake.y;
+ }
+ batchingCallback.dispatchLastEvent();
+ }
+
+ private static PostponedUpdate removePostponedUpdate(List<PostponedUpdate> updates,
+ int pos, boolean removal) {
+ for (int i = updates.size() - 1; i >= 0; i--) {
+ final PostponedUpdate update = updates.get(i);
+ if (update.posInOwnerList == pos && update.removal == removal) {
+ updates.remove(i);
+ for (int j = i; j < updates.size(); j++) {
+ // offset other ops since they swapped positions
+ updates.get(j).currentPos += removal ? 1 : -1;
+ }
+ return update;
+ }
+ }
+ return null;
+ }
+
+ private void dispatchAdditions(List<PostponedUpdate> postponedUpdates,
+ ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
+ if (!mDetectMoves) {
+ updateCallback.onInserted(start, count);
+ return;
+ }
+ for (int i = count - 1; i >= 0; i--) {
+ int status = mNewItemStatuses[globalIndex + i] & FLAG_MASK;
+ switch (status) {
+ case 0: // real addition
+ updateCallback.onInserted(start, 1);
+ for (PostponedUpdate update : postponedUpdates) {
+ update.currentPos += 1;
+ }
+ break;
+ case FLAG_MOVED_CHANGED:
+ case FLAG_MOVED_NOT_CHANGED:
+ final int pos = mNewItemStatuses[globalIndex + i] >> FLAG_OFFSET;
+ final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
+ true);
+ // the item was moved from that position
+ //noinspection ConstantConditions
+ updateCallback.onMoved(update.currentPos, start);
+ if (status == FLAG_MOVED_CHANGED) {
+ // also dispatch a change
+ updateCallback.onChanged(start, 1,
+ mCallback.getChangePayload(pos, globalIndex + i));
+ }
+ break;
+ case FLAG_IGNORE: // ignoring this
+ postponedUpdates.add(new PostponedUpdate(globalIndex + i, start, false));
+ break;
+ default:
+ throw new IllegalStateException(
+ "unknown flag for pos " + (globalIndex + i) + " " + Long
+ .toBinaryString(status));
+ }
+ }
+ }
+
+ private void dispatchRemovals(List<PostponedUpdate> postponedUpdates,
+ ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
+ if (!mDetectMoves) {
+ updateCallback.onRemoved(start, count);
+ return;
+ }
+ for (int i = count - 1; i >= 0; i--) {
+ final int status = mOldItemStatuses[globalIndex + i] & FLAG_MASK;
+ switch (status) {
+ case 0: // real removal
+ updateCallback.onRemoved(start + i, 1);
+ for (PostponedUpdate update : postponedUpdates) {
+ update.currentPos -= 1;
+ }
+ break;
+ case FLAG_MOVED_CHANGED:
+ case FLAG_MOVED_NOT_CHANGED:
+ final int pos = mOldItemStatuses[globalIndex + i] >> FLAG_OFFSET;
+ final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
+ false);
+ // the item was moved to that position. we do -1 because this is a move not
+ // add and removing current item offsets the target move by 1
+ //noinspection ConstantConditions
+ updateCallback.onMoved(start + i, update.currentPos - 1);
+ if (status == FLAG_MOVED_CHANGED) {
+ // also dispatch a change
+ updateCallback.onChanged(update.currentPos - 1, 1,
+ mCallback.getChangePayload(globalIndex + i, pos));
+ }
+ break;
+ case FLAG_IGNORE: // ignoring this
+ postponedUpdates.add(new PostponedUpdate(globalIndex + i, start + i, true));
+ break;
+ default:
+ throw new IllegalStateException(
+ "unknown flag for pos " + (globalIndex + i) + " " + Long
+ .toBinaryString(status));
+ }
+ }
+ }
+
+ @VisibleForTesting
+ List<Snake> getSnakes() {
+ return mSnakes;
+ }
+ }
+
+ /**
+ * Represents an update that we skipped because it was a move.
+ * <p>
+ * When an update is skipped, it is tracked as other updates are dispatched until the matching
+ * add/remove operation is found at which point the tracked position is used to dispatch the
+ * update.
+ */
+ private static class PostponedUpdate {
+
+ int posInOwnerList;
+
+ int currentPos;
+
+ boolean removal;
+
+ public PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) {
+ this.posInOwnerList = posInOwnerList;
+ this.currentPos = currentPos;
+ this.removal = removal;
+ }
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java b/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java
new file mode 100644
index 0000000..2136202
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/util/ListUpdateCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.util;
+
+/**
+ * An interface that can receive Update operations that are applied to a list.
+ * <p>
+ * This class can be used together with DiffUtil to detect changes between two lists.
+ */
+public interface ListUpdateCallback {
+ /**
+ * Called when {@code count} number of items are inserted at the given position.
+ *
+ * @param position The position of the new item.
+ * @param count The number of items that have been added.
+ */
+ void onInserted(int position, int count);
+
+ /**
+ * Called when {@code count} number of items are removed from the given position.
+ *
+ * @param position The position of the item which has been removed.
+ * @param count The number of items which have been removed.
+ */
+ void onRemoved(int position, int count);
+
+ /**
+ * Called when an item changes its position in the list.
+ *
+ * @param fromPosition The previous position of the item before the move.
+ * @param toPosition The new position of the item.
+ */
+ void onMoved(int fromPosition, int toPosition);
+
+ /**
+ * Called when {@code count} number of items are updated at the given position.
+ *
+ * @param position The position of the item which has been updated.
+ * @param count The number of items which has changed.
+ */
+ void onChanged(int position, int count, Object payload);
+}
diff --git a/v7/recyclerview/src/android/support/v7/util/SortedList.java b/v7/recyclerview/src/android/support/v7/util/SortedList.java
index 5ecb66f..1d48468 100644
--- a/v7/recyclerview/src/android/support/v7/util/SortedList.java
+++ b/v7/recyclerview/src/android/support/v7/util/SortedList.java
@@ -681,7 +681,7 @@
* SortedList calls the callback methods on this class to notify changes about the underlying
* data.
*/
- public static abstract class Callback<T2> implements Comparator<T2> {
+ public static abstract class Callback<T2> implements Comparator<T2>, ListUpdateCallback {
/**
* Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and
@@ -698,30 +698,6 @@
abstract public int compare(T2 o1, T2 o2);
/**
- * Called by the SortedList when an item is inserted at the given position.
- *
- * @param position The position of the new item.
- * @param count The number of items that have been added.
- */
- abstract public void onInserted(int position, int count);
-
- /**
- * Called by the SortedList when an item is removed from the given position.
- *
- * @param position The position of the item which has been removed.
- * @param count The number of items which have been removed.
- */
- abstract public void onRemoved(int position, int count);
-
- /**
- * Called by the SortedList when an item changes its position in the list.
- *
- * @param fromPosition The previous position of the item before the move.
- * @param toPosition The new position of the item.
- */
- abstract public void onMoved(int fromPosition, int toPosition);
-
- /**
* Called by the SortedList when the item at the given position is updated.
*
* @param position The position of the item which has been updated.
@@ -729,6 +705,11 @@
*/
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
@@ -781,16 +762,7 @@
public static class BatchedCallback<T2> extends Callback<T2> {
private final Callback<T2> mWrappedCallback;
- static final int TYPE_NONE = 0;
- static final int TYPE_ADD = 1;
- static final int TYPE_REMOVE = 2;
- static final int TYPE_CHANGE = 3;
- static final int TYPE_MOVE = 4;
-
- int mLastEventType = TYPE_NONE;
- int mLastEventPosition = -1;
- int mLastEventCount = -1;
-
+ private final BatchingListUpdateCallback mBatchingListUpdateCallback;
/**
* Creates a new BatchedCallback that wraps the provided Callback.
*
@@ -800,6 +772,7 @@
*/
public BatchedCallback(Callback<T2> wrappedCallback) {
mWrappedCallback = wrappedCallback;
+ mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback);
}
@Override
@@ -809,51 +782,22 @@
@Override
public void onInserted(int position, int count) {
- if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
- && position <= mLastEventPosition + mLastEventCount) {
- mLastEventCount += count;
- mLastEventPosition = Math.min(position, mLastEventPosition);
- return;
- }
- dispatchLastEvent();
- mLastEventPosition = position;
- mLastEventCount = count;
- mLastEventType = TYPE_ADD;
+ mBatchingListUpdateCallback.onInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
- if (mLastEventType == TYPE_REMOVE && mLastEventPosition == position) {
- mLastEventCount += count;
- return;
- }
- dispatchLastEvent();
- mLastEventPosition = position;
- mLastEventCount = count;
- mLastEventType = TYPE_REMOVE;
+ mBatchingListUpdateCallback.onRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
- dispatchLastEvent();//moves are not merged
- mWrappedCallback.onMoved(fromPosition, toPosition);
+ mBatchingListUpdateCallback.onInserted(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
- if (mLastEventType == TYPE_CHANGE &&
- !(position > mLastEventPosition + mLastEventCount
- || position + count < mLastEventPosition)) {
- // take potential overlap into account
- int previousEnd = mLastEventPosition + mLastEventCount;
- mLastEventPosition = Math.min(position, mLastEventPosition);
- mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
- return;
- }
- dispatchLastEvent();
- mLastEventPosition = position;
- mLastEventCount = count;
- mLastEventType = TYPE_CHANGE;
+ mBatchingListUpdateCallback.onChanged(position, count, null);
}
@Override
@@ -866,27 +810,12 @@
return mWrappedCallback.areItemsTheSame(item1, item2);
}
-
/**
* This method dispatches any pending event notifications to the wrapped Callback.
* You <b>must</b> always call this method after you are done with editing the SortedList.
*/
public void dispatchLastEvent() {
- if (mLastEventType == TYPE_NONE) {
- return;
- }
- switch (mLastEventType) {
- case TYPE_ADD:
- mWrappedCallback.onInserted(mLastEventPosition, mLastEventCount);
- break;
- case TYPE_REMOVE:
- mWrappedCallback.onRemoved(mLastEventPosition, mLastEventCount);
- break;
- case TYPE_CHANGE:
- mWrappedCallback.onChanged(mLastEventPosition, mLastEventCount);
- break;
- }
- mLastEventType = TYPE_NONE;
+ mBatchingListUpdateCallback.dispatchLastEvent();
}
}
}