/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.widget; import android.support.v4.util.Pools; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static android.support.v7.widget.RecyclerView.*; /** * Helper class that can enqueue and process adapter update operations. *

* To support animations, RecyclerView presents an older version the Adapter to best represent * previous state of the layout. Sometimes, this is not trivial when items are removed that were * not laid out, in which case, RecyclerView has no way of providing that item's view for * animations. *

* AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them * according to previously deferred operation and dispatch them before the first layout pass. It * also takes care of updating deferred UpdateOps since order of operations is changed by this * process. *

* Although operations may be forwarded to LayoutManager in different orders, resulting data set * is guaranteed to be the consistent. */ class AdapterHelper implements OpReorderer.Callback { final static int POSITION_TYPE_INVISIBLE = 0; final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1; private static final boolean DEBUG = false; private static final String TAG = "AHT"; private Pools.Pool mUpdateOpPool = new Pools.SimplePool(UpdateOp.POOL_SIZE); final ArrayList mPendingUpdates = new ArrayList(); final ArrayList mPostponedList = new ArrayList(); final Callback mCallback; Runnable mOnItemProcessedCallback; final boolean mDisableRecycler; final OpReorderer mOpReorderer; AdapterHelper(Callback callback) { this(callback, false); } AdapterHelper(Callback callback, boolean disableRecycler) { mCallback = callback; mDisableRecycler = disableRecycler; mOpReorderer = new OpReorderer(this); } AdapterHelper addUpdateOp(UpdateOp... ops) { Collections.addAll(mPendingUpdates, ops); return this; } void reset() { recycleUpdateOpsAndClearList(mPendingUpdates); recycleUpdateOpsAndClearList(mPostponedList); } void preProcess() { mOpReorderer.reorderOps(mPendingUpdates); final int count = mPendingUpdates.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { case UpdateOp.ADD: applyAdd(op); break; case UpdateOp.REMOVE: applyRemove(op); break; case UpdateOp.UPDATE: applyUpdate(op); break; case UpdateOp.MOVE: applyMove(op); break; } if (mOnItemProcessedCallback != null) { mOnItemProcessedCallback.run(); } } mPendingUpdates.clear(); } void consumePostponedUpdates() { final int count = mPostponedList.size(); for (int i = 0; i < count; i++) { mCallback.onDispatchSecondPass(mPostponedList.get(i)); } recycleUpdateOpsAndClearList(mPostponedList); } private void applyMove(UpdateOp op) { // MOVE ops are pre-processed so at this point, we know that item is still in the adapter. // otherwise, it would be converted into a REMOVE operation postponeAndUpdateViewHolders(op); } private void applyRemove(UpdateOp op) { int tmpStart = op.positionStart; int tmpCount = 0; int tmpEnd = op.positionStart + op.itemCount; int type = -1; for (int position = op.positionStart; position < tmpEnd; position++) { boolean typeChanged = false; ViewHolder vh = mCallback.findViewHolder(position); if (vh != null || canFindInPreLayout(position)) { // If a ViewHolder exists or this is a newly added item, we can defer this update // to post layout stage. // * For existing ViewHolders, we'll fake its existence in the pre-layout phase. // * For items that are added and removed in the same process cycle, they won't // have any effect in pre-layout since their add ops are already deferred to // post-layout pass. if (type == POSITION_TYPE_INVISIBLE) { // Looks like we have other updates that we cannot merge with this one. // Create an UpdateOp and dispatch it to LayoutManager. UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); dispatchAndUpdateViewHolders(newOp); typeChanged = true; } type = POSITION_TYPE_NEW_OR_LAID_OUT; } else { // This update cannot be recovered because we don't have a ViewHolder representing // this position. Instead, post it to LayoutManager immediately if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { // Looks like we have other updates that we cannot merge with this one. // Create UpdateOp op and dispatch it to LayoutManager. UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); postponeAndUpdateViewHolders(newOp); typeChanged = true; } type = POSITION_TYPE_INVISIBLE; } if (typeChanged) { position -= tmpCount; // also equal to tmpStart tmpEnd -= tmpCount; tmpCount = 1; } else { tmpCount++; } } if (tmpCount != op.itemCount) { // all 1 effect recycleUpdateOp(op); op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); } if (type == POSITION_TYPE_INVISIBLE) { dispatchAndUpdateViewHolders(op); } else { postponeAndUpdateViewHolders(op); } } private void applyUpdate(UpdateOp op) { int tmpStart = op.positionStart; int tmpCount = 0; int tmpEnd = op.positionStart + op.itemCount; int type = -1; for (int position = op.positionStart; position < tmpEnd; position++) { ViewHolder vh = mCallback.findViewHolder(position); if (vh != null || canFindInPreLayout(position)) { // deferred if (type == POSITION_TYPE_INVISIBLE) { UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); dispatchAndUpdateViewHolders(newOp); tmpCount = 0; tmpStart = position; } type = POSITION_TYPE_NEW_OR_LAID_OUT; } else { // applied if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); postponeAndUpdateViewHolders(newOp); tmpCount = 0; tmpStart = position; } type = POSITION_TYPE_INVISIBLE; } tmpCount++; } if (tmpCount != op.itemCount) { // all 1 effect recycleUpdateOp(op); op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); } if (type == POSITION_TYPE_INVISIBLE) { dispatchAndUpdateViewHolders(op); } else { postponeAndUpdateViewHolders(op); } } private void dispatchAndUpdateViewHolders(UpdateOp op) { // tricky part. // traverse all postpones and revert their changes on this op if necessary, apply updated // dispatch to them since now they are after this op. if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) { throw new IllegalArgumentException("should not dispatch add or move for pre layout"); } if (DEBUG) { Log.d(TAG, "dispatch (pre)" + op); Log.d(TAG, "postponed state before:"); for (UpdateOp updateOp : mPostponedList) { Log.d(TAG, updateOp.toString()); } Log.d(TAG, "----"); } // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial // TODO Since move ops are pushed to end, we should not need this anymore int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd); if (DEBUG) { Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart); } int tmpCnt = 1; int offsetPositionForPartial = op.positionStart; final int positionMultiplier; switch (op.cmd) { case UpdateOp.UPDATE: positionMultiplier = 1; break; case UpdateOp.REMOVE: positionMultiplier = 0; break; default: throw new IllegalArgumentException("op should be remove or update." + op); } for (int p = 1; p < op.itemCount; p++) { final int pos = op.positionStart + (positionMultiplier * p); int updatedPos = updatePositionWithPostponed(pos, op.cmd); if (DEBUG) { Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos); } boolean continuous = false; switch (op.cmd) { case UpdateOp.UPDATE: continuous = updatedPos == tmpStart + 1; break; case UpdateOp.REMOVE: continuous = updatedPos == tmpStart; break; } if (continuous) { tmpCnt++; } else { // need to dispatch this separately UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt); if (DEBUG) { Log.d(TAG, "need to dispatch separately " + tmp); } dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); recycleUpdateOp(tmp); if (op.cmd == UpdateOp.UPDATE) { offsetPositionForPartial += tmpCnt; } tmpStart = updatedPos;// need to remove previously dispatched tmpCnt = 1; } } recycleUpdateOp(op); if (tmpCnt > 0) { UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt); if (DEBUG) { Log.d(TAG, "dispatching:" + tmp); } dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); recycleUpdateOp(tmp); } if (DEBUG) { Log.d(TAG, "post dispatch"); Log.d(TAG, "postponed state after:"); for (UpdateOp updateOp : mPostponedList) { Log.d(TAG, updateOp.toString()); } Log.d(TAG, "----"); } } void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) { mCallback.onDispatchFirstPass(op); switch (op.cmd) { case UpdateOp.REMOVE: mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount); break; case UpdateOp.UPDATE: mCallback.markViewHoldersUpdated(offsetStart, op.itemCount); break; default: throw new IllegalArgumentException("only remove and update ops can be dispatched" + " in first pass"); } } private int updatePositionWithPostponed(int pos, int cmd) { final int count = mPostponedList.size(); for (int i = count - 1; i >= 0; i--) { UpdateOp postponed = mPostponedList.get(i); if (postponed.cmd == UpdateOp.MOVE) { int start, end; if (postponed.positionStart < postponed.itemCount) { start = postponed.positionStart; end = postponed.itemCount; } else { start = postponed.itemCount; end = postponed.positionStart; } if (pos >= start && pos <= end) { //i'm affected if (start == postponed.positionStart) { if (cmd == UpdateOp.ADD) { postponed.itemCount++; } else if (cmd == UpdateOp.REMOVE) { postponed.itemCount--; } // op moved to left, move it right to revert pos++; } else { if (cmd == UpdateOp.ADD) { postponed.positionStart++; } else if (cmd == UpdateOp.REMOVE) { postponed.positionStart--; } // op was moved right, move left to revert pos--; } } else if (pos < postponed.positionStart) { // postponed MV is outside the dispatched OP. if it is before, offset if (cmd == UpdateOp.ADD) { postponed.positionStart++; postponed.itemCount++; } else if (cmd == UpdateOp.REMOVE) { postponed.positionStart--; postponed.itemCount--; } } } else { if (postponed.positionStart <= pos) { if (postponed.cmd == UpdateOp.ADD) { pos -= postponed.itemCount; } else if (postponed.cmd == UpdateOp.REMOVE) { pos += postponed.itemCount; } } else { if (cmd == UpdateOp.ADD) { postponed.positionStart++; } else if (cmd == UpdateOp.REMOVE) { postponed.positionStart--; } } } if (DEBUG) { Log.d(TAG, "dispath (step" + i + ")"); Log.d(TAG, "postponed state:" + i + ", pos:" + pos); for (UpdateOp updateOp : mPostponedList) { Log.d(TAG, updateOp.toString()); } Log.d(TAG, "----"); } } for (int i = mPostponedList.size() - 1; i >= 0; i--) { UpdateOp op = mPostponedList.get(i); if (op.cmd == UpdateOp.MOVE) { if (op.itemCount == op.positionStart || op.itemCount < 0) { mPostponedList.remove(i); recycleUpdateOp(op); } } else if (op.itemCount <= 0) { mPostponedList.remove(i); recycleUpdateOp(op); } } return pos; } private boolean canFindInPreLayout(int position) { final int count = mPostponedList.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPostponedList.get(i); if (op.cmd == UpdateOp.MOVE) { if (findPositionOffset(op.itemCount, i + 1) == position) { return true; } } else if (op.cmd == UpdateOp.ADD) { // TODO optimize. final int end = op.positionStart + op.itemCount; for (int pos = op.positionStart; pos < end; pos++) { if (findPositionOffset(pos, i + 1) == position) { return true; } } } } return false; } private void applyAdd(UpdateOp op) { postponeAndUpdateViewHolders(op); } private void postponeAndUpdateViewHolders(UpdateOp op) { if (DEBUG) { Log.d(TAG, "postponing " + op); } mPostponedList.add(op); switch (op.cmd) { case UpdateOp.ADD: mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break; case UpdateOp.MOVE: mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break; case UpdateOp.REMOVE: mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount); break; case UpdateOp.UPDATE: mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount); break; default: throw new IllegalArgumentException("Unknown update op type for " + op); } } boolean hasPendingUpdates() { return mPendingUpdates.size() > 0; } int findPositionOffset(int position) { return findPositionOffset(position, 0); } int findPositionOffset(int position, int firstPostponedItem) { int count = mPostponedList.size(); for (int i = firstPostponedItem; i < count; ++i) { UpdateOp op = mPostponedList.get(i); if (op.cmd == UpdateOp.MOVE) { if (op.positionStart == position) { position = op.itemCount; } else { if (op.positionStart < position) { position--; // like a remove } if (op.itemCount <= position) { position++; // like an add } } } else if (op.positionStart <= position) { if (op.cmd == UpdateOp.REMOVE) { if (position < op.positionStart + op.itemCount) { return -1; } position -= op.itemCount; } else if (op.cmd == UpdateOp.ADD) { position += op.itemCount; } } } return position; } /** * @return True if updates should be processed. */ boolean onItemRangeChanged(int positionStart, int itemCount) { mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount)); return mPendingUpdates.size() == 1; } /** * @return True if updates should be processed. */ boolean onItemRangeInserted(int positionStart, int itemCount) { mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount)); return mPendingUpdates.size() == 1; } /** * @return True if updates should be processed. */ boolean onItemRangeRemoved(int positionStart, int itemCount) { mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount)); return mPendingUpdates.size() == 1; } /** * @return True if updates should be processed. */ boolean onItemRangeMoved(int from, int to, int itemCount) { if (from == to) { return false;//no-op } if (itemCount != 1) { throw new IllegalArgumentException("Moving more than 1 item is not supported yet"); } mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to)); return mPendingUpdates.size() == 1; } /** * Skips pre-processing and applies all updates in one pass. */ void consumeUpdatesInOnePass() { // we still consume postponed updates (if there is) in case there was a pre-process call // w/o a matching consumePostponedUpdates. consumePostponedUpdates(); final int count = mPendingUpdates.size(); for (int i = 0; i < count; i++) { UpdateOp op = mPendingUpdates.get(i); switch (op.cmd) { case UpdateOp.ADD: mCallback.onDispatchSecondPass(op); mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); break; case UpdateOp.REMOVE: mCallback.onDispatchSecondPass(op); mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount); break; case UpdateOp.UPDATE: mCallback.onDispatchSecondPass(op); mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount); break; case UpdateOp.MOVE: mCallback.onDispatchSecondPass(op); mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); break; } if (mOnItemProcessedCallback != null) { mOnItemProcessedCallback.run(); } } recycleUpdateOpsAndClearList(mPendingUpdates); } /** * Queued operation to happen when child views are updated. */ static class UpdateOp { static final int ADD = 0; static final int REMOVE = 1; static final int UPDATE = 2; static final int MOVE = 3; static final int POOL_SIZE = 30; int cmd; int positionStart; // holds the target position if this is a MOVE int itemCount; UpdateOp(int cmd, int positionStart, int itemCount) { this.cmd = cmd; this.positionStart = positionStart; this.itemCount = itemCount; } String cmdToString() { switch (cmd) { case ADD: return "add"; case REMOVE: return "rm"; case UPDATE: return "up"; case MOVE: return "mv"; } return "??"; } @Override public String toString() { return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } UpdateOp op = (UpdateOp) o; if (cmd != op.cmd) { return false; } if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) { // reverse of this is also true if (itemCount == op.positionStart && positionStart == op.itemCount) { return true; } } if (itemCount != op.itemCount) { return false; } if (positionStart != op.positionStart) { return false; } return true; } @Override public int hashCode() { int result = cmd; result = 31 * result + positionStart; result = 31 * result + itemCount; return result; } } @Override public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) { UpdateOp op = mUpdateOpPool.acquire(); if (op == null) { op = new UpdateOp(cmd, positionStart, itemCount); } else { op.cmd = cmd; op.positionStart = positionStart; op.itemCount = itemCount; } return op; } @Override public void recycleUpdateOp(UpdateOp op) { if (!mDisableRecycler) { mUpdateOpPool.release(op); } } void recycleUpdateOpsAndClearList(List ops) { final int count = ops.size(); for (int i = 0; i < count; i++) { recycleUpdateOp(ops.get(i)); } ops.clear(); } /** * Contract between AdapterHelper and RecyclerView. */ static interface Callback { ViewHolder findViewHolder(int position); void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); void markViewHoldersUpdated(int positionStart, int itemCount); void onDispatchFirstPass(UpdateOp updateOp); void onDispatchSecondPass(UpdateOp updateOp); void offsetPositionsForAdd(int positionStart, int itemCount); void offsetPositionsForMove(int from, int to); } }