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