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