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) {