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
17package android.support.v7.widget;
18
19import android.support.v4.util.Pools;
20import android.util.Log;
21
22import java.util.ArrayList;
23import java.util.Collections;
24import 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 */
44class 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
70    AdapterHelper(Callback callback) {
71        this(callback, false);
72    }
73
74    AdapterHelper(Callback callback, boolean disableRecycler) {
75        mCallback = callback;
76        mDisableRecycler = disableRecycler;
77        mOpReorderer = new OpReorderer(this);
78    }
79
80    AdapterHelper addUpdateOp(UpdateOp... ops) {
81        Collections.addAll(mPendingUpdates, ops);
82        return this;
83    }
84
85    void reset() {
86        recycleUpdateOpsAndClearList(mPendingUpdates);
87        recycleUpdateOpsAndClearList(mPostponedList);
88        mExistingUpdateTypes = 0;
89    }
90
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
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
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
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
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
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
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
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
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
430    private void applyAdd(UpdateOp op) {
431        postponeAndUpdateViewHolders(op);
432    }
433
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
458    boolean hasPendingUpdates() {
459        return mPendingUpdates.size() > 0;
460    }
461
462    boolean hasAnyUpdateTypes(int updateTypes) {
463        return (mExistingUpdateTypes & updateTypes) != 0;
464    }
465
466    int findPositionOffset(int position) {
467        return findPositionOffset(position, 0);
468    }
469
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     */
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     */
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     */
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     */
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     */
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
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
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
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
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
672        public String toString() {
673            return Integer.toHexString(System.identityHashCode(this))
674                    + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
675                    + ",p:" + payload + "]";
676        }
677
678        @Override
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
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
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
739    public void recycleUpdateOp(UpdateOp op) {
740        if (!mDisableRecycler) {
741            op.payload = null;
742            mUpdateOpPool.release(op);
743        }
744    }
745
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
759        RecyclerView.ViewHolder findViewHolder(int position);
760
761        void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
762
763        void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
764
765        void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
766
767        void onDispatchFirstPass(UpdateOp updateOp);
768
769        void onDispatchSecondPass(UpdateOp updateOp);
770
771        void offsetPositionsForAdd(int positionStart, int itemCount);
772
773        void offsetPositionsForMove(int from, int to);
774    }
775}
776