| /* |
| * Copyright 2018 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 androidx.recyclerview.widget; |
| |
| import androidx.annotation.IntRange; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import java.util.ArrayDeque; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * DiffUtil is a utility class that calculates the difference between two lists and outputs 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. See {@link ListAdapter} and |
| * {@link AsyncListDiffer} which can simplify the use of DiffUtil on a background thread. |
| * <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> |
| * Note that DiffUtil, {@link ListAdapter}, and {@link AsyncListDiffer} require the list to not |
| * mutate while in use. |
| * This generally means that both the lists themselves and their elements (or at least, the |
| * properties of elements used in diffing) should not be modified directly. Instead, new lists |
| * should be provided any time content changes. It's common for lists passed to DiffUtil to share |
| * elements that have not mutated, so it is not strictly required to reload all data to use |
| * DiffUtil. |
| * <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(MN) time where M is the total number of |
| * added items and N is the total number of 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. |
| * |
| * @see ListAdapter |
| * @see AsyncListDiffer |
| */ |
| public class DiffUtil { |
| private DiffUtil() { |
| // utility class, no instance. |
| } |
| |
| private static final Comparator<Diagonal> DIAGONAL_COMPARATOR = new Comparator<Diagonal>() { |
| @Override |
| public int compare(Diagonal o1, Diagonal o2) { |
| return o1.x - o2.x; |
| } |
| }; |
| |
| // 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. |
| */ |
| @NonNull |
| public static DiffResult calculateDiff(@NonNull 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. |
| */ |
| @NonNull |
| public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) { |
| final int oldSize = cb.getOldListSize(); |
| final int newSize = cb.getNewListSize(); |
| |
| final List<Diagonal> diagonals = 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 + 1) / 2; |
| // 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 CenteredArray forward = new CenteredArray(max * 2 + 1); |
| final CenteredArray backward = new CenteredArray(max * 2 + 1); |
| |
| // 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 = midPoint(range, cb, forward, backward); |
| if (snake != null) { |
| // if it has a diagonal, save it |
| if (snake.diagonalSize() > 0) { |
| diagonals.add(snake.toDiagonal()); |
| } |
| // 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; |
| left.oldListEnd = snake.startX; |
| left.newListEnd = snake.startY; |
| stack.add(left); |
| |
| // re-use range for right |
| //noinspection UnnecessaryLocalVariable |
| final Range right = range; |
| right.oldListEnd = range.oldListEnd; |
| right.newListEnd = range.newListEnd; |
| right.oldListStart = snake.endX; |
| right.newListStart = snake.endY; |
| stack.add(right); |
| } else { |
| rangePool.add(range); |
| } |
| |
| } |
| // sort snakes |
| Collections.sort(diagonals, DIAGONAL_COMPARATOR); |
| |
| return new DiffResult(cb, diagonals, |
| forward.backingData(), backward.backingData(), |
| detectMoves); |
| } |
| |
| /** |
| * Finds a middle snake in the given range. |
| */ |
| @Nullable |
| private static Snake midPoint( |
| Range range, |
| Callback cb, |
| CenteredArray forward, |
| CenteredArray backward) { |
| if (range.oldSize() < 1 || range.newSize() < 1) { |
| return null; |
| } |
| int max = (range.oldSize() + range.newSize() + 1) / 2; |
| forward.set(1, range.oldListStart); |
| backward.set(1, range.oldListEnd); |
| for (int d = 0; d < max; d++) { |
| Snake snake = forward(range, cb, forward, backward, d); |
| if (snake != null) { |
| return snake; |
| } |
| snake = backward(range, cb, forward, backward, d); |
| if (snake != null) { |
| return snake; |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static Snake forward( |
| Range range, |
| Callback cb, |
| CenteredArray forward, |
| CenteredArray backward, |
| int d) { |
| boolean checkForSnake = Math.abs(range.oldSize() - range.newSize()) % 2 == 1; |
| int delta = range.oldSize() - range.newSize(); |
| for (int k = -d; k <= d; k += 2) { |
| // we either come from d-1, k-1 OR d-1. k+1 |
| // as we move in steps of 2, array always holds both current and previous d values |
| // k = x - y and each array value holds the max X, y = x - k |
| final int startX; |
| final int startY; |
| int x, y; |
| if (k == -d || (k != d && forward.get(k + 1) > forward.get(k - 1))) { |
| // picking k + 1, incrementing Y (by simply not incrementing X) |
| x = startX = forward.get(k + 1); |
| } else { |
| // picking k - 1, incrementing X |
| startX = forward.get(k - 1); |
| x = startX + 1; |
| } |
| y = range.newListStart + (x - range.oldListStart) - k; |
| startY = (d == 0 || x != startX) ? y : y - 1; |
| // now find snake size |
| while (x < range.oldListEnd |
| && y < range.newListEnd |
| && cb.areItemsTheSame(x, y)) { |
| x++; |
| y++; |
| } |
| // now we have furthest reaching x, record it |
| forward.set(k, x); |
| if (checkForSnake) { |
| // see if we did pass over a backwards array |
| // mapping function: delta - k |
| int backwardsK = delta - k; |
| // if backwards K is calculated and it passed me, found match |
| if (backwardsK >= -d + 1 |
| && backwardsK <= d - 1 |
| && backward.get(backwardsK) <= x) { |
| // match |
| Snake snake = new Snake(); |
| snake.startX = startX; |
| snake.startY = startY; |
| snake.endX = x; |
| snake.endY = y; |
| snake.reverse = false; |
| return snake; |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static Snake backward( |
| Range range, |
| Callback cb, |
| CenteredArray forward, |
| CenteredArray backward, |
| int d) { |
| boolean checkForSnake = (range.oldSize() - range.newSize()) % 2 == 0; |
| int delta = range.oldSize() - range.newSize(); |
| // same as forward but we go backwards from end of the lists to be beginning |
| // this also means we'll try to optimize for minimizing x instead of maximizing it |
| for (int k = -d; k <= d; k += 2) { |
| // we either come from d-1, k-1 OR d-1, k+1 |
| // as we move in steps of 2, array always holds both current and previous d values |
| // k = x - y and each array value holds the MIN X, y = x - k |
| // when x's are equal, we prioritize deletion over insertion |
| final int startX; |
| final int startY; |
| int x, y; |
| |
| if (k == -d || (k != d && backward.get(k + 1) < backward.get(k - 1))) { |
| // picking k + 1, decrementing Y (by simply not decrementing X) |
| x = startX = backward.get(k + 1); |
| } else { |
| // picking k - 1, decrementing X |
| startX = backward.get(k - 1); |
| x = startX - 1; |
| } |
| y = range.newListEnd - ((range.oldListEnd - x) - k); |
| startY = (d == 0 || x != startX) ? y : y + 1; |
| // now find snake size |
| while (x > range.oldListStart |
| && y > range.newListStart |
| && cb.areItemsTheSame(x - 1, y - 1)) { |
| x--; |
| y--; |
| } |
| // now we have furthest point, record it (min X) |
| backward.set(k, x); |
| if (checkForSnake) { |
| // see if we did pass over a backwards array |
| // mapping function: delta - k |
| int forwardsK = delta - k; |
| // if forwards K is calculated and it passed me, found match |
| if (forwardsK >= -d |
| && forwardsK <= d |
| && forward.get(forwardsK) >= x) { |
| // match |
| Snake snake = new Snake(); |
| // assignment are reverse since we are a reverse snake |
| snake.startX = x; |
| snake.startY = y; |
| snake.endX = startX; |
| snake.endY = startY; |
| snake.reverse = true; |
| return snake; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * 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 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 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; |
| } |
| } |
| |
| /** |
| * Callback for calculating the diff between two non-null items in a list. |
| * <p> |
| * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles |
| * just the second of these, which allows separation of code that indexes into an array or List |
| * from the presentation-layer and content specific diffing code. |
| * |
| * @param <T> Type of items to compare. |
| */ |
| public abstract static class ItemCallback<T> { |
| /** |
| * Called to check whether two objects represent the same item. |
| * <p> |
| * For example, if your items have unique ids, this method should check their id equality. |
| * <p> |
| * Note: {@code null} items in the list are assumed to be the same as another {@code null} |
| * item and are assumed to not be the same as a non-{@code null} item. This callback will |
| * not be invoked for either of those cases. |
| * |
| * @param oldItem The item in the old list. |
| * @param newItem The item in the new list. |
| * @return True if the two items represent the same object or false if they are different. |
| * @see Callback#areItemsTheSame(int, int) |
| */ |
| public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem); |
| |
| /** |
| * Called to check whether two items have the same data. |
| * <p> |
| * This information is used to detect if the contents of an item have changed. |
| * <p> |
| * This method to check equality instead of {@link Object#equals(Object)} so that you can |
| * change its behavior depending on your UI. |
| * <p> |
| * For example, if you are using DiffUtil with a |
| * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should |
| * return whether the items' visual representations are the same. |
| * <p> |
| * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for |
| * these items. |
| * <p> |
| * Note: Two {@code null} items are assumed to represent the same contents. This callback |
| * will not be invoked for this case. |
| * |
| * @param oldItem The item in the old list. |
| * @param newItem The item in the new list. |
| * @return True if the contents of the items are the same or false if they are different. |
| * @see Callback#areContentsTheSame(int, int) |
| */ |
| public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem); |
| |
| /** |
| * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and |
| * {@link #areContentsTheSame(T, T)} returns false for them, this method is called 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 RecyclerView.ItemAnimator ItemAnimator} can use that |
| * information to run the correct animation. |
| * <p> |
| * Default implementation returns {@code null}. |
| * |
| * @see Callback#getChangePayload(int, int) |
| */ |
| @SuppressWarnings({"unused"}) |
| @Nullable |
| public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) { |
| return null; |
| } |
| } |
| |
| /** |
| * A diagonal is a match in the graph. |
| * Rather than snakes, we only record the diagonals in the path. |
| */ |
| static class Diagonal { |
| public final int x; |
| public final int y; |
| public final int size; |
| |
| Diagonal(int x, int y, int size) { |
| this.x = x; |
| this.y = y; |
| this.size = size; |
| } |
| |
| int endX() { |
| return x + size; |
| } |
| |
| int endY() { |
| return y + size; |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| static class Snake { |
| /** |
| * Position in the old list |
| */ |
| public int startX; |
| |
| /** |
| * Position in the new list |
| */ |
| public int startY; |
| |
| /** |
| * End position in the old list, exclusive |
| */ |
| public int endX; |
| |
| /** |
| * End position in the new list, exclusive |
| */ |
| public int endY; |
| |
| /** |
| * True if this snake was created in the reverse search, false otherwise. |
| */ |
| public boolean reverse; |
| |
| boolean hasAdditionOrRemoval() { |
| return endY - startY != endX - startX; |
| } |
| |
| boolean isAddition() { |
| return endY - startY > endX - startX; |
| } |
| |
| int diagonalSize() { |
| return Math.min(endX - startX, endY - startY); |
| } |
| |
| /** |
| * Extract the diagonal of the snake to make reasoning easier for the rest of the |
| * algorithm where we try to produce a path and also find moves. |
| */ |
| @NonNull |
| Diagonal toDiagonal() { |
| if (hasAdditionOrRemoval()) { |
| if (reverse) { |
| // snake edge it at the end |
| return new Diagonal(startX, startY, diagonalSize()); |
| } else { |
| // snake edge it at the beginning |
| if (isAddition()) { |
| return new Diagonal(startX, startY + 1, diagonalSize()); |
| } else { |
| return new Diagonal(startX + 1, startY, diagonalSize()); |
| } |
| } |
| } else { |
| // we are a pure diagonal |
| return new Diagonal(startX, startY, endX - startX); |
| } |
| } |
| } |
| |
| /** |
| * Represents a range in two lists that needs to be solved. |
| * <p> |
| * This internal class is used when running Myers' algorithm without recursion. |
| * <p> |
| * Ends are exclusive |
| */ |
| 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; |
| } |
| |
| int oldSize() { |
| return oldListEnd - oldListStart; |
| } |
| |
| int newSize() { |
| return newListEnd - newListStart; |
| } |
| } |
| |
| /** |
| * 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 { |
| /** |
| * Signifies an item not present in the list. |
| */ |
| public static final int NO_POSITION = -1; |
| |
| |
| /** |
| * 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; |
| // Item moved |
| private static final int FLAG_MOVED = FLAG_MOVED_CHANGED | FLAG_MOVED_NOT_CHANGED; |
| |
| // 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 = 4; |
| |
| private static final int FLAG_MASK = (1 << FLAG_OFFSET) - 1; |
| |
| // The diagonals extracted from The Myers' snakes. |
| private final List<Diagonal> mDiagonals; |
| |
| // 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 calculate 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 diagonals Matches between the two lists |
| * @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<Diagonal> diagonals, int[] oldItemStatuses, |
| int[] newItemStatuses, boolean detectMoves) { |
| mDiagonals = diagonals; |
| mOldItemStatuses = oldItemStatuses; |
| mNewItemStatuses = newItemStatuses; |
| Arrays.fill(mOldItemStatuses, 0); |
| Arrays.fill(mNewItemStatuses, 0); |
| mCallback = callback; |
| mOldListSize = callback.getOldListSize(); |
| mNewListSize = callback.getNewListSize(); |
| mDetectMoves = detectMoves; |
| addEdgeDiagonals(); |
| findMatchingItems(); |
| } |
| |
| /** |
| * Add edge diagonals so that we can iterate as long as there are diagonals w/o lots of |
| * null checks around |
| */ |
| private void addEdgeDiagonals() { |
| Diagonal first = mDiagonals.isEmpty() ? null : mDiagonals.get(0); |
| // see if we should add 1 to the 0,0 |
| if (first == null || first.x != 0 || first.y != 0) { |
| mDiagonals.add(0, new Diagonal(0, 0, 0)); |
| } |
| // always add one last |
| mDiagonals.add(new Diagonal(mOldListSize, mNewListSize, 0)); |
| } |
| |
| /** |
| * Find position mapping from old list to new list. |
| * If moves are requested, we'll also try to do an n^2 search between additions and |
| * removals to find moves. |
| */ |
| private void findMatchingItems() { |
| for (Diagonal diagonal : mDiagonals) { |
| for (int offset = 0; offset < diagonal.size; offset++) { |
| int posX = diagonal.x + offset; |
| int posY = diagonal.y + offset; |
| final boolean theSame = mCallback.areContentsTheSame(posX, posY); |
| final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED; |
| mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; |
| mNewItemStatuses[posY] = (posX << FLAG_OFFSET) | changeFlag; |
| } |
| } |
| // now all matches are marked, lets look for moves |
| if (mDetectMoves) { |
| // traverse each addition / removal from the end of the list, find matching |
| // addition removal from before |
| findMoveMatches(); |
| } |
| } |
| |
| private void findMoveMatches() { |
| // for each removal, find matching addition |
| int posX = 0; |
| for (Diagonal diagonal : mDiagonals) { |
| while (posX < diagonal.x) { |
| if (mOldItemStatuses[posX] == 0) { |
| // there is a removal, find matching addition from the rest |
| findMatchingAddition(posX); |
| } |
| posX++; |
| } |
| // snap back for the next diagonal |
| posX = diagonal.endX(); |
| } |
| } |
| |
| /** |
| * Search the whole list to find the addition for the given removal of position posX |
| * |
| * @param posX position in the old list |
| */ |
| private void findMatchingAddition(int posX) { |
| int posY = 0; |
| final int diagonalsSize = mDiagonals.size(); |
| for (int i = 0; i < diagonalsSize; i++) { |
| final Diagonal diagonal = mDiagonals.get(i); |
| while (posY < diagonal.y) { |
| // found some additions, evaluate |
| if (mNewItemStatuses[posY] == 0) { // not evaluated yet |
| boolean matching = mCallback.areItemsTheSame(posX, posY); |
| if (matching) { |
| // yay found it, set values |
| boolean contentsMatching = mCallback.areContentsTheSame(posX, posY); |
| final int changeFlag = contentsMatching ? FLAG_MOVED_NOT_CHANGED |
| : FLAG_MOVED_CHANGED; |
| // once we process one of these, it will mark the other one as ignored. |
| mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; |
| mNewItemStatuses[posY] = (posX << FLAG_OFFSET) | changeFlag; |
| return; |
| } |
| } |
| posY++; |
| } |
| posY = diagonal.endY(); |
| } |
| } |
| |
| /** |
| * Given a position in the old list, returns the position in the new list, or |
| * {@code NO_POSITION} if it was removed. |
| * |
| * @param oldListPosition Position of item in old list |
| * @return Position of item in new list, or {@code NO_POSITION} if not present. |
| * @see #NO_POSITION |
| * @see #convertNewPositionToOld(int) |
| */ |
| public int convertOldPositionToNew(@IntRange(from = 0) int oldListPosition) { |
| if (oldListPosition < 0 || oldListPosition >= mOldListSize) { |
| throw new IndexOutOfBoundsException("Index out of bounds - passed position = " |
| + oldListPosition + ", old list size = " + mOldListSize); |
| } |
| final int status = mOldItemStatuses[oldListPosition]; |
| if ((status & FLAG_MASK) == 0) { |
| return NO_POSITION; |
| } else { |
| return status >> FLAG_OFFSET; |
| } |
| } |
| |
| /** |
| * Given a position in the new list, returns the position in the old list, or |
| * {@code NO_POSITION} if it was removed. |
| * |
| * @param newListPosition Position of item in new list |
| * @return Position of item in old list, or {@code NO_POSITION} if not present. |
| * @see #NO_POSITION |
| * @see #convertOldPositionToNew(int) |
| */ |
| public int convertNewPositionToOld(@IntRange(from = 0) int newListPosition) { |
| if (newListPosition < 0 || newListPosition >= mNewListSize) { |
| throw new IndexOutOfBoundsException("Index out of bounds - passed position = " |
| + newListPosition + ", new list size = " + mNewListSize); |
| } |
| final int status = mNewItemStatuses[newListPosition]; |
| if ((status & FLAG_MASK) == 0) { |
| return NO_POSITION; |
| } else { |
| return status >> FLAG_OFFSET; |
| } |
| } |
| |
| /** |
| * Dispatches the update events to the given adapter. |
| * <p> |
| * For example, if you have an {@link 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 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 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. |
| * @see AdapterListUpdateCallback |
| */ |
| public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) { |
| dispatchUpdatesTo(new AdapterListUpdateCallback(adapter)); |
| } |
| |
| /** |
| * Dispatches update operations to the given Callback. |
| * <p> |
| * These updates are atomic such that the first update call affects 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(@NonNull 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; |
| } |
| // track up to date current list size for moves |
| // when a move is found, we record its position from the end of the list (which is |
| // less likely to change since we iterate in reverse). |
| // Later when we find the match of that move, we dispatch the update |
| int currentListSize = mOldListSize; |
| // list of postponed moves |
| final Collection<PostponedUpdate> postponedUpdates = new ArrayDeque<>(); |
| // posX and posY are exclusive |
| int posX = mOldListSize; |
| int posY = mNewListSize; |
| // iterate from end of the list to the beginning. |
| // this just makes offsets easier since changes in the earlier indices has an effect |
| // on the later indices. |
| for (int diagonalIndex = mDiagonals.size() - 1; diagonalIndex >= 0; diagonalIndex--) { |
| final Diagonal diagonal = mDiagonals.get(diagonalIndex); |
| int endX = diagonal.endX(); |
| int endY = diagonal.endY(); |
| // dispatch removals and additions until we reach to that diagonal |
| // first remove then add so that it can go into its place and we don't need |
| // to offset values |
| while (posX > endX) { |
| posX--; |
| // REMOVAL |
| int status = mOldItemStatuses[posX]; |
| if ((status & FLAG_MOVED) != 0) { |
| int newPos = status >> FLAG_OFFSET; |
| // get postponed addition |
| PostponedUpdate postponedUpdate = getPostponedUpdate(postponedUpdates, |
| newPos, false); |
| if (postponedUpdate != null) { |
| // this is an addition that was postponed. Now dispatch it. |
| int updatedNewPos = currentListSize - postponedUpdate.currentPos; |
| batchingCallback.onMoved(posX, updatedNewPos - 1); |
| if ((status & FLAG_MOVED_CHANGED) != 0) { |
| Object changePayload = mCallback.getChangePayload(posX, newPos); |
| batchingCallback.onChanged(updatedNewPos - 1, 1, changePayload); |
| } |
| } else { |
| // first time we are seeing this, we'll see a matching addition |
| postponedUpdates.add(new PostponedUpdate( |
| posX, |
| currentListSize - posX - 1, |
| true |
| )); |
| } |
| } else { |
| // simple removal |
| batchingCallback.onRemoved(posX, 1); |
| currentListSize--; |
| } |
| } |
| while (posY > endY) { |
| posY--; |
| // ADDITION |
| int status = mNewItemStatuses[posY]; |
| if ((status & FLAG_MOVED) != 0) { |
| // this is a move not an addition. |
| // see if this is postponed |
| int oldPos = status >> FLAG_OFFSET; |
| // get postponed removal |
| PostponedUpdate postponedUpdate = getPostponedUpdate(postponedUpdates, |
| oldPos, true); |
| // empty size returns 0 for indexOf |
| if (postponedUpdate == null) { |
| // postpone it until we see the removal |
| postponedUpdates.add(new PostponedUpdate( |
| posY, |
| currentListSize - posX, |
| false |
| )); |
| } else { |
| // oldPosFromEnd = foundListSize - posX |
| // we can find posX if we swap the list sizes |
| // posX = listSize - oldPosFromEnd |
| int updatedOldPos = currentListSize - postponedUpdate.currentPos - 1; |
| batchingCallback.onMoved(updatedOldPos, posX); |
| if ((status & FLAG_MOVED_CHANGED) != 0) { |
| Object changePayload = mCallback.getChangePayload(oldPos, posY); |
| batchingCallback.onChanged(posX, 1, changePayload); |
| } |
| } |
| } else { |
| // simple addition |
| batchingCallback.onInserted(posX, 1); |
| currentListSize++; |
| } |
| } |
| // now dispatch updates for the diagonal |
| posX = diagonal.x; |
| posY = diagonal.y; |
| for (int i = 0; i < diagonal.size; i++) { |
| // dispatch changes |
| if ((mOldItemStatuses[posX] & FLAG_MASK) == FLAG_CHANGED) { |
| Object changePayload = mCallback.getChangePayload(posX, posY); |
| batchingCallback.onChanged(posX, 1, changePayload); |
| } |
| posX++; |
| posY++; |
| } |
| // snap back for the next diagonal |
| posX = diagonal.x; |
| posY = diagonal.y; |
| } |
| batchingCallback.dispatchLastEvent(); |
| } |
| |
| @Nullable |
| private static PostponedUpdate getPostponedUpdate( |
| Collection<PostponedUpdate> postponedUpdates, |
| int posInList, |
| boolean removal) { |
| PostponedUpdate postponedUpdate = null; |
| Iterator<PostponedUpdate> itr = postponedUpdates.iterator(); |
| while (itr.hasNext()) { |
| PostponedUpdate update = itr.next(); |
| if (update.posInOwnerList == posInList && update.removal == removal) { |
| postponedUpdate = update; |
| itr.remove(); |
| break; |
| } |
| } |
| while (itr.hasNext()) { |
| // re-offset all others |
| PostponedUpdate update = itr.next(); |
| if (removal) { |
| update.currentPos--; |
| } else { |
| update.currentPos++; |
| } |
| } |
| return postponedUpdate; |
| } |
| } |
| |
| /** |
| * 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 { |
| /** |
| * position in the list that owns this item |
| */ |
| int posInOwnerList; |
| |
| /** |
| * position wrt to the end of the list |
| */ |
| int currentPos; |
| |
| /** |
| * true if this is a removal, false otherwise |
| */ |
| boolean removal; |
| |
| PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) { |
| this.posInOwnerList = posInOwnerList; |
| this.currentPos = currentPos; |
| this.removal = removal; |
| } |
| } |
| |
| /** |
| * Array wrapper w/ negative index support. |
| * We use this array instead of a regular array so that algorithm is easier to read without |
| * too many offsets when accessing the "k" array in the algorithm. |
| */ |
| static class CenteredArray { |
| private final int[] mData; |
| private final int mMid; |
| |
| CenteredArray(int size) { |
| mData = new int[size]; |
| mMid = mData.length / 2; |
| } |
| |
| int get(int index) { |
| return mData[index + mMid]; |
| } |
| |
| int[] backingData() { |
| return mData; |
| } |
| |
| void set(int index, int value) { |
| mData[index + mMid] = value; |
| } |
| |
| public void fill(int value) { |
| Arrays.fill(mData, value); |
| } |
| } |
| } |