Add move operation to RecyclerView adapter
Bug: 16127440
Change-Id: Iefd305e714a4bc6dad5f071dfba92aa5c5af8ca3
diff --git a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
index 5a56e1f..5f80bd9 100644
--- a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
@@ -17,6 +17,7 @@
package android.support.v7.widget;
import android.support.v4.util.Pools;
+import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
@@ -48,6 +49,10 @@
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "AHT";
+
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
@@ -80,6 +85,7 @@
}
void preProcess() {
+ pruneInvalidMoveOps();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
@@ -93,6 +99,9 @@
case UpdateOp.UPDATE:
applyUpdate(op);
break;
+ case UpdateOp.MOVE:
+ applyMove(op);
+ break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
@@ -101,6 +110,145 @@
mPendingUpdates.clear();
}
+ /**
+ * When an item is moved, we still represent it in pre-layout even if we don't have a
+ * ViewHolder for it. This may be a problem if item is removed in the same layout pass.
+ * This is why we make sure item is not removed and if it is removed, we replace MOVE op
+ * with a REMOVE op and update the remaining UpdateOps accordingly.
+ */
+ private void pruneInvalidMoveOps() {
+ for (int i = mPendingUpdates.size() - 1; i >= 0; i--) {
+ UpdateOp op = mPendingUpdates.get(i);
+ if (op.cmd != UpdateOp.MOVE) {
+ continue;
+ }
+ // IF MOVE(from) is a newly added item, we defer it gracefully.
+ // IF MOVE(to) is removed, we have to invalidate MOVE
+ final int count = mPendingUpdates.size();
+ int to = op.itemCount;
+ int removedIndex = -1;
+ for (int j = i + 1; j < count; j++) {
+ UpdateOp other = mPendingUpdates.get(j);
+ if (other.cmd == UpdateOp.ADD && other.positionStart <= to) {
+ to += other.itemCount;
+ } else if (other.cmd == UpdateOp.REMOVE && other.positionStart <= to) {
+ if (other.positionStart + other.itemCount > to) {
+ removedIndex = j;
+ break;
+ } else {
+ to -= other.itemCount;
+ }
+ } else if (other.cmd == UpdateOp.MOVE) {
+ if (to == other.positionStart) {
+ to = other.itemCount;
+ } else {
+ if (to > other.positionStart) {
+ to--;
+ }
+ if (to >= other.itemCount) {
+ to++;
+ }
+ }
+ }
+ }
+ if (removedIndex != -1) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "detected a move that has been removed at" + removedIndex + ":" + op);
+ }
+ to = op.itemCount;
+ op.itemCount = 1;
+ op.cmd = UpdateOp.REMOVE;
+ // now, update all remaining ops according to this :/.
+ for (int j = i + 1; j <= removedIndex; j++) {
+ UpdateOp other = mPendingUpdates.get(j);
+ switch (other.cmd) {
+ case UpdateOp.ADD:
+ if (other.positionStart > to) {
+ other.positionStart--;
+ } else {
+ to += other.itemCount;
+ }
+ break;
+ case UpdateOp.UPDATE:
+ if (other.positionStart > to) {
+ other.positionStart--;
+ } else if (other.positionStart <= to
+ && to < other.positionStart + other.itemCount) {
+ other.itemCount--;
+ }
+ break;
+ case UpdateOp.REMOVE:
+ if (other.positionStart <= to) {
+ if (other.positionStart + other.itemCount > to) {
+ other.itemCount--;
+ } else {
+ to -= other.itemCount;
+ }
+ } else {
+ other.positionStart--;
+ }
+ break;
+ case UpdateOp.MOVE:
+ if (to == other.positionStart) {
+ throw new IllegalStateException("move updated removed move. "
+ + "Should've detected earlier");
+ } else {
+ final int start, end, inBetweenDiff;
+ if (other.positionStart > other.itemCount) {
+ start = other.itemCount;
+ end = other.positionStart;
+ inBetweenDiff = 1;
+ } else {
+ start = other.positionStart;
+ end = other.itemCount;
+ inBetweenDiff = -1;
+ }
+ // since to is not added anymore, offset other if necessary
+ if (to < other.positionStart) {
+ other.positionStart--;
+ }
+ if (to <= other.itemCount) {
+ other.itemCount--;
+ }
+ // now apply other to "to" position
+ if (start <= to && end >= to) {
+ to += inBetweenDiff;
+ }
+ }
+ break;
+ }
+ }
+
+ for (int j = removedIndex; j > i; j--) {
+ UpdateOp other = mPendingUpdates.get(j);
+ if (other.cmd != UpdateOp.MOVE && other.itemCount <= 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Recycling no-op " + other);
+ }
+ mPendingUpdates.remove(j);
+ recycleUpdateOp(other);
+ } else if (other.cmd == UpdateOp.MOVE &&
+ (other.itemCount == other.positionStart ||
+ other.itemCount < 0)) {
+ if (DEBUG) {
+ Log.d(TAG, "Recycling no-op " + other);
+ }
+ mPendingUpdates.remove(j);
+ recycleUpdateOp(other);
+ }
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "after pruning is complete.");
+ for (UpdateOp op : mPendingUpdates) {
+ Log.d(TAG, op.toString());
+ }
+ Log.d(TAG, "------");
+ }
+ }
+
void consumePostponedUpdates() {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
@@ -109,6 +257,13 @@
recycleUpdateOpsAndClearList(mPostponedList);
}
+ private void applyMove(UpdateOp op) {
+ // MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
+ // otherwise, it would be converted into a REMOVE operation
+ mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+ postpone(op);
+ }
+
private void applyRemove(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
@@ -117,7 +272,7 @@
for (int position = op.positionStart; position < tmpEnd; position++) {
boolean typeChanged = false;
ViewHolder vh = mCallback.findViewHolder(position);
- if (vh != null || isNewlyAdded(position)) {
+ if (vh != null || canFindInPreLayout(position)) {
// If a ViewHolder exists or this is a newly added item, we can defer this update
// to post layout stage.
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
@@ -176,7 +331,7 @@
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
ViewHolder vh = mCallback.findViewHolder(position);
- if (vh != null || isNewlyAdded(position)) { // deferred
+ if (vh != null || canFindInPreLayout(position)) { // deferred
if (type == POSITION_TYPE_INVISIBLE) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
mCallback.markViewHoldersUpdated(newOp.positionStart, newOp.itemCount);
@@ -218,29 +373,176 @@
// tricky part.
// traverse all postpones and revert their changes on this op if necessary, apply updated
// dispatch to them since now they are after this op.
+ if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
+ throw new IllegalArgumentException("should not dispatch add or move for pre layout");
+ }
+ if (DEBUG) {
+ Log.d(TAG, "dispat (pre)" + op);
+ Log.d(TAG, "postponed state before:");
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
+ }
+
+ // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
+ int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
+ if (DEBUG) {
+ Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
+ }
+ int tmpCnt = 1;
+ for (int p = 1; p < op.itemCount; p++) {
+ int pos = -1;
+ switch (op.cmd) {
+ case UpdateOp.UPDATE:
+ pos = op.positionStart + p;
+ break;
+ case UpdateOp.REMOVE:
+ pos = op.positionStart;
+ }
+ int updatedPos = updatePositionWithPostponed(pos, op.cmd);
+ if (DEBUG) {
+ Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
+ }
+ boolean continuous = false;
+ switch (op.cmd) {
+ case UpdateOp.UPDATE:
+ continuous = updatedPos == tmpStart + 1;
+ break;
+ case UpdateOp.REMOVE:
+ continuous = updatedPos == tmpStart;
+ break;
+ }
+ if (continuous) {
+ tmpCnt++;
+ } else {
+ // need to dispatch this separately
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
+ if (DEBUG) {
+ Log.d(TAG, "need to dispatch separately " + tmp);
+ }
+ mCallback.onDispatchFirstPass(tmp);
+ recycleUpdateOp(tmp);
+ tmpStart = updatedPos;// need to remove previously dispatched
+ tmpCnt = 1;
+ }
+ }
+ recycleUpdateOp(op);
+ if (tmpCnt > 0) {
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
+ if (DEBUG) {
+ Log.d(TAG, "dispatching:" + tmp);
+ }
+ mCallback.onDispatchFirstPass(tmp);
+ recycleUpdateOp(tmp);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "post dispatch");
+ Log.d(TAG, "postponed state after:");
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
+ }
+ }
+
+ private int updatePositionWithPostponed(int pos, int cmd) {
final int count = mPostponedList.size();
for (int i = count - 1; i >= 0; i--) {
UpdateOp postponed = mPostponedList.get(i);
- if (postponed.positionStart <= op.positionStart) {
- op.positionStart += postponed.itemCount;
+ if (postponed.cmd == UpdateOp.MOVE) {
+ int start, end;
+ if (postponed.positionStart < postponed.itemCount) {
+ start = postponed.positionStart;
+ end = postponed.itemCount;
+ } else {
+ start = postponed.itemCount;
+ end = postponed.positionStart;
+ }
+ if (pos >= start && pos <= end) {
+ //i'm affected
+ if (start == postponed.positionStart) {
+ if (cmd == UpdateOp.ADD) {
+ postponed.itemCount++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.itemCount--;
+ }
+ // op moved to left, move it right to revert
+ pos++;
+ } else {
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ }
+ // op was moved right, move left to revert
+ pos--;
+ }
+ } else if (pos < postponed.positionStart) {
+ // postponed MV is outside the dispatched OP. if it is before, offset
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ postponed.itemCount++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ postponed.itemCount--;
+ }
+ }
} else {
- postponed.positionStart -= op.itemCount;
+ if (postponed.positionStart <= pos) {
+ if (postponed.cmd == UpdateOp.ADD) {
+ pos -= postponed.itemCount;
+ } else if (postponed.cmd == UpdateOp.REMOVE) {
+ pos += postponed.itemCount;
+ }
+ } else {
+ if (cmd == UpdateOp.ADD) {
+ postponed.positionStart++;
+ } else if (cmd == UpdateOp.REMOVE) {
+ postponed.positionStart--;
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "dispath (step" + i + ")");
+ Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
+ for (UpdateOp updateOp : mPostponedList) {
+ Log.d(TAG, updateOp.toString());
+ }
+ Log.d(TAG, "----");
}
}
- mCallback.onDispatchFirstPass(op);
- recycleUpdateOp(op);
+ for (int i = mPostponedList.size() - 1; i >= 0; i--) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.cmd == UpdateOp.MOVE) {
+ if (op.itemCount == op.positionStart || op.itemCount < 0) {
+ mPostponedList.remove(i);
+ recycleUpdateOp(op);
+ }
+ } else if (op.itemCount <= 0) {
+ mPostponedList.remove(i);
+ recycleUpdateOp(op);
+ }
+ }
+ return pos;
}
- private boolean isNewlyAdded(int position) {
+ private boolean canFindInPreLayout(int position) {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPostponedList.get(i);
- if (op.cmd != UpdateOp.ADD) {
- continue;
- }
- int updatedStart = findPositionOffset(op.positionStart, i + 1);
- if (updatedStart <= position && updatedStart + op.itemCount > position) {
- return true;
+ if (op.cmd == UpdateOp.MOVE) {
+ if (op.positionStart == position) {
+ return true;
+ }
+ } else if (op.cmd == UpdateOp.ADD) {
+ // TODO optimize.
+ final int end = op.positionStart + op.itemCount;
+ for (int pos = op.positionStart; pos < end; pos++) {
+ if (findPositionOffset(pos, i + 1) == position) {
+ return true;
+ }
+ }
}
}
return false;
@@ -252,6 +554,9 @@
}
private void postpone(UpdateOp op) {
+ if (DEBUG) {
+ Log.d(TAG, "postponing " + op);
+ }
mPostponedList.add(op);
}
@@ -264,19 +569,28 @@
}
int findPositionOffset(int position, int firstPostponedItem) {
- int offsetPosition = position;
int count = mPostponedList.size();
for (int i = firstPostponedItem; i < count; ++i) {
UpdateOp op = mPostponedList.get(i);
- if (op.positionStart <= offsetPosition) {
+ if (op.cmd == UpdateOp.MOVE) {
+ if (op.positionStart == position) {
+ // TODO check if this should be returned instead. probably not
+ position = op.itemCount;
+ } else if (op.positionStart < position) {
+ position--; // like a remove
+ }
+ if (op.itemCount <= position) {
+ position++; // like an add
+ }
+ } else if (op.positionStart <= position) {
if (op.cmd == UpdateOp.REMOVE) {
- offsetPosition -= op.itemCount;
+ position -= op.itemCount;
} else if (op.cmd == UpdateOp.ADD) {
- offsetPosition += op.itemCount;
+ position += op.itemCount;
}
}
}
- return offsetPosition;
+ return position;
}
/**
@@ -304,6 +618,20 @@
}
/**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeMoved(int from, int to, int itemCount) {
+ if (from == to) {
+ return false;//no-op
+ }
+ if (itemCount != 1) {
+ throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
+ }
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
* Skips pre-processing and applies all updates in one pass.
*/
void consumeUpdatesInOnePass() {
@@ -325,6 +653,9 @@
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
break;
+ case UpdateOp.MOVE:
+ mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
+ break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
@@ -344,12 +675,15 @@
static final int UPDATE = 2;
+ static final int MOVE = 3;
+
static final int POOL_SIZE = 30;
int cmd;
int positionStart;
+ // holds the target position if this is a MOVE
int itemCount;
UpdateOp(int cmd, int positionStart, int itemCount) {
@@ -366,6 +700,8 @@
return "rm";
case UPDATE:
return "up";
+ case MOVE:
+ return "mv";
}
return "??";
}
@@ -452,5 +788,7 @@
void onDispatchSecondPass(UpdateOp updateOp);
void offsetPositionsForAdd(int positionStart, int itemCount);
+
+ void offsetPositionsForMove(int from, int to);
}
}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 5002ef4..2ae23d0 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -350,6 +350,9 @@
case UpdateOp.UPDATE:
mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount);
break;
+ case UpdateOp.MOVE:
+ mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
+ break;
}
}
@@ -363,6 +366,13 @@
offsetPositionRecordsForInsert(positionStart, itemCount);
mItemsAddedOrRemoved = true;
}
+
+ @Override
+ public void offsetPositionsForMove(int from, int to) {
+ offsetPositionRecordsForMove(from, to);
+ // should we create mItemsMoved ?
+ mItemsAddedOrRemoved = true;
+ }
});
}
@@ -2046,6 +2056,40 @@
mRecycler.clearOldPositions();
}
+ void offsetPositionRecordsForMove(int from, int to) {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ final int start, end, inBetweenOffset;
+ if (from < to) {
+ start = from;
+ end = to;
+ inBetweenOffset = -1;
+ } else {
+ start = to;
+ end = from;
+ inBetweenOffset = 1;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder " +
+ holder);
+ }
+ if (holder.mPosition == from) {
+ holder.offsetPosition(to - from, false);
+ } else {
+ holder.offsetPosition(inBetweenOffset, false);
+ }
+
+ mState.mStructureChanged = true;
+ }
+ mRecycler.offsetPositionRecordsForMove(from, to);
+ requestLayout();
+ }
+
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
@@ -2619,6 +2663,14 @@
}
}
+ @Override
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
+ triggerUpdateProcessor();
+ }
+ }
+
void triggerUpdateProcessor() {
if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
@@ -3248,6 +3300,35 @@
getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter);
}
+ void offsetPositionRecordsForMove(int from, int to) {
+ final int start, end, inBetweenOffset;
+ if (from < to) {
+ start = from;
+ end = to;
+ inBetweenOffset = -1;
+ } else {
+ start = to;
+ end = from;
+ inBetweenOffset = 1;
+ }
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+ continue;
+ }
+ if (holder.mPosition == from) {
+ holder.offsetPosition(to - from, false);
+ } else {
+ holder.offsetPosition(inBetweenOffset, false);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder " +
+ holder);
+ }
+ }
+ }
+
void offsetPositionRecordsForInsert(int insertedAt, int count) {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
@@ -3650,6 +3731,21 @@
}
/**
+ * Notify any registered observers that the item reflected at <code>fromPosition</code>
+ * has been moved to <code>toPosition</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their
+ * positions may be altered.</p>
+ *
+ * @param fromPosition Previous position of the item.
+ * @param toPosition New position of the item.
+ */
+ public final void notifyItemMoved(int fromPosition, int toPosition) {
+ mObservable.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /**
* Notify any registered observers that the currently reflected <code>itemCount</code>
* items starting at <code>positionStart</code> have been newly inserted. The items
* previously located at <code>positionStart</code> and beyond can now be found starting
@@ -5120,6 +5216,22 @@
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
}
+ /**
+ * Called when an item is moved withing the adapter.
+ * <p>
+ * Note that, an item may also change position in response to another ADD/REMOVE/MOVE
+ * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved}
+ * is called.
+ *
+ * @param recyclerView
+ * @param from
+ * @param to
+ * @param itemCount
+ */
+ public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
+
+ }
+
/**
* <p>Override this method if you want to support scroll bars.</p>
@@ -5867,6 +5979,10 @@
public void onItemRangeRemoved(int positionStart, int itemCount) {
// do nothing
}
+
+ public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+ // do nothing
+ }
}
/**
@@ -6280,6 +6396,12 @@
mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
}
}
+
+ public void notifyItemMoved(int fromPosition, int toPosition) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1);
+ }
+ }
}
static class SavedState extends android.view.View.BaseSavedState {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java
index d2708c3..38ad9c7 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java
@@ -17,6 +17,7 @@
package android.support.v7.widget;
import android.test.AndroidTestCase;
+import android.util.Log;
import android.widget.TextView;
import java.util.ArrayList;
@@ -30,7 +31,9 @@
public class AdapterHelperTest extends AndroidTestCase {
- static final int NO_POSITION = -1;
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "AHT";
List<ViewHolder> mViewHolders;
@@ -42,12 +45,15 @@
TestAdapter mPreProcessClone; // we clone adapter pre-process to run operations to see result
+ private List<TestAdapter.Item> mPreLayoutItems;
+
@Override
protected void setUp() throws Exception {
cleanState();
}
private void cleanState() {
+ mPreLayoutItems = new ArrayList<TestAdapter.Item>();
mViewHolders = new ArrayList<ViewHolder>();
mFirstPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
mSecondPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
@@ -101,11 +107,17 @@
@Override
public void onDispatchFirstPass(AdapterHelper.UpdateOp updateOp) {
+ if (DEBUG) {
+ Log.d(TAG, "first pass:" + updateOp.toString());
+ }
mFirstPassUpdates.add(updateOp);
}
@Override
public void onDispatchSecondPass(AdapterHelper.UpdateOp updateOp) {
+ if (DEBUG) {
+ Log.d(TAG, "second pass:" + updateOp.toString());
+ }
mSecondPassUpdates.add(updateOp);
}
@@ -117,10 +129,33 @@
}
}
}
+
+ @Override
+ public void offsetPositionsForMove(int from, int to) {
+ final int start, end, inBetweenOffset;
+ if (from < to) {
+ start = from;
+ end = to;
+ inBetweenOffset = -1;
+ } else {
+ start = to;
+ end = from;
+ inBetweenOffset = 1;
+ }
+ for (ViewHolder holder : mViewHolders) {
+ if (holder == null || holder.mPosition < start || holder.mPosition > end) {
+ continue;
+ }
+ holder.offsetPosition(inBetweenOffset, false);
+ }
+ }
}, true);
}
void setupBasic(int count, int visibleStart, int visibleCount) {
+ if (DEBUG) {
+ Log.d(TAG, "setupBasic(" + count + "," + visibleStart + "," + visibleCount + ");");
+ }
mTestAdapter = new TestAdapter(count, mAdapterHelper);
for (int i = 0; i < visibleCount; i++) {
addViewHolder(visibleStart + i);
@@ -185,7 +220,7 @@
assertDispatch(1, 1);
}
- public void testSchenerio1() {
+ public void testScenario1() {
setupBasic(10, 3, 2);
rm(4, 1);
rm(3, 1);
@@ -201,7 +236,7 @@
assertDispatch(1, 1);
}
- public void testSchenerio2() {
+ public void testScenario2() {
setupBasic(10, 3, 3); // 3-4-5
add(4, 2); // 3 a b 4 5
rm(0, 1); // (0) 3(2) a(3) b(4) 4(3) 5(4)
@@ -210,7 +245,7 @@
assertDispatch(2, 2);
}
- public void testSchenerio3() {
+ public void testScenario3() {
setupBasic(10, 2, 2);
rm(0, 5);
preProcess();
@@ -218,8 +253,10 @@
assertOps(mFirstPassUpdates, rmOp(0, 2), rmOp(2, 1));
assertOps(mSecondPassUpdates, rmOp(0, 2));
}
+ // TODO test MOVE then remove items in between.
+ // TODO test MOVE then remove it, make sure it is not dispatched
- public void testSchenerio4() {
+ public void testScenario4() {
setupBasic(5, 0, 5);
// 0 1 2 3 4
// 0 1 2 a b 3 4
@@ -236,7 +273,7 @@
preProcess();
}
- public void testSchenerio5() {
+ public void testScenario5() {
setupBasic(5, 0, 5);
// 0 1 2 3 4
// 0 1 2 a b 3 4
@@ -248,34 +285,244 @@
preProcess();
}
+ public void testScenario6() {
+// setupBasic(47, 19, 24);
+// mv(11, 12);
+// add(24, 16);
+// rm(9, 3);
+ setupBasic(10, 5, 3);
+ mv(2, 3);
+ add(6, 4);
+ rm(4, 1);
+ preProcess();
+ }
+
+ public void testScenario8() {
+ setupBasic(68, 51, 13);
+ mv(22, 11);
+ mv(22, 52);
+ rm(37, 19);
+ add(12, 38);
+ preProcess();
+ }
+
+ public void testScenario9() {
+ setupBasic(44, 3, 7);
+ add(7, 21);
+ rm(31, 3);
+ rm(32, 11);
+ mv(29, 5);
+ mv(30, 32);
+ add(25, 32);
+ rm(15, 66);
+ preProcess();
+ }
+
+ public void testScenario10() {
+ setupBasic(14, 10, 3);
+ rm(4, 4);
+ add(5, 11);
+ mv(5, 18);
+ rm(2, 9);
+ preProcess();
+ }
+
+ public void testScenario11() {
+ setupBasic(78, 3, 64);
+ mv(34, 28);
+ add(1, 11);
+ rm(9, 74);
+ preProcess();
+ }
+
+ public void testScenario12() {
+ setupBasic(38, 9, 7);
+ rm(26, 3);
+ mv(29, 15);
+ rm(30, 1);
+ preProcess();
+ }
+
+ public void testScenario13() {
+ setupBasic(49, 41, 3);
+ rm(30, 13);
+ add(4, 10);
+ mv(3, 38);
+ mv(20, 17);
+ rm(18, 23);
+ preProcess();
+ }
+
+ public void testScenario14() {
+ setupBasic(24, 3, 11);
+ rm(2, 15);
+ mv(2, 1);
+ add(2, 34);
+ add(11, 3);
+ rm(10, 25);
+ rm(13, 6);
+ rm(4, 4);
+ rm(6, 4);
+ preProcess();
+ }
+
+ public void testScenario15() {
+ setupBasic(10, 8, 1);
+ mv(6, 1);
+ mv(1, 4);
+ rm(3, 1);
+ preProcess();
+ }
+
+ public void testScenario16() {
+ setupBasic(10, 3, 3);
+ rm(2, 1);
+ rm(1, 7);
+ rm(0, 1);
+ preProcess();
+ }
+
+ public void testScenario17() {
+ setupBasic(10, 8, 1);
+ mv(1, 0);
+ mv(5, 1);
+ rm(1, 7);
+ preProcess();
+ }
+
+ public void testScenario18() throws InterruptedException {
+ setupBasic(10, 1, 4);
+ add(2, 11);
+ rm(16, 1);
+ add(3, 1);
+ rm(9, 10);
+ preProcess();
+ }
+
+ public void testScenario19() {
+ setupBasic(10,8,1);
+ mv(9,7);
+ mv(9,3);
+ rm(5,4);
+ preProcess();
+ }
+
+ public void testScenario20() {
+ setupBasic(10,7,1);
+ mv(9,1);
+ mv(3,9);
+ rm(7,2);
+ preProcess();
+ }
+
+ public void testScenario21() {
+ setupBasic(10,5,2);
+ mv(1,0);
+ mv(9,1);
+ rm(2,3);
+ preProcess();
+ }
+
+ public void testScenario22() {
+ setupBasic(10,7,2);
+ add(2,16);
+ mv(20,9);
+ rm(17,6);
+ preProcess();
+ }
+
+ public void testScenario23() {
+ setupBasic(10,5,3);
+ mv(9,6);
+ add(4,15);
+ rm(21,3);
+ preProcess();
+ }
+
+ public void testScenario24() {
+ setupBasic(10,1,6);
+ add(6,5);
+ mv(14,6);
+ rm(7,6);
+ preProcess();
+ }
+
+ public void testScenario25() {
+ setupBasic(10,3,4);
+ mv(3,9);
+ mv(2,9);
+ rm(5,4);
+ preProcess();
+ }
+
+ public void testScenario26() {
+ setupBasic(10,4,4);
+ rm(3,5);
+ mv(2,0);
+ mv(1,0);
+ rm(1,1);
+ mv(0,2);
+ preProcess();
+ }
+
+ public void testScenario27() {
+ setupBasic(10,0,3);
+ mv(9,4);
+ mv(8,4);
+ add(7,10);
+ rm(5,5);
+ mv(6,5);
+ preProcess();
+ }
+
+ public void testMoveAdded() {
+ setupBasic(10, 2, 2);
+ add(3, 5);
+ mv(4, 2);
+ preProcess();
+ }
+
public void testRandom() {
Random random = new Random(System.nanoTime());
- for (int i = 0; i < 10; i++) {
- randomTest(random, i);
+ for (int i = 0; i < 1000; i++) {
+ randomTest(random, 5);
}
}
public void randomTest(Random random, int opCount) {
cleanState();
- final int count = 10 + random.nextInt(100);
+ if (DEBUG) {
+ Log.d(TAG, "randomTest");
+ }
+ final int count = 10;// + random.nextInt(100);
final int start = random.nextInt(count - 1);
final int layoutCount = Math.max(1, random.nextInt(count - start));
setupBasic(count, start, layoutCount);
while (opCount-- > 0) {
- final int op = random.nextInt(AdapterHelper.UpdateOp.REMOVE);
+ final int op = random.nextInt(3);
switch (op) {
- case AdapterHelper.UpdateOp.REMOVE:
+ case 0:
if (mTestAdapter.mItems.size() > 1) {
int s = random.nextInt(mTestAdapter.mItems.size() - 1);
int len = Math.max(1, random.nextInt(mTestAdapter.mItems.size() - s));
rm(s, len);
}
break;
- case AdapterHelper.UpdateOp.ADD:
- int s = random.nextInt(mTestAdapter.mItems.size() - 1);
+ case 1:
+ int s = mTestAdapter.mItems.size() == 0 ? 0 :
+ random.nextInt(mTestAdapter.mItems.size());
add(s, random.nextInt(50));
break;
+ case 2:
+ if (mTestAdapter.mItems.size() >= 2) {
+ int from = random.nextInt(mTestAdapter.mItems.size());
+ int to;
+ do {
+ to = random.nextInt(mTestAdapter.mItems.size());
+ } while (to == from);
+ mv(from, to);
+ }
}
}
preProcess();
@@ -296,6 +543,16 @@
void preProcess() {
mAdapterHelper.preProcess();
+ for (int i = 0; i < mPreProcessClone.mItems.size(); i++) {
+ TestAdapter.Item item = mPreProcessClone.mItems.get(i);
+ final int preLayoutIndex = mPreLayoutItems.indexOf(item);
+ final int endIndex = mTestAdapter.mItems.indexOf(item);
+ if (preLayoutIndex != -1 && endIndex != -1) {
+ assertEquals("find position offset should work properly for moved element " + i
+ + " at pre layout position " + preLayoutIndex + " and post layout position "
+ + endIndex, endIndex, mAdapterHelper.findPositionOffset(preLayoutIndex));
+ }
+ }
mAdapterHelper.consumePostponedUpdates();
// now assert these two adapters have identical data.
mPreProcessClone.applyOps(mFirstPassUpdates, mTestAdapter);
@@ -331,10 +588,38 @@
}
void add(int start, int count) {
+ if (DEBUG) {
+ Log.d(TAG, "add(" + start + "," + count + ");");
+ }
mTestAdapter.add(start, count);
}
+ boolean isItemLaidOut(int pos) {
+ for (ViewHolder viewHolder : mViewHolders) {
+ if (viewHolder.mOldPosition == pos) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void mv(int from, int to) {
+ if (DEBUG) {
+ Log.d(TAG, "mv(" + from + "," + to + ");");
+ }
+ mTestAdapter.move(from, to);
+ }
+
void rm(int start, int count) {
+ if (DEBUG) {
+ Log.d(TAG, "rm(" + start + "," + count + ");");
+ }
+ for (int i = start; i < start + count; i++) {
+ if (!isItemLaidOut(i)) {
+ TestAdapter.Item item = mTestAdapter.mItems.get(i);
+ mPreLayoutItems.remove(item);
+ }
+ }
mTestAdapter.remove(start, count);
}
@@ -370,6 +655,12 @@
));
}
+ public void move(int from, int to) {
+ mItems.add(to, mItems.remove(from));
+ mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
+ AdapterHelper.UpdateOp.MOVE, from, to
+ ));
+ }
public void remove(int index, int count) {
for (int i = 0; i < count; i++) {
mItems.remove(index);
@@ -415,6 +706,9 @@
mItems.get(i).handleUpdate();
}
break;
+ case AdapterHelper.UpdateOp.MOVE:
+ mItems.add(op.itemCount, mItems.remove(op.positionStart));
+ break;
}
}
}