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