package com.airbnb.epoxy; import android.support.annotation.Nullable; import com.airbnb.epoxy.UpdateOp.Type; import java.util.ArrayList; import java.util.List; import static com.airbnb.epoxy.UpdateOp.ADD; import static com.airbnb.epoxy.UpdateOp.MOVE; import static com.airbnb.epoxy.UpdateOp.REMOVE; import static com.airbnb.epoxy.UpdateOp.UPDATE; /** Helper class to collect changes in a diff, batching when possible. */ class UpdateOpHelper { final List<UpdateOp> opList = new ArrayList<>(); // We have to be careful to update all item positions in the list when we // do a MOVE. This adds some complexity. // To do this we keep track of all moves and apply them to an item when we // need the up to date position final List<UpdateOp> moves = new ArrayList<>(); private UpdateOp lastOp; private int numInsertions; private int numInsertionBatches; private int numRemovals; private int numRemovalBatches; void reset() { opList.clear(); moves.clear(); lastOp = null; numInsertions = 0; numInsertionBatches = 0; numRemovals = 0; numRemovalBatches = 0; } void add(int indexToInsert) { add(indexToInsert, 1); } void add(int startPosition, int itemCount) { numInsertions += itemCount; // We can append to a previously ADD batch if the new items are added anywhere in the // range of the previous batch batch boolean batchWithLast = isLastOp(ADD) && (lastOp.contains(startPosition) || lastOp.positionEnd() == startPosition); if (batchWithLast) { addItemsToLastOperation(itemCount, null); } else { numInsertionBatches++; addNewOperation(ADD, startPosition, itemCount); } } void update(int indexToChange) { update(indexToChange, null); } void update(final int indexToChange, EpoxyModel<?> payload) { if (isLastOp(UPDATE)) { if (lastOp.positionStart == indexToChange + 1) { // Change another item at the start of the batch range addItemsToLastOperation(1, payload); lastOp.positionStart = indexToChange; } else if (lastOp.positionEnd() == indexToChange) { // Add another item at the end of the batch range addItemsToLastOperation(1, payload); } else if (lastOp.contains(indexToChange)) { // This item is already included in the existing batch range, so we don't add any items // to the batch count, but we still need to add the new payload addItemsToLastOperation(0, payload); } else { // The item can't be batched with the previous update operation addNewOperation(UPDATE, indexToChange, 1, payload); } } else { addNewOperation(UPDATE, indexToChange, 1, payload); } } void remove(int indexToRemove) { remove(indexToRemove, 1); } void remove(int startPosition, int itemCount) { numRemovals += itemCount; boolean batchWithLast = false; if (isLastOp(REMOVE)) { if (lastOp.positionStart == startPosition) { // Remove additional items at the end of the batch range batchWithLast = true; } else if (lastOp.isAfter(startPosition) && startPosition + itemCount >= lastOp.positionStart) { // Removes additional items at the start and (possibly) end of the batch lastOp.positionStart = startPosition; batchWithLast = true; } } if (batchWithLast) { addItemsToLastOperation(itemCount, null); } else { numRemovalBatches++; addNewOperation(REMOVE, startPosition, itemCount); } } private boolean isLastOp(@UpdateOp.Type int updateType) { return lastOp != null && lastOp.type == updateType; } private void addNewOperation(@Type int type, int position, int itemCount) { addNewOperation(type, position, itemCount, null); } private void addNewOperation(@Type int type, int position, int itemCount, @Nullable EpoxyModel<?> payload) { lastOp = UpdateOp.instance(type, position, itemCount, payload); opList.add(lastOp); } private void addItemsToLastOperation(int numItemsToAdd, EpoxyModel<?> payload) { lastOp.itemCount += numItemsToAdd; lastOp.addPayload(payload); } void move(int from, int to) { // We can't batch moves lastOp = null; UpdateOp op = UpdateOp.instance(MOVE, from, to, null); opList.add(op); moves.add(op); } int getNumRemovals() { return numRemovals; } boolean hasRemovals() { return numRemovals > 0; } int getNumInsertions() { return numInsertions; } boolean hasInsertions() { return numInsertions > 0; } int getNumMoves() { return moves.size(); } int getNumInsertionBatches() { return numInsertionBatches; } int getNumRemovalBatches() { return numRemovalBatches; } }