diff --git a/build.gradle b/build.gradle
index d11b976..58ecbd0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -16,7 +16,7 @@ android {
}
dependencies {
- provided 'com.android.support:support-annotations:25.3.1'
+ provided 'com.android.support:support-annotations:25.4.0'
provided 'io.reactivex:rxandroid:1.2.1'
provided 'io.reactivex:rxjava:1.3.0'
}
diff --git a/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/BatchingListUpdateCallback.java b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/BatchingListUpdateCallback.java
new file mode 100644
index 0000000..1b801c6
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/BatchingListUpdateCallback.java
@@ -0,0 +1,126 @@
+/*
+ * 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 ru.touchin.roboswag.core.android.support.v7.util;
+
+/**
+ * Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
+ *
+ *
For instance, when 2 add operations comes that adds 2 consecutive elements,
+ * BatchingListUpdateCallback merges them and calls the wrapped callback only once.
+ *
+ *
This is a general purpose class and is also used by
+ * {@link DiffUtil.DiffResult DiffResult}
+ *
+ *
If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
+ * stream of update events drain.
+ */
+@SuppressWarnings({"PMD", "checkstyle:all"})
+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;
+
+ private final ListUpdateCallback mWrapped;
+
+ private int mLastEventType = TYPE_NONE;
+ private int mLastEventPosition = -1;
+ private int mLastEventCount = -1;
+ private 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/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/DiffUtil.java b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/DiffUtil.java
new file mode 100644
index 0000000..270c93b
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/DiffUtil.java
@@ -0,0 +1,787 @@
+/*
+ * 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 ru.touchin.roboswag.core.android.support.v7.util;
+
+import android.support.annotation.Nullable;
+
+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.
+ *
+ *
It can be used to calculate updates for a RecyclerView Adapter.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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 areSame list is composed of random UUID Strings and the tests are run on Nexus 5X with M)
+ *
+ * - 100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
+ *
- 100 items and 100 modifications: 3.82 ms, median: 3.75 ms
+ *
- 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
+ *
- 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
+ *
- 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
+ *
- 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
+ *
- 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
+ *
+ *
+ * Due to implementation constraints, the max size of the list can be 2^26.
+ */
+@SuppressWarnings({"PMD", "checkstyle:all"})
+public class DiffUtil {
+
+ private DiffUtil() {
+ // utility class, no instance.
+ }
+
+ private static final Comparator SNAKE_COMPARATOR = new Comparator() {
+ @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.
+ *
+ * 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 O(N^2) 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 snakes = new ArrayList<>();
+
+ // instead of a recursive implementation, we keep our own stack to avoid potential stack
+ // overflow exceptions
+ final List 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 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.
+ *
+ * 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.
+ *
+ * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
+ * so that you can change its behavior depending on your UI.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You can consume the updates in a DiffResult via
+ * {@link #dispatchUpdatesTo(ListUpdateCallback)}
+ */
+ 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.
+ *
+ * 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 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 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.
+ *
+ * This class also flags whether an item has been changed or not.
+ *
+ * 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 update operations to the given Callback.
+ *
+ * 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.
+ */
+ 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 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 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 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 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));
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents an update that we skipped because it was a move.
+ *
+ * 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/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/ListUpdateCallback.java b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/ListUpdateCallback.java
new file mode 100644
index 0000000..06d9dc0
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/android/support/v7/util/ListUpdateCallback.java
@@ -0,0 +1,59 @@
+/*
+ * 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 ru.touchin.roboswag.core.android.support.v7.util;
+
+/**
+ * An interface that can receive Update operations that are applied to a list.
+ *
+ *
This class can be used together with DiffUtil to detect changes between two lists.
+ */
+@SuppressWarnings({"PMD", "checkstyle:all"})
+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/src/main/java/ru/touchin/roboswag/core/observables/collections/Change.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/Change.java
deleted file mode 100644
index 876d25f..0000000
--- a/src/main/java/ru/touchin/roboswag/core/observables/collections/Change.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
- *
- * This file is part of RoboSwag library.
- *
- * 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 ru.touchin.roboswag.core.observables.collections;
-
-import android.support.annotation.NonNull;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Created by Gavriil Sitnikov on 23/05/16.
- * Class representing simple change of collection like insertion, remove or replacing/changing items.
- *
- * @param Type of changing collection's items.
- */
-public class Change {
-
- /**
- * Method to calculate changes between two collections.
- *
- * @param initialCollection Initial collection;
- * @param modifiedCollection Changed collection;
- * @param shrinkChangesToModifiedSize Flag to make position of changed items be less then modified collection size.
- * It is needed sometimes to not get exceptions like {@link ArrayIndexOutOfBoundsException}.
- * @param Type of collections items.
- * @return Changes between collections.
- */
- @NonNull
- public static Collection> calculateCollectionChanges(@NonNull final Collection initialCollection,
- @NonNull final Collection modifiedCollection,
- final boolean shrinkChangesToModifiedSize) {
- return new CollectionsChangesCalculator<>(initialCollection, modifiedCollection, shrinkChangesToModifiedSize).calculateChanges();
- }
-
- @NonNull
- private final Type type;
- @NonNull
- private final Collection changedItems;
- private final int start;
- private final int count;
-
- public Change(@NonNull final Type type, @NonNull final Collection changedItems, final int start) {
- this.type = type;
- this.changedItems = Collections.unmodifiableCollection(new ArrayList<>(changedItems));
- this.start = start;
- this.count = changedItems.size();
- }
-
- /**
- * Returns type of change.
- *
- * @return Type of change.
- */
- @NonNull
- public Type getType() {
- return type;
- }
-
- /**
- * Returns collection of items which this change applied to.
- *
- * @return Changed items.
- */
- @NonNull
- public Collection getChangedItems() {
- return changedItems;
- }
-
- /**
- * Returns first index of changed item.
- *
- * @return Start of change.
- */
- public int getStart() {
- return start;
- }
-
- /**
- * Returns count of changed items.
- *
- * @return Count of changed items.
- */
- public int getCount() {
- return count;
- }
-
- @NonNull
- @Override
- public String toString() {
- return type + " change of " + start + ":" + count;
- }
-
- /**
- * Type of change.
- */
- public enum Type {
- INSERTED,
- CHANGED,
- REMOVED
- }
-
- private static class CollectionsChangesCalculator {
-
- @NonNull
- private final Collection initialCollection;
- @NonNull
- private final Collection modifiedCollection;
- private final boolean shrinkChangesToModifiedSize;
- private int initialOffset;
- @NonNull
- private final Collection itemsToAdd = new ArrayList<>();
- private int currentSize;
- private int oldSize;
- private int newSize;
- private int couldBeAdded;
-
- public CollectionsChangesCalculator(@NonNull final Collection initialCollection,
- @NonNull final Collection modifiedCollection,
- final boolean shrinkChangesToModifiedSize) {
- this.initialCollection = initialCollection;
- this.modifiedCollection = modifiedCollection;
- this.shrinkChangesToModifiedSize = shrinkChangesToModifiedSize;
- }
-
- @NonNull
- public Collection> calculateChanges() {
- initialOffset = 0;
- itemsToAdd.clear();
- currentSize = 0;
- oldSize = initialCollection.size();
- newSize = modifiedCollection.size();
- couldBeAdded = modifiedCollection.size() - initialCollection.size();
- final List> result = new ArrayList<>();
- for (final TItem modifiedItem : modifiedCollection) {
- int foundPosition = 0;
- for (final Object initialObject : initialCollection) {
- if (foundPosition >= initialOffset && modifiedItem.equals(initialObject)) {
- if (tryAddSkipped(result) == MethodAction.RETURN
- || tryRemoveRest(result, initialOffset, foundPosition - initialOffset) == MethodAction.RETURN) {
- return result;
- }
- initialOffset = foundPosition + 1;
- currentSize++;
- break;
- }
- foundPosition++;
- }
- // if not found
- if (foundPosition >= initialCollection.size()) {
- itemsToAdd.add(modifiedItem);
- }
- }
-
- if (tryAddSkipped(result) == MethodAction.RETURN) {
- return result;
- }
- tryRemoveRest(result, initialOffset, initialCollection.size() - initialOffset);
- return result;
- }
-
- @NonNull
- private MethodAction tryAddSkipped(@NonNull final Collection> changes) {
- if (!itemsToAdd.isEmpty()) {
- if (shrinkChangesToModifiedSize && couldBeAdded < itemsToAdd.size()) {
- addSimpleDifferenceChanges(changes);
- return MethodAction.RETURN;
- }
- changes.add(new Change<>(Type.INSERTED, itemsToAdd, currentSize));
- currentSize += itemsToAdd.size();
- couldBeAdded -= itemsToAdd.size();
- itemsToAdd.clear();
- }
- return MethodAction.CONTINUE;
- }
-
- @NonNull
- private MethodAction tryRemoveRest(@NonNull final Collection> changes,
- final int initialOffset,
- final int itemsToRemove) {
- if (itemsToRemove > 0) {
- if (shrinkChangesToModifiedSize && couldBeAdded < -itemsToRemove) {
- addSimpleDifferenceChanges(changes);
- return MethodAction.RETURN;
- }
- changes.add(new Change<>(Change.Type.REMOVED,
- new ArrayList<>(initialCollection).subList(initialOffset, initialOffset + itemsToRemove),
- currentSize));
- }
- return MethodAction.CONTINUE;
- }
-
- private void addSimpleDifferenceChanges(@NonNull final Collection> changes) {
- changes.add(new Change<>(Type.CHANGED, new ArrayList<>(modifiedCollection).subList(currentSize, newSize), currentSize));
- if (oldSize - newSize > 0) {
- changes.add(new Change<>(Type.REMOVED, new ArrayList<>(initialCollection).subList(newSize, oldSize), newSize));
- }
- }
-
- private enum MethodAction {
- RETURN,
- CONTINUE
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableCollection.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableCollection.java
index d759085..c6e4057 100644
--- a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableCollection.java
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableCollection.java
@@ -27,7 +27,10 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
+import ru.touchin.roboswag.core.observables.collections.changes.Change;
+import ru.touchin.roboswag.core.observables.collections.changes.CollectionChanges;
import rx.Emitter;
import rx.Observable;
@@ -43,11 +46,11 @@ public abstract class ObservableCollection {
private int changesCount;
@NonNull
- private transient Observable> changesObservable;
+ private transient Observable> changesObservable;
@NonNull
private transient Observable> itemsObservable;
@Nullable
- private transient Emitter super CollectionChange> changesEmitter;
+ private transient Emitter super CollectionChanges> changesEmitter;
public ObservableCollection() {
this.changesObservable = createChangesObservable();
@@ -55,9 +58,9 @@ public abstract class ObservableCollection {
}
@NonNull
- private Observable> createChangesObservable() {
+ private Observable> createChangesObservable() {
return Observable
- .>create(emitter -> this.changesEmitter = emitter, Emitter.BackpressureMode.BUFFER)
+ .>create(emitter -> this.changesEmitter = emitter, Emitter.BackpressureMode.BUFFER)
.doOnUnsubscribe(() -> this.changesEmitter = null)
.share();
}
@@ -85,22 +88,28 @@ public abstract class ObservableCollection {
*
* @param change Change of collection.
*/
- protected void notifyAboutChange(@NonNull final Change change) {
- notifyAboutChanges(Collections.singleton(change));
+ protected void notifyAboutChange(@NonNull final List insertedItems,
+ @NonNull final List removedItems,
+ @NonNull final Change change) {
+ notifyAboutChanges(insertedItems, removedItems, Collections.singleton(change));
}
/**
* Method to notify that collection have changed.
*
+ * @param insertedItems Collection of inserted items;
+ * @param removedItems Collection of removed items;
* @param changes Changes of collection.
*/
- protected void notifyAboutChanges(@NonNull final Collection> changes) {
+ protected void notifyAboutChanges(@NonNull final List insertedItems,
+ @NonNull final List removedItems,
+ @NonNull final Collection changes) {
if (changes.isEmpty()) {
return;
}
changesCount++;
if (changesEmitter != null) {
- changesEmitter.onNext(new CollectionChange<>(changesCount, Collections.unmodifiableCollection(changes)));
+ changesEmitter.onNext(new CollectionChanges<>(changesCount, insertedItems, removedItems, changes));
}
}
@@ -110,7 +119,7 @@ public abstract class ObservableCollection {
* @return List of changes applied to collection.
*/
@NonNull
- public Observable> observeChanges() {
+ public Observable> observeChanges() {
return changesObservable;
}
@@ -170,41 +179,4 @@ public abstract class ObservableCollection {
this.itemsObservable = createItemsObservable();
}
- /**
- * Class which is representing change of collection. There could be multiple changes applied to collection.
- *
- * @param Type of collection's items.
- */
- public static class CollectionChange {
-
- private final int number;
- @NonNull
- private final Collection> changes;
-
- protected CollectionChange(final int number, @NonNull final Collection> changes) {
- this.number = number;
- this.changes = changes;
- }
-
- /**
- * Returns number of change.
- *
- * @return Number of change.
- */
- public int getNumber() {
- return number;
- }
-
- /**
- * Returns collection of changes.
- *
- * @return Collection of changes.
- */
- @NonNull
- public Collection> getChanges() {
- return changes;
- }
-
- }
-
}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java
index 6898560..158ccf3 100644
--- a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java
@@ -9,6 +9,7 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
+import ru.touchin.roboswag.core.observables.collections.changes.DefaultCollectionsChangesCalculator;
import rx.Scheduler;
import rx.Subscription;
import rx.functions.Func1;
@@ -23,6 +24,9 @@ import rx.schedulers.Schedulers;
*/
public class ObservableFilteredList extends ObservableCollection {
+ // we need to filter on 1 thread to prevent parallel filtering
+ private static final Scheduler FILTER_SCHEDULER = Schedulers.from(Executors.newSingleThreadExecutor());
+
@NonNull
private static List filterCollection(@NonNull final Collection sourceCollection,
@Nullable final Func1 filter) {
@@ -38,9 +42,6 @@ public class ObservableFilteredList extends ObservableCollection {
return result;
}
- // we need to filter on 1 thread to prevent parallel filtering
- @NonNull
- private final Scheduler filterScheduler = Schedulers.from(Executors.newSingleThreadExecutor());
@NonNull
private List filteredList;
@NonNull
@@ -106,11 +107,13 @@ public class ObservableFilteredList extends ObservableCollection {
sourceCollectionSubscription = null;
}
sourceCollectionSubscription = sourceCollection.observeItems()
- .observeOn(filterScheduler)
+ .observeOn(FILTER_SCHEDULER)
.subscribe(items -> {
final List oldFilteredList = filteredList;
filteredList = filterCollection(items, filter);
- notifyAboutChanges(Change.calculateCollectionChanges(oldFilteredList, filteredList, false));
+ final DefaultCollectionsChangesCalculator calculator
+ = new DefaultCollectionsChangesCalculator<>(oldFilteredList, filteredList, false);
+ notifyAboutChanges(calculator.calculateInsertedItems(), calculator.calculateRemovedItems(), calculator.calculateChanges());
});
}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableList.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableList.java
index 5610c64..2c08c31 100644
--- a/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableList.java
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableList.java
@@ -20,6 +20,7 @@
package ru.touchin.roboswag.core.observables.collections;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
@@ -31,6 +32,12 @@ import java.util.Collections;
import java.util.List;
import ru.touchin.roboswag.core.log.Lc;
+import ru.touchin.roboswag.core.observables.collections.changes.Change;
+import ru.touchin.roboswag.core.observables.collections.changes.ChangePayloadProducer;
+import ru.touchin.roboswag.core.observables.collections.changes.CollectionsChangesCalculator;
+import ru.touchin.roboswag.core.observables.collections.changes.DefaultCollectionsChangesCalculator;
+import ru.touchin.roboswag.core.observables.collections.changes.DiffCollectionsChangesCalculator;
+import ru.touchin.roboswag.core.observables.collections.changes.SameItemsPredicate;
/**
* Created by Gavriil Sitnikov on 23/05/16.
@@ -46,6 +53,11 @@ public class ObservableList extends ObservableCollection implement
@NonNull
private List items;
+ private boolean detectMoves;
+ @Nullable
+ private SameItemsPredicate sameItemsPredicate;
+ @Nullable
+ private ChangePayloadProducer changePayloadProducer;
public ObservableList() {
super();
@@ -75,7 +87,7 @@ public class ObservableList extends ObservableCollection implement
public void add(final int position, @NonNull final TItem item) {
synchronized (this) {
items.add(position, item);
- notifyAboutChange(new Change<>(Change.Type.INSERTED, Collections.singleton(item), position));
+ notifyAboutChange(Collections.singletonList(item), Collections.emptyList(), new Change.Inserted(position, 1));
}
}
@@ -98,7 +110,7 @@ public class ObservableList extends ObservableCollection implement
synchronized (this) {
if (!itemsToAdd.isEmpty()) {
items.addAll(position, itemsToAdd);
- notifyAboutChange(new Change<>(Change.Type.INSERTED, itemsToAdd, position));
+ notifyAboutChange(new ArrayList<>(itemsToAdd), Collections.emptyList(), new Change.Inserted(position, itemsToAdd.size()));
}
}
}
@@ -139,12 +151,11 @@ public class ObservableList extends ObservableCollection implement
return;
}
synchronized (this) {
- final List changedItems = new ArrayList<>(count);
+ final List removedItems = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
- changedItems.add(items.get(position));
- items.remove(position);
+ removedItems.add(items.remove(position));
}
- notifyAboutChange(new Change<>(Change.Type.REMOVED, changedItems, position));
+ notifyAboutChange(Collections.emptyList(), removedItems, new Change.Removed(position, count));
}
}
@@ -153,10 +164,10 @@ public class ObservableList extends ObservableCollection implement
*/
public void clear() {
synchronized (this) {
- final Change change = new Change<>(Change.Type.REMOVED, items, 0);
- if (!change.getChangedItems().isEmpty()) {
+ if (!items.isEmpty()) {
+ final List removedItems = new ArrayList<>(items);
items.clear();
- notifyAboutChange(change);
+ notifyAboutChange(Collections.emptyList(), removedItems, new Change.Removed(0, removedItems.size()));
}
}
}
@@ -203,7 +214,7 @@ public class ObservableList extends ObservableCollection implement
items.set(index, item);
index++;
}
- notifyAboutChange(new Change<>(Change.Type.CHANGED, updatedItems, position));
+ notifyAboutChange(Collections.emptyList(), Collections.emptyList(), new Change.Changed(position, updatedItems.size(), null));
}
}
@@ -214,10 +225,14 @@ public class ObservableList extends ObservableCollection implement
*/
public void set(@NonNull final Collection newItems) {
synchronized (this) {
- final Collection> changes = Change.calculateCollectionChanges(items, newItems, false);
+ final List oldList = new ArrayList<>(items);
+ final List newList = new ArrayList<>(newItems);
+ final CollectionsChangesCalculator calculator = sameItemsPredicate != null
+ ? new DiffCollectionsChangesCalculator<>(oldList, newList, detectMoves, sameItemsPredicate, changePayloadProducer)
+ : new DefaultCollectionsChangesCalculator<>(oldList, newList, false);
items.clear();
items.addAll(newItems);
- notifyAboutChanges(changes);
+ notifyAboutChanges(calculator.calculateInsertedItems(), calculator.calculateRemovedItems(), calculator.calculateChanges());
}
}
@@ -228,6 +243,37 @@ public class ObservableList extends ObservableCollection implement
}
}
+ /**
+ * Enable diff utils algorithm in collection changes.
+ *
+ * @param detectMoves The flag that determines whether the {@link Change.Moved} changes will be generated or not;
+ * @param sameItemsPredicate Predicate for the determination of the same elements;
+ * @param changePayloadProducer Function that calculate change payload when items the same but contents are different.
+ */
+ public void enableDiffUtils(final boolean detectMoves,
+ @NonNull final SameItemsPredicate sameItemsPredicate,
+ @Nullable final ChangePayloadProducer changePayloadProducer) {
+ this.detectMoves = detectMoves;
+ this.sameItemsPredicate = sameItemsPredicate;
+ this.changePayloadProducer = changePayloadProducer;
+ }
+
+ /**
+ * Disable diff utils algorithm.
+ */
+ public void disableDiffUtils() {
+ this.sameItemsPredicate = null;
+ }
+
+ /**
+ * Returns enabled flag of diff utils.
+ *
+ * @return true if diff utils is enabled.
+ */
+ public boolean diffUtilsIsEnabled() {
+ return sameItemsPredicate != null;
+ }
+
/**
* Returns position of item in list.
*
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/Change.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/Change.java
new file mode 100644
index 0000000..a1b1c80
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/Change.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.Nullable;
+
+/**
+ * Created by Gavriil Sitnikov on 23/05/16.
+ * Class representing simple change of collection like insertion, remove or replacing/changing items.
+ */
+public abstract class Change {
+
+ /**
+ * Represents a insert operation in collection.
+ */
+ public static class Inserted extends Change {
+
+ private final int position;
+ private final int count;
+
+ public Inserted(final int position, final int count) {
+ super();
+ this.position = position;
+ this.count = count;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ /**
+ * Represents a remove operation from collection.
+ */
+ public static class Removed extends Change {
+
+ private final int position;
+ private final int count;
+
+ public Removed(final int position, final int count) {
+ super();
+ this.position = position;
+ this.count = count;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ }
+
+ /**
+ * Represents a move operation in collection.
+ */
+ public static class Moved extends Change {
+
+ private final int fromPosition;
+ private final int toPosition;
+
+ public Moved(final int fromPosition, final int toPosition) {
+ super();
+ this.fromPosition = fromPosition;
+ this.toPosition = toPosition;
+ }
+
+ public int getFromPosition() {
+ return fromPosition;
+ }
+
+ public int getToPosition() {
+ return toPosition;
+ }
+
+ }
+
+ /**
+ * Represents a modification operation in a collection.
+ */
+ public static class Changed extends Change {
+
+ private final int position;
+ private final int count;
+ @Nullable
+ private final Object payload;
+
+ public Changed(final int position, final int count, @Nullable final Object payload) {
+ super();
+ this.position = position;
+ this.count = count;
+ this.payload = payload;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ @Nullable
+ public Object getPayload() {
+ return payload;
+ }
+
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/ChangePayloadProducer.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/ChangePayloadProducer.java
new file mode 100644
index 0000000..78f4319
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/ChangePayloadProducer.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+public interface ChangePayloadProducer {
+
+ @Nullable
+ Object getChangePayload(@NonNull TItem item1, @NonNull TItem item2);
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionChanges.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionChanges.java
new file mode 100644
index 0000000..f161155
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionChanges.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class which is representing change of collection. There could be multiple changes applied to collection.
+ */
+public class CollectionChanges {
+
+ private final int number;
+ @NonNull
+ private final List insertedItems;
+ @NonNull
+ private final List removedItems;
+ @NonNull
+ private final Collection changes;
+
+ public CollectionChanges(final int number,
+ @NonNull final List insertedItems,
+ @NonNull final List removedItems,
+ @NonNull final Collection changes) {
+ this.number = number;
+ this.insertedItems = Collections.unmodifiableList(insertedItems);
+ this.removedItems = Collections.unmodifiableList(removedItems);
+ this.changes = Collections.unmodifiableCollection(changes);
+ }
+
+ /**
+ * Returns number of change.
+ *
+ * @return Number of change.
+ */
+ public int getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns collection of changes.
+ *
+ * @return Collection of changes.
+ */
+ @NonNull
+ public Collection getChanges() {
+ return changes;
+ }
+
+ /**
+ * Returns inserted items in change.
+ *
+ * @return Inserted items.
+ */
+ @NonNull
+ public List getInsertedItems() {
+ return insertedItems;
+ }
+
+ /**
+ * Returns removed items in change.
+ *
+ * @return Removed items.
+ */
+ @NonNull
+ public List getRemovedItems() {
+ return removedItems;
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionsChangesCalculator.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionsChangesCalculator.java
new file mode 100644
index 0000000..d86147d
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/CollectionsChangesCalculator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+
+import java.util.List;
+
+public interface CollectionsChangesCalculator {
+
+ @NonNull
+ List calculateChanges();
+
+ @NonNull
+ List calculateInsertedItems();
+
+ @NonNull
+ List calculateRemovedItems();
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DefaultCollectionsChangesCalculator.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DefaultCollectionsChangesCalculator.java
new file mode 100644
index 0000000..883fed8
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DefaultCollectionsChangesCalculator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class DefaultCollectionsChangesCalculator implements CollectionsChangesCalculator {
+
+ @NonNull
+ private final Collection initialCollection;
+ @NonNull
+ private final Collection modifiedCollection;
+ private final boolean shrinkChangesToModifiedSize;
+ @NonNull
+ private final Collection itemsToAdd = new ArrayList<>();
+ private int currentSize;
+ private int oldSize;
+ private int newSize;
+ private int couldBeAdded;
+
+ /**
+ * Default calculator of changes between two collections.
+ *
+ * @param initialCollection Initial collection;
+ * @param modifiedCollection Changed collection;
+ * @param shrinkChangesToModifiedSize Flag to make position of changed items be less then modified collection size.
+ * It is needed sometimes to not get exceptions like {@link ArrayIndexOutOfBoundsException}.
+ */
+ public DefaultCollectionsChangesCalculator(@NonNull final Collection initialCollection,
+ @NonNull final Collection modifiedCollection,
+ final boolean shrinkChangesToModifiedSize) {
+ super();
+ this.initialCollection = initialCollection;
+ this.modifiedCollection = modifiedCollection;
+ this.shrinkChangesToModifiedSize = shrinkChangesToModifiedSize;
+ }
+
+ @NonNull
+ @Override
+ public List calculateChanges() {
+ int initialOffset = 0;
+ itemsToAdd.clear();
+ currentSize = 0;
+ oldSize = initialCollection.size();
+ newSize = modifiedCollection.size();
+ couldBeAdded = modifiedCollection.size() - initialCollection.size();
+ final List result = new ArrayList<>();
+ for (final TItem modifiedItem : modifiedCollection) {
+ int foundPosition = 0;
+ for (final Object initialObject : initialCollection) {
+ if (foundPosition >= initialOffset && modifiedItem.equals(initialObject)) {
+ if (tryAddSkipped(result) == MethodAction.RETURN
+ || tryRemoveRest(result, foundPosition - initialOffset) == MethodAction.RETURN) {
+ return result;
+ }
+ initialOffset = foundPosition + 1;
+ currentSize++;
+ break;
+ }
+ foundPosition++;
+ }
+ // if not found
+ if (foundPosition >= initialCollection.size()) {
+ itemsToAdd.add(modifiedItem);
+ }
+ }
+
+ if (tryAddSkipped(result) == MethodAction.RETURN) {
+ return result;
+ }
+ tryRemoveRest(result, initialCollection.size() - initialOffset);
+ return result;
+ }
+
+ @NonNull
+ @Override
+ public List calculateInsertedItems() {
+ final List insertedItems = new ArrayList<>();
+ for (final TItem newItem : modifiedCollection) {
+ if (!initialCollection.contains(newItem)) {
+ insertedItems.add(newItem);
+ }
+ }
+ return insertedItems;
+ }
+
+ @NonNull
+ @Override
+ public List calculateRemovedItems() {
+ final List removedItems = new ArrayList<>();
+ for (final TItem oldItem : initialCollection) {
+ if (!modifiedCollection.contains(oldItem)) {
+ removedItems.add(oldItem);
+ }
+ }
+ return removedItems;
+ }
+
+ @NonNull
+ private MethodAction tryAddSkipped(@NonNull final Collection changes) {
+ if (!itemsToAdd.isEmpty()) {
+ if (shrinkChangesToModifiedSize && couldBeAdded < itemsToAdd.size()) {
+ addSimpleDifferenceChanges(changes);
+ return MethodAction.RETURN;
+ }
+ changes.add(new Change.Inserted(currentSize, itemsToAdd.size()));
+ currentSize += itemsToAdd.size();
+ couldBeAdded -= itemsToAdd.size();
+ itemsToAdd.clear();
+ }
+ return MethodAction.CONTINUE;
+ }
+
+ @NonNull
+ private MethodAction tryRemoveRest(@NonNull final Collection changes, final int itemsToRemove) {
+ if (itemsToRemove > 0) {
+ if (shrinkChangesToModifiedSize && couldBeAdded < -itemsToRemove) {
+ addSimpleDifferenceChanges(changes);
+ return MethodAction.RETURN;
+ }
+ changes.add(new Change.Removed(currentSize, itemsToRemove));
+ }
+ return MethodAction.CONTINUE;
+ }
+
+ private void addSimpleDifferenceChanges(@NonNull final Collection changes) {
+ changes.add(new Change.Changed(currentSize, newSize - currentSize, null));
+ if (oldSize - newSize > 0) {
+ changes.add(new Change.Removed(newSize, oldSize - newSize));
+ }
+ }
+
+ private enum MethodAction {
+ RETURN,
+ CONTINUE
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DiffCollectionsChangesCalculator.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DiffCollectionsChangesCalculator.java
new file mode 100644
index 0000000..9115e6c
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/DiffCollectionsChangesCalculator.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import ru.touchin.roboswag.core.android.support.v7.util.DiffUtil;
+import ru.touchin.roboswag.core.android.support.v7.util.ListUpdateCallback;
+
+public class DiffCollectionsChangesCalculator extends DiffUtil.Callback implements CollectionsChangesCalculator {
+
+ @NonNull
+ private final List oldList;
+ @NonNull
+ private final List newList;
+ private final boolean detectMoves;
+ @NonNull
+ private final SameItemsPredicate sameItemsPredicate;
+ @Nullable
+ private final ChangePayloadProducer changePayloadProducer;
+
+ public DiffCollectionsChangesCalculator(@NonNull final List oldList,
+ @NonNull final List newList,
+ final boolean detectMoves,
+ @NonNull final SameItemsPredicate sameItemsPredicate,
+ @Nullable final ChangePayloadProducer changePayloadProducer) {
+ super();
+ this.oldList = oldList;
+ this.newList = newList;
+ this.detectMoves = detectMoves;
+ this.sameItemsPredicate = sameItemsPredicate;
+ this.changePayloadProducer = changePayloadProducer;
+ }
+
+ @NonNull
+ @Override
+ public List calculateChanges() {
+ final List changes = new ArrayList<>();
+ DiffUtil.calculateDiff(this, detectMoves).dispatchUpdatesTo(new ListUpdateCallback() {
+ @Override
+ public void onInserted(final int position, final int count) {
+ changes.add(new Change.Inserted(position, count));
+ }
+
+ @Override
+ public void onRemoved(final int position, final int count) {
+ changes.add(new Change.Removed(position, count));
+ }
+
+ @Override
+ public void onMoved(final int fromPosition, final int toPosition) {
+ changes.add(new Change.Moved(fromPosition, toPosition));
+ }
+
+ @Override
+ public void onChanged(final int position, final int count, @Nullable final Object payload) {
+ changes.add(new Change.Changed(position, count, payload));
+ }
+ });
+ return changes;
+ }
+
+ @NonNull
+ @Override
+ public List calculateInsertedItems() {
+ final List insertedItems = new ArrayList<>();
+ for (final TItem newItem : newList) {
+ if (!containsByPredicate(newItem, oldList)) {
+ insertedItems.add(newItem);
+ }
+ }
+ return insertedItems;
+ }
+
+ @NonNull
+ @Override
+ public List calculateRemovedItems() {
+ final List removedItems = new ArrayList<>();
+ for (final TItem oldItem : oldList) {
+ if (!containsByPredicate(oldItem, newList)) {
+ removedItems.add(oldItem);
+ }
+ }
+ return removedItems;
+ }
+
+ @Override
+ public int getOldListSize() {
+ return oldList.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return newList.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(final int oldItemPosition, final int newItemPosition) {
+ return sameItemsPredicate.areSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
+ }
+
+ @Override
+ public boolean areContentsTheSame(final int oldItemPosition, final int newItemPosition) {
+ return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
+ }
+
+ @Nullable
+ @Override
+ public Object getChangePayload(final int oldItemPosition, final int newItemPosition) {
+ return changePayloadProducer != null
+ ? changePayloadProducer.getChangePayload(oldList.get(oldItemPosition), newList.get(newItemPosition)) : null;
+ }
+
+ private boolean containsByPredicate(@NonNull final TItem searchedItem, @NonNull final List items) {
+ for (final TItem item : items) {
+ if (sameItemsPredicate.areSame(item, searchedItem)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/SameItemsPredicate.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/SameItemsPredicate.java
new file mode 100644
index 0000000..1cb03da
--- /dev/null
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/changes/SameItemsPredicate.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
+ *
+ * This file is part of RoboSwag library.
+ *
+ * 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 ru.touchin.roboswag.core.observables.collections.changes;
+
+import android.support.annotation.NonNull;
+
+public interface SameItemsPredicate {
+
+ boolean areSame(@NonNull TItem item1, @NonNull TItem item2);
+
+}
diff --git a/src/main/java/ru/touchin/roboswag/core/observables/collections/loadable/LoadingMoreList.java b/src/main/java/ru/touchin/roboswag/core/observables/collections/loadable/LoadingMoreList.java
index 38412ee..7f8092e 100644
--- a/src/main/java/ru/touchin/roboswag/core/observables/collections/loadable/LoadingMoreList.java
+++ b/src/main/java/ru/touchin/roboswag/core/observables/collections/loadable/LoadingMoreList.java
@@ -30,9 +30,10 @@ import java.util.NoSuchElementException;
import java.util.concurrent.Executors;
import ru.touchin.roboswag.core.log.Lc;
-import ru.touchin.roboswag.core.observables.collections.Change;
import ru.touchin.roboswag.core.observables.collections.ObservableCollection;
import ru.touchin.roboswag.core.observables.collections.ObservableList;
+import ru.touchin.roboswag.core.observables.collections.changes.Change;
+import ru.touchin.roboswag.core.observables.collections.changes.CollectionChanges;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import rx.Scheduler;
@@ -135,12 +136,14 @@ public class LoadingMoreList> observeChanges() {
+ public Observable> observeChanges() {
return innerList.observeChanges();
}
@Override
- protected void notifyAboutChanges(@NonNull final Collection> changes) {
+ protected void notifyAboutChanges(@NonNull final List insertedItems,
+ @NonNull final List removedItems,
+ @NonNull final Collection changes) {
Lc.assertion("Illegal operation. Modify getInnerList()");
}