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