1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v7.widget;
18 
19 import android.support.v4.util.Pools;
20 import android.util.Log;
21 
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 
26 import static android.support.v7.widget.RecyclerView.*;
27 
28 /**
29  * Helper class that can enqueue and process adapter update operations.
30  * <p>
31  * To support animations, RecyclerView presents an older version the Adapter to best represent
32  * previous state of the layout. Sometimes, this is not trivial when items are removed that were
33  * not laid out, in which case, RecyclerView has no way of providing that item's view for
34  * animations.
35  * <p>
36  * AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
37  * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
38  * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
39  * according to previously deferred operation and dispatch them before the first layout pass. It
40  * also takes care of updating deferred UpdateOps since order of operations is changed by this
41  * process.
42  * <p>
43  * Although operations may be forwarded to LayoutManager in different orders, resulting data set
44  * is guaranteed to be the consistent.
45  */
46 class AdapterHelper implements OpReorderer.Callback {
47 
48     final static int POSITION_TYPE_INVISIBLE = 0;
49 
50     final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
51 
52     private static final boolean DEBUG = false;
53 
54     private static final String TAG = "AHT";
55 
56     private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
57 
58     final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
59 
60     final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
61 
62     final Callback mCallback;
63 
64     Runnable mOnItemProcessedCallback;
65 
66     final boolean mDisableRecycler;
67 
68     final OpReorderer mOpReorderer;
69 
70     private int mExistingUpdateTypes = 0;
71 
AdapterHelper(Callback callback)72     AdapterHelper(Callback callback) {
73         this(callback, false);
74     }
75 
AdapterHelper(Callback callback, boolean disableRecycler)76     AdapterHelper(Callback callback, boolean disableRecycler) {
77         mCallback = callback;
78         mDisableRecycler = disableRecycler;
79         mOpReorderer = new OpReorderer(this);
80     }
81 
addUpdateOp(UpdateOp... ops)82     AdapterHelper addUpdateOp(UpdateOp... ops) {
83         Collections.addAll(mPendingUpdates, ops);
84         return this;
85     }
86 
reset()87     void reset() {
88         recycleUpdateOpsAndClearList(mPendingUpdates);
89         recycleUpdateOpsAndClearList(mPostponedList);
90         mExistingUpdateTypes = 0;
91     }
92 
preProcess()93     void preProcess() {
94         mOpReorderer.reorderOps(mPendingUpdates);
95         final int count = mPendingUpdates.size();
96         for (int i = 0; i < count; i++) {
97             UpdateOp op = mPendingUpdates.get(i);
98             switch (op.cmd) {
99                 case UpdateOp.ADD:
100                     applyAdd(op);
101                     break;
102                 case UpdateOp.REMOVE:
103                     applyRemove(op);
104                     break;
105                 case UpdateOp.UPDATE:
106                     applyUpdate(op);
107                     break;
108                 case UpdateOp.MOVE:
109                     applyMove(op);
110                     break;
111             }
112             if (mOnItemProcessedCallback != null) {
113                 mOnItemProcessedCallback.run();
114             }
115         }
116         mPendingUpdates.clear();
117     }
118 
consumePostponedUpdates()119     void consumePostponedUpdates() {
120         final int count = mPostponedList.size();
121         for (int i = 0; i < count; i++) {
122             mCallback.onDispatchSecondPass(mPostponedList.get(i));
123         }
124         recycleUpdateOpsAndClearList(mPostponedList);
125         mExistingUpdateTypes = 0;
126     }
127 
applyMove(UpdateOp op)128     private void applyMove(UpdateOp op) {
129         // MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
130         // otherwise, it would be converted into a REMOVE operation
131         postponeAndUpdateViewHolders(op);
132     }
133 
applyRemove(UpdateOp op)134     private void applyRemove(UpdateOp op) {
135         int tmpStart = op.positionStart;
136         int tmpCount = 0;
137         int tmpEnd = op.positionStart + op.itemCount;
138         int type = -1;
139         for (int position = op.positionStart; position < tmpEnd; position++) {
140             boolean typeChanged = false;
141             ViewHolder vh = mCallback.findViewHolder(position);
142             if (vh != null || canFindInPreLayout(position)) {
143                 // If a ViewHolder exists or this is a newly added item, we can defer this update
144                 // to post layout stage.
145                 // * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
146                 // * For items that are added and removed in the same process cycle, they won't
147                 // have any effect in pre-layout since their add ops are already deferred to
148                 // post-layout pass.
149                 if (type == POSITION_TYPE_INVISIBLE) {
150                     // Looks like we have other updates that we cannot merge with this one.
151                     // Create an UpdateOp and dispatch it to LayoutManager.
152                     UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
153                     dispatchAndUpdateViewHolders(newOp);
154                     typeChanged = true;
155                 }
156                 type = POSITION_TYPE_NEW_OR_LAID_OUT;
157             } else {
158                 // This update cannot be recovered because we don't have a ViewHolder representing
159                 // this position. Instead, post it to LayoutManager immediately
160                 if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
161                     // Looks like we have other updates that we cannot merge with this one.
162                     // Create UpdateOp op and dispatch it to LayoutManager.
163                     UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
164                     postponeAndUpdateViewHolders(newOp);
165                     typeChanged = true;
166                 }
167                 type = POSITION_TYPE_INVISIBLE;
168             }
169             if (typeChanged) {
170                 position -= tmpCount; // also equal to tmpStart
171                 tmpEnd -= tmpCount;
172                 tmpCount = 1;
173             } else {
174                 tmpCount++;
175             }
176         }
177         if (tmpCount != op.itemCount) { // all 1 effect
178             recycleUpdateOp(op);
179             op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
180         }
181         if (type == POSITION_TYPE_INVISIBLE) {
182             dispatchAndUpdateViewHolders(op);
183         } else {
184             postponeAndUpdateViewHolders(op);
185         }
186     }
187 
applyUpdate(UpdateOp op)188     private void applyUpdate(UpdateOp op) {
189         int tmpStart = op.positionStart;
190         int tmpCount = 0;
191         int tmpEnd = op.positionStart + op.itemCount;
192         int type = -1;
193         for (int position = op.positionStart; position < tmpEnd; position++) {
194             ViewHolder vh = mCallback.findViewHolder(position);
195             if (vh != null || canFindInPreLayout(position)) { // deferred
196                 if (type == POSITION_TYPE_INVISIBLE) {
197                     UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
198                             op.payload);
199                     dispatchAndUpdateViewHolders(newOp);
200                     tmpCount = 0;
201                     tmpStart = position;
202                 }
203                 type = POSITION_TYPE_NEW_OR_LAID_OUT;
204             } else { // applied
205                 if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
206                     UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
207                             op.payload);
208                     postponeAndUpdateViewHolders(newOp);
209                     tmpCount = 0;
210                     tmpStart = position;
211                 }
212                 type = POSITION_TYPE_INVISIBLE;
213             }
214             tmpCount++;
215         }
216         if (tmpCount != op.itemCount) { // all 1 effect
217             Object payload = op.payload;
218             recycleUpdateOp(op);
219             op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
220         }
221         if (type == POSITION_TYPE_INVISIBLE) {
222             dispatchAndUpdateViewHolders(op);
223         } else {
224             postponeAndUpdateViewHolders(op);
225         }
226     }
227 
dispatchAndUpdateViewHolders(UpdateOp op)228     private void dispatchAndUpdateViewHolders(UpdateOp op) {
229         // tricky part.
230         // traverse all postpones and revert their changes on this op if necessary, apply updated
231         // dispatch to them since now they are after this op.
232         if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
233             throw new IllegalArgumentException("should not dispatch add or move for pre layout");
234         }
235         if (DEBUG) {
236             Log.d(TAG, "dispatch (pre)" + op);
237             Log.d(TAG, "postponed state before:");
238             for (UpdateOp updateOp : mPostponedList) {
239                 Log.d(TAG, updateOp.toString());
240             }
241             Log.d(TAG, "----");
242         }
243 
244         // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
245         // TODO Since move ops are pushed to end, we should not need this anymore
246         int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
247         if (DEBUG) {
248             Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
249         }
250         int tmpCnt = 1;
251         int offsetPositionForPartial = op.positionStart;
252         final int positionMultiplier;
253         switch (op.cmd) {
254             case UpdateOp.UPDATE:
255                 positionMultiplier = 1;
256                 break;
257             case UpdateOp.REMOVE:
258                 positionMultiplier = 0;
259                 break;
260             default:
261                 throw new IllegalArgumentException("op should be remove or update." + op);
262         }
263         for (int p = 1; p < op.itemCount; p++) {
264             final int pos = op.positionStart + (positionMultiplier * p);
265             int updatedPos = updatePositionWithPostponed(pos, op.cmd);
266             if (DEBUG) {
267                 Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
268             }
269             boolean continuous = false;
270             switch (op.cmd) {
271                 case UpdateOp.UPDATE:
272                     continuous = updatedPos == tmpStart + 1;
273                     break;
274                 case UpdateOp.REMOVE:
275                     continuous = updatedPos == tmpStart;
276                     break;
277             }
278             if (continuous) {
279                 tmpCnt++;
280             } else {
281                 // need to dispatch this separately
282                 UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload);
283                 if (DEBUG) {
284                     Log.d(TAG, "need to dispatch separately " + tmp);
285                 }
286                 dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
287                 recycleUpdateOp(tmp);
288                 if (op.cmd == UpdateOp.UPDATE) {
289                     offsetPositionForPartial += tmpCnt;
290                 }
291                 tmpStart = updatedPos;// need to remove previously dispatched
292                 tmpCnt = 1;
293             }
294         }
295         Object payload = op.payload;
296         recycleUpdateOp(op);
297         if (tmpCnt > 0) {
298             UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload);
299             if (DEBUG) {
300                 Log.d(TAG, "dispatching:" + tmp);
301             }
302             dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
303             recycleUpdateOp(tmp);
304         }
305         if (DEBUG) {
306             Log.d(TAG, "post dispatch");
307             Log.d(TAG, "postponed state after:");
308             for (UpdateOp updateOp : mPostponedList) {
309                 Log.d(TAG, updateOp.toString());
310             }
311             Log.d(TAG, "----");
312         }
313     }
314 
dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart)315     void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
316         mCallback.onDispatchFirstPass(op);
317         switch (op.cmd) {
318             case UpdateOp.REMOVE:
319                 mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
320                 break;
321             case UpdateOp.UPDATE:
322                 mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
323                 break;
324             default:
325                 throw new IllegalArgumentException("only remove and update ops can be dispatched"
326                         + " in first pass");
327         }
328     }
329 
updatePositionWithPostponed(int pos, int cmd)330     private int updatePositionWithPostponed(int pos, int cmd) {
331         final int count = mPostponedList.size();
332         for (int i = count - 1; i >= 0; i--) {
333             UpdateOp postponed = mPostponedList.get(i);
334             if (postponed.cmd == UpdateOp.MOVE) {
335                 int start, end;
336                 if (postponed.positionStart < postponed.itemCount) {
337                     start = postponed.positionStart;
338                     end = postponed.itemCount;
339                 } else {
340                     start = postponed.itemCount;
341                     end = postponed.positionStart;
342                 }
343                 if (pos >= start && pos <= end) {
344                     //i'm affected
345                     if (start == postponed.positionStart) {
346                         if (cmd == UpdateOp.ADD) {
347                             postponed.itemCount++;
348                         } else if (cmd == UpdateOp.REMOVE) {
349                             postponed.itemCount--;
350                         }
351                         // op moved to left, move it right to revert
352                         pos++;
353                     } else {
354                         if (cmd == UpdateOp.ADD) {
355                             postponed.positionStart++;
356                         } else if (cmd == UpdateOp.REMOVE) {
357                             postponed.positionStart--;
358                         }
359                         // op was moved right, move left to revert
360                         pos--;
361                     }
362                 } else if (pos < postponed.positionStart) {
363                     // postponed MV is outside the dispatched OP. if it is before, offset
364                     if (cmd == UpdateOp.ADD) {
365                         postponed.positionStart++;
366                         postponed.itemCount++;
367                     } else if (cmd == UpdateOp.REMOVE) {
368                         postponed.positionStart--;
369                         postponed.itemCount--;
370                     }
371                 }
372             } else {
373                 if (postponed.positionStart <= pos) {
374                     if (postponed.cmd == UpdateOp.ADD) {
375                         pos -= postponed.itemCount;
376                     } else if (postponed.cmd == UpdateOp.REMOVE) {
377                         pos += postponed.itemCount;
378                     }
379                 } else {
380                     if (cmd == UpdateOp.ADD) {
381                         postponed.positionStart++;
382                     } else if (cmd == UpdateOp.REMOVE) {
383                         postponed.positionStart--;
384                     }
385                 }
386             }
387             if (DEBUG) {
388                 Log.d(TAG, "dispath (step" + i + ")");
389                 Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
390                 for (UpdateOp updateOp : mPostponedList) {
391                     Log.d(TAG, updateOp.toString());
392                 }
393                 Log.d(TAG, "----");
394             }
395         }
396         for (int i = mPostponedList.size() - 1; i >= 0; i--) {
397             UpdateOp op = mPostponedList.get(i);
398             if (op.cmd == UpdateOp.MOVE) {
399                 if (op.itemCount == op.positionStart || op.itemCount < 0) {
400                     mPostponedList.remove(i);
401                     recycleUpdateOp(op);
402                 }
403             } else if (op.itemCount <= 0) {
404                 mPostponedList.remove(i);
405                 recycleUpdateOp(op);
406             }
407         }
408         return pos;
409     }
410 
canFindInPreLayout(int position)411     private boolean canFindInPreLayout(int position) {
412         final int count = mPostponedList.size();
413         for (int i = 0; i < count; i++) {
414             UpdateOp op = mPostponedList.get(i);
415             if (op.cmd == UpdateOp.MOVE) {
416                 if (findPositionOffset(op.itemCount, i + 1) == position) {
417                     return true;
418                 }
419             } else if (op.cmd == UpdateOp.ADD) {
420                 // TODO optimize.
421                 final int end = op.positionStart + op.itemCount;
422                 for (int pos = op.positionStart; pos < end; pos++) {
423                     if (findPositionOffset(pos, i + 1) == position) {
424                         return true;
425                     }
426                 }
427             }
428         }
429         return false;
430     }
431 
applyAdd(UpdateOp op)432     private void applyAdd(UpdateOp op) {
433         postponeAndUpdateViewHolders(op);
434     }
435 
postponeAndUpdateViewHolders(UpdateOp op)436     private void postponeAndUpdateViewHolders(UpdateOp op) {
437         if (DEBUG) {
438             Log.d(TAG, "postponing " + op);
439         }
440         mPostponedList.add(op);
441         switch (op.cmd) {
442             case UpdateOp.ADD:
443                 mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
444                 break;
445             case UpdateOp.MOVE:
446                 mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
447                 break;
448             case UpdateOp.REMOVE:
449                 mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
450                         op.itemCount);
451                 break;
452             case UpdateOp.UPDATE:
453                 mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
454                 break;
455             default:
456                 throw new IllegalArgumentException("Unknown update op type for " + op);
457         }
458     }
459 
hasPendingUpdates()460     boolean hasPendingUpdates() {
461         return mPendingUpdates.size() > 0;
462     }
463 
hasAnyUpdateTypes(int updateTypes)464     boolean hasAnyUpdateTypes(int updateTypes) {
465         return (mExistingUpdateTypes & updateTypes) != 0;
466     }
467 
findPositionOffset(int position)468     int findPositionOffset(int position) {
469         return findPositionOffset(position, 0);
470     }
471 
findPositionOffset(int position, int firstPostponedItem)472     int findPositionOffset(int position, int firstPostponedItem) {
473         int count = mPostponedList.size();
474         for (int i = firstPostponedItem; i < count; ++i) {
475             UpdateOp op = mPostponedList.get(i);
476             if (op.cmd == UpdateOp.MOVE) {
477                 if (op.positionStart == position) {
478                     position = op.itemCount;
479                 } else {
480                     if (op.positionStart < position) {
481                         position--; // like a remove
482                     }
483                     if (op.itemCount <= position) {
484                         position++; // like an add
485                     }
486                 }
487             } else if (op.positionStart <= position) {
488                 if (op.cmd == UpdateOp.REMOVE) {
489                     if (position < op.positionStart + op.itemCount) {
490                         return -1;
491                     }
492                     position -= op.itemCount;
493                 } else if (op.cmd == UpdateOp.ADD) {
494                     position += op.itemCount;
495                 }
496             }
497         }
498         return position;
499     }
500 
501     /**
502      * @return True if updates should be processed.
503      */
onItemRangeChanged(int positionStart, int itemCount, Object payload)504     boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
505         if (itemCount < 1) {
506             return false;
507         }
508         mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
509         mExistingUpdateTypes |= UpdateOp.UPDATE;
510         return mPendingUpdates.size() == 1;
511     }
512 
513     /**
514      * @return True if updates should be processed.
515      */
onItemRangeInserted(int positionStart, int itemCount)516     boolean onItemRangeInserted(int positionStart, int itemCount) {
517         if (itemCount < 1) {
518             return false;
519         }
520         mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
521         mExistingUpdateTypes |= UpdateOp.ADD;
522         return mPendingUpdates.size() == 1;
523     }
524 
525     /**
526      * @return True if updates should be processed.
527      */
onItemRangeRemoved(int positionStart, int itemCount)528     boolean onItemRangeRemoved(int positionStart, int itemCount) {
529         if (itemCount < 1) {
530             return false;
531         }
532         mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
533         mExistingUpdateTypes |= UpdateOp.REMOVE;
534         return mPendingUpdates.size() == 1;
535     }
536 
537     /**
538      * @return True if updates should be processed.
539      */
onItemRangeMoved(int from, int to, int itemCount)540     boolean onItemRangeMoved(int from, int to, int itemCount) {
541         if (from == to) {
542             return false; // no-op
543         }
544         if (itemCount != 1) {
545             throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
546         }
547         mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
548         mExistingUpdateTypes |= UpdateOp.MOVE;
549         return mPendingUpdates.size() == 1;
550     }
551 
552     /**
553      * Skips pre-processing and applies all updates in one pass.
554      */
consumeUpdatesInOnePass()555     void consumeUpdatesInOnePass() {
556         // we still consume postponed updates (if there is) in case there was a pre-process call
557         // w/o a matching consumePostponedUpdates.
558         consumePostponedUpdates();
559         final int count = mPendingUpdates.size();
560         for (int i = 0; i < count; i++) {
561             UpdateOp op = mPendingUpdates.get(i);
562             switch (op.cmd) {
563                 case UpdateOp.ADD:
564                     mCallback.onDispatchSecondPass(op);
565                     mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
566                     break;
567                 case UpdateOp.REMOVE:
568                     mCallback.onDispatchSecondPass(op);
569                     mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
570                     break;
571                 case UpdateOp.UPDATE:
572                     mCallback.onDispatchSecondPass(op);
573                     mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
574                     break;
575                 case UpdateOp.MOVE:
576                     mCallback.onDispatchSecondPass(op);
577                     mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
578                     break;
579             }
580             if (mOnItemProcessedCallback != null) {
581                 mOnItemProcessedCallback.run();
582             }
583         }
584         recycleUpdateOpsAndClearList(mPendingUpdates);
585         mExistingUpdateTypes = 0;
586     }
587 
applyPendingUpdatesToPosition(int position)588     public int applyPendingUpdatesToPosition(int position) {
589         final int size = mPendingUpdates.size();
590         for (int i = 0; i < size; i ++) {
591             UpdateOp op = mPendingUpdates.get(i);
592             switch (op.cmd) {
593                 case UpdateOp.ADD:
594                     if (op.positionStart <= position) {
595                         position += op.itemCount;
596                     }
597                     break;
598                 case UpdateOp.REMOVE:
599                     if (op.positionStart <= position) {
600                         final int end = op.positionStart + op.itemCount;
601                         if (end > position) {
602                             return RecyclerView.NO_POSITION;
603                         }
604                         position -= op.itemCount;
605                     }
606                     break;
607                 case UpdateOp.MOVE:
608                     if (op.positionStart == position) {
609                         position = op.itemCount;//position end
610                     } else {
611                         if (op.positionStart < position) {
612                             position -= 1;
613                         }
614                         if (op.itemCount <= position) {
615                             position += 1;
616                         }
617                     }
618                     break;
619             }
620         }
621         return position;
622     }
623 
hasUpdates()624     boolean hasUpdates() {
625         return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty();
626     }
627 
628     /**
629      * Queued operation to happen when child views are updated.
630      */
631     static class UpdateOp {
632 
633         static final int ADD = 1;
634 
635         static final int REMOVE = 1 << 1;
636 
637         static final int UPDATE = 1 << 2;
638 
639         static final int MOVE = 1 << 3;
640 
641         static final int POOL_SIZE = 30;
642 
643         int cmd;
644 
645         int positionStart;
646 
647         Object payload;
648 
649         // holds the target position if this is a MOVE
650         int itemCount;
651 
UpdateOp(int cmd, int positionStart, int itemCount, Object payload)652         UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
653             this.cmd = cmd;
654             this.positionStart = positionStart;
655             this.itemCount = itemCount;
656             this.payload = payload;
657         }
658 
cmdToString()659         String cmdToString() {
660             switch (cmd) {
661                 case ADD:
662                     return "add";
663                 case REMOVE:
664                     return "rm";
665                 case UPDATE:
666                     return "up";
667                 case MOVE:
668                     return "mv";
669             }
670             return "??";
671         }
672 
673         @Override
toString()674         public String toString() {
675             return Integer.toHexString(System.identityHashCode(this))
676                     + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
677                     +",p:"+payload + "]";
678         }
679 
680         @Override
equals(Object o)681         public boolean equals(Object o) {
682             if (this == o) {
683                 return true;
684             }
685             if (o == null || getClass() != o.getClass()) {
686                 return false;
687             }
688 
689             UpdateOp op = (UpdateOp) o;
690 
691             if (cmd != op.cmd) {
692                 return false;
693             }
694             if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
695                 // reverse of this is also true
696                 if (itemCount == op.positionStart && positionStart == op.itemCount) {
697                     return true;
698                 }
699             }
700             if (itemCount != op.itemCount) {
701                 return false;
702             }
703             if (positionStart != op.positionStart) {
704                 return false;
705             }
706             if (payload != null) {
707                 if (!payload.equals(op.payload)) {
708                     return false;
709                 }
710             } else if (op.payload != null) {
711                 return false;
712             }
713 
714             return true;
715         }
716 
717         @Override
hashCode()718         public int hashCode() {
719             int result = cmd;
720             result = 31 * result + positionStart;
721             result = 31 * result + itemCount;
722             return result;
723         }
724     }
725 
726     @Override
obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload)727     public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
728         UpdateOp op = mUpdateOpPool.acquire();
729         if (op == null) {
730             op = new UpdateOp(cmd, positionStart, itemCount, payload);
731         } else {
732             op.cmd = cmd;
733             op.positionStart = positionStart;
734             op.itemCount = itemCount;
735             op.payload = payload;
736         }
737         return op;
738     }
739 
740     @Override
recycleUpdateOp(UpdateOp op)741     public void recycleUpdateOp(UpdateOp op) {
742         if (!mDisableRecycler) {
743             op.payload = null;
744             mUpdateOpPool.release(op);
745         }
746     }
747 
recycleUpdateOpsAndClearList(List<UpdateOp> ops)748     void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
749         final int count = ops.size();
750         for (int i = 0; i < count; i++) {
751             recycleUpdateOp(ops.get(i));
752         }
753         ops.clear();
754     }
755 
756     /**
757      * Contract between AdapterHelper and RecyclerView.
758      */
759     static interface Callback {
760 
findViewHolder(int position)761         ViewHolder findViewHolder(int position);
762 
offsetPositionsForRemovingInvisible(int positionStart, int itemCount)763         void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
764 
offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount)765         void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
766 
markViewHoldersUpdated(int positionStart, int itemCount, Object payloads)767         void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
768 
onDispatchFirstPass(UpdateOp updateOp)769         void onDispatchFirstPass(UpdateOp updateOp);
770 
onDispatchSecondPass(UpdateOp updateOp)771         void onDispatchSecondPass(UpdateOp updateOp);
772 
offsetPositionsForAdd(int positionStart, int itemCount)773         void offsetPositionsForAdd(int positionStart, int itemCount);
774 
offsetPositionsForMove(int from, int to)775         void offsetPositionsForMove(int from, int to);
776     }
777 }
778