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