Merge "RecyclerView: Add payload for efficient bind" into lmp-mr1-ub-dev
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index a7becb4..6ba5ae0 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -338,14 +338,17 @@
method public final boolean hasStableIds();
method public final void notifyDataSetChanged();
method public final void notifyItemChanged(int);
+ method public final void notifyItemChanged(int, java.lang.Object);
method public final void notifyItemInserted(int);
method public final void notifyItemMoved(int, int);
method public final void notifyItemRangeChanged(int, int);
+ method public final void notifyItemRangeChanged(int, int, java.lang.Object);
method public final void notifyItemRangeInserted(int, int);
method public final void notifyItemRangeRemoved(int, int);
method public final void notifyItemRemoved(int);
method public void onAttachedToRecyclerView(android.support.v7.widget.RecyclerView);
method public abstract void onBindViewHolder(VH, int);
+ method public void onBindViewHolder(VH, int, java.util.List<java.lang.Object>);
method public abstract VH onCreateViewHolder(android.view.ViewGroup, int);
method public void onDetachedFromRecyclerView(android.support.v7.widget.RecyclerView);
method public boolean onFailedToRecycleView(VH);
@@ -361,6 +364,7 @@
ctor public RecyclerView.AdapterDataObserver();
method public void onChanged();
method public void onItemRangeChanged(int, int);
+ method public void onItemRangeChanged(int, int, java.lang.Object);
method public void onItemRangeInserted(int, int);
method public void onItemRangeMoved(int, int, int);
method public void onItemRangeRemoved(int, int);
@@ -517,6 +521,7 @@
method public void onItemsMoved(android.support.v7.widget.RecyclerView, int, int, int);
method public void onItemsRemoved(android.support.v7.widget.RecyclerView, int, int);
method public void onItemsUpdated(android.support.v7.widget.RecyclerView, int, int);
+ method public void onItemsUpdated(android.support.v7.widget.RecyclerView, int, int, java.lang.Object);
method public void onLayoutChildren(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
method public void onMeasure(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State, int, int);
method public deprecated boolean onRequestChildFocus(android.support.v7.widget.RecyclerView, android.view.View, android.view.View);
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
index 68371c5..ba6ec71 100644
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
@@ -114,11 +114,12 @@
}
@Override
- public void markViewHoldersUpdated(int positionStart, int itemCount) {
+ public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
final int positionEnd = positionStart + itemCount;
for (ViewHolder holder : mViewHolders) {
if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
holder.addFlags(ViewHolder.FLAG_UPDATE);
+ holder.addChangePayload(payload);
}
}
}
@@ -815,6 +816,15 @@
}
@Test
+ public void testPayloads() {
+ setupBasic(10, 2, 2);
+ up(3, 3, "payload");
+ preProcess();
+ assertOps(mFirstPassUpdates, upOp(4, 2, "payload"));
+ assertOps(mSecondPassUpdates, upOp(3, 1, "payload"));
+ }
+
+ @Test
public void testRandom() throws Throwable {
mCollectLogs = true;
Random random = new Random(System.nanoTime());
@@ -840,7 +850,7 @@
setupBasic(count, start, layoutCount);
while (opCount-- > 0) {
- final int op = nextInt(random, 4);
+ final int op = nextInt(random, 5);
switch (op) {
case 0:
if (mTestAdapter.mItems.size() > 1) {
@@ -871,6 +881,13 @@
up(s, len);
}
break;
+ case 4:
+ if (mTestAdapter.mItems.size() > 1) {
+ s = nextInt(random, mTestAdapter.mItems.size() - 1);
+ int len = Math.max(1, nextInt(random, mTestAdapter.mItems.size() - s));
+ up(s, len, Integer.toString(s));
+ }
+ break;
}
}
preProcess();
@@ -945,7 +962,11 @@
}
AdapterHelper.UpdateOp op(int cmd, int start, int count) {
- return new AdapterHelper.UpdateOp(cmd, start, count);
+ return new AdapterHelper.UpdateOp(cmd, start, count, null);
+ }
+
+ AdapterHelper.UpdateOp op(int cmd, int start, int count, Object payload) {
+ return new AdapterHelper.UpdateOp(cmd, start, count, payload);
}
AdapterHelper.UpdateOp addOp(int start, int count) {
@@ -956,8 +977,8 @@
return op(AdapterHelper.UpdateOp.REMOVE, start, count);
}
- AdapterHelper.UpdateOp upOp(int start, int count) {
- return op(AdapterHelper.UpdateOp.UPDATE, start, count);
+ AdapterHelper.UpdateOp upOp(int start, int count, Object payload) {
+ return op(AdapterHelper.UpdateOp.UPDATE, start, count, payload);
}
void add(int start, int count) {
@@ -1003,6 +1024,13 @@
mTestAdapter.update(start, count);
}
+ void up(int start, int count, Object payload) {
+ if (DEBUG) {
+ log("up(" + start + "," + count + "," + payload + ");");
+ }
+ mTestAdapter.update(start, count, payload);
+ }
+
static class TestAdapter {
List<Item> mItems;
@@ -1027,14 +1055,14 @@
mItems.add(index + i, item);
}
mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
- AdapterHelper.UpdateOp.ADD, index, count
+ AdapterHelper.UpdateOp.ADD, index, count, null
));
}
public void move(int from, int to) {
mItems.add(to, mItems.remove(from));
mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
- AdapterHelper.UpdateOp.MOVE, from, to
+ AdapterHelper.UpdateOp.MOVE, from, to, null
));
}
@@ -1043,16 +1071,20 @@
mItems.remove(index);
}
mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
- AdapterHelper.UpdateOp.REMOVE, index, count
+ AdapterHelper.UpdateOp.REMOVE, index, count, null
));
}
public void update(int index, int count) {
+ update(index, count, null);
+ }
+
+ public void update(int index, int count, Object payload) {
for (int i = 0; i < count; i++) {
- mItems.get(index + i).update();
+ mItems.get(index + i).update(payload);
}
mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
- AdapterHelper.UpdateOp.UPDATE, index, count
+ AdapterHelper.UpdateOp.UPDATE, index, count, payload
));
}
@@ -1080,7 +1112,7 @@
break;
case AdapterHelper.UpdateOp.UPDATE:
for (int i = 0; i < op.itemCount; i++) {
- mItems.get(i).handleUpdate();
+ mItems.get(op.positionStart + i).handleUpdate(op.payload);
}
break;
case AdapterHelper.UpdateOp.MOVE:
@@ -1107,22 +1139,25 @@
private int mVersionCount = 0;
- private int mUpdateCount;
+ private ArrayList<Object> mPayloads = new ArrayList<Object>();
public Item() {
id = itemCounter.incrementAndGet();
}
- public void update() {
+ public void update(Object payload) {
+ mPayloads.add(payload);
mVersionCount++;
}
- public void handleUpdate() {
+ public void handleUpdate(Object payload) {
+ assertSame(payload, mPayloads.get(0));
+ mPayloads.remove(0);
mVersionCount--;
}
public int getUpdateCount() {
- return mUpdateCount;
+ return mVersionCount;
}
}
}
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/OpReorderTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/OpReorderTest.java
index 4289aea..06bfce6 100644
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/OpReorderTest.java
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/OpReorderTest.java
@@ -49,8 +49,8 @@
OpReorderer mOpReorderer = new OpReorderer(new OpReorderer.Callback() {
@Override
- public UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount) {
- return new UpdateOp(cmd, startPosition, itemCount);
+ public UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload) {
+ return new UpdateOp(cmd, startPosition, itemCount, payload);
}
@Override
@@ -283,20 +283,20 @@
UpdateOp rm(int start, int count) {
updatedItemCount -= count;
- return record(new UpdateOp(REMOVE, start, count));
+ return record(new UpdateOp(REMOVE, start, count, null));
}
UpdateOp mv(int from, int to) {
- return record(new UpdateOp(MOVE, from, to));
+ return record(new UpdateOp(MOVE, from, to, null));
}
UpdateOp add(int start, int count) {
updatedItemCount += count;
- return record(new UpdateOp(ADD, start, count));
+ return record(new UpdateOp(ADD, start, count, null));
}
UpdateOp up(int start, int count) {
- return record(new UpdateOp(UPDATE, start, count));
+ return record(new UpdateOp(UPDATE, start, count, null));
}
UpdateOp record(UpdateOp op) {
@@ -407,7 +407,7 @@
private List<UpdateOp> rewriteOps(List<UpdateOp> updateOps) {
List<UpdateOp> copy = new ArrayList<UpdateOp>();
for (UpdateOp op : updateOps) {
- copy.add(new UpdateOp(op.cmd, op.positionStart, op.itemCount));
+ copy.add(new UpdateOp(op.cmd, op.positionStart, op.itemCount, null));
}
mOpReorderer.reorderOps(copy);
return copy;
diff --git a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
index 032449c..e9feab8 100644
--- a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
@@ -145,7 +145,7 @@
if (type == POSITION_TYPE_INVISIBLE) {
// Looks like we have other updates that we cannot merge with this one.
// Create an UpdateOp and dispatch it to LayoutManager.
- UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
dispatchAndUpdateViewHolders(newOp);
typeChanged = true;
}
@@ -156,7 +156,7 @@
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
// Looks like we have other updates that we cannot merge with this one.
// Create UpdateOp op and dispatch it to LayoutManager.
- UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
postponeAndUpdateViewHolders(newOp);
typeChanged = true;
}
@@ -172,7 +172,7 @@
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
- op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
@@ -190,7 +190,8 @@
ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) { // deferred
if (type == POSITION_TYPE_INVISIBLE) {
- UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+ op.payload);
dispatchAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
@@ -198,7 +199,8 @@
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else { // applied
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
- UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
+ op.payload);
postponeAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
@@ -208,8 +210,9 @@
tmpCount++;
}
if (tmpCount != op.itemCount) { // all 1 effect
+ Object payload = op.payload;
recycleUpdateOp(op);
- op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
+ op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
@@ -272,7 +275,7 @@
tmpCnt++;
} else {
// need to dispatch this separately
- UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload);
if (DEBUG) {
Log.d(TAG, "need to dispatch separately " + tmp);
}
@@ -285,9 +288,10 @@
tmpCnt = 1;
}
}
+ Object payload = op.payload;
recycleUpdateOp(op);
if (tmpCnt > 0) {
- UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
+ UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload);
if (DEBUG) {
Log.d(TAG, "dispatching:" + tmp);
}
@@ -311,7 +315,7 @@
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
break;
case UpdateOp.UPDATE:
- mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
+ mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("only remove and update ops can be dispatched"
@@ -442,7 +446,7 @@
op.itemCount);
break;
case UpdateOp.UPDATE:
- mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
@@ -489,8 +493,8 @@
/**
* @return True if updates should be processed.
*/
- boolean onItemRangeChanged(int positionStart, int itemCount) {
- mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
+ boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
return mPendingUpdates.size() == 1;
}
@@ -498,7 +502,7 @@
* @return True if updates should be processed.
*/
boolean onItemRangeInserted(int positionStart, int itemCount) {
- mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
return mPendingUpdates.size() == 1;
}
@@ -506,7 +510,7 @@
* @return True if updates should be processed.
*/
boolean onItemRangeRemoved(int positionStart, int itemCount) {
- mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
return mPendingUpdates.size() == 1;
}
@@ -520,7 +524,7 @@
if (itemCount != 1) {
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
}
- mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
return mPendingUpdates.size() == 1;
}
@@ -545,7 +549,7 @@
break;
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
- mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
case UpdateOp.MOVE:
mCallback.onDispatchSecondPass(op);
@@ -614,13 +618,16 @@
int positionStart;
+ Object payload;
+
// holds the target position if this is a MOVE
int itemCount;
- UpdateOp(int cmd, int positionStart, int itemCount) {
+ UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
this.cmd = cmd;
this.positionStart = positionStart;
this.itemCount = itemCount;
+ this.payload = payload;
}
String cmdToString() {
@@ -639,7 +646,9 @@
@Override
public String toString() {
- return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
+ return Integer.toHexString(System.identityHashCode(this))
+ + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
+ +",p:"+payload + "]";
}
@Override
@@ -668,6 +677,13 @@
if (positionStart != op.positionStart) {
return false;
}
+ if (payload != null) {
+ if (!payload.equals(op.payload)) {
+ return false;
+ }
+ } else if (op.payload != null) {
+ return false;
+ }
return true;
}
@@ -682,14 +698,15 @@
}
@Override
- public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
+ public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
UpdateOp op = mUpdateOpPool.acquire();
if (op == null) {
- op = new UpdateOp(cmd, positionStart, itemCount);
+ op = new UpdateOp(cmd, positionStart, itemCount, payload);
} else {
op.cmd = cmd;
op.positionStart = positionStart;
op.itemCount = itemCount;
+ op.payload = payload;
}
return op;
}
@@ -697,6 +714,7 @@
@Override
public void recycleUpdateOp(UpdateOp op) {
if (!mDisableRecycler) {
+ op.payload = null;
mUpdateOpPool.release(op);
}
}
@@ -720,7 +738,7 @@
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
- void markViewHoldersUpdated(int positionStart, int itemCount);
+ void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
void onDispatchFirstPass(UpdateOp updateOp);
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index ed135d8..b628978 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -209,7 +209,8 @@
}
@Override
- public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+ Object payload) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/OpReorderer.java b/v7/recyclerview/src/android/support/v7/widget/OpReorderer.java
index e123ce8..db01a0c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/OpReorderer.java
+++ b/v7/recyclerview/src/android/support/v7/widget/OpReorderer.java
@@ -100,7 +100,7 @@
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
final int remaining = removeOp.positionStart + removeOp.itemCount
- moveOp.positionStart;
- extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining);
+ extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null);
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
}
@@ -187,7 +187,7 @@
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
// moved item is updated. add an update for it
updateOp.itemCount--;
- extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1);
+ extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload);
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= updateOp.positionStart) {
@@ -195,7 +195,8 @@
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
final int remaining = updateOp.positionStart + updateOp.itemCount
- moveOp.positionStart;
- extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
+ extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining,
+ updateOp.payload);
updateOp.itemCount -= remaining;
}
list.set(update, moveOp);
@@ -230,7 +231,7 @@
static interface Callback {
- UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
+ UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload);
void recycleUpdateOp(UpdateOp op);
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index d884caf..dcf6806 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -643,8 +643,8 @@
}
@Override
- public void markViewHoldersUpdated(int positionStart, int itemCount) {
- viewRangeUpdate(positionStart, itemCount);
+ public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
+ viewRangeUpdate(positionStart, itemCount, payload);
mItemsChanged = true;
}
@@ -662,7 +662,8 @@
mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
break;
case UpdateOp.UPDATE:
- mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount);
+ mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
+ op.payload);
break;
case UpdateOp.MOVE:
mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
@@ -3264,7 +3265,7 @@
* @param positionStart Adapter position to start at
* @param itemCount Number of views that must explicitly be rebound
*/
- void viewRangeUpdate(int positionStart, int itemCount) {
+ void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
final int childCount = mChildHelper.getUnfilteredChildCount();
final int positionEnd = positionStart + itemCount;
@@ -3278,6 +3279,7 @@
// We re-bind these view holders after pre-processing is complete so that
// ViewHolders have their final positions assigned.
holder.addFlags(ViewHolder.FLAG_UPDATE);
+ holder.addChangePayload(payload);
if (supportsChangeAnimations()) {
holder.addFlags(ViewHolder.FLAG_CHANGED);
}
@@ -3990,9 +3992,9 @@
}
@Override
- public void onItemRangeChanged(int positionStart, int itemCount) {
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
assertNotInLayoutOrScroll(null);
- if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount)) {
+ if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
triggerUpdateProcessor();
}
}
@@ -4989,6 +4991,7 @@
final ViewHolder holder = mCachedViews.get(i);
if (holder != null) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ holder.addChangePayload(null);
}
}
} else {
@@ -5080,9 +5083,9 @@
* layout file.
* <p>
* The new ViewHolder will be used to display items of the adapter using
- * {@link #onBindViewHolder(ViewHolder, int)}. Since it will be re-used to display different
- * items in the data set, it is a good idea to cache references to sub views of the View to
- * avoid unnecessary {@link View#findViewById(int)} calls.
+ * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display
+ * different items in the data set, it is a good idea to cache references to sub views of
+ * the View to avoid unnecessary {@link View#findViewById(int)} calls.
*
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
@@ -5095,23 +5098,59 @@
public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
/**
+ * Called by RecyclerView to display the data at the specified position. This method should
+ * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given
+ * position.
+ * <p>
+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the <code>position</code> parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+ * have the updated adapter position.
+ *
+ * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can
+ * handle effcient partial bind.
+ *
+ * @param holder The ViewHolder which should be updated to represent the contents of the
+ * item at the given position in the data set.
+ * @param position The position of the item within the adapter's data set.
+ */
+ public abstract void onBindViewHolder(VH holder, int position);
+
+ /**
* Called by RecyclerView to display the data at the specified position. This method
* should update the contents of the {@link ViewHolder#itemView} to reflect the item at
* the given position.
* <p>
- * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this
- * method again if the position of the item changes in the data set unless the item itself
- * is invalidated or the new position cannot be determined. For this reason, you should only
- * use the <code>position</code> parameter while acquiring the related data item inside this
- * method and should not keep a copy of it. If you need the position of an item later on
- * (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will have
- * the updated adapter position.
+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method
+ * again if the position of the item changes in the data set unless the item itself is
+ * invalidated or the new position cannot be determined. For this reason, you should only
+ * use the <code>position</code> parameter while acquiring the related data item inside
+ * this method and should not keep a copy of it. If you need the position of an item later
+ * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will
+ * have the updated adapter position.
+ * <p>
+ * Partial bind vs full bind:
+ * <p>
+ * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
+ * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty,
+ * the ViewHolder is currently bound to old data and Adapter may run an efficient partial
+ * update using the payload info. If the payload is empty, Adapter must run a full bind.
+ * Adapter should not assume that the payload passed in notify methods will be received by
+ * onBindViewHolder(). For example when the view is not attached to the screen, the
+ * payload in notifyItemChange() will be simply dropped.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
+ * @param payloads A non-null list of merged payloads. Can be empty list if requires full
+ * update.
*/
- public abstract void onBindViewHolder(VH holder, int position);
+ public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
+ onBindViewHolder(holder, position);
+ }
/**
* This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new
@@ -5143,7 +5182,8 @@
ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
TraceCompat.beginSection(TRACE_BIND_VIEW_TAG);
- onBindViewHolder(holder, position);
+ onBindViewHolder(holder, position, holder.getUnmodifiedPayloads());
+ holder.clearPayload();
TraceCompat.endSection();
}
@@ -5390,6 +5430,7 @@
/**
* Notify any registered observers that the item at <code>position</code> has changed.
+ * Equivalent to calling <code>notifyItemChanged(position, null);</code>.
*
* <p>This is an item change event, not a structural change event. It indicates that any
* reflection of the data at <code>position</code> is out of date and should be updated.
@@ -5404,8 +5445,37 @@
}
/**
+ * Notify any registered observers that the item at <code>position</code> has changed with an
+ * optional payload object.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data at <code>position</code> is out of date and should be updated.
+ * The item at <code>position</code> retains the same identity.
+ * </p>
+ *
+ * <p>
+ * Client can optionally pass a payload for partial change. These payloads will be merged
+ * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+ * item is already represented by a ViewHolder and it will be rebound to the same
+ * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+ * payloads on that item and prevent future payload until
+ * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+ * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+ * attached, the payload will be simply dropped.
+ *
+ * @param position Position of the item that has changed
+ * @param payload Optional parameter, use null to identify a "full" update
+ *
+ * @see #notifyItemRangeChanged(int, int)
+ */
+ public final void notifyItemChanged(int position, Object payload) {
+ mObservable.notifyItemRangeChanged(position, 1, payload);
+ }
+
+ /**
* Notify any registered observers that the <code>itemCount</code> items starting at
* position <code>positionStart</code> have changed.
+ * Equivalent to calling <code>notifyItemRangeChanged(position, itemCount, null);</code>.
*
* <p>This is an item change event, not a structural change event. It indicates that
* any reflection of the data in the given position range is out of date and should
@@ -5421,6 +5491,36 @@
}
/**
+ * Notify any registered observers that the <code>itemCount</code> items starting at
+ * position<code>positionStart</code> have changed. An optional payload can be
+ * passed to each changed item.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data in the given position range is out of date and should be updated.
+ * The items in the given range retain the same identity.
+ * </p>
+ *
+ * <p>
+ * Client can optionally pass a payload for partial change. These payloads will be merged
+ * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
+ * item is already represented by a ViewHolder and it will be rebound to the same
+ * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing
+ * payloads on that item and prevent future payload until
+ * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume
+ * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not
+ * attached, the payload will be simply dropped.
+ *
+ * @param positionStart Position of the first item that has changed
+ * @param itemCount Number of items that have changed
+ * @param payload Optional parameter, use null to identify a "full" update
+ *
+ * @see #notifyItemChanged(int)
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount, payload);
+ }
+
+ /**
* Notify any registered observers that the item reflected at <code>position</code>
* has been newly inserted. The item previously at <code>position</code> is now at
* position <code>position + 1</code>.
@@ -7154,6 +7254,8 @@
/**
* Called when items have been changed in the adapter.
+ * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)}
+ * instead, then this callback will not be invoked.
*
* @param recyclerView
* @param positionStart
@@ -7163,6 +7265,20 @@
}
/**
+ * Called when items have been changed in the adapter and with optional payload.
+ * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ * @param payload
+ */
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+ Object payload) {
+ onItemsUpdated(recyclerView, positionStart, 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
@@ -8020,8 +8136,18 @@
*/
static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9;
+ /**
+ * Set when a addChangePayload(null) is called
+ */
+ static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10;
+
private int mFlags;
+ private static final List<Object> FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST;
+
+ List<Object> mPayloads = null;
+ List<Object> mUnmodifiedPayloads = null;
+
private int mIsRecyclableCount = 0;
// If non-null, view is currently considered scrap and may be reused for other data by the
@@ -8246,6 +8372,43 @@
mFlags |= flags;
}
+ void addChangePayload(Object payload) {
+ if (payload == null) {
+ addFlags(FLAG_ADAPTER_FULLUPDATE);
+ } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+ createPayloadsIfNeeded();
+ mPayloads.add(payload);
+ }
+ }
+
+ private void createPayloadsIfNeeded() {
+ if (mPayloads == null) {
+ mPayloads = new ArrayList<Object>();
+ mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads);
+ }
+ }
+
+ void clearPayload() {
+ if (mPayloads != null) {
+ mPayloads.clear();
+ }
+ mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE;
+ }
+
+ List<Object> getUnmodifiedPayloads() {
+ if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) {
+ if (mPayloads == null || mPayloads.size() == 0) {
+ // Initial state, no update being called.
+ return FULLUPDATE_PAYLOADS;
+ }
+ // there are none-null payloads
+ return mUnmodifiedPayloads;
+ } else {
+ // a full update has been called.
+ return FULLUPDATE_PAYLOADS;
+ }
+ }
+
void resetInternal() {
mFlags = 0;
mPosition = NO_POSITION;
@@ -8255,6 +8418,7 @@
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
+ clearPayload();
}
@Override
@@ -8514,6 +8678,12 @@
// do nothing
}
+ public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+ // fallback to onItemRangeChanged(positionStart, itemCount) if app
+ // does not override this method.
+ onItemRangeChanged(positionStart, itemCount);
+ }
+
public void onItemRangeInserted(int positionStart, int itemCount) {
// do nothing
}
@@ -8957,12 +9127,16 @@
}
public void notifyItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount, null);
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount, Object payload) {
// since onItemRangeChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
- mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
}
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 76114c4..322fe34 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -1344,7 +1344,8 @@
}
@Override
- public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
+ Object payload) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 3eda5ae..3945a7c 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -382,6 +382,112 @@
mLayoutManager.waitForLayout(2);
}
+ private static boolean listEquals(List list1, List list2) {
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ for (int i= 0; i < list1.size(); i++) {
+ if (!list1.get(i).equals(list2.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void testChangeWithPayload(final boolean supportsChangeAnim,
+ Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
+ throws Throwable {
+ final List<Object> expectedPayloads = new ArrayList<Object>();
+ final int changedIndex = 3;
+ TestAdapter testAdapter = new TestAdapter(10) {
+ @Override
+ public int getItemViewType(int position) {
+ return 1;
+ }
+
+ @Override
+ public TestViewHolder onCreateViewHolder(ViewGroup parent,
+ int viewType) {
+ TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
+ if (DEBUG) {
+ Log.d(TAG, " onCreateVH" + vh.toString());
+ }
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position, List<Object> payloads) {
+ super.onBindViewHolder(holder, position);
+ if (DEBUG) {
+ Log.d(TAG, " onBind to " + position + "" + holder.toString());
+ }
+ assertTrue(listEquals(payloads, expectedPayloads));
+ }
+ };
+ testAdapter.setHasStableIds(false);
+ setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
+ mRecyclerView.getItemAnimator().setSupportsChangeAnimations(supportsChangeAnim);
+
+ int numTests = notifyPayloads.length;
+ for (int i= 0; i < numTests; i++) {
+ mLayoutManager.expectLayouts(1);
+ expectedPayloads.clear();
+ for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
+ expectedPayloads.add(expectedPayloadsInOnBind[i][j]);
+ }
+ final Object[] payloadsToSend = notifyPayloads[i];
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int j = 0; j < payloadsToSend.length; j++) {
+ mTestAdapter.notifyItemChanged(changedIndex, payloadsToSend[j]);
+ }
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+ }
+ }
+
+ public void testCrossFadingChangeAnimationWithPayload() throws Throwable {
+ // for crossfading change animation, will receive EMPTY payload in onBindViewHolder
+ testChangeWithPayload(true,
+ new Object[][]{
+ new Object[]{"abc"},
+ new Object[]{"abc", null, "cdf"},
+ new Object[]{"abc", null},
+ new Object[]{null, "abc"},
+ new Object[]{"abc", "cdf"}
+ },
+ new Object[][]{
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[0]
+ });
+ }
+
+ public void testNoChangeAnimationWithPayload() throws Throwable {
+ // for Change Animation disabled, payload should match the payloads unless
+ // null payload is fired.
+ testChangeWithPayload(false,
+ new Object[][]{
+ new Object[]{"abc"},
+ new Object[]{"abc", null, "cdf"},
+ new Object[]{"abc", null},
+ new Object[]{null, "abc"},
+ new Object[]{"abc", "cdf"}
+ },
+ new Object[][]{
+ new Object[]{"abc"},
+ new Object[0],
+ new Object[0],
+ new Object[0],
+ new Object[]{"abc", "cdf"}
+ });
+ }
+
public void testRecycleDuringAnimations() throws Throwable {
final AtomicInteger childCount = new AtomicInteger(0);
final TestAdapter adapter = new TestAdapter(1000) {