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