diff --git a/app/build.gradle b/app/build.gradle index 88b0e572c8..756a43373d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -608,7 +608,9 @@ dependencies { // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview // https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection - implementation "androidx.recyclerview:recyclerview:$recyclerview_version" + //implementation "androidx.recyclerview:recyclerview:$recyclerview_version" + implementation "androidx.customview:customview:1.1.0" + implementation "androidx.customview:customview-poolingcontainer:1.0.0" //implementation "androidx.recyclerview:recyclerview-selection:1.1.0" // 1.2.0-alpha01 // https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout diff --git a/app/src/main/java/androidx/recyclerview/widget/AdapterHelper.java b/app/src/main/java/androidx/recyclerview/widget/AdapterHelper.java new file mode 100644 index 0000000000..004f7445ce --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/AdapterHelper.java @@ -0,0 +1,776 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.util.Log; + +import androidx.core.util.Pools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 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. + */ +final class AdapterHelper implements OpReorderer.Callback { + + static final int POSITION_TYPE_INVISIBLE = 0; + + static final 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; + + private int mExistingUpdateTypes = 0; + + 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); + mExistingUpdateTypes = 0; + } + + 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); + mExistingUpdateTypes = 0; + } + + 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; + RecyclerView.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, null); + 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, null); + 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, null); + } + 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++) { + RecyclerView.ViewHolder vh = mCallback.findViewHolder(position); + if (vh != null || canFindInPreLayout(position)) { // deferred + if (type == POSITION_TYPE_INVISIBLE) { + UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, + op.payload); + 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, + op.payload); + postponeAndUpdateViewHolders(newOp); + tmpCount = 0; + tmpStart = position; + } + type = POSITION_TYPE_INVISIBLE; + } + tmpCount++; + } + if (tmpCount != op.itemCount) { // all 1 effect + Object payload = op.payload; + recycleUpdateOp(op); + op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload); + } + 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, op.payload); + 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; + } + } + Object payload = op.payload; + recycleUpdateOp(op); + if (tmpCnt > 0) { + UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload); + 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, op.payload); + 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, op.payload); + break; + default: + throw new IllegalArgumentException("Unknown update op type for " + op); + } + } + + boolean hasPendingUpdates() { + return mPendingUpdates.size() > 0; + } + + boolean hasAnyUpdateTypes(int updateTypes) { + return (mExistingUpdateTypes & updateTypes) != 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, Object payload) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload)); + mExistingUpdateTypes |= UpdateOp.UPDATE; + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeInserted(int positionStart, int itemCount) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null)); + mExistingUpdateTypes |= UpdateOp.ADD; + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeRemoved(int positionStart, int itemCount) { + if (itemCount < 1) { + return false; + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null)); + mExistingUpdateTypes |= UpdateOp.REMOVE; + 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, null)); + mExistingUpdateTypes |= UpdateOp.MOVE; + 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, op.payload); + break; + case UpdateOp.MOVE: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); + break; + } + if (mOnItemProcessedCallback != null) { + mOnItemProcessedCallback.run(); + } + } + recycleUpdateOpsAndClearList(mPendingUpdates); + mExistingUpdateTypes = 0; + } + + public int applyPendingUpdatesToPosition(int position) { + final int size = mPendingUpdates.size(); + for (int i = 0; i < size; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + if (op.positionStart <= position) { + position += op.itemCount; + } + break; + case UpdateOp.REMOVE: + if (op.positionStart <= position) { + final int end = op.positionStart + op.itemCount; + if (end > position) { + return RecyclerView.NO_POSITION; + } + position -= op.itemCount; + } + break; + case UpdateOp.MOVE: + if (op.positionStart == position) { + position = op.itemCount; //position end + } else { + if (op.positionStart < position) { + position -= 1; + } + if (op.itemCount <= position) { + position += 1; + } + } + break; + } + } + return position; + } + + boolean hasUpdates() { + return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty(); + } + + /** + * Queued operation to happen when child views are updated. + */ + static final class UpdateOp { + + static final int ADD = 1; + + static final int REMOVE = 1 << 1; + + static final int UPDATE = 1 << 2; + + static final int MOVE = 1 << 3; + + static final int POOL_SIZE = 30; + + int cmd; + + int positionStart; + + Object payload; + + // holds the target position if this is a MOVE + int itemCount; + + UpdateOp(int cmd, int positionStart, int itemCount, Object payload) { + this.cmd = cmd; + this.positionStart = positionStart; + this.itemCount = itemCount; + this.payload = payload; + } + + 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 Integer.toHexString(System.identityHashCode(this)) + + "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + + ",p:" + payload + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof UpdateOp)) { + 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; + } + if (payload != null) { + if (!payload.equals(op.payload)) { + return false; + } + } else if (op.payload != null) { + 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, Object payload) { + UpdateOp op = mUpdateOpPool.acquire(); + if (op == null) { + op = new UpdateOp(cmd, positionStart, itemCount, payload); + } else { + op.cmd = cmd; + op.positionStart = positionStart; + op.itemCount = itemCount; + op.payload = payload; + } + return op; + } + + @Override + public void recycleUpdateOp(UpdateOp op) { + if (!mDisableRecycler) { + op.payload = null; + 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. + */ + interface Callback { + + RecyclerView.ViewHolder findViewHolder(int position); + + void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); + + void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); + + void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads); + + void onDispatchFirstPass(UpdateOp updateOp); + + void onDispatchSecondPass(UpdateOp updateOp); + + void offsetPositionsForAdd(int positionStart, int itemCount); + + void offsetPositionsForMove(int from, int to); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java b/app/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java new file mode 100644 index 0000000000..ec94f9c445 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/AdapterListUpdateCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; + +/** + * ListUpdateCallback that dispatches update events to the given adapter. + * + * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) + */ +public final class AdapterListUpdateCallback implements ListUpdateCallback { + @NonNull + private final RecyclerView.Adapter mAdapter; + + /** + * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. + * + * @param adapter The Adapter to send updates to. + */ + public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + /** {@inheritDoc} */ + @Override + public void onInserted(int position, int count) { + mAdapter.notifyItemRangeInserted(position, count); + } + + /** {@inheritDoc} */ + @Override + public void onRemoved(int position, int count) { + mAdapter.notifyItemRangeRemoved(position, count); + } + + /** {@inheritDoc} */ + @Override + public void onMoved(int fromPosition, int toPosition) { + mAdapter.notifyItemMoved(fromPosition, toPosition); + } + + /** {@inheritDoc} */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChanged(int position, int count, Object payload) { + mAdapter.notifyItemRangeChanged(position, count, payload); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java b/app/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java new file mode 100644 index 0000000000..ccd9cfae63 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/AsyncDifferConfig.java @@ -0,0 +1,148 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * Configuration object for {@link ListAdapter}, {@link AsyncListDiffer}, and similar + * background-thread list diffing adapter logic. + *

+ * At minimum, defines item diffing behavior with a {@link DiffUtil.ItemCallback}, used to compute + * item differences to pass to a RecyclerView adapter. + * + * @param Type of items in the lists, and being compared. + */ +public final class AsyncDifferConfig { + @Nullable + private final Executor mMainThreadExecutor; + @NonNull + private final Executor mBackgroundThreadExecutor; + @NonNull + private final DiffUtil.ItemCallback mDiffCallback; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + AsyncDifferConfig( + @Nullable Executor mainThreadExecutor, + @NonNull Executor backgroundThreadExecutor, + @NonNull DiffUtil.ItemCallback diffCallback) { + mMainThreadExecutor = mainThreadExecutor; + mBackgroundThreadExecutor = backgroundThreadExecutor; + mDiffCallback = diffCallback; + } + + /** @hide */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY) + @Nullable + public Executor getMainThreadExecutor() { + return mMainThreadExecutor; + } + + @SuppressWarnings("WeakerAccess") + @NonNull + public Executor getBackgroundThreadExecutor() { + return mBackgroundThreadExecutor; + } + + @SuppressWarnings("WeakerAccess") + @NonNull + public DiffUtil.ItemCallback getDiffCallback() { + return mDiffCallback; + } + + /** + * Builder class for {@link AsyncDifferConfig}. + * + * @param + */ + public static final class Builder { + @Nullable + private Executor mMainThreadExecutor; + private Executor mBackgroundThreadExecutor; + private final DiffUtil.ItemCallback mDiffCallback; + + public Builder(@NonNull DiffUtil.ItemCallback diffCallback) { + mDiffCallback = diffCallback; + } + + /** + * If provided, defines the main thread executor used to dispatch adapter update + * notifications on the main thread. + *

+ * If not provided or null, it will default to the main thread. + * + * @param executor The executor which can run tasks in the UI thread. + * @return this + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @NonNull + public Builder setMainThreadExecutor(@Nullable Executor executor) { + mMainThreadExecutor = executor; + return this; + } + + /** + * If provided, defines the background executor used to calculate the diff between an old + * and a new list. + *

+ * If not provided or null, defaults to two thread pool executor, shared by all + * ListAdapterConfigs. + * + * @param executor The background executor to run list diffing. + * @return this + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + @NonNull + public Builder setBackgroundThreadExecutor(@Nullable Executor executor) { + mBackgroundThreadExecutor = executor; + return this; + } + + /** + * Creates a {@link AsyncListDiffer} with the given parameters. + * + * @return A new AsyncDifferConfig. + */ + @NonNull + public AsyncDifferConfig build() { + if (mBackgroundThreadExecutor == null) { + synchronized (sExecutorLock) { + if (sDiffExecutor == null) { + sDiffExecutor = Executors.newFixedThreadPool(2); + } + } + mBackgroundThreadExecutor = sDiffExecutor; + } + return new AsyncDifferConfig<>( + mMainThreadExecutor, + mBackgroundThreadExecutor, + mDiffCallback); + } + + // TODO: remove the below once supportlib has its own appropriate executors + private static final Object sExecutorLock = new Object(); + private static Executor sDiffExecutor = null; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java b/app/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java new file mode 100644 index 0000000000..c1fdb87858 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java @@ -0,0 +1,405 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; + +/** + * Helper for computing the difference between two lists via {@link DiffUtil} on a background + * thread. + *

+ * It can be connected to a + * {@link RecyclerView.Adapter RecyclerView.Adapter}, and will signal the + * adapter of changes between sumbitted lists. + *

+ * For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the + * AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an + * adapter base class to support asynchronous List diffing isn't convenient. + *

+ * The AsyncListDiffer can consume the values from a LiveData of List and present the + * data simply for an adapter. It computes differences in list contents via {@link DiffUtil} on a + * background thread as new Lists are received. + *

+ * Use {@link #getCurrentList()} to access the current List, and present its data objects. Diff + * results will be dispatched to the ListUpdateCallback immediately before the current list is + * updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can + * safely access list items and total size via {@link #getCurrentList()}. + *

+ * A complete usage pattern with Room would look like this: + *

+ * {@literal @}Dao
+ * interface UserDao {
+ *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ *     public abstract LiveData<List<User>> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ *     public final LiveData<List<User>> usersList;
+ *     public MyViewModel(UserDao userDao) {
+ *         usersList = userDao.usersByLastName();
+ *     }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ *     {@literal @}Override
+ *     public void onCreate(Bundle savedState) {
+ *         super.onCreate(savedState);
+ *         MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
+ *         RecyclerView recyclerView = findViewById(R.id.user_list);
+ *         UserAdapter adapter = new UserAdapter();
+ *         viewModel.usersList.observe(this, list -> adapter.submitList(list));
+ *         recyclerView.setAdapter(adapter);
+ *     }
+ * }
+ *
+ * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
+ *     private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK);
+ *     {@literal @}Override
+ *     public int getItemCount() {
+ *         return mDiffer.getCurrentList().size();
+ *     }
+ *     public void submitList(List<User> list) {
+ *         mDiffer.submitList(list);
+ *     }
+ *     {@literal @}Override
+ *     public void onBindViewHolder(UserViewHolder holder, int position) {
+ *         User user = mDiffer.getCurrentList().get(position);
+ *         holder.bindTo(user);
+ *     }
+ *     public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK
+ *             = new DiffUtil.ItemCallback<User>() {
+ *         {@literal @}Override
+ *         public boolean areItemsTheSame(
+ *                 {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ *             // User properties may have changed if reloaded from the DB, but ID is fixed
+ *             return oldUser.getId() == newUser.getId();
+ *         }
+ *         {@literal @}Override
+ *         public boolean areContentsTheSame(
+ *                 {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ *             // NOTE: if you use equals, your object must properly override Object#equals()
+ *             // Incorrectly returning false here will result in too many animations.
+ *             return oldUser.equals(newUser);
+ *         }
+ *     }
+ * }
+ * + * @param Type of the lists this AsyncListDiffer will receive. + * + * @see DiffUtil + * @see AdapterListUpdateCallback + */ +public class AsyncListDiffer { + private final ListUpdateCallback mUpdateCallback; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final AsyncDifferConfig mConfig; + Executor mMainThreadExecutor; + + private static class MainThreadExecutor implements Executor { + final Handler mHandler = new Handler(Looper.getMainLooper()); + MainThreadExecutor() {} + @Override + public void execute(@NonNull Runnable command) { + mHandler.post(command); + } + } + + // TODO: use MainThreadExecutor from supportlib once one exists + private static final Executor sMainThreadExecutor = new MainThreadExecutor(); + + /** + * Listener for when the current List is updated. + * + * @param Type of items in List + */ + public interface ListListener { + /** + * Called after the current List has been updated. + * + * @param previousList The previous list. + * @param currentList The new current list. + */ + void onCurrentListChanged(@NonNull List previousList, @NonNull List currentList); + } + + private final List> mListeners = new CopyOnWriteArrayList<>(); + + /** + * Convenience for + * {@code AsyncListDiffer(new AdapterListUpdateCallback(adapter), + * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());} + * + * @param adapter Adapter to dispatch position updates to. + * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when + * + * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) + */ + public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter, + @NonNull DiffUtil.ItemCallback diffCallback) { + this(new AdapterListUpdateCallback(adapter), + new AsyncDifferConfig.Builder<>(diffCallback).build()); + } + + /** + * Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch + * updates to. + * + * @param listUpdateCallback Callback to dispatch updates to. + * @param config Config to define background work Executor, and DiffUtil.ItemCallback for + * computing List diffs. + * + * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter) + */ + @SuppressWarnings("WeakerAccess") + public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback, + @NonNull AsyncDifferConfig config) { + mUpdateCallback = listUpdateCallback; + mConfig = config; + if (config.getMainThreadExecutor() != null) { + mMainThreadExecutor = config.getMainThreadExecutor(); + } else { + mMainThreadExecutor = sMainThreadExecutor; + } + } + + @Nullable + private List mList; + + /** + * Non-null, unmodifiable version of mList. + *

+ * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise + */ + @NonNull + private List mReadOnlyList = Collections.emptyList(); + + // Max generation of currently scheduled runnable + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mMaxScheduledGeneration; + + /** + * Get the current List - any diffing to present this list has already been computed and + * dispatched via the ListUpdateCallback. + *

+ * If a null List, or no List has been submitted, an empty list will be returned. + *

+ * The returned list may not be mutated - mutations to content must be done through + * {@link #submitList(List)}. + * + * @return current List. + */ + @NonNull + public List getCurrentList() { + return mReadOnlyList; + } + + /** + * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background + * thread. + *

+ * If a List is already present, a diff will be computed asynchronously on a background thread. + * When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}), + * and the new List will be swapped in. + * + * @param newList The new List. + */ + @SuppressWarnings("WeakerAccess") + public void submitList(@Nullable final List newList) { + submitList(newList, null); + } + + /** + * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background + * thread. + *

+ * If a List is already present, a diff will be computed asynchronously on a background thread. + * When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}), + * and the new List will be swapped in. + *

+ * The commit callback can be used to know when the List is committed, but note that it + * may not be executed. If List B is submitted immediately after List A, and is + * committed directly, the callback associated with List A will not be run. + * + * @param newList The new List. + * @param commitCallback Optional runnable that is executed when the List is committed, if + * it is committed. + */ + @SuppressWarnings("WeakerAccess") + public void submitList(@Nullable final List newList, + @Nullable final Runnable commitCallback) { + // incrementing generation means any currently-running diffs are discarded when they finish + final int runGeneration = ++mMaxScheduledGeneration; + + if (newList == mList) { + // nothing to do (Note - still had to inc generation, since may have ongoing work) + if (commitCallback != null) { + commitCallback.run(); + } + return; + } + + final List previousList = mReadOnlyList; + + // fast simple remove all + if (newList == null) { + //noinspection ConstantConditions + int countRemoved = mList.size(); + mList = null; + mReadOnlyList = Collections.emptyList(); + // notify last, after list is updated + mUpdateCallback.onRemoved(0, countRemoved); + onCurrentListChanged(previousList, commitCallback); + return; + } + + // fast simple first insert + if (mList == null) { + mList = newList; + mReadOnlyList = Collections.unmodifiableList(newList); + // notify last, after list is updated + mUpdateCallback.onInserted(0, newList.size()); + onCurrentListChanged(previousList, commitCallback); + return; + } + + final List oldList = mList; + mConfig.getBackgroundThreadExecutor().execute(new Runnable() { + @Override + public void run() { + final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return oldList.size(); + } + + @Override + public int getNewListSize() { + return newList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition); + T newItem = newList.get(newItemPosition); + if (oldItem != null && newItem != null) { + return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem); + } + // If both items are null we consider them the same. + return oldItem == null && newItem == null; + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition); + T newItem = newList.get(newItemPosition); + if (oldItem != null && newItem != null) { + return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem); + } + if (oldItem == null && newItem == null) { + return true; + } + // There is an implementation bug if we reach this point. Per the docs, this + // method should only be invoked when areItemsTheSame returns true. That + // only occurs when both items are non-null or both are null and both of + // those cases are handled above. + throw new AssertionError(); + } + + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + T oldItem = oldList.get(oldItemPosition); + T newItem = newList.get(newItemPosition); + if (oldItem != null && newItem != null) { + return mConfig.getDiffCallback().getChangePayload(oldItem, newItem); + } + // There is an implementation bug if we reach this point. Per the docs, this + // method should only be invoked when areItemsTheSame returns true AND + // areContentsTheSame returns false. That only occurs when both items are + // non-null which is the only case handled above. + throw new AssertionError(); + } + }); + + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + if (mMaxScheduledGeneration == runGeneration) { + latchList(newList, result, commitCallback); + } + } + }); + } + }); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void latchList( + @NonNull List newList, + @NonNull DiffUtil.DiffResult diffResult, + @Nullable Runnable commitCallback) { + final List previousList = mReadOnlyList; + mList = newList; + // notify last, after list is updated + mReadOnlyList = Collections.unmodifiableList(newList); + diffResult.dispatchUpdatesTo(mUpdateCallback); + onCurrentListChanged(previousList, commitCallback); + } + + private void onCurrentListChanged(@NonNull List previousList, + @Nullable Runnable commitCallback) { + // current list is always mReadOnlyList + for (ListListener listener : mListeners) { + listener.onCurrentListChanged(previousList, mReadOnlyList); + } + if (commitCallback != null) { + commitCallback.run(); + } + } + + /** + * Add a ListListener to receive updates when the current List changes. + * + * @param listener Listener to receive updates. + * + * @see #getCurrentList() + * @see #removeListListener(ListListener) + */ + public void addListListener(@NonNull ListListener listener) { + mListeners.add(listener); + } + + /** + * Remove a previously registered ListListener. + * + * @param listener Previously registered listener. + * @see #getCurrentList() + * @see #addListListener(ListListener) + */ + public void removeListListener(@NonNull ListListener listener) { + mListeners.remove(listener); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java b/app/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java new file mode 100644 index 0000000000..b50f6a8137 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/AsyncListUtil.java @@ -0,0 +1,596 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; + +/** + * A utility class that supports asynchronous content loading. + *

+ * It can be used to load Cursor data in chunks without querying the Cursor on the UI Thread while + * keeping UI and cache synchronous for better user experience. + *

+ * It loads the data on a background thread and keeps only a limited number of fixed sized + * chunks in memory at all times. + *

+ * {@link AsyncListUtil} queries the currently visible range through {@link ViewCallback}, + * loads the required data items in the background through {@link DataCallback}, and notifies a + * {@link ViewCallback} when the data is loaded. It may load some extra items for smoother + * scrolling. + *

+ * Note that this class uses a single thread to load the data, so it suitable to load data from + * secondary storage such as disk, but not from network. + *

+ * This class is designed to work with {@link RecyclerView}, but it does + * not depend on it and can be used with other list views. + * + */ +public class AsyncListUtil { + static final String TAG = "AsyncListUtil"; + + static final boolean DEBUG = false; + + final Class mTClass; + final int mTileSize; + final DataCallback mDataCallback; + final ViewCallback mViewCallback; + + final TileList mTileList; + + final ThreadUtil.MainThreadCallback mMainThreadProxy; + final ThreadUtil.BackgroundCallback mBackgroundProxy; + + final int[] mTmpRange = new int[2]; + final int[] mPrevRange = new int[2]; + final int[] mTmpRangeExtended = new int[2]; + + boolean mAllowScrollHints; + private int mScrollHint = ViewCallback.HINT_SCROLL_NONE; + + int mItemCount = 0; + + int mDisplayedGeneration = 0; + int mRequestedGeneration = mDisplayedGeneration; + + final SparseIntArray mMissingPositions = new SparseIntArray(); + + void log(String s, Object... args) { + Log.d(TAG, "[MAIN] " + String.format(s, args)); + } + + /** + * Creates an AsyncListUtil. + * + * @param klass Class of the data item. + * @param tileSize Number of item per chunk loaded at once. + * @param dataCallback Data access callback. + * @param viewCallback Callback for querying visible item range and update notifications. + */ + public AsyncListUtil(@NonNull Class klass, int tileSize, + @NonNull DataCallback dataCallback, @NonNull ViewCallback viewCallback) { + mTClass = klass; + mTileSize = tileSize; + mDataCallback = dataCallback; + mViewCallback = viewCallback; + + mTileList = new TileList(mTileSize); + + ThreadUtil threadUtil = new MessageThreadUtil(); + mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback); + mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback); + + refresh(); + } + + private boolean isRefreshPending() { + return mRequestedGeneration != mDisplayedGeneration; + } + + /** + * Updates the currently visible item range. + * + *

+ * Identifies the data items that have not been loaded yet and initiates loading them in the + * background. Should be called from the view's scroll listener (such as + * {@link RecyclerView.OnScrollListener#onScrolled}). + */ + public void onRangeChanged() { + if (isRefreshPending()) { + return; // Will update range will the refresh result arrives. + } + updateRange(); + mAllowScrollHints = true; + } + + /** + * Forces reloading the data. + *

+ * Discards all the cached data and reloads all required data items for the currently visible + * range. To be called when the data item count and/or contents has changed. + */ + public void refresh() { + mMissingPositions.clear(); + mBackgroundProxy.refresh(++mRequestedGeneration); + } + + /** + * Returns the data item at the given position or null if it has not been loaded + * yet. + * + *

+ * If this method has been called for a specific position and returned null, then + * {@link ViewCallback#onItemLoaded(int)} will be called when it finally loads. Note that if + * this position stays outside of the cached item range (as defined by + * {@link ViewCallback#extendRangeInto} method), then the callback will never be called for + * this position. + * + * @param position Item position. + * + * @return The data item at the given position or null if it has not been loaded + * yet. + */ + @Nullable + public T getItem(int position) { + if (position < 0 || position >= mItemCount) { + throw new IndexOutOfBoundsException(position + " is not within 0 and " + mItemCount); + } + T item = mTileList.getItemAt(position); + if (item == null && !isRefreshPending()) { + mMissingPositions.put(position, 0); + } + return item; + } + + /** + * Returns the number of items in the data set. + * + *

+ * This is the number returned by a recent call to + * {@link DataCallback#refreshData()}. + * + * @return Number of items. + */ + public int getItemCount() { + return mItemCount; + } + + void updateRange() { + mViewCallback.getItemRangeInto(mTmpRange); + if (mTmpRange[0] > mTmpRange[1] || mTmpRange[0] < 0) { + return; + } + if (mTmpRange[1] >= mItemCount) { + // Invalid range may arrive soon after the refresh. + return; + } + + if (!mAllowScrollHints) { + mScrollHint = ViewCallback.HINT_SCROLL_NONE; + } else if (mTmpRange[0] > mPrevRange[1] || mPrevRange[0] > mTmpRange[1]) { + // Ranges do not intersect, long leap not a scroll. + mScrollHint = ViewCallback.HINT_SCROLL_NONE; + } else if (mTmpRange[0] < mPrevRange[0]) { + mScrollHint = ViewCallback.HINT_SCROLL_DESC; + } else if (mTmpRange[0] > mPrevRange[0]) { + mScrollHint = ViewCallback.HINT_SCROLL_ASC; + } + + mPrevRange[0] = mTmpRange[0]; + mPrevRange[1] = mTmpRange[1]; + + mViewCallback.extendRangeInto(mTmpRange, mTmpRangeExtended, mScrollHint); + mTmpRangeExtended[0] = Math.min(mTmpRange[0], Math.max(mTmpRangeExtended[0], 0)); + mTmpRangeExtended[1] = + Math.max(mTmpRange[1], Math.min(mTmpRangeExtended[1], mItemCount - 1)); + + mBackgroundProxy.updateRange(mTmpRange[0], mTmpRange[1], + mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint); + } + + private final ThreadUtil.MainThreadCallback + mMainThreadCallback = new ThreadUtil.MainThreadCallback() { + @Override + public void updateItemCount(int generation, int itemCount) { + if (DEBUG) { + log("updateItemCount: size=%d, gen #%d", itemCount, generation); + } + if (!isRequestedGeneration(generation)) { + return; + } + mItemCount = itemCount; + mViewCallback.onDataRefresh(); + mDisplayedGeneration = mRequestedGeneration; + recycleAllTiles(); + + mAllowScrollHints = false; // Will be set to true after a first real scroll. + // There will be no scroll event if the size change does not affect the current range. + updateRange(); + } + + @Override + public void addTile(int generation, TileList.Tile tile) { + if (!isRequestedGeneration(generation)) { + if (DEBUG) { + log("recycling an older generation tile @%d", tile.mStartPosition); + } + mBackgroundProxy.recycleTile(tile); + return; + } + TileList.Tile duplicate = mTileList.addOrReplace(tile); + if (duplicate != null) { + Log.e(TAG, "duplicate tile @" + duplicate.mStartPosition); + mBackgroundProxy.recycleTile(duplicate); + } + if (DEBUG) { + log("gen #%d, added tile @%d, total tiles: %d", + generation, tile.mStartPosition, mTileList.size()); + } + int endPosition = tile.mStartPosition + tile.mItemCount; + int index = 0; + while (index < mMissingPositions.size()) { + final int position = mMissingPositions.keyAt(index); + if (tile.mStartPosition <= position && position < endPosition) { + mMissingPositions.removeAt(index); + mViewCallback.onItemLoaded(position); + } else { + index++; + } + } + } + + @Override + public void removeTile(int generation, int position) { + if (!isRequestedGeneration(generation)) { + return; + } + TileList.Tile tile = mTileList.removeAtPos(position); + if (tile == null) { + Log.e(TAG, "tile not found @" + position); + return; + } + if (DEBUG) { + log("recycling tile @%d, total tiles: %d", tile.mStartPosition, mTileList.size()); + } + mBackgroundProxy.recycleTile(tile); + } + + private void recycleAllTiles() { + if (DEBUG) { + log("recycling all %d tiles", mTileList.size()); + } + for (int i = 0; i < mTileList.size(); i++) { + mBackgroundProxy.recycleTile(mTileList.getAtIndex(i)); + } + mTileList.clear(); + } + + private boolean isRequestedGeneration(int generation) { + return generation == mRequestedGeneration; + } + }; + + private final ThreadUtil.BackgroundCallback + mBackgroundCallback = new ThreadUtil.BackgroundCallback() { + + private TileList.Tile mRecycledRoot; + + final SparseBooleanArray mLoadedTiles = new SparseBooleanArray(); + + private int mGeneration; + private int mItemCount; + + private int mFirstRequiredTileStart; + private int mLastRequiredTileStart; + + @Override + public void refresh(int generation) { + mGeneration = generation; + mLoadedTiles.clear(); + mItemCount = mDataCallback.refreshData(); + mMainThreadProxy.updateItemCount(mGeneration, mItemCount); + } + + @Override + public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd, + int scrollHint) { + if (DEBUG) { + log("updateRange: %d..%d extended to %d..%d, scroll hint: %d", + rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint); + } + + if (rangeStart > rangeEnd) { + return; + } + + final int firstVisibleTileStart = getTileStart(rangeStart); + final int lastVisibleTileStart = getTileStart(rangeEnd); + + mFirstRequiredTileStart = getTileStart(extRangeStart); + mLastRequiredTileStart = getTileStart(extRangeEnd); + if (DEBUG) { + log("requesting tile range: %d..%d", + mFirstRequiredTileStart, mLastRequiredTileStart); + } + + // All pending tile requests are removed by ThreadUtil at this point. + // Re-request all required tiles in the most optimal order. + if (scrollHint == ViewCallback.HINT_SCROLL_DESC) { + requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true); + requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint, + false); + } else { + requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false); + requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint, + true); + } + } + + private int getTileStart(int position) { + return position - position % mTileSize; + } + + private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint, + boolean backwards) { + for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) { + int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i; + if (DEBUG) { + log("requesting tile @%d", tileStart); + } + mBackgroundProxy.loadTile(tileStart, scrollHint); + } + } + + @Override + public void loadTile(int position, int scrollHint) { + if (isTileLoaded(position)) { + if (DEBUG) { + log("already loaded tile @%d", position); + } + return; + } + TileList.Tile tile = acquireTile(); + tile.mStartPosition = position; + tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition); + mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount); + flushTileCache(scrollHint); + addTile(tile); + } + + @Override + public void recycleTile(TileList.Tile tile) { + if (DEBUG) { + log("recycling tile @%d", tile.mStartPosition); + } + mDataCallback.recycleData(tile.mItems, tile.mItemCount); + + tile.mNext = mRecycledRoot; + mRecycledRoot = tile; + } + + private TileList.Tile acquireTile() { + if (mRecycledRoot != null) { + TileList.Tile result = mRecycledRoot; + mRecycledRoot = mRecycledRoot.mNext; + return result; + } + return new TileList.Tile(mTClass, mTileSize); + } + + private boolean isTileLoaded(int position) { + return mLoadedTiles.get(position); + } + + private void addTile(TileList.Tile tile) { + mLoadedTiles.put(tile.mStartPosition, true); + mMainThreadProxy.addTile(mGeneration, tile); + if (DEBUG) { + log("loaded tile @%d, total tiles: %d", tile.mStartPosition, mLoadedTiles.size()); + } + } + + private void removeTile(int position) { + mLoadedTiles.delete(position); + mMainThreadProxy.removeTile(mGeneration, position); + if (DEBUG) { + log("flushed tile @%d, total tiles: %s", position, mLoadedTiles.size()); + } + } + + private void flushTileCache(int scrollHint) { + final int cacheSizeLimit = mDataCallback.getMaxCachedTiles(); + while (mLoadedTiles.size() >= cacheSizeLimit) { + int firstLoadedTileStart = mLoadedTiles.keyAt(0); + int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1); + int startMargin = mFirstRequiredTileStart - firstLoadedTileStart; + int endMargin = lastLoadedTileStart - mLastRequiredTileStart; + if (startMargin > 0 && (startMargin >= endMargin || + (scrollHint == ViewCallback.HINT_SCROLL_ASC))) { + removeTile(firstLoadedTileStart); + } else if (endMargin > 0 && (startMargin < endMargin || + (scrollHint == ViewCallback.HINT_SCROLL_DESC))){ + removeTile(lastLoadedTileStart); + } else { + // Could not flush on either side, bail out. + return; + } + } + } + + private void log(String s, Object... args) { + Log.d(TAG, "[BKGR] " + String.format(s, args)); + } + }; + + /** + * The callback that provides data access for {@link AsyncListUtil}. + * + *

+ * All methods are called on the background thread. + */ + public static abstract class DataCallback { + + /** + * Refresh the data set and return the new data item count. + * + *

+ * If the data is being accessed through {@link android.database.Cursor} this is where + * the new cursor should be created. + * + * @return Data item count. + */ + @WorkerThread + public abstract int refreshData(); + + /** + * Fill the given tile. + * + *

+ * The provided tile might be a recycled tile, in which case it will already have objects. + * It is suggested to re-use these objects if possible in your use case. + * + * @param startPosition The start position in the list. + * @param itemCount The data item count. + * @param data The data item array to fill into. Should not be accessed beyond + * itemCount. + */ + @WorkerThread + public abstract void fillData(@NonNull T[] data, int startPosition, int itemCount); + + /** + * Recycle the objects created in {@link #fillData} if necessary. + * + * + * @param data Array of data items. Should not be accessed beyond itemCount. + * @param itemCount The data item count. + */ + @WorkerThread + public void recycleData(@NonNull T[] data, int itemCount) { + } + + /** + * Returns tile cache size limit (in tiles). + * + *

+ * The actual number of cached tiles will be the maximum of this value and the number of + * tiles that is required to cover the range returned by + * {@link ViewCallback#extendRangeInto(int[], int[], int)}. + *

+ * For example, if this method returns 10, and the most + * recent call to {@link ViewCallback#extendRangeInto(int[], int[], int)} returned + * {100, 179}, and the tile size is 5, then the maximum number of cached tiles will be 16. + *

+ * However, if the tile size is 20, then the maximum number of cached tiles will be 10. + *

+ * The default implementation returns 10. + * + * @return Maximum cache size. + */ + @WorkerThread + public int getMaxCachedTiles() { + return 10; + } + } + + /** + * The callback that links {@link AsyncListUtil} with the list view. + * + *

+ * All methods are called on the main thread. + */ + public static abstract class ViewCallback { + + /** + * No scroll direction hint available. + */ + public static final int HINT_SCROLL_NONE = 0; + + /** + * Scrolling in descending order (from higher to lower positions in the order of the backing + * storage). + */ + public static final int HINT_SCROLL_DESC = 1; + + /** + * Scrolling in ascending order (from lower to higher positions in the order of the backing + * storage). + */ + public static final int HINT_SCROLL_ASC = 2; + + /** + * Compute the range of visible item positions. + *

+ * outRange[0] is the position of the first visible item (in the order of the backing + * storage). + *

+ * outRange[1] is the position of the last visible item (in the order of the backing + * storage). + *

+ * Negative positions and positions greater or equal to {@link #getItemCount} are invalid. + * If the returned range contains invalid positions it is ignored (no item will be loaded). + * + * @param outRange The visible item range. + */ + @UiThread + public abstract void getItemRangeInto(@NonNull int[] outRange); + + /** + * Compute a wider range of items that will be loaded for smoother scrolling. + * + *

+ * If there is no scroll hint, the default implementation extends the visible range by half + * its length in both directions. If there is a scroll hint, the range is extended by + * its full length in the scroll direction, and by half in the other direction. + *

+ * For example, if range is {100, 200} and scrollHint + * is {@link #HINT_SCROLL_ASC}, then outRange will be {50, 300}. + *

+ * However, if scrollHint is {@link #HINT_SCROLL_NONE}, then + * outRange will be {50, 250} + * + * @param range Visible item range. + * @param outRange Extended range. + * @param scrollHint The scroll direction hint. + */ + @UiThread + public void extendRangeInto(@NonNull int[] range, @NonNull int[] outRange, int scrollHint) { + final int fullRange = range[1] - range[0] + 1; + final int halfRange = fullRange / 2; + outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange); + outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange); + } + + /** + * Called when the entire data set has changed. + */ + @UiThread + public abstract void onDataRefresh(); + + /** + * Called when an item at the given position is loaded. + * @param position Item position. + */ + @UiThread + public abstract void onItemLoaded(int position); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java b/app/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java new file mode 100644 index 0000000000..bad8cc942e --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/BatchingListUpdateCallback.java @@ -0,0 +1,128 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; + +/** + * Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged. + *

+ * For instance, when 2 add operations comes that adds 2 consecutive elements, + * BatchingListUpdateCallback merges them and calls the wrapped callback only once. + *

+ * This is a general purpose class and is also used by + * {@link DiffUtil.DiffResult DiffResult} and + * {@link SortedList} to minimize the number of updates that are dispatched. + *

+ * If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the + * stream of update events drain. + */ +public class BatchingListUpdateCallback implements ListUpdateCallback { + private static final int TYPE_NONE = 0; + private static final int TYPE_ADD = 1; + private static final int TYPE_REMOVE = 2; + private static final int TYPE_CHANGE = 3; + + final ListUpdateCallback mWrapped; + + int mLastEventType = TYPE_NONE; + int mLastEventPosition = -1; + int mLastEventCount = -1; + Object mLastEventPayload = null; + + public BatchingListUpdateCallback(@NonNull ListUpdateCallback callback) { + mWrapped = callback; + } + + /** + * BatchingListUpdateCallback holds onto the last event to see if it can be merged with the + * next one. When stream of events finish, you should call this method to dispatch the last + * event. + */ + public void dispatchLastEvent() { + if (mLastEventType == TYPE_NONE) { + return; + } + switch (mLastEventType) { + case TYPE_ADD: + mWrapped.onInserted(mLastEventPosition, mLastEventCount); + break; + case TYPE_REMOVE: + mWrapped.onRemoved(mLastEventPosition, mLastEventCount); + break; + case TYPE_CHANGE: + mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload); + break; + } + mLastEventPayload = null; + mLastEventType = TYPE_NONE; + } + + @Override + public void onInserted(int position, int count) { + if (mLastEventType == TYPE_ADD && position >= mLastEventPosition + && position <= mLastEventPosition + mLastEventCount) { + mLastEventCount += count; + mLastEventPosition = Math.min(position, mLastEventPosition); + return; + } + dispatchLastEvent(); + mLastEventPosition = position; + mLastEventCount = count; + mLastEventType = TYPE_ADD; + } + + @Override + public void onRemoved(int position, int count) { + if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position && + mLastEventPosition <= position + count) { + mLastEventCount += count; + mLastEventPosition = position; + return; + } + dispatchLastEvent(); + mLastEventPosition = position; + mLastEventCount = count; + mLastEventType = TYPE_REMOVE; + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + dispatchLastEvent(); // moves are not merged + mWrapped.onMoved(fromPosition, toPosition); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChanged(int position, int count, Object payload) { + if (mLastEventType == TYPE_CHANGE && + !(position > mLastEventPosition + mLastEventCount + || position + count < mLastEventPosition || mLastEventPayload != payload)) { + // take potential overlap into account + int previousEnd = mLastEventPosition + mLastEventCount; + mLastEventPosition = Math.min(position, mLastEventPosition); + mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition; + return; + } + dispatchLastEvent(); + mLastEventPosition = position; + mLastEventCount = count; + mLastEventPayload = payload; + mLastEventType = TYPE_CHANGE; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ChildHelper.java b/app/src/main/java/androidx/recyclerview/widget/ChildHelper.java new file mode 100644 index 0000000000..1541ba8fd3 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ChildHelper.java @@ -0,0 +1,601 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to manage children. + *

+ * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods + * provided by this class. Regular methods are the ones that replicate ViewGroup methods + * like getChildAt, getChildCount etc. These methods ignore hidden children. + *

+ * When RecyclerView needs direct access to the view group children, it can call unfiltered + * methods like get getUnfilteredChildCount or getUnfilteredChildAt. + */ +class ChildHelper { + + private static final boolean DEBUG = false; + + private static final String TAG = "ChildrenHelper"; + + /** Not in call to removeView/removeViewAt/removeViewIfHidden. */ + private static final int REMOVE_STATUS_NONE = 0; + + /** Within a call to removeView/removeViewAt. */ + private static final int REMOVE_STATUS_IN_REMOVE = 1; + + /** Within a call to removeViewIfHidden. */ + private static final int REMOVE_STATUS_IN_REMOVE_IF_HIDDEN = 2; + + final Callback mCallback; + + final Bucket mBucket; + + final List mHiddenViews; + + /** + * One of REMOVE_STATUS_NONE, REMOVE_STATUS_IN_REMOVE, REMOVE_STATUS_IN_REMOVE_IF_HIDDEN. + * removeView and removeViewIfHidden may call each other: + * 1. removeView triggers removeViewIfHidden: this happens when removeView stops the item + * animation. removeViewIfHidden should do nothing. + * 2. removeView triggers removeView: this should not happen. + * 3. removeViewIfHidden triggers removeViewIfHidden: this should not happen, since the + * animation was stopped before the first removeViewIfHidden, it won't trigger another + * removeViewIfHidden. + * 4. removeViewIfHidden triggers removeView: this should not happen. + */ + private int mRemoveStatus = REMOVE_STATUS_NONE; + /** The view to remove in REMOVE_STATUS_IN_REMOVE. */ + private View mViewInRemoveView; + + ChildHelper(Callback callback) { + mCallback = callback; + mBucket = new Bucket(); + mHiddenViews = new ArrayList(); + } + + /** + * Marks a child view as hidden + * + * @param child View to hide. + */ + private void hideViewInternal(View child) { + mHiddenViews.add(child); + mCallback.onEnteredHiddenState(child); + } + + /** + * Unmarks a child view as hidden. + * + * @param child View to hide. + */ + private boolean unhideViewInternal(View child) { + if (mHiddenViews.remove(child)) { + mCallback.onLeftHiddenState(child); + return true; + } else { + return false; + } + } + + /** + * Adds a view to the ViewGroup + * + * @param child View to add. + * @param hidden If set to true, this item will be invisible from regular methods. + */ + void addView(View child, boolean hidden) { + addView(child, -1, hidden); + } + + /** + * Add a view to the ViewGroup at an index + * + * @param child View to add. + * @param index Index of the child from the regular perspective (excluding hidden views). + * ChildHelper offsets this index to actual ViewGroup index. + * @param hidden If set to true, this item will be invisible from regular methods. + */ + void addView(View child, int index, boolean hidden) { + final int offset; + if (index < 0) { + offset = mCallback.getChildCount(); + } else { + offset = getOffset(index); + } + mBucket.insert(offset, hidden); + if (hidden) { + hideViewInternal(child); + } + mCallback.addView(child, offset); + if (DEBUG) { + Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this); + } + } + + private int getOffset(int index) { + if (index < 0) { + return -1; //anything below 0 won't work as diff will be undefined. + } + final int limit = mCallback.getChildCount(); + int offset = index; + while (offset < limit) { + final int removedBefore = mBucket.countOnesBefore(offset); + final int diff = index - (offset - removedBefore); + if (diff == 0) { + while (mBucket.get(offset)) { // ensure this offset is not hidden + offset++; + } + return offset; + } else { + offset += diff; + } + } + return -1; + } + + /** + * Removes the provided View from underlying RecyclerView. + * + * @param view The view to remove. + */ + void removeView(View view) { + if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { + throw new IllegalStateException("Cannot call removeView(At) within removeView(At)"); + } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { + throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden"); + } + try { + mRemoveStatus = REMOVE_STATUS_IN_REMOVE; + mViewInRemoveView = view; + int index = mCallback.indexOfChild(view); + if (index < 0) { + return; + } + if (mBucket.remove(index)) { + unhideViewInternal(view); + } + mCallback.removeViewAt(index); + if (DEBUG) { + Log.d(TAG, "remove View off:" + index + "," + this); + } + } finally { + mRemoveStatus = REMOVE_STATUS_NONE; + mViewInRemoveView = null; + } + } + + /** + * Removes the view at the provided index from RecyclerView. + * + * @param index Index of the child from the regular perspective (excluding hidden views). + * ChildHelper offsets this index to actual ViewGroup index. + */ + void removeViewAt(int index) { + if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { + throw new IllegalStateException("Cannot call removeView(At) within removeView(At)"); + } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { + throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden"); + } + try { + final int offset = getOffset(index); + final View view = mCallback.getChildAt(offset); + if (view == null) { + return; + } + mRemoveStatus = REMOVE_STATUS_IN_REMOVE; + mViewInRemoveView = view; + if (mBucket.remove(offset)) { + unhideViewInternal(view); + } + mCallback.removeViewAt(offset); + if (DEBUG) { + Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this); + } + } finally { + mRemoveStatus = REMOVE_STATUS_NONE; + mViewInRemoveView = null; + } + } + + /** + * Returns the child at provided index. + * + * @param index Index of the child to return in regular perspective. + */ + View getChildAt(int index) { + final int offset = getOffset(index); + return mCallback.getChildAt(offset); + } + + /** + * Removes all views from the ViewGroup including the hidden ones. + */ + void removeAllViewsUnfiltered() { + mBucket.reset(); + for (int i = mHiddenViews.size() - 1; i >= 0; i--) { + mCallback.onLeftHiddenState(mHiddenViews.get(i)); + mHiddenViews.remove(i); + } + mCallback.removeAllViews(); + if (DEBUG) { + Log.d(TAG, "removeAllViewsUnfiltered"); + } + } + + /** + * This can be used to find a disappearing view by position. + * + * @param position The adapter position of the item. + * @return A hidden view with a valid ViewHolder that matches the position. + */ + View findHiddenNonRemovedView(int position) { + final int count = mHiddenViews.size(); + for (int i = 0; i < count; i++) { + final View view = mHiddenViews.get(i); + RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view); + if (holder.getLayoutPosition() == position + && !holder.isInvalid() + && !holder.isRemoved()) { + return view; + } + } + return null; + } + + /** + * Attaches the provided view to the underlying ViewGroup. + * + * @param child Child to attach. + * @param index Index of the child to attach in regular perspective. + * @param layoutParams LayoutParams for the child. + * @param hidden If set to true, this item will be invisible to the regular methods. + */ + void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams, + boolean hidden) { + final int offset; + if (index < 0) { + offset = mCallback.getChildCount(); + } else { + offset = getOffset(index); + } + mBucket.insert(offset, hidden); + if (hidden) { + hideViewInternal(child); + } + mCallback.attachViewToParent(child, offset, layoutParams); + if (DEBUG) { + Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," + + "h:" + hidden + ", " + this); + } + } + + /** + * Returns the number of children that are not hidden. + * + * @return Number of children that are not hidden. + * @see #getChildAt(int) + */ + int getChildCount() { + return mCallback.getChildCount() - mHiddenViews.size(); + } + + /** + * Returns the total number of children. + * + * @return The total number of children including the hidden views. + * @see #getUnfilteredChildAt(int) + */ + int getUnfilteredChildCount() { + return mCallback.getChildCount(); + } + + /** + * Returns a child by ViewGroup offset. ChildHelper won't offset this index. + * + * @param index ViewGroup index of the child to return. + * @return The view in the provided index. + */ + View getUnfilteredChildAt(int index) { + return mCallback.getChildAt(index); + } + + /** + * Detaches the view at the provided index. + * + * @param index Index of the child to return in regular perspective. + */ + void detachViewFromParent(int index) { + final int offset = getOffset(index); + mBucket.remove(offset); + mCallback.detachViewFromParent(offset); + if (DEBUG) { + Log.d(TAG, "detach view from parent " + index + ", off:" + offset); + } + } + + /** + * Returns the index of the child in regular perspective. + * + * @param child The child whose index will be returned. + * @return The regular perspective index of the child or -1 if it does not exists. + */ + int indexOfChild(View child) { + final int index = mCallback.indexOfChild(child); + if (index == -1) { + return -1; + } + if (mBucket.get(index)) { + if (DEBUG) { + throw new IllegalArgumentException("cannot get index of a hidden child"); + } else { + return -1; + } + } + // reverse the index + return index - mBucket.countOnesBefore(index); + } + + /** + * Returns whether a View is visible to LayoutManager or not. + * + * @param view The child view to check. Should be a child of the Callback. + * @return True if the View is not visible to LayoutManager + */ + boolean isHidden(View view) { + return mHiddenViews.contains(view); + } + + /** + * Marks a child view as hidden. + * + * @param view The view to hide. + */ + void hide(View view) { + final int offset = mCallback.indexOfChild(view); + if (offset < 0) { + throw new IllegalArgumentException("view is not a child, cannot hide " + view); + } + if (DEBUG && mBucket.get(offset)) { + throw new RuntimeException("trying to hide same view twice, how come ? " + view); + } + mBucket.set(offset); + hideViewInternal(view); + if (DEBUG) { + Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this); + } + } + + /** + * Moves a child view from hidden list to regular list. + * Calling this method should probably be followed by a detach, otherwise, it will suddenly + * show up in LayoutManager's children list. + * + * @param view The hidden View to unhide + */ + void unhide(View view) { + final int offset = mCallback.indexOfChild(view); + if (offset < 0) { + throw new IllegalArgumentException("view is not a child, cannot hide " + view); + } + if (!mBucket.get(offset)) { + throw new RuntimeException("trying to unhide a view that was not hidden" + view); + } + mBucket.clear(offset); + unhideViewInternal(view); + } + + @Override + public String toString() { + return mBucket.toString() + ", hidden list:" + mHiddenViews.size(); + } + + /** + * Removes a view from the ViewGroup if it is hidden. + * + * @param view The view to remove. + * @return True if the View is found and it is hidden. False otherwise. + */ + boolean removeViewIfHidden(View view) { + if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) { + if (mViewInRemoveView != view) { + throw new IllegalStateException("Cannot call removeViewIfHidden within removeView" + + "(At) for a different view"); + } + // removeView ends the ItemAnimation and triggers removeViewIfHidden + return false; + } else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) { + throw new IllegalStateException("Cannot call removeViewIfHidden within" + + " removeViewIfHidden"); + } + try { + mRemoveStatus = REMOVE_STATUS_IN_REMOVE_IF_HIDDEN; + final int index = mCallback.indexOfChild(view); + if (index == -1) { + if (unhideViewInternal(view) && DEBUG) { + throw new IllegalStateException("view is in hidden list but not in view group"); + } + return true; + } + if (mBucket.get(index)) { + mBucket.remove(index); + if (!unhideViewInternal(view) && DEBUG) { + throw new IllegalStateException( + "removed a hidden view but it is not in hidden views list"); + } + mCallback.removeViewAt(index); + return true; + } + return false; + } finally { + mRemoveStatus = REMOVE_STATUS_NONE; + } + } + + /** + * Bitset implementation that provides methods to offset indices. + */ + static class Bucket { + + static final int BITS_PER_WORD = Long.SIZE; + + static final long LAST_BIT = 1L << (Long.SIZE - 1); + + long mData = 0; + + Bucket mNext; + + void set(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + mNext.set(index - BITS_PER_WORD); + } else { + mData |= 1L << index; + } + } + + private void ensureNext() { + if (mNext == null) { + mNext = new Bucket(); + } + } + + void clear(int index) { + if (index >= BITS_PER_WORD) { + if (mNext != null) { + mNext.clear(index - BITS_PER_WORD); + } + } else { + mData &= ~(1L << index); + } + + } + + boolean get(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return mNext.get(index - BITS_PER_WORD); + } else { + return (mData & (1L << index)) != 0; + } + } + + void reset() { + mData = 0; + if (mNext != null) { + mNext.reset(); + } + } + + void insert(int index, boolean value) { + if (index >= BITS_PER_WORD) { + ensureNext(); + mNext.insert(index - BITS_PER_WORD, value); + } else { + final boolean lastBit = (mData & LAST_BIT) != 0; + long mask = (1L << index) - 1; + final long before = mData & mask; + final long after = (mData & ~mask) << 1; + mData = before | after; + if (value) { + set(index); + } else { + clear(index); + } + if (lastBit || mNext != null) { + ensureNext(); + mNext.insert(0, lastBit); + } + } + } + + boolean remove(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return mNext.remove(index - BITS_PER_WORD); + } else { + long mask = (1L << index); + final boolean value = (mData & mask) != 0; + mData &= ~mask; + mask = mask - 1; + final long before = mData & mask; + // cannot use >> because it adds one. + final long after = Long.rotateRight(mData & ~mask, 1); + mData = before | after; + if (mNext != null) { + if (mNext.get(0)) { + set(BITS_PER_WORD - 1); + } + mNext.remove(0); + } + return value; + } + } + + int countOnesBefore(int index) { + if (mNext == null) { + if (index >= BITS_PER_WORD) { + return Long.bitCount(mData); + } + return Long.bitCount(mData & ((1L << index) - 1)); + } + if (index < BITS_PER_WORD) { + return Long.bitCount(mData & ((1L << index) - 1)); + } else { + return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); + } + } + + @Override + public String toString() { + return mNext == null ? Long.toBinaryString(mData) + : mNext.toString() + "xx" + Long.toBinaryString(mData); + } + } + + interface Callback { + + int getChildCount(); + + void addView(View child, int index); + + int indexOfChild(View view); + + void removeViewAt(int index); + + View getChildAt(int offset); + + void removeAllViews(); + + RecyclerView.ViewHolder getChildViewHolder(View view); + + void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams); + + void detachViewFromParent(int offset); + + void onEnteredHiddenState(View child); + + void onLeftHiddenState(View child); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java b/app/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java new file mode 100644 index 0000000000..45b719fe71 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ConcatAdapter.java @@ -0,0 +1,481 @@ +/* + * Copyright 2020 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 androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS; + +import android.util.Pair; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * An {@link Adapter} implementation that presents the contents of multiple adapters in sequence. + * + *

+ * MyAdapter adapter1 = ...;
+ * AnotherAdapter adapter2 = ...;
+ * ConcatAdapter concatenated = new ConcatAdapter(adapter1, adapter2);
+ * recyclerView.setAdapter(concatenated);
+ * 
+ *

+ * By default, {@link ConcatAdapter} isolates view types of nested adapters from each other such + * that + * it will change the view type before reporting it back to the {@link RecyclerView} to avoid any + * conflicts between the view types of added adapters. This also means each added adapter will have + * its own isolated pool of {@link ViewHolder}s, with no re-use in between added adapters. + *

+ * If your {@link Adapter}s share the same view types, and can support sharing {@link ViewHolder} + * s between added adapters, provide an instance of {@link Config} where you set + * {@link Config#isolateViewTypes} to {@code false}. A common usage pattern for this is to return + * the {@code R.layout.} from the {@link Adapter#getItemViewType(int)} method. + *

+ * When an added adapter calls one of the {@code notify} methods, {@link ConcatAdapter} properly + * offsets values before reporting it back to the {@link RecyclerView}. + * If an adapter calls {@link Adapter#notifyDataSetChanged()}, {@link ConcatAdapter} also calls + * {@link Adapter#notifyDataSetChanged()} as calling + * {@link Adapter#notifyItemRangeChanged(int, int)} will confuse the {@link RecyclerView}. + * You are highly encouraged to to use {@link SortedList} or {@link ListAdapter} to avoid + * calling {@link Adapter#notifyDataSetChanged()}. + *

+ * Whether {@link ConcatAdapter} should support stable ids is defined in the {@link Config} + * object. Calling {@link Adapter#setHasStableIds(boolean)} has no effect. See documentation + * for {@link Config.StableIdMode} for details on how to configure {@link ConcatAdapter} to use + * stable ids. By default, it will not use stable ids and sub adapter stable ids will be ignored. + * Similar to the case above, you are highly encouraged to use {@link ListAdapter}, which will + * automatically calculate the changes in the data set for you so you won't need stable ids. + *

+ * It is common to find the adapter position of a {@link ViewHolder} to handle user action on the + * {@link ViewHolder}. For those cases, instead of calling {@link ViewHolder#getAdapterPosition()}, + * use {@link ViewHolder#getBindingAdapterPosition()}. If your adapters share {@link ViewHolder}s, + * you can use the {@link ViewHolder#getBindingAdapter()} method to find the adapter which last + * bound that {@link ViewHolder}. + */ +@SuppressWarnings("unchecked") +public final class ConcatAdapter extends Adapter { + static final String TAG = "ConcatAdapter"; + /** + * Bulk of the logic is in the controller to keep this class isolated to the public API. + */ + private final ConcatAdapterController mController; + + /** + * Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given + * order. + * + * @param adapters The list of adapters to add + */ + @SafeVarargs + public ConcatAdapter(@NonNull Adapter... adapters) { + this(Config.DEFAULT, adapters); + } + + /** + * Creates a ConcatAdapter with the given config and the given adapters in the given order. + * + * @param config The configuration for this ConcatAdapter + * @param adapters The list of adapters to add + * @see Config.Builder + */ + @SafeVarargs + public ConcatAdapter( + @NonNull Config config, + @NonNull Adapter... adapters) { + this(config, Arrays.asList(adapters)); + } + + /** + * Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given + * order. + * + * @param adapters The list of adapters to add + */ + public ConcatAdapter(@NonNull List> adapters) { + this(Config.DEFAULT, adapters); + } + + /** + * Creates a ConcatAdapter with the given config and the given adapters in the given order. + * + * @param config The configuration for this ConcatAdapter + * @param adapters The list of adapters to add + * @see Config.Builder + */ + public ConcatAdapter( + @NonNull Config config, + @NonNull List> adapters) { + mController = new ConcatAdapterController(this, config); + for (Adapter adapter : adapters) { + addAdapter(adapter); + } + // go through super as we override it to be no-op + super.setHasStableIds(mController.hasStableIds()); + } + + /** + * Appends the given adapter to the existing list of adapters and notifies the observers of + * this {@link ConcatAdapter}. + * + * @param adapter The new adapter to add + * @return {@code true} if the adapter is successfully added because it did not already exist, + * {@code false} otherwise. + * @see #addAdapter(int, Adapter) + * @see #removeAdapter(Adapter) + */ + public boolean addAdapter(@NonNull Adapter adapter) { + return mController.addAdapter((Adapter) adapter); + } + + /** + * Adds the given adapter to the given index among other adapters that are already added. + * + * @param index The index into which to insert the adapter. ConcatAdapter will throw an + * {@link IndexOutOfBoundsException} if the index is not between 0 and current + * adapter count (inclusive). + * @param adapter The new adapter to add to the adapters list. + * @return {@code true} if the adapter is successfully added because it did not already exist, + * {@code false} otherwise. + * @see #addAdapter(Adapter) + * @see #removeAdapter(Adapter) + */ + public boolean addAdapter(int index, @NonNull Adapter adapter) { + return mController.addAdapter(index, (Adapter) adapter); + } + + /** + * Removes the given adapter from the adapters list if it exists + * + * @param adapter The adapter to remove + * @return {@code true} if the adapter was previously added to this {@code ConcatAdapter} and + * now removed or {@code false} if it couldn't be found. + */ + public boolean removeAdapter(@NonNull Adapter adapter) { + return mController.removeAdapter((Adapter) adapter); + } + + @Override + public int getItemViewType(int position) { + return mController.getItemViewType(position); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return mController.onCreateViewHolder(parent, viewType); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + mController.onBindViewHolder(holder, position); + } + + /** + * Calling this method is an error and will result in an {@link UnsupportedOperationException}. + * You should use the {@link Config} object passed into the ConcatAdapter to configure this + * behavior. + * + * @param hasStableIds Whether items in data set have unique identifiers or not. + */ + @Override + public void setHasStableIds(boolean hasStableIds) { + throw new UnsupportedOperationException( + "Calling setHasStableIds is not allowed on the ConcatAdapter. " + + "Use the Config object passed in the constructor to control this " + + "behavior"); + } + + /** + * Calling this method is an error and will result in an {@link UnsupportedOperationException}. + * + * ConcatAdapter infers this value from added {@link Adapter}s. + * + * @param strategy The saved state restoration strategy for this Adapter such that + * {@link ConcatAdapter} will allow state restoration only if all added + * adapters allow it or + * there are no adapters. + */ + @Override + public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) { + // do nothing + throw new UnsupportedOperationException( + "Calling setStateRestorationPolicy is not allowed on the ConcatAdapter." + + " This value is inferred from added adapters"); + } + + @Override + public long getItemId(int position) { + return mController.getItemId(position); + } + + /** + * Internal method called by the ConcatAdapterController. + */ + void internalSetStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) { + super.setStateRestorationPolicy(strategy); + } + + @Override + public int getItemCount() { + return mController.getTotalCount(); + } + + @Override + public boolean onFailedToRecycleView(@NonNull ViewHolder holder) { + return mController.onFailedToRecycleView(holder); + } + + @Override + public void onViewAttachedToWindow(@NonNull ViewHolder holder) { + mController.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(@NonNull ViewHolder holder) { + mController.onViewDetachedFromWindow(holder); + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + mController.onViewRecycled(holder); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + mController.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mController.onDetachedFromRecyclerView(recyclerView); + } + + /** + * Returns an unmodifiable copy of the list of adapters in this {@link ConcatAdapter}. + * Note that this is a copy hence future changes in the ConcatAdapter are not reflected in + * this list. + * + * @return A copy of the list of adapters in this ConcatAdapter. + */ + @NonNull + public List> getAdapters() { + return Collections.unmodifiableList(mController.getCopyOfAdapters()); + } + + /** + * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. + * + * If the given {@link Adapter} is not part of this {@link ConcatAdapter}, + * {@link RecyclerView#NO_POSITION} is returned. + * + * @param adapter The adapter which is a sub adapter of this ConcatAdapter or itself. + * @param viewHolder The view holder whose local position in the given adapter will be + * returned. + * @param localPosition The position of the given {@link ViewHolder} in this {@link Adapter}. + * @return The local position of the given {@link ViewHolder} in the given {@link Adapter} or + * {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item or the + * given {@link Adapter} is not part of this ConcatAdapter. + */ + @Override + public int findRelativeAdapterPositionIn( + @NonNull Adapter adapter, + @NonNull ViewHolder viewHolder, + int localPosition) { + return mController.getLocalAdapterPosition(adapter, viewHolder, localPosition); + } + + + /** + * Retrieve the adapter and local position for a given position in this {@code ConcatAdapter}. + * + * This allows for retrieving wrapped adapter information in situations where you don't have a + * {@link ViewHolder}, such as within a + * {@link androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup} in which you want to + * look up information from the source adapter. + * + * @param globalPosition The position in this {@code ConcatAdapter}. + * @return a Pair with the first element set to the wrapped {@code Adapter} containing that + * position and the second element set to the local position in the wrapped adapter + * @throws IllegalArgumentException if the specified {@code globalPosition} does not + * correspond to a valid element of this adapter. That is, if {@code globalPosition} is less + * than 0 or greater than the total number of items in the {@code ConcatAdapter} + */ + @NonNull + public Pair, Integer> getWrappedAdapterAndPosition(int + globalPosition) { + return mController.getWrappedAdapterAndPosition(globalPosition); + } + + /** + * The configuration object for a {@link ConcatAdapter}. + */ + public static final class Config { + /** + * If {@code false}, {@link ConcatAdapter} assumes all assigned adapters share a global + * view type pool such that they use the same view types to refer to the same + * {@link ViewHolder}s. + *

+ * Setting this to {@code false} will allow nested adapters to share {@link ViewHolder}s but + * it also means these adapters should not have conflicting view types + * ({@link Adapter#getItemViewType(int)}) such that two different adapters return the same + * view type for different {@link ViewHolder}s. + * + * By default, it is set to {@code true} which means {@link ConcatAdapter} will isolate + * view types across adapters, preventing them from using the same {@link ViewHolder}s. + */ + public final boolean isolateViewTypes; + + /** + * Defines whether the {@link ConcatAdapter} should support stable ids or not + * ({@link Adapter#hasStableIds()}. + *

+ * There are 3 possible options: + * + * {@link StableIdMode#NO_STABLE_IDS}: In this mode, {@link ConcatAdapter} ignores the + * stable + * ids reported by sub adapters. This is the default mode. + * + * {@link StableIdMode#ISOLATED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return + * {@code true} from {@link ConcatAdapter#hasStableIds()} and will require all added + * {@link Adapter}s to have stable ids. As two different adapters may return same stable ids + * because they are unaware of each-other, {@link ConcatAdapter} will isolate each + * {@link Adapter}'s id pool from each other such that it will overwrite the reported stable + * id before reporting back to the {@link RecyclerView}. In this mode, the value returned + * from {@link ViewHolder#getItemId()} might differ from the value returned from + * {@link Adapter#getItemId(int)}. + * + * {@link StableIdMode#SHARED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return + * {@code true} from {@link ConcatAdapter#hasStableIds()} and will require all added + * {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS}, + * {@link ConcatAdapter} will not override the returned item ids. In this mode, + * child {@link Adapter}s must be aware of each-other and never return the same id unless + * an item is moved between {@link Adapter}s. + * + * Default value is {@link StableIdMode#NO_STABLE_IDS}. + */ + @NonNull + public final StableIdMode stableIdMode; + + + /** + * Default configuration for {@link ConcatAdapter} where {@link Config#isolateViewTypes} + * is set to {@code true} and {@link Config#stableIdMode} is set to + * {@link StableIdMode#NO_STABLE_IDS}. + */ + @NonNull + public static final Config DEFAULT = new Config(true, NO_STABLE_IDS); + + Config(boolean isolateViewTypes, @NonNull StableIdMode stableIdMode) { + this.isolateViewTypes = isolateViewTypes; + this.stableIdMode = stableIdMode; + } + + /** + * Defines how {@link ConcatAdapter} handle stable ids ({@link Adapter#hasStableIds()}). + */ + public enum StableIdMode { + /** + * In this mode, {@link ConcatAdapter} ignores the stable + * ids reported by sub adapters. This is the default mode. + * Adding an {@link Adapter} with stable ids will result in a warning as it will be + * ignored. + */ + NO_STABLE_IDS, + /** + * In this mode, {@link ConcatAdapter} will return {@code true} from + * {@link ConcatAdapter#hasStableIds()} and will require all added + * {@link Adapter}s to have stable ids. As two different adapters may return + * same stable ids because they are unaware of each-other, {@link ConcatAdapter} will + * isolate each {@link Adapter}'s id pool from each other such that it will overwrite + * the reported stable id before reporting back to the {@link RecyclerView}. In this + * mode, the value returned from {@link ViewHolder#getItemId()} might differ from the + * value returned from {@link Adapter#getItemId(int)}. + * + * Adding an adapter without stable ids will result in an + * {@link IllegalArgumentException}. + */ + ISOLATED_STABLE_IDS, + /** + * In this mode, {@link ConcatAdapter} will return {@code true} from + * {@link ConcatAdapter#hasStableIds()} and will require all added + * {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS}, + * {@link ConcatAdapter} will not override the returned item ids. In this mode, + * child {@link Adapter}s must be aware of each-other and never return the same id + * unless and item is moved between {@link Adapter}s. + * Adding an adapter without stable ids will result in an + * {@link IllegalArgumentException}. + */ + SHARED_STABLE_IDS + } + + /** + * The builder for {@link Config} class. + */ + public static final class Builder { + private boolean mIsolateViewTypes = DEFAULT.isolateViewTypes; + private StableIdMode mStableIdMode = DEFAULT.stableIdMode; + + /** + * Sets whether {@link ConcatAdapter} should isolate view types of nested adapters from + * each other. + * + * @param isolateViewTypes {@code true} if {@link ConcatAdapter} should override view + * types of nested adapters to avoid view type + * conflicts, {@code false} otherwise. + * Defaults to {@link Config#DEFAULT}'s + * {@link Config#isolateViewTypes} value ({@code true}). + * @return this + * @see Config#isolateViewTypes + */ + @NonNull + public Builder setIsolateViewTypes(boolean isolateViewTypes) { + mIsolateViewTypes = isolateViewTypes; + return this; + } + + /** + * Sets how the {@link ConcatAdapter} should handle stable ids + * ({@link Adapter#hasStableIds()}). See documentation in {@link Config#stableIdMode} + * for details. + * + * @param stableIdMode The stable id mode for the {@link ConcatAdapter}. Defaults to + * {@link Config#DEFAULT}'s {@link Config#stableIdMode} value + * ({@link StableIdMode#NO_STABLE_IDS}). + * @return this + * @see Config#stableIdMode + */ + @NonNull + public Builder setStableIdMode(@NonNull StableIdMode stableIdMode) { + mStableIdMode = stableIdMode; + return this; + } + + /** + * @return A new instance of {@link Config} with the given parameters. + */ + @NonNull + public Config build() { + return new Config(mIsolateViewTypes, mStableIdMode); + } + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java b/app/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java new file mode 100644 index 0000000000..d31540c649 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ConcatAdapterController.java @@ -0,0 +1,528 @@ +/* + * Copyright 2020 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 androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS; +import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS; +import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS; +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW; +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT; +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY; +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + +import android.util.Log; +import android.util.Pair; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Preconditions; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; + +/** + * All logic for the {@link ConcatAdapter} is here so that we can clearly see a separation + * between an adapter implementation and merging logic. + */ +class ConcatAdapterController implements NestedAdapterWrapper.Callback { + private final ConcatAdapter mConcatAdapter; + + /** + * Holds the mapping from the view type to the adapter which reported that type. + */ + private final ViewTypeStorage mViewTypeStorage; + + /** + * We hold onto the list of attached recyclerviews so that we can dispatch attach/detach to + * any adapter that was added later on. + * Probably does not need to be a weak reference but playing safe here. + */ + private List> mAttachedRecyclerViews = new ArrayList<>(); + + /** + * Keeps the information about which ViewHolder is bound by which adapter. + * It is set in onBind, reset at onRecycle. + */ + private final IdentityHashMap + mBinderLookup = new IdentityHashMap<>(); + + private List mWrappers = new ArrayList<>(); + + // keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯ + private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition(); + + @NonNull + private final ConcatAdapter.Config.StableIdMode mStableIdMode; + + /** + * This is where we keep stable ids, if supported + */ + private final StableIdStorage mStableIdStorage; + + ConcatAdapterController( + ConcatAdapter concatAdapter, + ConcatAdapter.Config config) { + mConcatAdapter = concatAdapter; + + // setup view type handling + if (config.isolateViewTypes) { + mViewTypeStorage = new ViewTypeStorage.IsolatedViewTypeStorage(); + } else { + mViewTypeStorage = new ViewTypeStorage.SharedIdRangeViewTypeStorage(); + } + + // setup stable id handling + mStableIdMode = config.stableIdMode; + if (config.stableIdMode == NO_STABLE_IDS) { + mStableIdStorage = new StableIdStorage.NoStableIdStorage(); + } else if (config.stableIdMode == ISOLATED_STABLE_IDS) { + mStableIdStorage = new StableIdStorage.IsolatedStableIdStorage(); + } else if (config.stableIdMode == SHARED_STABLE_IDS) { + mStableIdStorage = new StableIdStorage.SharedPoolStableIdStorage(); + } else { + throw new IllegalArgumentException("unknown stable id mode"); + } + } + + @Nullable + private NestedAdapterWrapper findWrapperFor(Adapter adapter) { + final int index = indexOfWrapper(adapter); + if (index == -1) { + return null; + } + return mWrappers.get(index); + } + + private int indexOfWrapper(Adapter adapter) { + final int limit = mWrappers.size(); + for (int i = 0; i < limit; i++) { + if (mWrappers.get(i).adapter == adapter) { + return i; + } + } + return -1; + } + + /** + * return true if added, false otherwise. + * + * @see ConcatAdapter#addAdapter(Adapter) + */ + boolean addAdapter(Adapter adapter) { + return addAdapter(mWrappers.size(), adapter); + } + + /** + * return true if added, false otherwise. + * throws exception if index is out of bounds + * + * @see ConcatAdapter#addAdapter(int, Adapter) + */ + boolean addAdapter(int index, Adapter adapter) { + if (index < 0 || index > mWrappers.size()) { + throw new IndexOutOfBoundsException("Index must be between 0 and " + + mWrappers.size() + ". Given:" + index); + } + if (hasStableIds()) { + Preconditions.checkArgument(adapter.hasStableIds(), + "All sub adapters must have stable ids when stable id mode " + + "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS"); + } else { + if (adapter.hasStableIds()) { + Log.w(ConcatAdapter.TAG, "Stable ids in the adapter will be ignored as the" + + " ConcatAdapter is configured not to have stable ids"); + } + } + NestedAdapterWrapper existing = findWrapperFor(adapter); + if (existing != null) { + return false; + } + NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this, + mViewTypeStorage, mStableIdStorage.createStableIdLookup()); + mWrappers.add(index, wrapper); + // notify attach for all recyclerview + for (WeakReference reference : mAttachedRecyclerViews) { + RecyclerView recyclerView = reference.get(); + if (recyclerView != null) { + adapter.onAttachedToRecyclerView(recyclerView); + } + } + // new items, notify add for them + if (wrapper.getCachedItemCount() > 0) { + mConcatAdapter.notifyItemRangeInserted( + countItemsBefore(wrapper), + wrapper.getCachedItemCount() + ); + } + // reset state restoration strategy + calculateAndUpdateStateRestorationPolicy(); + return true; + } + + boolean removeAdapter(Adapter adapter) { + final int index = indexOfWrapper(adapter); + if (index == -1) { + return false; + } + NestedAdapterWrapper wrapper = mWrappers.get(index); + int offset = countItemsBefore(wrapper); + mWrappers.remove(index); + mConcatAdapter.notifyItemRangeRemoved(offset, wrapper.getCachedItemCount()); + // notify detach for all recyclerviews + for (WeakReference reference : mAttachedRecyclerViews) { + RecyclerView recyclerView = reference.get(); + if (recyclerView != null) { + adapter.onDetachedFromRecyclerView(recyclerView); + } + } + wrapper.dispose(); + calculateAndUpdateStateRestorationPolicy(); + return true; + } + + private int countItemsBefore(NestedAdapterWrapper wrapper) { + int count = 0; + for (NestedAdapterWrapper item : mWrappers) { + if (item != wrapper) { + count += item.getCachedItemCount(); + } else { + break; + } + } + return count; + } + + public long getItemId(int globalPosition) { + WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); + long globalItemId = wrapperAndPos.mWrapper.getItemId(wrapperAndPos.mLocalPosition); + releaseWrapperAndLocalPosition(wrapperAndPos); + return globalItemId; + } + + @Override + public void onChanged(@NonNull NestedAdapterWrapper wrapper) { + // TODO should we notify more cleverly, maybe in v2 + mConcatAdapter.notifyDataSetChanged(); + calculateAndUpdateStateRestorationPolicy(); + } + + @Override + public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mConcatAdapter.notifyItemRangeChanged( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount, @Nullable Object payload) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mConcatAdapter.notifyItemRangeChanged( + positionStart + offset, + itemCount, + payload + ); + } + + @Override + public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + final int offset = countItemsBefore(nestedAdapterWrapper); + mConcatAdapter.notifyItemRangeInserted( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, int itemCount) { + int offset = countItemsBefore(nestedAdapterWrapper); + mConcatAdapter.notifyItemRangeRemoved( + positionStart + offset, + itemCount + ); + } + + @Override + public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper, + int fromPosition, int toPosition) { + int offset = countItemsBefore(nestedAdapterWrapper); + mConcatAdapter.notifyItemMoved( + fromPosition + offset, + toPosition + offset + ); + } + + @Override + public void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper) { + calculateAndUpdateStateRestorationPolicy(); + } + + private void calculateAndUpdateStateRestorationPolicy() { + StateRestorationPolicy newPolicy = computeStateRestorationPolicy(); + if (newPolicy != mConcatAdapter.getStateRestorationPolicy()) { + mConcatAdapter.internalSetStateRestorationPolicy(newPolicy); + } + } + + private StateRestorationPolicy computeStateRestorationPolicy() { + for (NestedAdapterWrapper wrapper : mWrappers) { + StateRestorationPolicy strategy = + wrapper.adapter.getStateRestorationPolicy(); + if (strategy == PREVENT) { + // one adapter can block all + return PREVENT; + } else if (strategy == PREVENT_WHEN_EMPTY && wrapper.getCachedItemCount() == 0) { + // an adapter wants to allow w/ size but we need to make sure there is no prevent + return PREVENT; + } + } + return ALLOW; + } + + public int getTotalCount() { + // should we cache this as well ? + int total = 0; + for (NestedAdapterWrapper wrapper : mWrappers) { + total += wrapper.getCachedItemCount(); + } + return total; + } + + public int getItemViewType(int globalPosition) { + WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); + int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition); + releaseWrapperAndLocalPosition(wrapperAndPos); + return itemViewType; + } + + public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) { + NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType); + return wrapper.onCreateViewHolder(parent, globalViewType); + } + + public Pair, Integer> getWrappedAdapterAndPosition( + int globalPosition) { + WrapperAndLocalPosition wrapper = findWrapperAndLocalPosition(globalPosition); + Pair, Integer> pair = new Pair<>(wrapper.mWrapper.adapter, + wrapper.mLocalPosition); + releaseWrapperAndLocalPosition(wrapper); + return pair; + } + + /** + * Always call {@link #releaseWrapperAndLocalPosition(WrapperAndLocalPosition)} when you are + * done with it + */ + @NonNull + private WrapperAndLocalPosition findWrapperAndLocalPosition( + int globalPosition + ) { + WrapperAndLocalPosition result; + if (mReusableHolder.mInUse) { + result = new WrapperAndLocalPosition(); + } else { + mReusableHolder.mInUse = true; + result = mReusableHolder; + } + int localPosition = globalPosition; + for (NestedAdapterWrapper wrapper : mWrappers) { + if (wrapper.getCachedItemCount() > localPosition) { + result.mWrapper = wrapper; + result.mLocalPosition = localPosition; + break; + } + localPosition -= wrapper.getCachedItemCount(); + } + if (result.mWrapper == null) { + throw new IllegalArgumentException("Cannot find wrapper for " + globalPosition); + } + return result; + } + + private void releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition) { + wrapperAndLocalPosition.mInUse = false; + wrapperAndLocalPosition.mWrapper = null; + wrapperAndLocalPosition.mLocalPosition = -1; + mReusableHolder = wrapperAndLocalPosition; + } + + public void onBindViewHolder(ViewHolder holder, int globalPosition) { + WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition); + mBinderLookup.put(holder, wrapperAndPos.mWrapper); + wrapperAndPos.mWrapper.onBindViewHolder(holder, wrapperAndPos.mLocalPosition); + releaseWrapperAndLocalPosition(wrapperAndPos); + } + + public boolean canRestoreState() { + for (NestedAdapterWrapper wrapper : mWrappers) { + if (!wrapper.adapter.canRestoreState()) { + return false; + } + } + return true; + } + + public void onViewAttachedToWindow(ViewHolder holder) { + NestedAdapterWrapper wrapper = getWrapper(holder); + wrapper.adapter.onViewAttachedToWindow(holder); + } + + public void onViewDetachedFromWindow(ViewHolder holder) { + NestedAdapterWrapper wrapper = getWrapper(holder); + wrapper.adapter.onViewDetachedFromWindow(holder); + } + + public void onViewRecycled(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + wrapper.adapter.onViewRecycled(holder); + mBinderLookup.remove(holder); + } + + public boolean onFailedToRecycleView(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + final boolean result = wrapper.adapter.onFailedToRecycleView(holder); + mBinderLookup.remove(holder); + return result; + } + + @NonNull + private NestedAdapterWrapper getWrapper(ViewHolder holder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(holder); + if (wrapper == null) { + throw new IllegalStateException("Cannot find wrapper for " + holder + + ", seems like it is not bound by this adapter: " + this); + } + return wrapper; + } + + private boolean isAttachedTo(RecyclerView recyclerView) { + for (WeakReference reference : mAttachedRecyclerViews) { + if (reference.get() == recyclerView) { + return true; + } + } + return false; + } + + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + if (isAttachedTo(recyclerView)) { + return; + } + mAttachedRecyclerViews.add(new WeakReference<>(recyclerView)); + for (NestedAdapterWrapper wrapper : mWrappers) { + wrapper.adapter.onAttachedToRecyclerView(recyclerView); + } + } + + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + for (int i = mAttachedRecyclerViews.size() - 1; i >= 0; i--) { + WeakReference reference = mAttachedRecyclerViews.get(i); + if (reference.get() == null) { + mAttachedRecyclerViews.remove(i); + } else if (reference.get() == recyclerView) { + mAttachedRecyclerViews.remove(i); + break; // here we can break as we don't keep duplicates + } + } + for (NestedAdapterWrapper wrapper : mWrappers) { + wrapper.adapter.onDetachedFromRecyclerView(recyclerView); + } + } + + public int getLocalAdapterPosition( + Adapter adapter, + ViewHolder viewHolder, + int globalPosition + ) { + NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); + if (wrapper == null) { + return NO_POSITION; + } + int itemsBefore = countItemsBefore(wrapper); + // local position is globalPosition - itemsBefore + int localPosition = globalPosition - itemsBefore; + // Early error detection: + int itemCount = wrapper.adapter.getItemCount(); + if (localPosition < 0 || localPosition >= itemCount) { + throw new IllegalStateException("Detected inconsistent adapter updates. The" + + " local position of the view holder maps to " + localPosition + " which" + + " is out of bounds for the adapter with size " + + itemCount + "." + + "Make sure to immediately call notify methods in your adapter when you " + + "change the backing data" + + "viewHolder:" + viewHolder + + "adapter:" + adapter); + } + return wrapper.adapter.findRelativeAdapterPositionIn(adapter, viewHolder, localPosition); + } + + + @Nullable + public Adapter getBoundAdapter(ViewHolder viewHolder) { + NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder); + if (wrapper == null) { + return null; + } + return wrapper.adapter; + } + + @SuppressWarnings("MixedMutabilityReturnType") + public List> getCopyOfAdapters() { + if (mWrappers.isEmpty()) { + return Collections.emptyList(); + } + List> adapters = new ArrayList<>(mWrappers.size()); + for (NestedAdapterWrapper wrapper : mWrappers) { + adapters.add(wrapper.adapter); + } + return adapters; + } + + public boolean hasStableIds() { + return mStableIdMode != NO_STABLE_IDS; + } + + /** + * Helper class to hold onto wrapper and local position without allocating objects as this is + * a very common call. + */ + static class WrapperAndLocalPosition { + NestedAdapterWrapper mWrapper; + int mLocalPosition; + boolean mInUse; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java b/app/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java new file mode 100644 index 0000000000..a520aa98d9 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/DefaultItemAnimator.java @@ -0,0 +1,674 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; + +import java.util.ArrayList; +import java.util.List; + +/** + * This implementation of {@link RecyclerView.ItemAnimator} provides basic + * animations on remove, add, and move events that happen to the items in + * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default. + * + * @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator) + */ +public class DefaultItemAnimator extends SimpleItemAnimator { + private static final boolean DEBUG = false; + + private static TimeInterpolator sDefaultInterpolator; + + private ArrayList mPendingRemovals = new ArrayList<>(); + private ArrayList mPendingAdditions = new ArrayList<>(); + private ArrayList mPendingMoves = new ArrayList<>(); + private ArrayList mPendingChanges = new ArrayList<>(); + + ArrayList> mAdditionsList = new ArrayList<>(); + ArrayList> mMovesList = new ArrayList<>(); + ArrayList> mChangesList = new ArrayList<>(); + + ArrayList mAddAnimations = new ArrayList<>(); + ArrayList mMoveAnimations = new ArrayList<>(); + ArrayList mRemoveAnimations = new ArrayList<>(); + ArrayList mChangeAnimations = new ArrayList<>(); + + private static class MoveInfo { + public RecyclerView.ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + this.holder = holder; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + } + + private static class ChangeInfo { + public RecyclerView.ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + this(oldHolder, newHolder); + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public String toString() { + return "ChangeInfo{" + + "oldHolder=" + oldHolder + + ", newHolder=" + newHolder + + ", fromX=" + fromX + + ", fromY=" + fromY + + ", toX=" + toX + + ", toY=" + toY + + '}'; + } + } + + @Override + public void runPendingAnimations() { + boolean removalsPending = !mPendingRemovals.isEmpty(); + boolean movesPending = !mPendingMoves.isEmpty(); + boolean changesPending = !mPendingChanges.isEmpty(); + boolean additionsPending = !mPendingAdditions.isEmpty(); + if (!removalsPending && !movesPending && !additionsPending && !changesPending) { + // nothing to animate + return; + } + // First, remove stuff + for (RecyclerView.ViewHolder holder : mPendingRemovals) { + animateRemoveImpl(holder); + } + mPendingRemovals.clear(); + // Next, move stuff + if (movesPending) { + final ArrayList moves = new ArrayList<>(); + moves.addAll(mPendingMoves); + mMovesList.add(moves); + mPendingMoves.clear(); + Runnable mover = new Runnable() { + @Override + public void run() { + for (MoveInfo moveInfo : moves) { + animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, + moveInfo.toX, moveInfo.toY); + } + moves.clear(); + mMovesList.remove(moves); + } + }; + if (removalsPending) { + View view = moves.get(0).holder.itemView; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList<>(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + RecyclerView.ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList<>(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + @Override + public void run() { + for (RecyclerView.ViewHolder holder : additions) { + animateAddImpl(holder); + } + additions.clear(); + mAdditionsList.remove(additions); + } + }; + if (removalsPending || movesPending || changesPending) { + long removeDuration = removalsPending ? getRemoveDuration() : 0; + long moveDuration = movesPending ? getMoveDuration() : 0; + long changeDuration = changesPending ? getChangeDuration() : 0; + long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); + View view = additions.get(0).itemView; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public boolean animateRemove(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()).alpha(0).setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + view.setAlpha(1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public boolean animateAdd(final RecyclerView.ViewHolder holder) { + resetAnimation(holder); + holder.itemView.setAlpha(0); + mPendingAdditions.add(holder); + return true; + } + + void animateAddImpl(final RecyclerView.ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimator animation = view.animate(); + mAddAnimations.add(holder); + animation.alpha(1).setDuration(getAddDuration()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchAddStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + view.setAlpha(1); + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchAddFinished(holder); + mAddAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += (int) holder.itemView.getTranslationX(); + fromY += (int) holder.itemView.getTranslationY(); + resetAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + view.setTranslationX(-deltaX); + } + if (deltaY != 0) { + view.setTranslationY(-deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { + final View view = holder.itemView; + final int deltaX = toX - fromX; + final int deltaY = toY - fromY; + if (deltaX != 0) { + view.animate().translationX(0); + } + if (deltaY != 0) { + view.animate().translationY(0); + } + // TODO: make EndActions end listeners instead, since end actions aren't called when + // vpas are canceled (and can't end them. why?) + // need listener functionality in VPACompat for this. Ick. + final ViewPropertyAnimator animation = view.animate(); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchMoveStarting(holder); + } + + @Override + public void onAnimationCancel(Animator animator) { + if (deltaX != 0) { + view.setTranslationX(0); + } + if (deltaY != 0) { + view.setTranslationY(0); + } + } + + @Override + public void onAnimationEnd(Animator animator) { + animation.setListener(null); + dispatchMoveFinished(holder); + mMoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public boolean animateChange(RecyclerView.ViewHolder oldHolder, + RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) { + if (oldHolder == newHolder) { + // Don't know how to run change animations when the same view holder is re-used. + // run a move animation to handle position changes. + return animateMove(oldHolder, fromLeft, fromTop, toLeft, toTop); + } + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toLeft - fromLeft - prevTranslationX); + int deltaY = (int) (toTop - fromTop - prevTranslationY); + // recover prev translation state after ending animation + oldHolder.itemView.setTranslationX(prevTranslationX); + oldHolder.itemView.setTranslationY(prevTranslationY); + oldHolder.itemView.setAlpha(prevAlpha); + if (newHolder != null) { + // carry over translation values + resetAnimation(newHolder); + newHolder.itemView.setTranslationX(-deltaX); + newHolder.itemView.setTranslationY(-deltaY); + newHolder.itemView.setAlpha(0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop)); + return true; + } + + void animateChangeImpl(final ChangeInfo changeInfo) { + final RecyclerView.ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; + final View newView = newHolder != null ? newHolder.itemView : null; + if (view != null) { + final ViewPropertyAnimator oldViewAnim = view.animate().setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(Animator animator) { + oldViewAnim.setListener(null); + view.setAlpha(1); + view.setTranslationX(0); + view.setTranslationY(0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimator newViewAnimation = newView.animate(); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()) + .alpha(1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animator) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + @Override + public void onAnimationEnd(Animator animator) { + newViewAnimation.setListener(null); + newView.setAlpha(1); + newView.setTranslationX(0); + newView.setTranslationY(0); + dispatchChangeFinished(changeInfo.newHolder, false); + mChangeAnimations.remove(changeInfo.newHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + } + + private void endChangeAnimation(List infoList, RecyclerView.ViewHolder item) { + for (int i = infoList.size() - 1; i >= 0; i--) { + ChangeInfo changeInfo = infoList.get(i); + if (endChangeAnimationIfNecessary(changeInfo, item)) { + if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { + infoList.remove(changeInfo); + } + } + } + } + + private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { + if (changeInfo.oldHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); + } + if (changeInfo.newHolder != null) { + endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); + } + } + private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { + boolean oldItem = false; + if (changeInfo.newHolder == item) { + changeInfo.newHolder = null; + } else if (changeInfo.oldHolder == item) { + changeInfo.oldHolder = null; + oldItem = true; + } else { + return false; + } + item.itemView.setAlpha(1); + item.itemView.setTranslationX(0); + item.itemView.setTranslationY(0); + dispatchChangeFinished(item, oldItem); + return true; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void endAnimation(RecyclerView.ViewHolder item) { + final View view = item.itemView; + // this will trigger end callback which should set properties to their target values. + view.animate().cancel(); + // TODO if some other animations are chained to end, how do we cancel them as well? + for (int i = mPendingMoves.size() - 1; i >= 0; i--) { + MoveInfo moveInfo = mPendingMoves.get(i); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + view.setAlpha(1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + } + + for (int i = mChangesList.size() - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + endChangeAnimation(changes, item); + if (changes.isEmpty()) { + mChangesList.remove(i); + } + } + for (int i = mMovesList.size() - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + for (int j = moves.size() - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + if (moveInfo.holder == item) { + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(i); + } + break; + } + } + } + for (int i = mAdditionsList.size() - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + if (additions.remove(item)) { + view.setAlpha(1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + //noinspection PointlessBooleanExpression,ConstantConditions + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + //noinspection PointlessBooleanExpression,ConstantConditions + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + private void resetAnimation(RecyclerView.ViewHolder holder) { + if (sDefaultInterpolator == null) { + sDefaultInterpolator = new ValueAnimator().getInterpolator(); + } + holder.itemView.animate().setInterpolator(sDefaultInterpolator); + endAnimation(holder); + } + + @Override + public boolean isRunning() { + return (!mPendingAdditions.isEmpty() + || !mPendingChanges.isEmpty() + || !mPendingMoves.isEmpty() + || !mPendingRemovals.isEmpty() + || !mMoveAnimations.isEmpty() + || !mRemoveAnimations.isEmpty() + || !mAddAnimations.isEmpty() + || !mChangeAnimations.isEmpty() + || !mMovesList.isEmpty() + || !mAdditionsList.isEmpty() + || !mChangesList.isEmpty()); + } + + /** + * Check the state of currently pending and running animations. If there are none + * pending/running, call {@link #dispatchAnimationsFinished()} to notify any + * listeners. + */ + void dispatchFinishedWhenDone() { + if (!isRunning()) { + dispatchAnimationsFinished(); + } + } + + @Override + public void endAnimations() { + int count = mPendingMoves.size(); + for (int i = count - 1; i >= 0; i--) { + MoveInfo item = mPendingMoves.get(i); + View view = item.holder.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(item.holder); + mPendingMoves.remove(i); + } + count = mPendingRemovals.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + RecyclerView.ViewHolder item = mPendingAdditions.get(i); + item.itemView.setAlpha(1); + dispatchAddFinished(item); + mPendingAdditions.remove(i); + } + count = mPendingChanges.size(); + for (int i = count - 1; i >= 0; i--) { + endChangeAnimationIfNecessary(mPendingChanges.get(i)); + } + mPendingChanges.clear(); + if (!isRunning()) { + return; + } + + int listCount = mMovesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList moves = mMovesList.get(i); + count = moves.size(); + for (int j = count - 1; j >= 0; j--) { + MoveInfo moveInfo = moves.get(j); + RecyclerView.ViewHolder item = moveInfo.holder; + View view = item.itemView; + view.setTranslationY(0); + view.setTranslationX(0); + dispatchMoveFinished(moveInfo.holder); + moves.remove(j); + if (moves.isEmpty()) { + mMovesList.remove(moves); + } + } + } + listCount = mAdditionsList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList additions = mAdditionsList.get(i); + count = additions.size(); + for (int j = count - 1; j >= 0; j--) { + RecyclerView.ViewHolder item = additions.get(j); + View view = item.itemView; + view.setAlpha(1); + dispatchAddFinished(item); + additions.remove(j); + if (additions.isEmpty()) { + mAdditionsList.remove(additions); + } + } + } + listCount = mChangesList.size(); + for (int i = listCount - 1; i >= 0; i--) { + ArrayList changes = mChangesList.get(i); + count = changes.size(); + for (int j = count - 1; j >= 0; j--) { + endChangeAnimationIfNecessary(changes.get(j)); + if (changes.isEmpty()) { + mChangesList.remove(changes); + } + } + } + + cancelAll(mRemoveAnimations); + cancelAll(mMoveAnimations); + cancelAll(mAddAnimations); + cancelAll(mChangeAnimations); + + dispatchAnimationsFinished(); + } + + void cancelAll(List viewHolders) { + for (int i = viewHolders.size() - 1; i >= 0; i--) { + viewHolders.get(i).itemView.animate().cancel(); + } + } + + /** + * {@inheritDoc} + *

+ * If the payload list is not empty, DefaultItemAnimator returns true. + * When this is the case: + *

    + *
  • If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both + * ViewHolder arguments will be the same instance. + *
  • + *
  • + * If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, + * then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and + * run a move animation instead. + *
  • + *
+ */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, + @NonNull List payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/DiffUtil.java b/app/src/main/java/androidx/recyclerview/widget/DiffUtil.java new file mode 100644 index 0000000000..940901e5a4 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/DiffUtil.java @@ -0,0 +1,1058 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * DiffUtil is a utility class that calculates the difference between two lists and outputs a + * list of update operations that converts the first list into the second one. + *

+ * It can be used to calculate updates for a RecyclerView Adapter. See {@link ListAdapter} and + * {@link AsyncListDiffer} which can simplify the use of DiffUtil on a background thread. + *

+ * DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates + * to convert one list into another. Myers's algorithm does not handle items that are moved so + * DiffUtil runs a second pass on the result to detect items that were moved. + *

+ * Note that DiffUtil, {@link ListAdapter}, and {@link AsyncListDiffer} require the list to not + * mutate while in use. + * This generally means that both the lists themselves and their elements (or at least, the + * properties of elements used in diffing) should not be modified directly. Instead, new lists + * should be provided any time content changes. It's common for lists passed to DiffUtil to share + * elements that have not mutated, so it is not strictly required to reload all data to use + * DiffUtil. + *

+ * If the lists are large, this operation may take significant time so you are advised to run this + * on a background thread, get the {@link DiffResult} then apply it on the RecyclerView on the main + * thread. + *

+ * This algorithm is optimized for space and uses O(N) space to find the minimal + * number of addition and removal operations between the two lists. It has O(N + D^2) expected time + * performance where D is the length of the edit script. + *

+ * If move detection is enabled, it takes an additional O(MN) time where M is the total number of + * added items and N is the total number of removed items. If your lists are already sorted by + * the same constraint (e.g. a created timestamp for a list of posts), you can disable move + * detection to improve performance. + *

+ * The actual runtime of the algorithm significantly depends on the number of changes in the list + * and the cost of your comparison methods. Below are some average run times for reference: + * (The test list is composed of random UUID Strings and the tests are run on Nexus 5X with M) + *

    + *
  • 100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms + *
  • 100 items and 100 modifications: 3.82 ms, median: 3.75 ms + *
  • 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms + *
  • 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms + *
  • 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms + *
  • 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms + *
  • 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms + *
+ *

+ * Due to implementation constraints, the max size of the list can be 2^26. + * + * @see ListAdapter + * @see AsyncListDiffer + */ +public class DiffUtil { + private DiffUtil() { + // utility class, no instance. + } + + private static final Comparator DIAGONAL_COMPARATOR = new Comparator() { + @Override + public int compare(Diagonal o1, Diagonal o2) { + return o1.x - o2.x; + } + }; + + // Myers' algorithm uses two lists as axis labels. In DiffUtil's implementation, `x` axis is + // used for old list and `y` axis is used for new list. + + /** + * Calculates the list of update operations that can covert one list into the other one. + * + * @param cb The callback that acts as a gateway to the backing list data + * @return A DiffResult that contains the information about the edit sequence to convert the + * old list into the new list. + */ + @NonNull + public static DiffResult calculateDiff(@NonNull Callback cb) { + return calculateDiff(cb, true); + } + + /** + * Calculates the list of update operations that can covert one list into the other one. + *

+ * If your old and new lists are sorted by the same constraint and items never move (swap + * positions), you can disable move detection which takes O(N^2) time where + * N is the number of added, moved, removed items. + * + * @param cb The callback that acts as a gateway to the backing list data + * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise. + * + * @return A DiffResult that contains the information about the edit sequence to convert the + * old list into the new list. + */ + @NonNull + public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) { + final int oldSize = cb.getOldListSize(); + final int newSize = cb.getNewListSize(); + + final List diagonals = new ArrayList<>(); + + // instead of a recursive implementation, we keep our own stack to avoid potential stack + // overflow exceptions + final List stack = new ArrayList<>(); + + stack.add(new Range(0, oldSize, 0, newSize)); + + final int max = (oldSize + newSize + 1) / 2; + // allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the + // paper for details) + // These arrays lines keep the max reachable position for each k-line. + final CenteredArray forward = new CenteredArray(max * 2 + 1); + final CenteredArray backward = new CenteredArray(max * 2 + 1); + + // We pool the ranges to avoid allocations for each recursive call. + final List rangePool = new ArrayList<>(); + while (!stack.isEmpty()) { + final Range range = stack.remove(stack.size() - 1); + final Snake snake = midPoint(range, cb, forward, backward); + if (snake != null) { + // if it has a diagonal, save it + if (snake.diagonalSize() > 0) { + diagonals.add(snake.toDiagonal()); + } + // add new ranges for left and right + final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove( + rangePool.size() - 1); + left.oldListStart = range.oldListStart; + left.newListStart = range.newListStart; + left.oldListEnd = snake.startX; + left.newListEnd = snake.startY; + stack.add(left); + + // re-use range for right + //noinspection UnnecessaryLocalVariable + final Range right = range; + right.oldListEnd = range.oldListEnd; + right.newListEnd = range.newListEnd; + right.oldListStart = snake.endX; + right.newListStart = snake.endY; + stack.add(right); + } else { + rangePool.add(range); + } + + } + // sort snakes + Collections.sort(diagonals, DIAGONAL_COMPARATOR); + + return new DiffResult(cb, diagonals, + forward.backingData(), backward.backingData(), + detectMoves); + } + + /** + * Finds a middle snake in the given range. + */ + @Nullable + private static Snake midPoint( + Range range, + Callback cb, + CenteredArray forward, + CenteredArray backward) { + if (range.oldSize() < 1 || range.newSize() < 1) { + return null; + } + int max = (range.oldSize() + range.newSize() + 1) / 2; + forward.set(1, range.oldListStart); + backward.set(1, range.oldListEnd); + for (int d = 0; d < max; d++) { + Snake snake = forward(range, cb, forward, backward, d); + if (snake != null) { + return snake; + } + snake = backward(range, cb, forward, backward, d); + if (snake != null) { + return snake; + } + } + return null; + } + + @Nullable + private static Snake forward( + Range range, + Callback cb, + CenteredArray forward, + CenteredArray backward, + int d) { + boolean checkForSnake = Math.abs(range.oldSize() - range.newSize()) % 2 == 1; + int delta = range.oldSize() - range.newSize(); + for (int k = -d; k <= d; k += 2) { + // we either come from d-1, k-1 OR d-1. k+1 + // as we move in steps of 2, array always holds both current and previous d values + // k = x - y and each array value holds the max X, y = x - k + final int startX; + final int startY; + int x, y; + if (k == -d || (k != d && forward.get(k + 1) > forward.get(k - 1))) { + // picking k + 1, incrementing Y (by simply not incrementing X) + x = startX = forward.get(k + 1); + } else { + // picking k - 1, incrementing X + startX = forward.get(k - 1); + x = startX + 1; + } + y = range.newListStart + (x - range.oldListStart) - k; + startY = (d == 0 || x != startX) ? y : y - 1; + // now find snake size + while (x < range.oldListEnd + && y < range.newListEnd + && cb.areItemsTheSame(x, y)) { + x++; + y++; + } + // now we have furthest reaching x, record it + forward.set(k, x); + if (checkForSnake) { + // see if we did pass over a backwards array + // mapping function: delta - k + int backwardsK = delta - k; + // if backwards K is calculated and it passed me, found match + if (backwardsK >= -d + 1 + && backwardsK <= d - 1 + && backward.get(backwardsK) <= x) { + // match + Snake snake = new Snake(); + snake.startX = startX; + snake.startY = startY; + snake.endX = x; + snake.endY = y; + snake.reverse = false; + return snake; + } + } + } + return null; + } + + @Nullable + private static Snake backward( + Range range, + Callback cb, + CenteredArray forward, + CenteredArray backward, + int d) { + boolean checkForSnake = (range.oldSize() - range.newSize()) % 2 == 0; + int delta = range.oldSize() - range.newSize(); + // same as forward but we go backwards from end of the lists to be beginning + // this also means we'll try to optimize for minimizing x instead of maximizing it + for (int k = -d; k <= d; k += 2) { + // we either come from d-1, k-1 OR d-1, k+1 + // as we move in steps of 2, array always holds both current and previous d values + // k = x - y and each array value holds the MIN X, y = x - k + // when x's are equal, we prioritize deletion over insertion + final int startX; + final int startY; + int x, y; + + if (k == -d || (k != d && backward.get(k + 1) < backward.get(k - 1))) { + // picking k + 1, decrementing Y (by simply not decrementing X) + x = startX = backward.get(k + 1); + } else { + // picking k - 1, decrementing X + startX = backward.get(k - 1); + x = startX - 1; + } + y = range.newListEnd - ((range.oldListEnd - x) - k); + startY = (d == 0 || x != startX) ? y : y + 1; + // now find snake size + while (x > range.oldListStart + && y > range.newListStart + && cb.areItemsTheSame(x - 1, y - 1)) { + x--; + y--; + } + // now we have furthest point, record it (min X) + backward.set(k, x); + if (checkForSnake) { + // see if we did pass over a backwards array + // mapping function: delta - k + int forwardsK = delta - k; + // if forwards K is calculated and it passed me, found match + if (forwardsK >= -d + && forwardsK <= d + && forward.get(forwardsK) >= x) { + // match + Snake snake = new Snake(); + // assignment are reverse since we are a reverse snake + snake.startX = x; + snake.startY = y; + snake.endX = startX; + snake.endY = startY; + snake.reverse = true; + return snake; + } + } + } + return null; + } + + /** + * A Callback class used by DiffUtil while calculating the diff between two lists. + */ + public abstract static class Callback { + /** + * Returns the size of the old list. + * + * @return The size of the old list. + */ + public abstract int getOldListSize(); + + /** + * Returns the size of the new list. + * + * @return The size of the new list. + */ + public abstract int getNewListSize(); + + /** + * Called by the DiffUtil to decide whether two object represent the same Item. + *

+ * For example, if your items have unique ids, this method should check their id equality. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list + * @return True if the two items represent the same object or false if they are different. + */ + public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition); + + /** + * Called by the DiffUtil when it wants to check whether two items have the same data. + * DiffUtil uses this information to detect if the contents of an item has changed. + *

+ * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)} + * so that you can change its behavior depending on your UI. + * For example, if you are using DiffUtil with a + * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same. + *

+ * This method is called only if {@link #areItemsTheSame(int, int)} returns + * {@code true} for these items. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list which replaces the + * oldItem + * @return True if the contents of the items are the same or false if they are different. + */ + public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition); + + /** + * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and + * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil + * calls this method to get a payload about the change. + *

+ * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the + * particular field that changed in the item and your + * {@link RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + *

+ * Default implementation returns {@code null}. + * + * @param oldItemPosition The position of the item in the old list + * @param newItemPosition The position of the item in the new list + * @return A payload object that represents the change between the two items. + */ + @Nullable + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + return null; + } + } + + /** + * Callback for calculating the diff between two non-null items in a list. + *

+ * {@link Callback} serves two roles - list indexing, and item diffing. ItemCallback handles + * just the second of these, which allows separation of code that indexes into an array or List + * from the presentation-layer and content specific diffing code. + * + * @param Type of items to compare. + */ + public abstract static class ItemCallback { + /** + * Called to check whether two objects represent the same item. + *

+ * For example, if your items have unique ids, this method should check their id equality. + *

+ * Note: {@code null} items in the list are assumed to be the same as another {@code null} + * item and are assumed to not be the same as a non-{@code null} item. This callback will + * not be invoked for either of those cases. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the two items represent the same object or false if they are different. + * @see Callback#areItemsTheSame(int, int) + */ + public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * Called to check whether two items have the same data. + *

+ * This information is used to detect if the contents of an item have changed. + *

+ * This method to check equality instead of {@link Object#equals(Object)} so that you can + * change its behavior depending on your UI. + *

+ * For example, if you are using DiffUtil with a + * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same. + *

+ * This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for + * these items. + *

+ * Note: Two {@code null} items are assumed to represent the same contents. This callback + * will not be invoked for this case. + * + * @param oldItem The item in the old list. + * @param newItem The item in the new list. + * @return True if the contents of the items are the same or false if they are different. + * @see Callback#areContentsTheSame(int, int) + */ + public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem); + + /** + * When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and + * {@link #areContentsTheSame(T, T)} returns false for them, this method is called to + * get a payload about the change. + *

+ * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the + * particular field that changed in the item and your + * {@link RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + *

+ * Default implementation returns {@code null}. + * + * @see Callback#getChangePayload(int, int) + */ + @SuppressWarnings({"unused"}) + @Nullable + public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) { + return null; + } + } + + /** + * A diagonal is a match in the graph. + * Rather than snakes, we only record the diagonals in the path. + */ + static class Diagonal { + public final int x; + public final int y; + public final int size; + + Diagonal(int x, int y, int size) { + this.x = x; + this.y = y; + this.size = size; + } + + int endX() { + return x + size; + } + + int endY() { + return y + size; + } + } + + /** + * Snakes represent a match between two lists. It is optionally prefixed or postfixed with an + * add or remove operation. See the Myers' paper for details. + */ + @SuppressWarnings("WeakerAccess") + static class Snake { + /** + * Position in the old list + */ + public int startX; + + /** + * Position in the new list + */ + public int startY; + + /** + * End position in the old list, exclusive + */ + public int endX; + + /** + * End position in the new list, exclusive + */ + public int endY; + + /** + * True if this snake was created in the reverse search, false otherwise. + */ + public boolean reverse; + + boolean hasAdditionOrRemoval() { + return endY - startY != endX - startX; + } + + boolean isAddition() { + return endY - startY > endX - startX; + } + + int diagonalSize() { + return Math.min(endX - startX, endY - startY); + } + + /** + * Extract the diagonal of the snake to make reasoning easier for the rest of the + * algorithm where we try to produce a path and also find moves. + */ + @NonNull + Diagonal toDiagonal() { + if (hasAdditionOrRemoval()) { + if (reverse) { + // snake edge it at the end + return new Diagonal(startX, startY, diagonalSize()); + } else { + // snake edge it at the beginning + if (isAddition()) { + return new Diagonal(startX, startY + 1, diagonalSize()); + } else { + return new Diagonal(startX + 1, startY, diagonalSize()); + } + } + } else { + // we are a pure diagonal + return new Diagonal(startX, startY, endX - startX); + } + } + } + + /** + * Represents a range in two lists that needs to be solved. + *

+ * This internal class is used when running Myers' algorithm without recursion. + *

+ * Ends are exclusive + */ + static class Range { + + int oldListStart, oldListEnd; + + int newListStart, newListEnd; + + public Range() { + } + + public Range(int oldListStart, int oldListEnd, int newListStart, int newListEnd) { + this.oldListStart = oldListStart; + this.oldListEnd = oldListEnd; + this.newListStart = newListStart; + this.newListEnd = newListEnd; + } + + int oldSize() { + return oldListEnd - oldListStart; + } + + int newSize() { + return newListEnd - newListStart; + } + } + + /** + * This class holds the information about the result of a + * {@link DiffUtil#calculateDiff(Callback, boolean)} call. + *

+ * You can consume the updates in a DiffResult via + * {@link #dispatchUpdatesTo(ListUpdateCallback)} or directly stream the results into a + * {@link RecyclerView.Adapter} via {@link #dispatchUpdatesTo(RecyclerView.Adapter)}. + */ + public static class DiffResult { + /** + * Signifies an item not present in the list. + */ + public static final int NO_POSITION = -1; + + + /** + * While reading the flags below, keep in mind that when multiple items move in a list, + * Myers's may pick any of them as the anchor item and consider that one NOT_CHANGED while + * picking others as additions and removals. This is completely fine as we later detect + * all moves. + *

+ * Below, when an item is mentioned to stay in the same "location", it means we won't + * dispatch a move/add/remove for it, it DOES NOT mean the item is still in the same + * position. + */ + // item stayed the same. + private static final int FLAG_NOT_CHANGED = 1; + // item stayed in the same location but changed. + private static final int FLAG_CHANGED = FLAG_NOT_CHANGED << 1; + // Item has moved and also changed. + private static final int FLAG_MOVED_CHANGED = FLAG_CHANGED << 1; + // Item has moved but did not change. + private static final int FLAG_MOVED_NOT_CHANGED = FLAG_MOVED_CHANGED << 1; + // Item moved + private static final int FLAG_MOVED = FLAG_MOVED_CHANGED | FLAG_MOVED_NOT_CHANGED; + + // since we are re-using the int arrays that were created in the Myers' step, we mask + // change flags + private static final int FLAG_OFFSET = 4; + + private static final int FLAG_MASK = (1 << FLAG_OFFSET) - 1; + + // The diagonals extracted from The Myers' snakes. + private final List mDiagonals; + + // The list to keep oldItemStatuses. As we traverse old items, we assign flags to them + // which also includes whether they were a real removal or a move (and its new index). + private final int[] mOldItemStatuses; + // The list to keep newItemStatuses. As we traverse new items, we assign flags to them + // which also includes whether they were a real addition or a move(and its old index). + private final int[] mNewItemStatuses; + // The callback that was given to calculate diff method. + private final Callback mCallback; + + private final int mOldListSize; + + private final int mNewListSize; + + private final boolean mDetectMoves; + + /** + * @param callback The callback that was used to calculate the diff + * @param diagonals Matches between the two lists + * @param oldItemStatuses An int[] that can be re-purposed to keep metadata + * @param newItemStatuses An int[] that can be re-purposed to keep metadata + * @param detectMoves True if this DiffResult will try to detect moved items + */ + DiffResult(Callback callback, List diagonals, int[] oldItemStatuses, + int[] newItemStatuses, boolean detectMoves) { + mDiagonals = diagonals; + mOldItemStatuses = oldItemStatuses; + mNewItemStatuses = newItemStatuses; + Arrays.fill(mOldItemStatuses, 0); + Arrays.fill(mNewItemStatuses, 0); + mCallback = callback; + mOldListSize = callback.getOldListSize(); + mNewListSize = callback.getNewListSize(); + mDetectMoves = detectMoves; + addEdgeDiagonals(); + findMatchingItems(); + } + + /** + * Add edge diagonals so that we can iterate as long as there are diagonals w/o lots of + * null checks around + */ + private void addEdgeDiagonals() { + Diagonal first = mDiagonals.isEmpty() ? null : mDiagonals.get(0); + // see if we should add 1 to the 0,0 + if (first == null || first.x != 0 || first.y != 0) { + mDiagonals.add(0, new Diagonal(0, 0, 0)); + } + // always add one last + mDiagonals.add(new Diagonal(mOldListSize, mNewListSize, 0)); + } + + /** + * Find position mapping from old list to new list. + * If moves are requested, we'll also try to do an n^2 search between additions and + * removals to find moves. + */ + private void findMatchingItems() { + for (Diagonal diagonal : mDiagonals) { + for (int offset = 0; offset < diagonal.size; offset++) { + int posX = diagonal.x + offset; + int posY = diagonal.y + offset; + final boolean theSame = mCallback.areContentsTheSame(posX, posY); + final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED; + mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; + mNewItemStatuses[posY] = (posX << FLAG_OFFSET) | changeFlag; + } + } + // now all matches are marked, lets look for moves + if (mDetectMoves) { + // traverse each addition / removal from the end of the list, find matching + // addition removal from before + findMoveMatches(); + } + } + + private void findMoveMatches() { + // for each removal, find matching addition + int posX = 0; + for (Diagonal diagonal : mDiagonals) { + while (posX < diagonal.x) { + if (mOldItemStatuses[posX] == 0) { + // there is a removal, find matching addition from the rest + findMatchingAddition(posX); + } + posX++; + } + // snap back for the next diagonal + posX = diagonal.endX(); + } + } + + /** + * Search the whole list to find the addition for the given removal of position posX + * + * @param posX position in the old list + */ + private void findMatchingAddition(int posX) { + int posY = 0; + final int diagonalsSize = mDiagonals.size(); + for (int i = 0; i < diagonalsSize; i++) { + final Diagonal diagonal = mDiagonals.get(i); + while (posY < diagonal.y) { + // found some additions, evaluate + if (mNewItemStatuses[posY] == 0) { // not evaluated yet + boolean matching = mCallback.areItemsTheSame(posX, posY); + if (matching) { + // yay found it, set values + boolean contentsMatching = mCallback.areContentsTheSame(posX, posY); + final int changeFlag = contentsMatching ? FLAG_MOVED_NOT_CHANGED + : FLAG_MOVED_CHANGED; + // once we process one of these, it will mark the other one as ignored. + mOldItemStatuses[posX] = (posY << FLAG_OFFSET) | changeFlag; + mNewItemStatuses[posY] = (posX << FLAG_OFFSET) | changeFlag; + return; + } + } + posY++; + } + posY = diagonal.endY(); + } + } + + /** + * Given a position in the old list, returns the position in the new list, or + * {@code NO_POSITION} if it was removed. + * + * @param oldListPosition Position of item in old list + * @return Position of item in new list, or {@code NO_POSITION} if not present. + * @see #NO_POSITION + * @see #convertNewPositionToOld(int) + */ + public int convertOldPositionToNew(@IntRange(from = 0) int oldListPosition) { + if (oldListPosition < 0 || oldListPosition >= mOldListSize) { + throw new IndexOutOfBoundsException("Index out of bounds - passed position = " + + oldListPosition + ", old list size = " + mOldListSize); + } + final int status = mOldItemStatuses[oldListPosition]; + if ((status & FLAG_MASK) == 0) { + return NO_POSITION; + } else { + return status >> FLAG_OFFSET; + } + } + + /** + * Given a position in the new list, returns the position in the old list, or + * {@code NO_POSITION} if it was removed. + * + * @param newListPosition Position of item in new list + * @return Position of item in old list, or {@code NO_POSITION} if not present. + * @see #NO_POSITION + * @see #convertOldPositionToNew(int) + */ + public int convertNewPositionToOld(@IntRange(from = 0) int newListPosition) { + if (newListPosition < 0 || newListPosition >= mNewListSize) { + throw new IndexOutOfBoundsException("Index out of bounds - passed position = " + + newListPosition + ", new list size = " + mNewListSize); + } + final int status = mNewItemStatuses[newListPosition]; + if ((status & FLAG_MASK) == 0) { + return NO_POSITION; + } else { + return status >> FLAG_OFFSET; + } + } + + /** + * Dispatches the update events to the given adapter. + *

+ * For example, if you have an {@link RecyclerView.Adapter Adapter} + * that is backed by a {@link List}, you can swap the list with the new one then call this + * method to dispatch all updates to the RecyclerView. + *

+         *     List oldList = mAdapter.getData();
+         *     DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldList, newList));
+         *     mAdapter.setData(newList);
+         *     result.dispatchUpdatesTo(mAdapter);
+         * 
+ *

+ * Note that the RecyclerView requires you to dispatch adapter updates immediately when you + * change the data (you cannot defer {@code notify*} calls). The usage above adheres to this + * rule because updates are sent to the adapter right after the backing data is changed, + * before RecyclerView tries to read it. + *

+ * On the other hand, if you have another + * {@link RecyclerView.AdapterDataObserver AdapterDataObserver} + * that tries to process events synchronously, this may confuse that observer because the + * list is instantly moved to its final state while the adapter updates are dispatched later + * on, one by one. If you have such an + * {@link RecyclerView.AdapterDataObserver AdapterDataObserver}, + * you can use + * {@link #dispatchUpdatesTo(ListUpdateCallback)} to handle each modification + * manually. + * + * @param adapter A RecyclerView adapter which was displaying the old list and will start + * displaying the new list. + * @see AdapterListUpdateCallback + */ + public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) { + dispatchUpdatesTo(new AdapterListUpdateCallback(adapter)); + } + + /** + * Dispatches update operations to the given Callback. + *

+ * These updates are atomic such that the first update call affects every update call that + * comes after it (the same as RecyclerView). + * + * @param updateCallback The callback to receive the update operations. + * @see #dispatchUpdatesTo(RecyclerView.Adapter) + */ + public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) { + final BatchingListUpdateCallback batchingCallback; + + if (updateCallback instanceof BatchingListUpdateCallback) { + batchingCallback = (BatchingListUpdateCallback) updateCallback; + } else { + batchingCallback = new BatchingListUpdateCallback(updateCallback); + // replace updateCallback with a batching callback and override references to + // updateCallback so that we don't call it directly by mistake + //noinspection UnusedAssignment + updateCallback = batchingCallback; + } + // track up to date current list size for moves + // when a move is found, we record its position from the end of the list (which is + // less likely to change since we iterate in reverse). + // Later when we find the match of that move, we dispatch the update + int currentListSize = mOldListSize; + // list of postponed moves + final Collection postponedUpdates = new ArrayDeque<>(); + // posX and posY are exclusive + int posX = mOldListSize; + int posY = mNewListSize; + // iterate from end of the list to the beginning. + // this just makes offsets easier since changes in the earlier indices has an effect + // on the later indices. + for (int diagonalIndex = mDiagonals.size() - 1; diagonalIndex >= 0; diagonalIndex--) { + final Diagonal diagonal = mDiagonals.get(diagonalIndex); + int endX = diagonal.endX(); + int endY = diagonal.endY(); + // dispatch removals and additions until we reach to that diagonal + // first remove then add so that it can go into its place and we don't need + // to offset values + while (posX > endX) { + posX--; + // REMOVAL + int status = mOldItemStatuses[posX]; + if ((status & FLAG_MOVED) != 0) { + int newPos = status >> FLAG_OFFSET; + // get postponed addition + PostponedUpdate postponedUpdate = getPostponedUpdate(postponedUpdates, + newPos, false); + if (postponedUpdate != null) { + // this is an addition that was postponed. Now dispatch it. + int updatedNewPos = currentListSize - postponedUpdate.currentPos; + batchingCallback.onMoved(posX, updatedNewPos - 1); + if ((status & FLAG_MOVED_CHANGED) != 0) { + Object changePayload = mCallback.getChangePayload(posX, newPos); + batchingCallback.onChanged(updatedNewPos - 1, 1, changePayload); + } + } else { + // first time we are seeing this, we'll see a matching addition + postponedUpdates.add(new PostponedUpdate( + posX, + currentListSize - posX - 1, + true + )); + } + } else { + // simple removal + batchingCallback.onRemoved(posX, 1); + currentListSize--; + } + } + while (posY > endY) { + posY--; + // ADDITION + int status = mNewItemStatuses[posY]; + if ((status & FLAG_MOVED) != 0) { + // this is a move not an addition. + // see if this is postponed + int oldPos = status >> FLAG_OFFSET; + // get postponed removal + PostponedUpdate postponedUpdate = getPostponedUpdate(postponedUpdates, + oldPos, true); + // empty size returns 0 for indexOf + if (postponedUpdate == null) { + // postpone it until we see the removal + postponedUpdates.add(new PostponedUpdate( + posY, + currentListSize - posX, + false + )); + } else { + // oldPosFromEnd = foundListSize - posX + // we can find posX if we swap the list sizes + // posX = listSize - oldPosFromEnd + int updatedOldPos = currentListSize - postponedUpdate.currentPos - 1; + batchingCallback.onMoved(updatedOldPos, posX); + if ((status & FLAG_MOVED_CHANGED) != 0) { + Object changePayload = mCallback.getChangePayload(oldPos, posY); + batchingCallback.onChanged(posX, 1, changePayload); + } + } + } else { + // simple addition + batchingCallback.onInserted(posX, 1); + currentListSize++; + } + } + // now dispatch updates for the diagonal + posX = diagonal.x; + posY = diagonal.y; + for (int i = 0; i < diagonal.size; i++) { + // dispatch changes + if ((mOldItemStatuses[posX] & FLAG_MASK) == FLAG_CHANGED) { + Object changePayload = mCallback.getChangePayload(posX, posY); + batchingCallback.onChanged(posX, 1, changePayload); + } + posX++; + posY++; + } + // snap back for the next diagonal + posX = diagonal.x; + posY = diagonal.y; + } + batchingCallback.dispatchLastEvent(); + } + + @Nullable + private static PostponedUpdate getPostponedUpdate( + Collection postponedUpdates, + int posInList, + boolean removal) { + PostponedUpdate postponedUpdate = null; + Iterator itr = postponedUpdates.iterator(); + while (itr.hasNext()) { + PostponedUpdate update = itr.next(); + if (update.posInOwnerList == posInList && update.removal == removal) { + postponedUpdate = update; + itr.remove(); + break; + } + } + while (itr.hasNext()) { + // re-offset all others + PostponedUpdate update = itr.next(); + if (removal) { + update.currentPos--; + } else { + update.currentPos++; + } + } + return postponedUpdate; + } + } + + /** + * Represents an update that we skipped because it was a move. + *

+ * When an update is skipped, it is tracked as other updates are dispatched until the matching + * add/remove operation is found at which point the tracked position is used to dispatch the + * update. + */ + private static class PostponedUpdate { + /** + * position in the list that owns this item + */ + int posInOwnerList; + + /** + * position wrt to the end of the list + */ + int currentPos; + + /** + * true if this is a removal, false otherwise + */ + boolean removal; + + PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) { + this.posInOwnerList = posInOwnerList; + this.currentPos = currentPos; + this.removal = removal; + } + } + + /** + * Array wrapper w/ negative index support. + * We use this array instead of a regular array so that algorithm is easier to read without + * too many offsets when accessing the "k" array in the algorithm. + */ + static class CenteredArray { + private final int[] mData; + private final int mMid; + + CenteredArray(int size) { + mData = new int[size]; + mMid = mData.length / 2; + } + + int get(int index) { + return mData[index + mMid]; + } + + int[] backingData() { + return mData; + } + + void set(int index, int value) { + mData[index + mMid] = value; + } + + public void fill(int value) { + Arrays.fill(mData, value); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java b/app/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java new file mode 100644 index 0000000000..b4598edfed --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/DividerItemDecoration.java @@ -0,0 +1,194 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * DividerItemDecoration is a {@link RecyclerView.ItemDecoration} that can be used as a divider + * between items of a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and + * {@link #VERTICAL} orientations. + * + *

+ *     mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
+ *             mLayoutManager.getOrientation());
+ *     recyclerView.addItemDecoration(mDividerItemDecoration);
+ * 
+ */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + private static final String TAG = "DividerItem"; + private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; + + private Drawable mDivider; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + private int mOrientation; + + private final Rect mBounds = new Rect(); + + /** + * Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a + * {@link LinearLayoutManager}. + * + * @param context Current context, it will be used to access resources. + * @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + if (mDivider == null) { + Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this " + + "DividerItemDecoration. Please set that attribute all call setDrawable()"); + } + a.recycle(); + setOrientation(orientation); + } + + /** + * Sets the orientation for this divider. This should be called if + * {@link RecyclerView.LayoutManager} changes orientation. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException( + "Invalid orientation. It should be either HORIZONTAL or VERTICAL"); + } + mOrientation = orientation; + } + + /** + * Sets the {@link Drawable} for this divider. + * + * @param drawable Drawable that should be used as a divider. + */ + public void setDrawable(@NonNull Drawable drawable) { + if (drawable == null) { + throw new IllegalArgumentException("Drawable cannot be null."); + } + mDivider = drawable; + } + + /** + * @return the {@link Drawable} for this divider. + */ + @Nullable + public Drawable getDrawable() { + return mDivider; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (parent.getLayoutManager() == null || mDivider == null) { + return; + } + if (mOrientation == VERTICAL) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + private void drawVertical(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int left; + final int right; + //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. + if (parent.getClipToPadding()) { + left = parent.getPaddingLeft(); + right = parent.getWidth() - parent.getPaddingRight(); + canvas.clipRect(left, parent.getPaddingTop(), right, + parent.getHeight() - parent.getPaddingBottom()); + } else { + left = 0; + right = parent.getWidth(); + } + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + parent.getDecoratedBoundsWithMargins(child, mBounds); + final int bottom = mBounds.bottom + Math.round(child.getTranslationY()); + final int top = bottom - mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + private void drawHorizontal(Canvas canvas, RecyclerView parent) { + canvas.save(); + final int top; + final int bottom; + //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides. + if (parent.getClipToPadding()) { + top = parent.getPaddingTop(); + bottom = parent.getHeight() - parent.getPaddingBottom(); + canvas.clipRect(parent.getPaddingLeft(), top, + parent.getWidth() - parent.getPaddingRight(), bottom); + } else { + top = 0; + bottom = parent.getHeight(); + } + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds); + final int right = mBounds.right + Math.round(child.getTranslationX()); + final int left = right - mDivider.getIntrinsicWidth(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(canvas); + } + canvas.restore(); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + if (mDivider == null) { + outRect.set(0, 0, 0, 0); + return; + } + if (mOrientation == VERTICAL) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/FastScroller.java b/app/src/main/java/androidx/recyclerview/widget/FastScroller.java new file mode 100644 index 0000000000..180adcc54a --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/FastScroller.java @@ -0,0 +1,588 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.view.MotionEvent; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.view.ViewCompat; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class responsible to animate and provide a fast scroller. + */ +@VisibleForTesting +class FastScroller extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener { + @IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING}) + @Retention(RetentionPolicy.SOURCE) + private @interface State { } + // Scroll thumb not showing + private static final int STATE_HIDDEN = 0; + // Scroll thumb visible and moving along with the scrollbar + private static final int STATE_VISIBLE = 1; + // Scroll thumb being dragged by user + private static final int STATE_DRAGGING = 2; + + @IntDef({DRAG_X, DRAG_Y, DRAG_NONE}) + @Retention(RetentionPolicy.SOURCE) + private @interface DragState{ } + private static final int DRAG_NONE = 0; + private static final int DRAG_X = 1; + private static final int DRAG_Y = 2; + + @IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN, + ANIMATION_STATE_FADING_OUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface AnimationState { } + private static final int ANIMATION_STATE_OUT = 0; + private static final int ANIMATION_STATE_FADING_IN = 1; + private static final int ANIMATION_STATE_IN = 2; + private static final int ANIMATION_STATE_FADING_OUT = 3; + + private static final int SHOW_DURATION_MS = 500; + private static final int HIDE_DELAY_AFTER_VISIBLE_MS = 1500; + private static final int HIDE_DELAY_AFTER_DRAGGING_MS = 1200; + private static final int HIDE_DURATION_MS = 500; + private static final int SCROLLBAR_FULL_OPAQUE = 255; + + private static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed}; + private static final int[] EMPTY_STATE_SET = new int[]{}; + + private final int mScrollbarMinimumRange; + private final int mMargin; + + // Final values for the vertical scroll bar + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final StateListDrawable mVerticalThumbDrawable; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Drawable mVerticalTrackDrawable; + private final int mVerticalThumbWidth; + private final int mVerticalTrackWidth; + + // Final values for the horizontal scroll bar + private final StateListDrawable mHorizontalThumbDrawable; + private final Drawable mHorizontalTrackDrawable; + private final int mHorizontalThumbHeight; + private final int mHorizontalTrackHeight; + + // Dynamic values for the vertical scroll bar + @VisibleForTesting int mVerticalThumbHeight; + @VisibleForTesting int mVerticalThumbCenterY; + @VisibleForTesting float mVerticalDragY; + + // Dynamic values for the horizontal scroll bar + @VisibleForTesting int mHorizontalThumbWidth; + @VisibleForTesting int mHorizontalThumbCenterX; + @VisibleForTesting float mHorizontalDragX; + + private int mRecyclerViewWidth = 0; + private int mRecyclerViewHeight = 0; + + private RecyclerView mRecyclerView; + /** + * Whether the document is long/wide enough to require scrolling. If not, we don't show the + * relevant scroller. + */ + private boolean mNeedVerticalScrollbar = false; + private boolean mNeedHorizontalScrollbar = false; + @State private int mState = STATE_HIDDEN; + @DragState private int mDragState = DRAG_NONE; + + private final int[] mVerticalRange = new int[2]; + private final int[] mHorizontalRange = new int[2]; + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1); + @SuppressWarnings("WeakerAccess") /* synthetic access */ + @AnimationState int mAnimationState = ANIMATION_STATE_OUT; + private final Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hide(HIDE_DURATION_MS); + } + }; + private final RecyclerView.OnScrollListener + mOnScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + updateScrollPosition(recyclerView.computeHorizontalScrollOffset(), + recyclerView.computeVerticalScrollOffset()); + } + }; + + FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable, + Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, + Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange, + int margin) { + mVerticalThumbDrawable = verticalThumbDrawable; + mVerticalTrackDrawable = verticalTrackDrawable; + mHorizontalThumbDrawable = horizontalThumbDrawable; + mHorizontalTrackDrawable = horizontalTrackDrawable; + mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth()); + mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth()); + mHorizontalThumbHeight = Math + .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth()); + mHorizontalTrackHeight = Math + .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth()); + mScrollbarMinimumRange = scrollbarMinimumRange; + mMargin = margin; + mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE); + mVerticalTrackDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE); + + mShowHideAnimator.addListener(new AnimatorListener()); + mShowHideAnimator.addUpdateListener(new AnimatorUpdater()); + + attachToRecyclerView(recyclerView); + } + + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + setupCallbacks(); + } + } + + private void setupCallbacks() { + mRecyclerView.addItemDecoration(this); + mRecyclerView.addOnItemTouchListener(this); + mRecyclerView.addOnScrollListener(mOnScrollListener); + } + + private void destroyCallbacks() { + mRecyclerView.removeItemDecoration(this); + mRecyclerView.removeOnItemTouchListener(this); + mRecyclerView.removeOnScrollListener(mOnScrollListener); + cancelHide(); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void requestRedraw() { + mRecyclerView.invalidate(); + } + + void setState(@State int state) { + if (state == STATE_DRAGGING && mState != STATE_DRAGGING) { + mVerticalThumbDrawable.setState(PRESSED_STATE_SET); + cancelHide(); + } + + if (state == STATE_HIDDEN) { + requestRedraw(); + } else { + show(); + } + + if (mState == STATE_DRAGGING && state != STATE_DRAGGING) { + mVerticalThumbDrawable.setState(EMPTY_STATE_SET); + resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS); + } else if (state == STATE_VISIBLE) { + resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS); + } + mState = state; + } + + private boolean isLayoutRTL() { + return ViewCompat.getLayoutDirection(mRecyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL; + } + + public boolean isDragging() { + return mState == STATE_DRAGGING; + } + + @VisibleForTesting boolean isVisible() { + return mState == STATE_VISIBLE; + } + + public void show() { + switch (mAnimationState) { + case ANIMATION_STATE_FADING_OUT: + mShowHideAnimator.cancel(); + // fall through + case ANIMATION_STATE_OUT: + mAnimationState = ANIMATION_STATE_FADING_IN; + mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1); + mShowHideAnimator.setDuration(SHOW_DURATION_MS); + mShowHideAnimator.setStartDelay(0); + mShowHideAnimator.start(); + break; + } + } + + @VisibleForTesting + void hide(int duration) { + switch (mAnimationState) { + case ANIMATION_STATE_FADING_IN: + mShowHideAnimator.cancel(); + // fall through + case ANIMATION_STATE_IN: + mAnimationState = ANIMATION_STATE_FADING_OUT; + mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 0); + mShowHideAnimator.setDuration(duration); + mShowHideAnimator.start(); + break; + } + } + + private void cancelHide() { + mRecyclerView.removeCallbacks(mHideRunnable); + } + + private void resetHideDelay(int delay) { + cancelHide(); + mRecyclerView.postDelayed(mHideRunnable, delay); + } + + @Override + public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + if (mRecyclerViewWidth != mRecyclerView.getWidth() + || mRecyclerViewHeight != mRecyclerView.getHeight()) { + mRecyclerViewWidth = mRecyclerView.getWidth(); + mRecyclerViewHeight = mRecyclerView.getHeight(); + // This is due to the different events ordering when keyboard is opened or + // retracted vs rotate. Hence to avoid corner cases we just disable the + // scroller when size changed, and wait until the scroll position is recomputed + // before showing it back. + setState(STATE_HIDDEN); + return; + } + + if (mAnimationState != ANIMATION_STATE_OUT) { + if (mNeedVerticalScrollbar) { + drawVerticalScrollbar(canvas); + } + if (mNeedHorizontalScrollbar) { + drawHorizontalScrollbar(canvas); + } + } + } + + private void drawVerticalScrollbar(Canvas canvas) { + int viewWidth = mRecyclerViewWidth; + + int left = viewWidth - mVerticalThumbWidth; + int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2; + mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight); + mVerticalTrackDrawable + .setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight); + + if (isLayoutRTL()) { + mVerticalTrackDrawable.draw(canvas); + canvas.translate(mVerticalThumbWidth, top); + canvas.scale(-1, 1); + mVerticalThumbDrawable.draw(canvas); + canvas.scale(-1, 1); + canvas.translate(-mVerticalThumbWidth, -top); + } else { + canvas.translate(left, 0); + mVerticalTrackDrawable.draw(canvas); + canvas.translate(0, top); + mVerticalThumbDrawable.draw(canvas); + canvas.translate(-left, -top); + } + } + + private void drawHorizontalScrollbar(Canvas canvas) { + int viewHeight = mRecyclerViewHeight; + + int top = viewHeight - mHorizontalThumbHeight; + int left = mHorizontalThumbCenterX - mHorizontalThumbWidth / 2; + mHorizontalThumbDrawable.setBounds(0, 0, mHorizontalThumbWidth, mHorizontalThumbHeight); + mHorizontalTrackDrawable + .setBounds(0, 0, mRecyclerViewWidth, mHorizontalTrackHeight); + + canvas.translate(0, top); + mHorizontalTrackDrawable.draw(canvas); + canvas.translate(left, 0); + mHorizontalThumbDrawable.draw(canvas); + canvas.translate(-left, -top); + } + + /** + * Notify the scroller of external change of the scroll, e.g. through dragging or flinging on + * the view itself. + * + * @param offsetX The new scroll X offset. + * @param offsetY The new scroll Y offset. + */ + void updateScrollPosition(int offsetX, int offsetY) { + int verticalContentLength = mRecyclerView.computeVerticalScrollRange(); + int verticalVisibleLength = mRecyclerViewHeight; + mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0 + && mRecyclerViewHeight >= mScrollbarMinimumRange; + + int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange(); + int horizontalVisibleLength = mRecyclerViewWidth; + mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0 + && mRecyclerViewWidth >= mScrollbarMinimumRange; + + if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) { + if (mState != STATE_HIDDEN) { + setState(STATE_HIDDEN); + } + return; + } + + if (mNeedVerticalScrollbar) { + float middleScreenPos = offsetY + verticalVisibleLength / 2.0f; + mVerticalThumbCenterY = + (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength); + mVerticalThumbHeight = Math.min(verticalVisibleLength, + (verticalVisibleLength * verticalVisibleLength) / verticalContentLength); + } + + if (mNeedHorizontalScrollbar) { + float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f; + mHorizontalThumbCenterX = + (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength); + mHorizontalThumbWidth = Math.min(horizontalVisibleLength, + (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength); + } + + if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) { + setState(STATE_VISIBLE); + } + } + + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent ev) { + final boolean handled; + if (mState == STATE_VISIBLE) { + boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY()); + boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY()); + if (ev.getAction() == MotionEvent.ACTION_DOWN + && (insideVerticalThumb || insideHorizontalThumb)) { + if (insideHorizontalThumb) { + mDragState = DRAG_X; + mHorizontalDragX = (int) ev.getX(); + } else if (insideVerticalThumb) { + mDragState = DRAG_Y; + mVerticalDragY = (int) ev.getY(); + } + + setState(STATE_DRAGGING); + handled = true; + } else { + handled = false; + } + } else if (mState == STATE_DRAGGING) { + handled = true; + } else { + handled = false; + } + return handled; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent me) { + if (mState == STATE_HIDDEN) { + return; + } + + if (me.getAction() == MotionEvent.ACTION_DOWN) { + boolean insideVerticalThumb = isPointInsideVerticalThumb(me.getX(), me.getY()); + boolean insideHorizontalThumb = isPointInsideHorizontalThumb(me.getX(), me.getY()); + if (insideVerticalThumb || insideHorizontalThumb) { + if (insideHorizontalThumb) { + mDragState = DRAG_X; + mHorizontalDragX = (int) me.getX(); + } else if (insideVerticalThumb) { + mDragState = DRAG_Y; + mVerticalDragY = (int) me.getY(); + } + setState(STATE_DRAGGING); + } + } else if (me.getAction() == MotionEvent.ACTION_UP && mState == STATE_DRAGGING) { + mVerticalDragY = 0; + mHorizontalDragX = 0; + setState(STATE_VISIBLE); + mDragState = DRAG_NONE; + } else if (me.getAction() == MotionEvent.ACTION_MOVE && mState == STATE_DRAGGING) { + show(); + if (mDragState == DRAG_X) { + horizontalScrollTo(me.getX()); + } + if (mDragState == DRAG_Y) { + verticalScrollTo(me.getY()); + } + } + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } + + private void verticalScrollTo(float y) { + final int[] scrollbarRange = getVerticalRange(); + y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y)); + if (Math.abs(mVerticalThumbCenterY - y) < 2) { + return; + } + int scrollingBy = scrollTo(mVerticalDragY, y, scrollbarRange, + mRecyclerView.computeVerticalScrollRange(), + mRecyclerView.computeVerticalScrollOffset(), mRecyclerViewHeight); + if (scrollingBy != 0) { + mRecyclerView.scrollBy(0, scrollingBy); + } + mVerticalDragY = y; + } + + private void horizontalScrollTo(float x) { + final int[] scrollbarRange = getHorizontalRange(); + x = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], x)); + if (Math.abs(mHorizontalThumbCenterX - x) < 2) { + return; + } + + int scrollingBy = scrollTo(mHorizontalDragX, x, scrollbarRange, + mRecyclerView.computeHorizontalScrollRange(), + mRecyclerView.computeHorizontalScrollOffset(), mRecyclerViewWidth); + if (scrollingBy != 0) { + mRecyclerView.scrollBy(scrollingBy, 0); + } + + mHorizontalDragX = x; + } + + private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange, + int scrollOffset, int viewLength) { + int scrollbarLength = scrollbarRange[1] - scrollbarRange[0]; + if (scrollbarLength == 0) { + return 0; + } + float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength); + int totalPossibleOffset = scrollRange - viewLength; + int scrollingBy = (int) (percentage * totalPossibleOffset); + int absoluteOffset = scrollOffset + scrollingBy; + if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) { + return scrollingBy; + } else { + return 0; + } + } + + @VisibleForTesting + boolean isPointInsideVerticalThumb(float x, float y) { + return (isLayoutRTL() ? x <= mVerticalThumbWidth + : x >= mRecyclerViewWidth - mVerticalThumbWidth) + && y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2 + && y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2; + } + + @VisibleForTesting + boolean isPointInsideHorizontalThumb(float x, float y) { + return (y >= mRecyclerViewHeight - mHorizontalThumbHeight) + && x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2 + && x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2; + } + + @VisibleForTesting + Drawable getHorizontalTrackDrawable() { + return mHorizontalTrackDrawable; + } + + @VisibleForTesting + Drawable getHorizontalThumbDrawable() { + return mHorizontalThumbDrawable; + } + + @VisibleForTesting + Drawable getVerticalTrackDrawable() { + return mVerticalTrackDrawable; + } + + @VisibleForTesting + Drawable getVerticalThumbDrawable() { + return mVerticalThumbDrawable; + } + + /** + * Gets the (min, max) vertical positions of the vertical scroll bar. + */ + private int[] getVerticalRange() { + mVerticalRange[0] = mMargin; + mVerticalRange[1] = mRecyclerViewHeight - mMargin; + return mVerticalRange; + } + + /** + * Gets the (min, max) horizontal positions of the horizontal scroll bar. + */ + private int[] getHorizontalRange() { + mHorizontalRange[0] = mMargin; + mHorizontalRange[1] = mRecyclerViewWidth - mMargin; + return mHorizontalRange; + } + + private class AnimatorListener extends AnimatorListenerAdapter { + + private boolean mCanceled = false; + + AnimatorListener() { + } + + @Override + public void onAnimationEnd(Animator animation) { + // Cancel is always followed by a new directive, so don't update state. + if (mCanceled) { + mCanceled = false; + return; + } + if ((float) mShowHideAnimator.getAnimatedValue() == 0) { + mAnimationState = ANIMATION_STATE_OUT; + setState(STATE_HIDDEN); + } else { + mAnimationState = ANIMATION_STATE_IN; + requestRedraw(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + } + + private class AnimatorUpdater implements AnimatorUpdateListener { + AnimatorUpdater() { + } + + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue())); + mVerticalThumbDrawable.setAlpha(alpha); + mVerticalTrackDrawable.setAlpha(alpha); + requestRedraw(); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/GapWorker.java b/app/src/main/java/androidx/recyclerview/widget/GapWorker.java new file mode 100644 index 0000000000..5bbdcf1b48 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/GapWorker.java @@ -0,0 +1,407 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.core.os.TraceCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.concurrent.TimeUnit; + +final class GapWorker implements Runnable { + + static final ThreadLocal sGapWorker = new ThreadLocal<>(); + + ArrayList mRecyclerViews = new ArrayList<>(); + long mPostTimeNs; + long mFrameIntervalNs; + + static class Task { + public boolean immediate; + public int viewVelocity; + public int distanceToItem; + public RecyclerView view; + public int position; + + public void clear() { + immediate = false; + viewVelocity = 0; + distanceToItem = 0; + view = null; + position = 0; + } + } + + /** + * Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects + * are pooled in the ArrayList, and never removed to avoid allocations, but always cleared + * in between calls. + */ + private ArrayList mTasks = new ArrayList<>(); + + /** + * Prefetch information associated with a specific RecyclerView. + */ + @SuppressLint("VisibleForTests") + static class LayoutPrefetchRegistryImpl + implements RecyclerView.LayoutManager.LayoutPrefetchRegistry { + int mPrefetchDx; + int mPrefetchDy; + int[] mPrefetchArray; + + int mCount; + + void setPrefetchVector(int dx, int dy) { + mPrefetchDx = dx; + mPrefetchDy = dy; + } + + void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) { + mCount = 0; + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + + final RecyclerView.LayoutManager layout = view.mLayout; + if (view.mAdapter != null + && layout != null + && layout.isItemPrefetchEnabled()) { + if (nested) { + // nested prefetch, only if no adapter updates pending. Note: we don't query + // view.hasPendingAdapterUpdates(), as first layout may not have occurred + if (!view.mAdapterHelper.hasPendingUpdates()) { + layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this); + } + } else { + // momentum based prefetch, only if we trust current child/adapter state + if (!view.hasPendingAdapterUpdates()) { + layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy, + view.mState, this); + } + } + + if (mCount > layout.mPrefetchMaxCountObserved) { + layout.mPrefetchMaxCountObserved = mCount; + layout.mPrefetchMaxObservedInInitialPrefetch = nested; + view.mRecycler.updateViewCacheSize(); + } + } + } + + @Override + public void addPosition(int layoutPosition, int pixelDistance) { + if (layoutPosition < 0) { + throw new IllegalArgumentException("Layout positions must be non-negative"); + } + + if (pixelDistance < 0) { + throw new IllegalArgumentException("Pixel distance must be non-negative"); + } + + // allocate or expand array as needed, doubling when needed + final int storagePosition = mCount * 2; + if (mPrefetchArray == null) { + mPrefetchArray = new int[4]; + Arrays.fill(mPrefetchArray, -1); + } else if (storagePosition >= mPrefetchArray.length) { + final int[] oldArray = mPrefetchArray; + mPrefetchArray = new int[storagePosition * 2]; + System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length); + } + + // add position + mPrefetchArray[storagePosition] = layoutPosition; + mPrefetchArray[storagePosition + 1] = pixelDistance; + + mCount++; + } + + boolean lastPrefetchIncludedPosition(int position) { + if (mPrefetchArray != null) { + final int count = mCount * 2; + for (int i = 0; i < count; i += 2) { + if (mPrefetchArray[i] == position) return true; + } + } + return false; + } + + /** + * Called when prefetch indices are no longer valid for cache prioritization. + */ + void clearPrefetchPositions() { + if (mPrefetchArray != null) { + Arrays.fill(mPrefetchArray, -1); + } + mCount = 0; + } + } + + public void add(RecyclerView recyclerView) { + if (RecyclerView.sDebugAssertionsEnabled && mRecyclerViews.contains(recyclerView)) { + throw new IllegalStateException("RecyclerView already present in worker list!"); + } + mRecyclerViews.add(recyclerView); + } + + public void remove(RecyclerView recyclerView) { + boolean removeSuccess = mRecyclerViews.remove(recyclerView); + if (RecyclerView.sDebugAssertionsEnabled && !removeSuccess) { + throw new IllegalStateException("RecyclerView removal failed!"); + } + } + + /** + * Schedule a prefetch immediately after the current traversal. + */ + void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) { + if (recyclerView.isAttachedToWindow()) { + if (RecyclerView.sDebugAssertionsEnabled && !mRecyclerViews.contains(recyclerView)) { + throw new IllegalStateException("attempting to post unregistered view!"); + } + if (mPostTimeNs == 0) { + mPostTimeNs = recyclerView.getNanoTime(); + recyclerView.post(this); + } + } + + recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy); + } + + static Comparator sTaskComparator = new Comparator() { + @Override + public int compare(Task lhs, Task rhs) { + // first, prioritize non-cleared tasks + if ((lhs.view == null) != (rhs.view == null)) { + return lhs.view == null ? 1 : -1; + } + + // then prioritize immediate + if (lhs.immediate != rhs.immediate) { + return lhs.immediate ? -1 : 1; + } + + // then prioritize _highest_ view velocity + int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity; + if (deltaViewVelocity != 0) return deltaViewVelocity; + + // then prioritize _lowest_ distance to item + int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem; + if (deltaDistanceToItem != 0) return deltaDistanceToItem; + + return 0; + } + }; + + private void buildTaskList() { + // Update PrefetchRegistry in each view + final int viewCount = mRecyclerViews.size(); + int totalTaskCount = 0; + for (int i = 0; i < viewCount; i++) { + RecyclerView view = mRecyclerViews.get(i); + if (view.getWindowVisibility() == View.VISIBLE) { + view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false); + totalTaskCount += view.mPrefetchRegistry.mCount; + } + } + + // Populate task list from prefetch data... + mTasks.ensureCapacity(totalTaskCount); + int totalTaskIndex = 0; + for (int i = 0; i < viewCount; i++) { + RecyclerView view = mRecyclerViews.get(i); + if (view.getWindowVisibility() != View.VISIBLE) { + // Invisible view, don't bother prefetching + continue; + } + + LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry; + final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx) + + Math.abs(prefetchRegistry.mPrefetchDy); + for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) { + final Task task; + if (totalTaskIndex >= mTasks.size()) { + task = new Task(); + mTasks.add(task); + } else { + task = mTasks.get(totalTaskIndex); + } + final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1]; + + task.immediate = distanceToItem <= viewVelocity; + task.viewVelocity = viewVelocity; + task.distanceToItem = distanceToItem; + task.view = view; + task.position = prefetchRegistry.mPrefetchArray[j]; + + totalTaskIndex++; + } + } + + // ... and priority sort + Collections.sort(mTasks, sTaskComparator); + } + + static boolean isPrefetchPositionAttached(RecyclerView view, int position) { + final int childCount = view.mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + View attachedView = view.mChildHelper.getUnfilteredChildAt(i); + RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView); + // Note: can use mPosition here because adapter doesn't have pending updates + if (holder.mPosition == position && !holder.isInvalid()) { + return true; + } + } + return false; + } + + private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view, + int position, long deadlineNs) { + if (isPrefetchPositionAttached(view, position)) { + // don't attempt to prefetch attached views + return null; + } + + RecyclerView.Recycler recycler = view.mRecycler; + RecyclerView.ViewHolder holder; + try { + view.onEnterLayoutOrScroll(); + holder = recycler.tryGetViewHolderForPositionByDeadline( + position, false, deadlineNs); + + if (holder != null) { + if (holder.isBound() && !holder.isInvalid()) { + // Only give the view a chance to go into the cache if binding succeeded + // Note that we must use public method, since item may need cleanup + recycler.recycleView(holder.itemView); + } else { + // Didn't bind, so we can't cache the view, but it will stay in the pool until + // next prefetch/traversal. If a View fails to bind, it means we didn't have + // enough time prior to the deadline (and won't for other instances of this + // type, during this GapWorker prefetch pass). + recycler.addViewHolderToRecycledViewPool(holder, false); + } + } + } finally { + view.onExitLayoutOrScroll(false); + } + return holder; + } + + private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView, + long deadlineNs) { + if (innerView == null) { + return; + } + + if (innerView.mDataSetHasChangedAfterLayout + && innerView.mChildHelper.getUnfilteredChildCount() != 0) { + // RecyclerView has new data, but old attached views. Clear everything, so that + // we can prefetch without partially stale data. + innerView.removeAndRecycleViews(); + } + + // do nested prefetch! + final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry; + innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true); + + if (innerPrefetchRegistry.mCount != 0) { + try { + TraceCompat.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG); + innerView.mState.prepareForNestedPrefetch(innerView.mAdapter); + for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) { + // Note that we ignore immediate flag for inner items because + // we have lower confidence they're needed next frame. + final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i]; + prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs); + } + } finally { + TraceCompat.endSection(); + } + } + } + + private void flushTaskWithDeadline(Task task, long deadlineNs) { + long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs; + RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view, + task.position, taskDeadlineNs); + if (holder != null + && holder.mNestedRecyclerView != null + && holder.isBound() + && !holder.isInvalid()) { + prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs); + } + } + + private void flushTasksWithDeadline(long deadlineNs) { + for (int i = 0; i < mTasks.size(); i++) { + final Task task = mTasks.get(i); + if (task.view == null) { + break; // done with populated tasks + } + flushTaskWithDeadline(task, deadlineNs); + task.clear(); + } + } + + void prefetch(long deadlineNs) { + buildTaskList(); + flushTasksWithDeadline(deadlineNs); + } + + @Override + public void run() { + try { + TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG); + + if (mRecyclerViews.isEmpty()) { + // abort - no work to do + return; + } + + // Query most recent vsync so we can predict next one. Note that drawing time not yet + // valid in animation/input callbacks, so query it here to be safe. + final int size = mRecyclerViews.size(); + long latestFrameVsyncMs = 0; + for (int i = 0; i < size; i++) { + RecyclerView view = mRecyclerViews.get(i); + if (view.getWindowVisibility() == View.VISIBLE) { + latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs); + } + } + + if (latestFrameVsyncMs == 0) { + // abort - either no views visible, or couldn't get last vsync for estimating next + return; + } + + long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs; + + prefetch(nextFrameNs); + + // TODO: consider rescheduling self, if there's more work to do + } finally { + mPostTimeNs = 0; + TraceCompat.endSection(); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/app/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java new file mode 100644 index 0000000000..01935b50c0 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java @@ -0,0 +1,1450 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.GridView; + +import androidx.annotation.NonNull; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import java.util.Arrays; + +/** + * A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid. + *

+ * By default, each item occupies 1 span. You can change it by providing a custom + * {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}. + */ +public class GridLayoutManager extends LinearLayoutManager { + + private static final boolean DEBUG = false; + private static final String TAG = "GridLayoutManager"; + public static final int DEFAULT_SPAN_COUNT = -1; + /** + * Span size have been changed but we've not done a new layout calculation. + */ + boolean mPendingSpanCountChange = false; + int mSpanCount = DEFAULT_SPAN_COUNT; + /** + * Right borders for each span. + *

For i-th item start is {@link #mCachedBorders}[i-1] + 1 + * and end is {@link #mCachedBorders}[i]. + */ + int [] mCachedBorders; + /** + * Temporary array to keep views in layoutChunk method + */ + View[] mSet; + final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray(); + final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray(); + SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup(); + // re-used variable to acquire decor insets from RecyclerView + final Rect mDecorInsets = new Rect(); + + private boolean mUsingSpansToEstimateScrollBarDimensions; + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". If spanCount is not specified in the XML, it defaults to a + * single column. + * + * {@link androidx.recyclerview.R.attr#spanCount} + */ + public GridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setSpanCount(properties.spanCount); + } + + /** + * Creates a vertical GridLayoutManager + * + * @param context Current context, will be used to access resources. + * @param spanCount The number of columns in the grid + */ + public GridLayoutManager(Context context, int spanCount) { + super(context); + setSpanCount(spanCount); + } + + /** + * @param context Current context, will be used to access resources. + * @param spanCount The number of columns or rows in the grid + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link + * #VERTICAL}. + * @param reverseLayout When set to true, layouts from end to start. + */ + public GridLayoutManager(Context context, int spanCount, + @RecyclerView.Orientation int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + setSpanCount(spanCount); + } + + /** + * stackFromEnd is not supported by GridLayoutManager. Consider using + * {@link #setReverseLayout(boolean)}. + */ + @Override + public void setStackFromEnd(boolean stackFromEnd) { + if (stackFromEnd) { + throw new UnsupportedOperationException( + "GridLayoutManager does not support stack from end." + + " Consider using reverse layout"); + } + super.setStackFromEnd(false); + } + + @Override + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return mSpanCount; + } + if (state.getItemCount() < 1) { + return 0; + } + + // Row count is one more than the last item's row index. + return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; + } + + @Override + public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return mSpanCount; + } + if (state.getItemCount() < 1) { + return 0; + } + + // Column count is one more than the last item's column index. + return getSpanGroupIndex(recycler, state, state.getItemCount() - 1) + 1; + } + + @Override + public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, + RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) { + ViewGroup.LayoutParams lp = host.getLayoutParams(); + if (!(lp instanceof LayoutParams)) { + super.onInitializeAccessibilityNodeInfoForItem(host, info); + return; + } + LayoutParams glp = (LayoutParams) lp; + int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewLayoutPosition()); + if (mOrientation == HORIZONTAL) { + info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( + glp.getSpanIndex(), glp.getSpanSize(), + spanGroupIndex, 1, false, false)); + } else { // VERTICAL + info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain( + spanGroupIndex , 1, + glp.getSpanIndex(), glp.getSpanSize(), false, false)); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler, + @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(recycler, state, info); + // Set the class name so this is treated as a grid. A11y services should identify grids + // and list via CollectionInfos, but an almost empty grid may be incorrectly identified + // as a list. + info.setClassName(GridView.class.getName()); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + if (state.isPreLayout()) { + cachePreLayoutSpanMapping(); + } + super.onLayoutChildren(recycler, state); + if (DEBUG) { + validateChildOrder(); + } + clearPreLayoutSpanMappingCache(); + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSpanCountChange = false; + } + + private void clearPreLayoutSpanMappingCache() { + mPreLayoutSpanSizeCache.clear(); + mPreLayoutSpanIndexCache.clear(); + } + + private void cachePreLayoutSpanMapping() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + final int viewPosition = lp.getViewLayoutPosition(); + mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize()); + mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex()); + } + } + + @Override + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { + mSpanSizeLookup.invalidateSpanIndexCache(); + mSpanSizeLookup.invalidateSpanGroupIndexCache(); + } + + @Override + public void onItemsChanged(RecyclerView recyclerView) { + mSpanSizeLookup.invalidateSpanIndexCache(); + mSpanSizeLookup.invalidateSpanGroupIndexCache(); + } + + @Override + public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { + mSpanSizeLookup.invalidateSpanIndexCache(); + mSpanSizeLookup.invalidateSpanGroupIndexCache(); + } + + @Override + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + Object payload) { + mSpanSizeLookup.invalidateSpanIndexCache(); + mSpanSizeLookup.invalidateSpanGroupIndexCache(); + } + + @Override + public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { + mSpanSizeLookup.invalidateSpanIndexCache(); + mSpanSizeLookup.invalidateSpanGroupIndexCache(); + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + if (mOrientation == HORIZONTAL) { + return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT); + } else { + return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof ViewGroup.MarginLayoutParams) { + return new LayoutParams((ViewGroup.MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + /** + * Sets the source to get the number of spans occupied by each item in the adapter. + * + * @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans + * occupied by each item + */ + public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) { + mSpanSizeLookup = spanSizeLookup; + } + + /** + * Returns the current {@link SpanSizeLookup} used by the GridLayoutManager. + * + * @return The current {@link SpanSizeLookup} used by the GridLayoutManager. + */ + public SpanSizeLookup getSpanSizeLookup() { + return mSpanSizeLookup; + } + + private void updateMeasurements() { + int totalSpace; + if (getOrientation() == VERTICAL) { + totalSpace = getWidth() - getPaddingRight() - getPaddingLeft(); + } else { + totalSpace = getHeight() - getPaddingBottom() - getPaddingTop(); + } + calculateItemBorders(totalSpace); + } + + @Override + public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { + if (mCachedBorders == null) { + super.setMeasuredDimension(childrenBounds, wSpec, hSpec); + } + final int width, height; + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + if (mOrientation == VERTICAL) { + final int usedHeight = childrenBounds.height() + verticalPadding; + height = chooseSize(hSpec, usedHeight, getMinimumHeight()); + width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1] + horizontalPadding, + getMinimumWidth()); + } else { + final int usedWidth = childrenBounds.width() + horizontalPadding; + width = chooseSize(wSpec, usedWidth, getMinimumWidth()); + height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1] + verticalPadding, + getMinimumHeight()); + } + setMeasuredDimension(width, height); + } + + /** + * @param totalSpace Total available space after padding is removed + */ + private void calculateItemBorders(int totalSpace) { + mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace); + } + + /** + * @param cachedBorders The out array + * @param spanCount number of spans + * @param totalSpace total available space after padding is removed + * @return The updated array. Might be the same instance as the provided array if its size + * has not changed. + */ + static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) { + if (cachedBorders == null || cachedBorders.length != spanCount + 1 + || cachedBorders[cachedBorders.length - 1] != totalSpace) { + cachedBorders = new int[spanCount + 1]; + } + cachedBorders[0] = 0; + int sizePerSpan = totalSpace / spanCount; + int sizePerSpanRemainder = totalSpace % spanCount; + int consumedPixels = 0; + int additionalSize = 0; + for (int i = 1; i <= spanCount; i++) { + int itemSize = sizePerSpan; + additionalSize += sizePerSpanRemainder; + if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) { + itemSize += 1; + additionalSize -= spanCount; + } + consumedPixels += itemSize; + cachedBorders[i] = consumedPixels; + } + return cachedBorders; + } + + int getSpaceForSpanRange(int startSpan, int spanSize) { + if (mOrientation == VERTICAL && isLayoutRTL()) { + return mCachedBorders[mSpanCount - startSpan] + - mCachedBorders[mSpanCount - startSpan - spanSize]; + } else { + return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan]; + } + } + + @Override + void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, + AnchorInfo anchorInfo, int itemDirection) { + super.onAnchorReady(recycler, state, anchorInfo, itemDirection); + updateMeasurements(); + if (state.getItemCount() > 0 && !state.isPreLayout()) { + ensureAnchorIsInCorrectSpan(recycler, state, anchorInfo, itemDirection); + } + ensureViewSet(); + } + + private void ensureViewSet() { + if (mSet == null || mSet.length != mSpanCount) { + mSet = new View[mSpanCount]; + } + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { + updateMeasurements(); + ensureViewSet(); + return super.scrollHorizontallyBy(dx, recycler, state); + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { + updateMeasurements(); + ensureViewSet(); + return super.scrollVerticallyBy(dy, recycler, state); + } + + private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler, + RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) { + final boolean layingOutInPrimaryDirection = + itemDirection == LayoutState.ITEM_DIRECTION_TAIL; + int span = getSpanIndex(recycler, state, anchorInfo.mPosition); + if (layingOutInPrimaryDirection) { + // choose span 0 + while (span > 0 && anchorInfo.mPosition > 0) { + anchorInfo.mPosition--; + span = getSpanIndex(recycler, state, anchorInfo.mPosition); + } + } else { + // choose the max span we can get. hopefully last one + final int indexLimit = state.getItemCount() - 1; + int pos = anchorInfo.mPosition; + int bestSpan = span; + while (pos < indexLimit) { + int next = getSpanIndex(recycler, state, pos + 1); + if (next > bestSpan) { + pos += 1; + bestSpan = next; + } else { + break; + } + } + anchorInfo.mPosition = pos; + } + } + + @Override + View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { + + int start = 0; + int end = getChildCount(); + int diff = 1; + if (traverseChildrenInReverseOrder) { + start = getChildCount() - 1; + end = -1; + diff = -1; + } + + int itemCount = state.getItemCount(); + + ensureLayoutState(); + View invalidMatch = null; + View outOfBoundsMatch = null; + + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); + + for (int i = start; i != end; i += diff) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + final int span = getSpanIndex(recycler, state, position); + if (span != 0) { + continue; + } + if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { + if (invalidMatch == null) { + invalidMatch = view; // removed item, least preferred + } + } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd + || mOrientationHelper.getDecoratedEnd(view) < boundsStart) { + if (outOfBoundsMatch == null) { + outOfBoundsMatch = view; // item is not visible, less preferred + } + } else { + return view; + } + } + } + return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; + } + + private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state, + int viewPosition) { + if (!state.isPreLayout()) { + return mSpanSizeLookup.getCachedSpanGroupIndex(viewPosition, mSpanCount); + } + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition); + if (adapterPosition == -1) { + if (DEBUG) { + throw new RuntimeException("Cannot find span group index for position " + + viewPosition); + } + Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition); + return 0; + } + return mSpanSizeLookup.getCachedSpanGroupIndex(adapterPosition, mSpanCount); + } + + private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { + if (!state.isPreLayout()) { + return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount); + } + final int cached = mPreLayoutSpanIndexCache.get(pos, -1); + if (cached != -1) { + return cached; + } + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); + if (adapterPosition == -1) { + if (DEBUG) { + throw new RuntimeException("Cannot find span index for pre layout position. It is" + + " not cached, not in the adapter. Pos:" + pos); + } + Log.w(TAG, "Cannot find span size for pre layout position. It is" + + " not cached, not in the adapter. Pos:" + pos); + return 0; + } + return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount); + } + + private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) { + if (!state.isPreLayout()) { + return mSpanSizeLookup.getSpanSize(pos); + } + final int cached = mPreLayoutSpanSizeCache.get(pos, -1); + if (cached != -1) { + return cached; + } + final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos); + if (adapterPosition == -1) { + if (DEBUG) { + throw new RuntimeException("Cannot find span size for pre layout position. It is" + + " not cached, not in the adapter. Pos:" + pos); + } + Log.w(TAG, "Cannot find span size for pre layout position. It is" + + " not cached, not in the adapter. Pos:" + pos); + return 1; + } + return mSpanSizeLookup.getSpanSize(adapterPosition); + } + + @Override + void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + int remainingSpan = mSpanCount; + int count = 0; + while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { + final int pos = layoutState.mCurrentPosition; + layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); + final int spanSize = mSpanSizeLookup.getSpanSize(pos); + remainingSpan -= spanSize; + layoutState.mCurrentPosition += layoutState.mItemDirection; + count++; + } + } + + @Override + void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, + LayoutState layoutState, LayoutChunkResult result) { + final int otherDirSpecMode = mOrientationHelper.getModeInOther(); + final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY; + final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0; + // if grid layout's dimensions are not specified, let the new row change the measurements + // This is not perfect since we not covering all rows but still solves an important case + // where they may have a header row which should be laid out according to children. + if (flexibleInOtherDir) { + updateMeasurements(); // reset measurements + } + final boolean layingOutInPrimaryDirection = + layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL; + int count = 0; + int remainingSpan = mSpanCount; + if (!layingOutInPrimaryDirection) { + int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition); + int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition); + remainingSpan = itemSpanIndex + itemSpanSize; + } + while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) { + int pos = layoutState.mCurrentPosition; + final int spanSize = getSpanSize(recycler, state, pos); + if (spanSize > mSpanCount) { + throw new IllegalArgumentException("Item at position " + pos + " requires " + + spanSize + " spans but GridLayoutManager has only " + mSpanCount + + " spans."); + } + remainingSpan -= spanSize; + if (remainingSpan < 0) { + break; // item did not fit into this row or column + } + View view = layoutState.next(recycler); + if (view == null) { + break; + } + mSet[count] = view; + count++; + } + + if (count == 0) { + result.mFinished = true; + return; + } + + int maxSize = 0; + float maxSizeInOther = 0; // use a float to get size per span + + // we should assign spans before item decor offsets are calculated + assignSpans(recycler, state, count, layingOutInPrimaryDirection); + for (int i = 0; i < count; i++) { + View view = mSet[i]; + if (layoutState.mScrapList == null) { + if (layingOutInPrimaryDirection) { + addView(view); + } else { + addView(view, 0); + } + } else { + if (layingOutInPrimaryDirection) { + addDisappearingView(view); + } else { + addDisappearingView(view, 0); + } + } + calculateItemDecorationsForChild(view, mDecorInsets); + + measureChild(view, otherDirSpecMode, false); + final int size = mOrientationHelper.getDecoratedMeasurement(view); + if (size > maxSize) { + maxSize = size; + } + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) + / lp.mSpanSize; + if (otherSize > maxSizeInOther) { + maxSizeInOther = otherSize; + } + } + if (flexibleInOtherDir) { + // re-distribute columns + guessMeasurement(maxSizeInOther, currentOtherDirSize); + // now we should re-measure any item that was match parent. + maxSize = 0; + for (int i = 0; i < count; i++) { + View view = mSet[i]; + measureChild(view, View.MeasureSpec.EXACTLY, true); + final int size = mOrientationHelper.getDecoratedMeasurement(view); + if (size > maxSize) { + maxSize = size; + } + } + } + + // Views that did not measure the maxSize has to be re-measured + // We will stop doing this once we introduce Gravity in the GLM layout params + for (int i = 0; i < count; i++) { + final View view = mSet[i]; + if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) { + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect decorInsets = lp.mDecorInsets; + final int verticalInsets = decorInsets.top + decorInsets.bottom + + lp.topMargin + lp.bottomMargin; + final int horizontalInsets = decorInsets.left + decorInsets.right + + lp.leftMargin + lp.rightMargin; + final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); + final int wSpec; + final int hSpec; + if (mOrientation == VERTICAL) { + wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, + horizontalInsets, lp.width, false); + hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets, + View.MeasureSpec.EXACTLY); + } else { + wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets, + View.MeasureSpec.EXACTLY); + hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY, + verticalInsets, lp.height, false); + } + measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true); + } + } + + result.mConsumed = maxSize; + + int left = 0, right = 0, top = 0, bottom = 0; + if (mOrientation == VERTICAL) { + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + bottom = layoutState.mOffset; + top = bottom - maxSize; + } else { + top = layoutState.mOffset; + bottom = top + maxSize; + } + } else { + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + right = layoutState.mOffset; + left = right - maxSize; + } else { + left = layoutState.mOffset; + right = left + maxSize; + } + } + for (int i = 0; i < count; i++) { + View view = mSet[i]; + LayoutParams params = (LayoutParams) view.getLayoutParams(); + if (mOrientation == VERTICAL) { + if (isLayoutRTL()) { + right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex]; + left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); + } else { + left = getPaddingLeft() + mCachedBorders[params.mSpanIndex]; + right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); + } + } else { + top = getPaddingTop() + mCachedBorders[params.mSpanIndex]; + bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); + } + // We calculate everything with View's bounding box (which includes decor and margins) + // To calculate correct layout position, we subtract margins. + layoutDecoratedWithMargins(view, left, top, right, bottom); + if (DEBUG) { + Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin) + + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize); + } + // Consume the available space if the view is not removed OR changed + if (params.isItemRemoved() || params.isItemChanged()) { + result.mIgnoreConsumed = true; + } + result.mFocusable |= view.hasFocusable(); + } + Arrays.fill(mSet, null); + } + + /** + * Measures a child with currently known information. This is not necessarily the child's final + * measurement. (see fillChunk for details). + * + * @param view The child view to be measured + * @param otherDirParentSpecMode The RV measure spec that should be used in the secondary + * orientation + * @param alreadyMeasured True if we've already measured this view once + */ + private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) { + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect decorInsets = lp.mDecorInsets; + final int verticalInsets = decorInsets.top + decorInsets.bottom + + lp.topMargin + lp.bottomMargin; + final int horizontalInsets = decorInsets.left + decorInsets.right + + lp.leftMargin + lp.rightMargin; + final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize); + final int wSpec; + final int hSpec; + if (mOrientation == VERTICAL) { + wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, + horizontalInsets, lp.width, false); + hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(), + verticalInsets, lp.height, true); + } else { + hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode, + verticalInsets, lp.height, false); + wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(), + horizontalInsets, lp.width, true); + } + measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured); + } + + /** + * This is called after laying out a row (if vertical) or a column (if horizontal) when the + * RecyclerView does not have exact measurement specs. + *

+ * Here we try to assign a best guess width or height and re-do the layout to update other + * views that wanted to MATCH_PARENT in the non-scroll orientation. + * + * @param maxSizeInOther The maximum size per span ratio from the measurement of the children. + * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below. + */ + private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) { + final int contentSize = Math.round(maxSizeInOther * mSpanCount); + // always re-calculate because borders were stretched during the fill + calculateItemBorders(Math.max(contentSize, currentOtherDirSize)); + } + + private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec, + boolean alreadyMeasured) { + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + final boolean measure; + if (alreadyMeasured) { + measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp); + } else { + measure = shouldMeasureChild(child, widthSpec, heightSpec, lp); + } + if (measure) { + child.measure(widthSpec, heightSpec); + } + } + + private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count, + boolean layingOutInPrimaryDirection) { + // spans are always assigned from 0 to N no matter if it is RTL or not. + // RTL is used only when positioning the view. + int span, start, end, diff; + // make sure we traverse from min position to max position + if (layingOutInPrimaryDirection) { + start = 0; + end = count; + diff = 1; + } else { + start = count - 1; + end = -1; + diff = -1; + } + span = 0; + for (int i = start; i != end; i += diff) { + View view = mSet[i]; + LayoutParams params = (LayoutParams) view.getLayoutParams(); + params.mSpanSize = getSpanSize(recycler, state, getPosition(view)); + params.mSpanIndex = span; + span += params.mSpanSize; + } + } + + /** + * Returns the number of spans laid out by this grid. + * + * @return The number of spans + * @see #setSpanCount(int) + */ + public int getSpanCount() { + return mSpanCount; + } + + /** + * Sets the number of spans to be laid out. + *

+ * If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns. + * If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows. + * + * @param spanCount The total number of spans in the grid + * @see #getSpanCount() + */ + public void setSpanCount(int spanCount) { + if (spanCount == mSpanCount) { + return; + } + mPendingSpanCountChange = true; + if (spanCount < 1) { + throw new IllegalArgumentException("Span count should be at least 1. Provided " + + spanCount); + } + mSpanCount = spanCount; + mSpanSizeLookup.invalidateSpanIndexCache(); + requestLayout(); + } + + /** + * A helper class to provide the number of spans each item occupies. + *

+ * Default implementation sets each item to occupy exactly 1 span. + * + * @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup) + */ + public abstract static class SpanSizeLookup { + + final SparseIntArray mSpanIndexCache = new SparseIntArray(); + final SparseIntArray mSpanGroupIndexCache = new SparseIntArray(); + + private boolean mCacheSpanIndices = false; + private boolean mCacheSpanGroupIndices = false; + + /** + * Returns the number of span occupied by the item at position. + * + * @param position The adapter position of the item + * @return The number of spans occupied by the item at the provided position + */ + public abstract int getSpanSize(int position); + + /** + * Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or + * not. By default these values are not cached. If you are not overriding + * {@link #getSpanIndex(int, int)} with something highly performant, you should set this + * to true for better performance. + * + * @param cacheSpanIndices Whether results of getSpanIndex should be cached or not. + */ + public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) { + if (!cacheSpanIndices) { + mSpanGroupIndexCache.clear(); + } + mCacheSpanIndices = cacheSpanIndices; + } + + /** + * Sets whether the results of {@link #getSpanGroupIndex(int, int)} method should be cached + * or not. By default these values are not cached. If you are not overriding + * {@link #getSpanGroupIndex(int, int)} with something highly performant, and you are using + * spans to calculate scrollbar offset and range, you should set this to true for better + * performance. + * + * @param cacheSpanGroupIndices Whether results of getGroupSpanIndex should be cached or + * not. + */ + public void setSpanGroupIndexCacheEnabled(boolean cacheSpanGroupIndices) { + if (!cacheSpanGroupIndices) { + mSpanGroupIndexCache.clear(); + } + mCacheSpanGroupIndices = cacheSpanGroupIndices; + } + + /** + * Clears the span index cache. GridLayoutManager automatically calls this method when + * adapter changes occur. + */ + public void invalidateSpanIndexCache() { + mSpanIndexCache.clear(); + } + + /** + * Clears the span group index cache. GridLayoutManager automatically calls this method + * when adapter changes occur. + */ + public void invalidateSpanGroupIndexCache() { + mSpanGroupIndexCache.clear(); + } + + /** + * Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not. + * + * @return True if results of {@link #getSpanIndex(int, int)} are cached. + */ + public boolean isSpanIndexCacheEnabled() { + return mCacheSpanIndices; + } + + /** + * Returns whether results of {@link #getSpanGroupIndex(int, int)} method are cached or not. + * + * @return True if results of {@link #getSpanGroupIndex(int, int)} are cached. + */ + public boolean isSpanGroupIndexCacheEnabled() { + return mCacheSpanGroupIndices; + } + + int getCachedSpanIndex(int position, int spanCount) { + if (!mCacheSpanIndices) { + return getSpanIndex(position, spanCount); + } + final int existing = mSpanIndexCache.get(position, -1); + if (existing != -1) { + return existing; + } + final int value = getSpanIndex(position, spanCount); + mSpanIndexCache.put(position, value); + return value; + } + + int getCachedSpanGroupIndex(int position, int spanCount) { + if (!mCacheSpanGroupIndices) { + return getSpanGroupIndex(position, spanCount); + } + final int existing = mSpanGroupIndexCache.get(position, -1); + if (existing != -1) { + return existing; + } + final int value = getSpanGroupIndex(position, spanCount); + mSpanGroupIndexCache.put(position, value); + return value; + } + + /** + * Returns the final span index of the provided position. + *

+ * If you have a faster way to calculate span index for your items, you should override + * this method. Otherwise, you should enable span index cache + * ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is + * disabled, default implementation traverses all items from 0 to + * position. When caching is enabled, it calculates from the closest cached + * value before the position. + *

+ * If you override this method, you need to make sure it is consistent with + * {@link #getSpanSize(int)}. GridLayoutManager does not call this method for + * each item. It is called only for the reference item and rest of the items + * are assigned to spans based on the reference item. For example, you cannot assign a + * position to span 2 while span 1 is empty. + *

+ * Note that span offsets always start with 0 and are not affected by RTL. + * + * @param position The position of the item + * @param spanCount The total number of spans in the grid + * @return The final span position of the item. Should be between 0 (inclusive) and + * spanCount(exclusive) + */ + public int getSpanIndex(int position, int spanCount) { + int positionSpanSize = getSpanSize(position); + if (positionSpanSize == spanCount) { + return 0; // quick return for full-span items + } + int span = 0; + int startPos = 0; + // If caching is enabled, try to jump + if (mCacheSpanIndices) { + int prevKey = findFirstKeyLessThan(mSpanIndexCache, position); + if (prevKey >= 0) { + span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey); + startPos = prevKey + 1; + } + } + for (int i = startPos; i < position; i++) { + int size = getSpanSize(i); + span += size; + if (span == spanCount) { + span = 0; + } else if (span > spanCount) { + // did not fit, moving to next row / column + span = size; + } + } + if (span + positionSpanSize <= spanCount) { + return span; + } + return 0; + } + + static int findFirstKeyLessThan(SparseIntArray cache, int position) { + int lo = 0; + int hi = cache.size() - 1; + + while (lo <= hi) { + // Using unsigned shift here to divide by two because it is guaranteed to not + // overflow. + final int mid = (lo + hi) >>> 1; + final int midVal = cache.keyAt(mid); + if (midVal < position) { + lo = mid + 1; + } else { + hi = mid - 1; + } + } + int index = lo - 1; + if (index >= 0 && index < cache.size()) { + return cache.keyAt(index); + } + return -1; + } + + /** + * Returns the index of the group this position belongs. + *

+ * For example, if grid has 3 columns and each item occupies 1 span, span group index + * for item 1 will be 0, item 5 will be 1. + * + * @param adapterPosition The position in adapter + * @param spanCount The total number of spans in the grid + * @return The index of the span group including the item at the given adapter position + */ + public int getSpanGroupIndex(int adapterPosition, int spanCount) { + int span = 0; + int group = 0; + int start = 0; + if (mCacheSpanGroupIndices) { + // This finds the first non empty cached group cache key. + int prevKey = findFirstKeyLessThan(mSpanGroupIndexCache, adapterPosition); + if (prevKey != -1) { + group = mSpanGroupIndexCache.get(prevKey); + start = prevKey + 1; + span = getCachedSpanIndex(prevKey, spanCount) + getSpanSize(prevKey); + if (span == spanCount) { + span = 0; + group++; + } + } + } + int positionSpanSize = getSpanSize(adapterPosition); + for (int i = start; i < adapterPosition; i++) { + int size = getSpanSize(i); + span += size; + if (span == spanCount) { + span = 0; + group++; + } else if (span > spanCount) { + // did not fit, moving to next row / column + span = size; + group++; + } + } + if (span + positionSpanSize > spanCount) { + group++; + } + return group; + } + } + + @Override + public View onFocusSearchFailed(View focused, int direction, + RecyclerView.Recycler recycler, RecyclerView.State state) { + View prevFocusedChild = findContainingItemView(focused); + if (prevFocusedChild == null) { + return null; + } + LayoutParams lp = (LayoutParams) prevFocusedChild.getLayoutParams(); + final int prevSpanStart = lp.mSpanIndex; + final int prevSpanEnd = lp.mSpanIndex + lp.mSpanSize; + View view = super.onFocusSearchFailed(focused, direction, recycler, state); + if (view == null) { + return null; + } + // LinearLayoutManager finds the last child. What we want is the child which has the same + // spanIndex. + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); + final boolean ascend = (layoutDir == LayoutState.LAYOUT_END) != mShouldReverseLayout; + final int start, inc, limit; + if (ascend) { + start = getChildCount() - 1; + inc = -1; + limit = -1; + } else { + start = 0; + inc = 1; + limit = getChildCount(); + } + final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL(); + + // The focusable candidate to be picked if no perfect focusable candidate is found. + // The best focusable candidate is the one with the highest amount of span overlap with + // the currently focused view. + View focusableWeakCandidate = null; // somewhat matches but not strong + int focusableWeakCandidateSpanIndex = -1; + int focusableWeakCandidateOverlap = 0; // how many spans overlap + + // The unfocusable candidate to become visible on the screen next, if no perfect or + // weak focusable candidates are found to receive focus next. + // We are only interested in partially visible unfocusable views. These are views that are + // not fully visible, that is either partially overlapping, or out-of-bounds and right below + // or above RV's padded bounded area. The best unfocusable candidate is the one with the + // highest amount of span overlap with the currently focused view. + View unfocusableWeakCandidate = null; // somewhat matches but not strong + int unfocusableWeakCandidateSpanIndex = -1; + int unfocusableWeakCandidateOverlap = 0; // how many spans overlap + + // The span group index of the start child. This indicates the span group index of the + // next focusable item to receive focus, if a focusable item within the same span group + // exists. Any focusable item beyond this group index are not relevant since they + // were already stored in the layout before onFocusSearchFailed call and were not picked + // by the focusSearch algorithm. + int focusableSpanGroupIndex = getSpanGroupIndex(recycler, state, start); + for (int i = start; i != limit; i += inc) { + int spanGroupIndex = getSpanGroupIndex(recycler, state, i); + View candidate = getChildAt(i); + if (candidate == prevFocusedChild) { + break; + } + + if (candidate.hasFocusable() && spanGroupIndex != focusableSpanGroupIndex) { + // We are past the allowable span group index for the next focusable item. + // The search only continues if no focusable weak candidates have been found up + // until this point, in order to find the best unfocusable candidate to become + // visible on the screen next. + if (focusableWeakCandidate != null) { + break; + } + continue; + } + + final LayoutParams candidateLp = (LayoutParams) candidate.getLayoutParams(); + final int candidateStart = candidateLp.mSpanIndex; + final int candidateEnd = candidateLp.mSpanIndex + candidateLp.mSpanSize; + if (candidate.hasFocusable() && candidateStart == prevSpanStart + && candidateEnd == prevSpanEnd) { + return candidate; // perfect match + } + boolean assignAsWeek = false; + if ((candidate.hasFocusable() && focusableWeakCandidate == null) + || (!candidate.hasFocusable() && unfocusableWeakCandidate == null)) { + assignAsWeek = true; + } else { + int maxStart = Math.max(candidateStart, prevSpanStart); + int minEnd = Math.min(candidateEnd, prevSpanEnd); + int overlap = minEnd - maxStart; + if (candidate.hasFocusable()) { + if (overlap > focusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == focusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > focusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } + } else if (focusableWeakCandidate == null + && isViewPartiallyVisible(candidate, false, true)) { + if (overlap > unfocusableWeakCandidateOverlap) { + assignAsWeek = true; + } else if (overlap == unfocusableWeakCandidateOverlap + && preferLastSpan == (candidateStart + > unfocusableWeakCandidateSpanIndex)) { + assignAsWeek = true; + } + } + } + + if (assignAsWeek) { + if (candidate.hasFocusable()) { + focusableWeakCandidate = candidate; + focusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + focusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } else { + unfocusableWeakCandidate = candidate; + unfocusableWeakCandidateSpanIndex = candidateLp.mSpanIndex; + unfocusableWeakCandidateOverlap = Math.min(candidateEnd, prevSpanEnd) + - Math.max(candidateStart, prevSpanStart); + } + } + } + return (focusableWeakCandidate != null) ? focusableWeakCandidate : unfocusableWeakCandidate; + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null && !mPendingSpanCountChange; + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollRangeWithSpanInfo(state); + } else { + return super.computeHorizontalScrollRange(state); + } + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollRangeWithSpanInfo(state); + } else { + return super.computeVerticalScrollRange(state); + } + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollOffsetWithSpanInfo(state); + } else { + return super.computeHorizontalScrollOffset(state); + } + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + if (mUsingSpansToEstimateScrollBarDimensions) { + return computeScrollOffsetWithSpanInfo(state); + } else { + return super.computeVerticalScrollOffset(state); + } + } + + /** + * When this flag is set, the scroll offset and scroll range calculations will take account + * of span information. + * + *

This is will increase the accuracy of the scroll bar's size and offset but will require + * more calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)}". + * + *

This additional accuracy may or may not be needed, depending on the characteristics of + * your layout. You will likely benefit from this accuracy when: + * + *

    + *
  • The variation in item span sizes is large. + *
  • The size of your data set is small (if your data set is large, the scrollbar will + * likely be very small anyway, and thus the increased accuracy has less impact). + *
  • Calls to {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast. + *
+ * + *

If you decide to enable this feature, you should be sure that calls to + * {@link SpanSizeLookup#getSpanGroupIndex(int, int)} are fast, that set span group index + * caching is set to true via a call to + * {@link SpanSizeLookup#setSpanGroupIndexCacheEnabled(boolean), + * and span index caching is also enabled via a call to + * {@link SpanSizeLookup#setSpanIndexCacheEnabled(boolean)}}. + */ + public void setUsingSpansToEstimateScrollbarDimensions( + boolean useSpansToEstimateScrollBarDimensions) { + mUsingSpansToEstimateScrollBarDimensions = useSpansToEstimateScrollBarDimensions; + } + + /** + * Returns true if the scroll offset and scroll range calculations take account of span + * information. See {@link #setUsingSpansToEstimateScrollbarDimensions(boolean)} for more + * information on this topic. Defaults to {@code false}. + * + * @return true if the scroll offset and scroll range calculations take account of span + * information. + */ + public boolean isUsingSpansToEstimateScrollbarDimensions() { + return mUsingSpansToEstimateScrollBarDimensions; + } + + private int computeScrollRangeWithSpanInfo(RecyclerView.State state) { + if (getChildCount() == 0 || state.getItemCount() == 0) { + return 0; + } + ensureLayoutState(); + + View startChild = findFirstVisibleChildClosestToStart(!isSmoothScrollbarEnabled(), true); + View endChild = findFirstVisibleChildClosestToEnd(!isSmoothScrollbarEnabled(), true); + + if (startChild == null || endChild == null) { + return 0; + } + if (!isSmoothScrollbarEnabled()) { + return mSpanSizeLookup.getCachedSpanGroupIndex( + state.getItemCount() - 1, mSpanCount) + 1; + } + + // smooth scrollbar enabled. try to estimate better. + final int laidOutArea = mOrientationHelper.getDecoratedEnd(endChild) + - mOrientationHelper.getDecoratedStart(startChild); + + final int firstVisibleSpan = + mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); + final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, + mSpanCount) + 1; + final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; + + // estimate a size for full list. + return (int) (((float) laidOutArea / laidOutSpans) * totalSpans); + } + + private int computeScrollOffsetWithSpanInfo(RecyclerView.State state) { + if (getChildCount() == 0 || state.getItemCount() == 0) { + return 0; + } + ensureLayoutState(); + + boolean smoothScrollEnabled = isSmoothScrollbarEnabled(); + View startChild = findFirstVisibleChildClosestToStart(!smoothScrollEnabled, true); + View endChild = findFirstVisibleChildClosestToEnd(!smoothScrollEnabled, true); + if (startChild == null || endChild == null) { + return 0; + } + int startChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), + mSpanCount); + int endChildSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + + final int minSpan = Math.min(startChildSpan, endChildSpan); + final int maxSpan = Math.max(startChildSpan, endChildSpan); + final int totalSpans = mSpanSizeLookup.getCachedSpanGroupIndex(state.getItemCount() - 1, + mSpanCount) + 1; + + final int spansBefore = mShouldReverseLayout + ? Math.max(0, totalSpans - maxSpan - 1) + : Math.max(0, minSpan); + if (!smoothScrollEnabled) { + return spansBefore; + } + final int laidOutArea = Math.abs(mOrientationHelper.getDecoratedEnd(endChild) + - mOrientationHelper.getDecoratedStart(startChild)); + + final int firstVisibleSpan = + mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(startChild), mSpanCount); + final int lastVisibleSpan = mSpanSizeLookup.getCachedSpanGroupIndex(getPosition(endChild), + mSpanCount); + final int laidOutSpans = lastVisibleSpan - firstVisibleSpan + 1; + final float avgSizePerSpan = (float) laidOutArea / laidOutSpans; + + return Math.round(spansBefore * avgSizePerSpan + (mOrientationHelper.getStartAfterPadding() + - mOrientationHelper.getDecoratedStart(startChild))); + } + + /** + * Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span. + */ + public static final class DefaultSpanSizeLookup extends SpanSizeLookup { + + @Override + public int getSpanSize(int position) { + return 1; + } + + @Override + public int getSpanIndex(int position, int spanCount) { + return position % spanCount; + } + } + + /** + * LayoutParams used by GridLayoutManager. + *

+ * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the + * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is + * expected to fill all of the space given to it. + */ + public static class LayoutParams extends RecyclerView.LayoutParams { + + /** + * Span Id for Views that are not laid out yet. + */ + public static final int INVALID_SPAN_ID = -1; + + int mSpanIndex = INVALID_SPAN_ID; + + int mSpanSize = 0; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + /** + * Returns the current span index of this View. If the View is not laid out yet, the return + * value is undefined. + *

+ * Starting with RecyclerView 24.2.0, span indices are always indexed from position 0 + * even if the layout is RTL. In a vertical GridLayoutManager, leftmost span is span + * 0 if the layout is LTR and rightmost span is span 0 if the layout is + * RTL. Prior to 24.2.0, it was the opposite which was conflicting with + * {@link SpanSizeLookup#getSpanIndex(int, int)}. + *

+ * If the View occupies multiple spans, span with the minimum index is returned. + * + * @return The span index of the View. + */ + public int getSpanIndex() { + return mSpanIndex; + } + + /** + * Returns the number of spans occupied by this View. If the View not laid out yet, the + * return value is undefined. + * + * @return The number of spans occupied by this View. + */ + public int getSpanSize() { + return mSpanSize; + } + } + +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java b/app/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java new file mode 100644 index 0000000000..2865dadd18 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ItemTouchHelper.java @@ -0,0 +1,2494 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.animation.Interpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.view.GestureDetectorCompat; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.R; +import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a utility class to add swipe to dismiss and drag & drop support to RecyclerView. + *

+ * It works with a RecyclerView and a Callback class, which configures what type of interactions + * are enabled and also receives events when user performs these actions. + *

+ * Depending on which functionality you support, you should override + * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder)} and / or + * {@link Callback#onSwiped(ViewHolder, int)}. + *

+ * This class is designed to work with any LayoutManager but for certain situations, it can be + * optimized for your custom LayoutManager by extending methods in the + * {@link ItemTouchHelper.Callback} class or implementing {@link ItemTouchHelper.ViewDropHandler} + * interface in your LayoutManager. + *

+ * By default, ItemTouchHelper moves the items' translateX/Y properties to reposition them. You can + * customize these behaviors by overriding {@link Callback#onChildDraw(Canvas, RecyclerView, + * ViewHolder, float, float, int, boolean)} + * or {@link Callback#onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean)}. + *

+ * Most of the time you only need to override onChildDraw. + */ +public class ItemTouchHelper extends RecyclerView.ItemDecoration + implements RecyclerView.OnChildAttachStateChangeListener { + + /** + * Up direction, used for swipe & drag control. + */ + public static final int UP = 1; + + /** + * Down direction, used for swipe & drag control. + */ + public static final int DOWN = 1 << 1; + + /** + * Left direction, used for swipe & drag control. + */ + public static final int LEFT = 1 << 2; + + /** + * Right direction, used for swipe & drag control. + */ + public static final int RIGHT = 1 << 3; + + // If you change these relative direction values, update Callback#convertToAbsoluteDirection, + // Callback#convertToRelativeDirection. + /** + * Horizontal start direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout + * direction. Used for swipe & drag control. + */ + public static final int START = LEFT << 2; + + /** + * Horizontal end direction. Resolved to LEFT or RIGHT depending on RecyclerView's layout + * direction. Used for swipe & drag control. + */ + public static final int END = RIGHT << 2; + + /** + * ItemTouchHelper is in idle state. At this state, either there is no related motion event by + * the user or latest motion events have not yet triggered a swipe or drag. + */ + public static final int ACTION_STATE_IDLE = 0; + + /** + * A View is currently being swiped. + */ + @SuppressWarnings("WeakerAccess") + public static final int ACTION_STATE_SWIPE = 1; + + /** + * A View is currently being dragged. + */ + @SuppressWarnings("WeakerAccess") + public static final int ACTION_STATE_DRAG = 2; + + /** + * Animation type for views which are swiped successfully. + */ + @SuppressWarnings("WeakerAccess") + public static final int ANIMATION_TYPE_SWIPE_SUCCESS = 1 << 1; + + /** + * Animation type for views which are not completely swiped thus will animate back to their + * original position. + */ + @SuppressWarnings("WeakerAccess") + public static final int ANIMATION_TYPE_SWIPE_CANCEL = 1 << 2; + + /** + * Animation type for views that were dragged and now will animate to their final position. + */ + @SuppressWarnings("WeakerAccess") + public static final int ANIMATION_TYPE_DRAG = 1 << 3; + + private static final String TAG = "ItemTouchHelper"; + + private static final boolean DEBUG = false; + + private static final int ACTIVE_POINTER_ID_NONE = -1; + + static final int DIRECTION_FLAG_COUNT = 8; + + private static final int ACTION_MODE_IDLE_MASK = (1 << DIRECTION_FLAG_COUNT) - 1; + + static final int ACTION_MODE_SWIPE_MASK = ACTION_MODE_IDLE_MASK << DIRECTION_FLAG_COUNT; + + static final int ACTION_MODE_DRAG_MASK = ACTION_MODE_SWIPE_MASK << DIRECTION_FLAG_COUNT; + + /** + * The unit we are using to track velocity + */ + private static final int PIXELS_PER_SECOND = 1000; + + /** + * Views, whose state should be cleared after they are detached from RecyclerView. + * This is necessary after swipe dismissing an item. We wait until animator finishes its job + * to clean these views. + */ + final List mPendingCleanup = new ArrayList<>(); + + /** + * Re-use array to calculate dx dy for a ViewHolder + */ + private final float[] mTmpPosition = new float[2]; + + /** + * Currently selected view holder + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + ViewHolder mSelected = null; + + /** + * The reference coordinates for the action start. For drag & drop, this is the time long + * press is completed vs for swipe, this is the initial touch point. + */ + float mInitialTouchX; + + float mInitialTouchY; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + private float mSwipeEscapeVelocity; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + private float mMaxSwipeVelocity; + + /** + * The diff between the last event and initial touch. + */ + float mDx; + + float mDy; + + /** + * The coordinates of the selected view at the time it is selected. We record these values + * when action starts so that we can consistently position it even if LayoutManager moves the + * View. + */ + private float mSelectedStartX; + + private float mSelectedStartY; + + /** + * The pointer we are tracking. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mActivePointerId = ACTIVE_POINTER_ID_NONE; + + /** + * Developer callback which controls the behavior of ItemTouchHelper. + */ + @NonNull + Callback mCallback; + + /** + * Current mode. + */ + private int mActionState = ACTION_STATE_IDLE; + + /** + * The direction flags obtained from unmasking + * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current + * action state. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mSelectedFlags; + + /** + * When a View is dragged or swiped and needs to go back to where it was, we create a Recover + * Animation and animate it to its location using this custom Animator, instead of using + * framework Animators. + * Using framework animators has the side effect of clashing with ItemAnimator, creating + * jumpy UIs. + */ + @VisibleForTesting + List mRecoverAnimations = new ArrayList<>(); + + private int mSlop; + + RecyclerView mRecyclerView; + + /** + * When user drags a view to the edge, we start scrolling the LayoutManager as long as View + * is partially out of bounds. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final Runnable mScrollRunnable = new Runnable() { + @Override + public void run() { + if (mSelected != null && scrollIfNecessary()) { + if (mSelected != null) { //it might be lost during scrolling + moveIfNecessary(mSelected); + } + mRecyclerView.removeCallbacks(mScrollRunnable); + ViewCompat.postOnAnimation(mRecyclerView, this); + } + } + }; + + /** + * Used for detecting fling swipe + */ + VelocityTracker mVelocityTracker; + + //re-used list for selecting a swap target + private List mSwapTargets; + + //re used for for sorting swap targets + private List mDistances; + + /** + * If drag & drop is supported, we use child drawing order to bring them to front. + */ + private RecyclerView.ChildDrawingOrderCallback mChildDrawingOrderCallback = null; + + /** + * This keeps a reference to the child dragged by the user. Even after user stops dragging, + * until view reaches its final position (end of recover animation), we keep a reference so + * that it can be drawn above other children. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + View mOverdrawChild = null; + + /** + * We cache the position of the overdraw child to avoid recalculating it each time child + * position callback is called. This value is invalidated whenever a child is attached or + * detached. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + int mOverdrawChildPosition = -1; + + /** + * Used to detect long press. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + GestureDetectorCompat mGestureDetector; + + /** + * Callback for when long press occurs. + */ + private ItemTouchHelperGestureListener mItemTouchHelperGestureListener; + + private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, + @NonNull MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event); + } + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + mActivePointerId = event.getPointerId(0); + mInitialTouchX = event.getX(); + mInitialTouchY = event.getY(); + obtainVelocityTracker(); + if (mSelected == null) { + final RecoverAnimation animation = findAnimation(event); + if (animation != null) { + mInitialTouchX -= animation.mX; + mInitialTouchY -= animation.mY; + endRecoverAnimation(animation.mViewHolder, true); + if (mPendingCleanup.remove(animation.mViewHolder.itemView)) { + mCallback.clearView(mRecyclerView, animation.mViewHolder); + } + select(animation.mViewHolder, animation.mActionState); + updateDxDy(event, mSelectedFlags, 0); + } + } + } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mActivePointerId = ACTIVE_POINTER_ID_NONE; + select(null, ACTION_STATE_IDLE); + } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) { + // in a non scroll orientation, if distance change is above threshold, we + // can select the item + final int index = event.findPointerIndex(mActivePointerId); + if (DEBUG) { + Log.d(TAG, "pointer index " + index); + } + if (index >= 0) { + checkSelectForSwipe(action, event, index); + } + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + return mSelected != null; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) { + mGestureDetector.onTouchEvent(event); + if (DEBUG) { + Log.d(TAG, + "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event); + } + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(event); + } + if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { + return; + } + final int action = event.getActionMasked(); + final int activePointerIndex = event.findPointerIndex(mActivePointerId); + if (activePointerIndex >= 0) { + checkSelectForSwipe(action, event, activePointerIndex); + } + ViewHolder viewHolder = mSelected; + if (viewHolder == null) { + return; + } + switch (action) { + case MotionEvent.ACTION_MOVE: { + // Find the index of the active pointer and fetch its position + if (activePointerIndex >= 0) { + updateDxDy(event, mSelectedFlags, activePointerIndex); + moveIfNecessary(viewHolder); + mRecyclerView.removeCallbacks(mScrollRunnable); + mScrollRunnable.run(); + mRecyclerView.invalidate(); + } + break; + } + case MotionEvent.ACTION_CANCEL: + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + // fall through + case MotionEvent.ACTION_UP: + select(null, ACTION_STATE_IDLE); + mActivePointerId = ACTIVE_POINTER_ID_NONE; + break; + case MotionEvent.ACTION_POINTER_UP: { + final int pointerIndex = event.getActionIndex(); + final int pointerId = event.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = event.getPointerId(newPointerIndex); + updateDxDy(event, mSelectedFlags, pointerIndex); + } + break; + } + } + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (!disallowIntercept) { + return; + } + select(null, ACTION_STATE_IDLE); + } + }; + + /** + * Temporary rect instance that is used when we need to lookup Item decorations. + */ + private Rect mTmpRect; + + /** + * When user started to drag scroll. Reset when we don't scroll + */ + private long mDragScrollStartTimeInMs; + + /** + * Creates an ItemTouchHelper that will work with the given Callback. + *

+ * You can attach ItemTouchHelper to a RecyclerView via + * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration, + * an onItemTouchListener and a Child attach / detach listener to the RecyclerView. + * + * @param callback The Callback which controls the behavior of this touch helper. + */ + public ItemTouchHelper(@NonNull Callback callback) { + mCallback = callback; + } + + private static boolean hitTest(View child, float x, float y, float left, float top) { + return x >= left + && x <= left + child.getWidth() + && y >= top + && y <= top + child.getHeight(); + } + + /** + * Attaches the ItemTouchHelper to the provided RecyclerView. If TouchHelper is already + * attached to a RecyclerView, it will first detach from the previous one. You can call this + * method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove ItemTouchHelper from the current + * RecyclerView. + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (recyclerView != null) { + final Resources resources = recyclerView.getResources(); + mSwipeEscapeVelocity = resources + .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity); + mMaxSwipeVelocity = resources + .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity); + setupCallbacks(); + } + } + + private void setupCallbacks() { + ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext()); + mSlop = vc.getScaledTouchSlop(); + mRecyclerView.addItemDecoration(this); + mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); + mRecyclerView.addOnChildAttachStateChangeListener(this); + startGestureDetection(); + } + + private void destroyCallbacks() { + mRecyclerView.removeItemDecoration(this); + mRecyclerView.removeOnItemTouchListener(mOnItemTouchListener); + mRecyclerView.removeOnChildAttachStateChangeListener(this); + // clean all attached + final int recoverAnimSize = mRecoverAnimations.size(); + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation recoverAnimation = mRecoverAnimations.get(0); + recoverAnimation.cancel(); + mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); + } + mRecoverAnimations.clear(); + mOverdrawChild = null; + mOverdrawChildPosition = -1; + releaseVelocityTracker(); + stopGestureDetection(); + } + + private void startGestureDetection() { + mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener(); + mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(), + mItemTouchHelperGestureListener); + } + + private void stopGestureDetection() { + if (mItemTouchHelperGestureListener != null) { + mItemTouchHelperGestureListener.doNotReactToLongPress(); + mItemTouchHelperGestureListener = null; + } + if (mGestureDetector != null) { + mGestureDetector = null; + } + } + + private void getSelectedDxDy(float[] outPosition) { + if ((mSelectedFlags & (LEFT | RIGHT)) != 0) { + outPosition[0] = mSelectedStartX + mDx - mSelected.itemView.getLeft(); + } else { + outPosition[0] = mSelected.itemView.getTranslationX(); + } + if ((mSelectedFlags & (UP | DOWN)) != 0) { + outPosition[1] = mSelectedStartY + mDy - mSelected.itemView.getTop(); + } else { + outPosition[1] = mSelected.itemView.getTranslationY(); + } + } + + @Override + public void onDrawOver( + @NonNull Canvas c, + @NonNull RecyclerView parent, + @NonNull RecyclerView.State state + ) { + float dx = 0, dy = 0; + if (mSelected != null) { + getSelectedDxDy(mTmpPosition); + dx = mTmpPosition[0]; + dy = mTmpPosition[1]; + } + mCallback.onDrawOver(c, parent, mSelected, + mRecoverAnimations, mActionState, dx, dy); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + // we don't know if RV changed something so we should invalidate this index. + mOverdrawChildPosition = -1; + float dx = 0, dy = 0; + if (mSelected != null) { + getSelectedDxDy(mTmpPosition); + dx = mTmpPosition[0]; + dy = mTmpPosition[1]; + } + mCallback.onDraw(c, parent, mSelected, + mRecoverAnimations, mActionState, dx, dy); + } + + /** + * Starts dragging or swiping the given View. Call with null if you want to clear it. + * + * @param selected The ViewHolder to drag or swipe. Can be null if you want to cancel the + * current action, but may not be null if actionState is ACTION_STATE_DRAG. + * @param actionState The type of action + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void select(@Nullable ViewHolder selected, int actionState) { + if (selected == mSelected && actionState == mActionState) { + return; + } + mDragScrollStartTimeInMs = Long.MIN_VALUE; + final int prevActionState = mActionState; + // prevent duplicate animations + endRecoverAnimation(selected, true); + mActionState = actionState; + if (actionState == ACTION_STATE_DRAG) { + if (selected == null) { + throw new IllegalArgumentException("Must pass a ViewHolder when dragging"); + } + + // we remove after animation is complete. this means we only elevate the last drag + // child but that should perform good enough as it is very hard to start dragging a + // new child before the previous one settles. + mOverdrawChild = selected.itemView; + addChildDrawingOrderCallback(); + } + int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) + - 1; + boolean preventLayout = false; + + if (mSelected != null) { + final ViewHolder prevSelected = mSelected; + if (prevSelected.itemView.getParent() != null) { + final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0 + : swipeIfNecessary(prevSelected); + releaseVelocityTracker(); + // find where we should animate to + final float targetTranslateX, targetTranslateY; + int animationType; + switch (swipeDir) { + case LEFT: + case RIGHT: + case START: + case END: + targetTranslateY = 0; + targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth(); + break; + case UP: + case DOWN: + targetTranslateX = 0; + targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight(); + break; + default: + targetTranslateX = 0; + targetTranslateY = 0; + } + if (prevActionState == ACTION_STATE_DRAG) { + animationType = ANIMATION_TYPE_DRAG; + } else if (swipeDir > 0) { + animationType = ANIMATION_TYPE_SWIPE_SUCCESS; + } else { + animationType = ANIMATION_TYPE_SWIPE_CANCEL; + } + getSelectedDxDy(mTmpPosition); + final float currentTranslateX = mTmpPosition[0]; + final float currentTranslateY = mTmpPosition[1]; + final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType, + prevActionState, currentTranslateX, currentTranslateY, + targetTranslateX, targetTranslateY) { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (this.mOverridden) { + return; + } + if (swipeDir <= 0) { + // this is a drag or failed swipe. recover immediately + mCallback.clearView(mRecyclerView, prevSelected); + // full cleanup will happen on onDrawOver + } else { + // wait until remove animation is complete. + mPendingCleanup.add(prevSelected.itemView); + mIsPendingCleanup = true; + if (swipeDir > 0) { + // Animation might be ended by other animators during a layout. + // We defer callback to avoid editing adapter during a layout. + postDispatchSwipe(this, swipeDir); + } + } + // removed from the list after it is drawn for the last time + if (mOverdrawChild == prevSelected.itemView) { + removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); + } + } + }; + final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType, + targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY); + rv.setDuration(duration); + mRecoverAnimations.add(rv); + rv.start(); + preventLayout = true; + } else { + removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView); + mCallback.clearView(mRecyclerView, prevSelected); + } + mSelected = null; + } + if (selected != null) { + mSelectedFlags = + (mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask) + >> (mActionState * DIRECTION_FLAG_COUNT); + mSelectedStartX = selected.itemView.getLeft(); + mSelectedStartY = selected.itemView.getTop(); + mSelected = selected; + + if (actionState == ACTION_STATE_DRAG) { + mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + final ViewParent rvParent = mRecyclerView.getParent(); + if (rvParent != null) { + rvParent.requestDisallowInterceptTouchEvent(mSelected != null); + } + if (!preventLayout) { + mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout(); + } + mCallback.onSelectedChanged(mSelected, mActionState); + mRecyclerView.invalidate(); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void postDispatchSwipe(final RecoverAnimation anim, final int swipeDir) { + // wait until animations are complete. + mRecyclerView.post(new Runnable() { + @Override + public void run() { + if (mRecyclerView != null && mRecyclerView.isAttachedToWindow() + && !anim.mOverridden + && anim.mViewHolder.getAbsoluteAdapterPosition() + != RecyclerView.NO_POSITION) { + final RecyclerView.ItemAnimator animator = mRecyclerView.getItemAnimator(); + // if animator is running or we have other active recover animations, we try + // not to call onSwiped because DefaultItemAnimator is not good at merging + // animations. Instead, we wait and batch. + if ((animator == null || !animator.isRunning(null)) + && !hasRunningRecoverAnim()) { + mCallback.onSwiped(anim.mViewHolder, swipeDir); + } else { + mRecyclerView.post(this); + } + } + } + }); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean hasRunningRecoverAnim() { + final int size = mRecoverAnimations.size(); + for (int i = 0; i < size; i++) { + if (!mRecoverAnimations.get(i).mEnded) { + return true; + } + } + return false; + } + + /** + * If user drags the view to the edge, trigger a scroll if necessary. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean scrollIfNecessary() { + if (mSelected == null) { + mDragScrollStartTimeInMs = Long.MIN_VALUE; + return false; + } + final long now = System.currentTimeMillis(); + final long scrollDuration = mDragScrollStartTimeInMs + == Long.MIN_VALUE ? 0 : now - mDragScrollStartTimeInMs; + RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + if (mTmpRect == null) { + mTmpRect = new Rect(); + } + int scrollX = 0; + int scrollY = 0; + lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect); + if (lm.canScrollHorizontally()) { + int curX = (int) (mSelectedStartX + mDx); + final int leftDiff = curX - mTmpRect.left - mRecyclerView.getPaddingLeft(); + if (mDx < 0 && leftDiff < 0) { + scrollX = leftDiff; + } else if (mDx > 0) { + final int rightDiff = + curX + mSelected.itemView.getWidth() + mTmpRect.right + - (mRecyclerView.getWidth() - mRecyclerView.getPaddingRight()); + if (rightDiff > 0) { + scrollX = rightDiff; + } + } + } + if (lm.canScrollVertically()) { + int curY = (int) (mSelectedStartY + mDy); + final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop(); + if (mDy < 0 && topDiff < 0) { + scrollY = topDiff; + } else if (mDy > 0) { + final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom + - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom()); + if (bottomDiff > 0) { + scrollY = bottomDiff; + } + } + } + if (scrollX != 0) { + scrollX = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, + mSelected.itemView.getWidth(), scrollX, + mRecyclerView.getWidth(), scrollDuration); + } + if (scrollY != 0) { + scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView, + mSelected.itemView.getHeight(), scrollY, + mRecyclerView.getHeight(), scrollDuration); + } + if (scrollX != 0 || scrollY != 0) { + if (mDragScrollStartTimeInMs == Long.MIN_VALUE) { + mDragScrollStartTimeInMs = now; + } + mRecyclerView.scrollBy(scrollX, scrollY); + return true; + } + mDragScrollStartTimeInMs = Long.MIN_VALUE; + return false; + } + + private List findSwapTargets(ViewHolder viewHolder) { + if (mSwapTargets == null) { + mSwapTargets = new ArrayList<>(); + mDistances = new ArrayList<>(); + } else { + mSwapTargets.clear(); + mDistances.clear(); + } + final int margin = mCallback.getBoundingBoxMargin(); + final int left = Math.round(mSelectedStartX + mDx) - margin; + final int top = Math.round(mSelectedStartY + mDy) - margin; + final int right = left + viewHolder.itemView.getWidth() + 2 * margin; + final int bottom = top + viewHolder.itemView.getHeight() + 2 * margin; + final int centerX = (left + right) / 2; + final int centerY = (top + bottom) / 2; + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + final int childCount = lm.getChildCount(); + for (int i = 0; i < childCount; i++) { + View other = lm.getChildAt(i); + if (other == viewHolder.itemView) { + continue; //myself! + } + if (other.getBottom() < top || other.getTop() > bottom + || other.getRight() < left || other.getLeft() > right) { + continue; + } + final ViewHolder otherVh = mRecyclerView.getChildViewHolder(other); + if (mCallback.canDropOver(mRecyclerView, mSelected, otherVh)) { + // find the index to add + final int dx = Math.abs(centerX - (other.getLeft() + other.getRight()) / 2); + final int dy = Math.abs(centerY - (other.getTop() + other.getBottom()) / 2); + final int dist = dx * dx + dy * dy; + + int pos = 0; + final int cnt = mSwapTargets.size(); + for (int j = 0; j < cnt; j++) { + if (dist > mDistances.get(j)) { + pos++; + } else { + break; + } + } + mSwapTargets.add(pos, otherVh); + mDistances.add(pos, dist); + } + } + return mSwapTargets; + } + + /** + * Checks if we should swap w/ another view holder. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void moveIfNecessary(ViewHolder viewHolder) { + if (mRecyclerView.isLayoutRequested()) { + return; + } + if (mActionState != ACTION_STATE_DRAG) { + return; + } + + final float threshold = mCallback.getMoveThreshold(viewHolder); + final int x = (int) (mSelectedStartX + mDx); + final int y = (int) (mSelectedStartY + mDy); + if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold + && Math.abs(x - viewHolder.itemView.getLeft()) + < viewHolder.itemView.getWidth() * threshold) { + return; + } + List swapTargets = findSwapTargets(viewHolder); + if (swapTargets.size() == 0) { + return; + } + // may swap. + ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y); + if (target == null) { + mSwapTargets.clear(); + mDistances.clear(); + return; + } + final int toPosition = target.getAbsoluteAdapterPosition(); + final int fromPosition = viewHolder.getAbsoluteAdapterPosition(); + if (mCallback.onMove(mRecyclerView, viewHolder, target)) { + // keep target visible + mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, + target, toPosition, x, y); + } + } + + @Override + public void onChildViewAttachedToWindow(@NonNull View view) { + } + + @Override + public void onChildViewDetachedFromWindow(@NonNull View view) { + removeChildDrawingOrderCallbackIfNecessary(view); + final ViewHolder holder = mRecyclerView.getChildViewHolder(view); + if (holder == null) { + return; + } + if (mSelected != null && holder == mSelected) { + select(null, ACTION_STATE_IDLE); + } else { + endRecoverAnimation(holder, false); // this may push it into pending cleanup list. + if (mPendingCleanup.remove(holder.itemView)) { + mCallback.clearView(mRecyclerView, holder); + } + } + } + + /** + * Returns the animation type or 0 if cannot be found. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void endRecoverAnimation(ViewHolder viewHolder, boolean override) { + final int recoverAnimSize = mRecoverAnimations.size(); + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + if (anim.mViewHolder == viewHolder) { + anim.mOverridden |= override; + if (!anim.mEnded) { + anim.cancel(); + } + mRecoverAnimations.remove(i); + return; + } + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + outRect.setEmpty(); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void obtainVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + } + mVelocityTracker = VelocityTracker.obtain(); + } + + private void releaseVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private ViewHolder findSwipedView(MotionEvent motionEvent) { + final RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); + if (mActivePointerId == ACTIVE_POINTER_ID_NONE) { + return null; + } + final int pointerIndex = motionEvent.findPointerIndex(mActivePointerId); + final float dx = motionEvent.getX(pointerIndex) - mInitialTouchX; + final float dy = motionEvent.getY(pointerIndex) - mInitialTouchY; + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); + + if (absDx < mSlop && absDy < mSlop) { + return null; + } + if (absDx > absDy && lm.canScrollHorizontally()) { + return null; + } else if (absDy > absDx && lm.canScrollVertically()) { + return null; + } + View child = findChildView(motionEvent); + if (child == null) { + return null; + } + return mRecyclerView.getChildViewHolder(child); + } + + /** + * Checks whether we should select a View for swiping. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { + if (mSelected != null || action != MotionEvent.ACTION_MOVE + || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { + return; + } + if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { + return; + } + final ViewHolder vh = findSwipedView(motionEvent); + if (vh == null) { + return; + } + final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh); + + final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK) + >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE); + + if (swipeFlags == 0) { + return; + } + + // mDx and mDy are only set in allowed directions. We use custom x/y here instead of + // updateDxDy to avoid swiping if user moves more in the other direction + final float x = motionEvent.getX(pointerIndex); + final float y = motionEvent.getY(pointerIndex); + + // Calculate the distance moved + final float dx = x - mInitialTouchX; + final float dy = y - mInitialTouchY; + // swipe target is chose w/o applying flags so it does not really check if swiping in that + // direction is allowed. This why here, we use mDx mDy to check slope value again. + final float absDx = Math.abs(dx); + final float absDy = Math.abs(dy); + + if (absDx < mSlop && absDy < mSlop) { + return; + } + if (absDx > absDy) { + if (dx < 0 && (swipeFlags & LEFT) == 0) { + return; + } + if (dx > 0 && (swipeFlags & RIGHT) == 0) { + return; + } + } else { + if (dy < 0 && (swipeFlags & UP) == 0) { + return; + } + if (dy > 0 && (swipeFlags & DOWN) == 0) { + return; + } + } + mDx = mDy = 0f; + mActivePointerId = motionEvent.getPointerId(0); + select(vh, ACTION_STATE_SWIPE); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + View findChildView(MotionEvent event) { + // first check elevated views, if none, then call RV + final float x = event.getX(); + final float y = event.getY(); + if (mSelected != null) { + final View selectedView = mSelected.itemView; + if (hitTest(selectedView, x, y, mSelectedStartX + mDx, mSelectedStartY + mDy)) { + return selectedView; + } + } + for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + final View view = anim.mViewHolder.itemView; + if (hitTest(view, x, y, anim.mX, anim.mY)) { + return view; + } + } + return mRecyclerView.findChildViewUnder(x, y); + } + + /** + * Starts dragging the provided ViewHolder. By default, ItemTouchHelper starts a drag when a + * View is long pressed. You can disable that behavior by overriding + * {@link ItemTouchHelper.Callback#isLongPressDragEnabled()}. + *

+ * For this method to work: + *

    + *
  • The provided ViewHolder must be a child of the RecyclerView to which this + * ItemTouchHelper + * is attached.
  • + *
  • {@link ItemTouchHelper.Callback} must have dragging enabled.
  • + *
  • There must be a previous touch event that was reported to the ItemTouchHelper + * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener + * grabs previous events, this should work as expected.
  • + *
+ * + * For example, if you would like to let your user to be able to drag an Item by touching one + * of its descendants, you may implement it as follows: + *
+     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+     *         public boolean onTouch(View v, MotionEvent event) {
+     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+     *                 mItemTouchHelper.startDrag(viewHolder);
+     *             }
+     *             return false;
+     *         }
+     *     });
+     * 
+ *

+ * + * @param viewHolder The ViewHolder to start dragging. It must be a direct child of + * RecyclerView. + * @see ItemTouchHelper.Callback#isItemViewSwipeEnabled() + */ + public void startDrag(@NonNull ViewHolder viewHolder) { + if (!mCallback.hasDragFlag(mRecyclerView, viewHolder)) { + Log.e(TAG, "Start drag has been called but dragging is not enabled"); + return; + } + if (viewHolder.itemView.getParent() != mRecyclerView) { + Log.e(TAG, "Start drag has been called with a view holder which is not a child of " + + "the RecyclerView which is controlled by this ItemTouchHelper."); + return; + } + obtainVelocityTracker(); + mDx = mDy = 0f; + select(viewHolder, ACTION_STATE_DRAG); + } + + /** + * Starts swiping the provided ViewHolder. By default, ItemTouchHelper starts swiping a View + * when user swipes their finger (or mouse pointer) over the View. You can disable this + * behavior + * by overriding {@link ItemTouchHelper.Callback} + *

+ * For this method to work: + *

    + *
  • The provided ViewHolder must be a child of the RecyclerView to which this + * ItemTouchHelper is attached.
  • + *
  • {@link ItemTouchHelper.Callback} must have swiping enabled.
  • + *
  • There must be a previous touch event that was reported to the ItemTouchHelper + * through RecyclerView's ItemTouchListener mechanism. As long as no other ItemTouchListener + * grabs previous events, this should work as expected.
  • + *
+ * + * For example, if you would like to let your user to be able to swipe an Item by touching one + * of its descendants, you may implement it as follows: + *
+     *     viewHolder.dragButton.setOnTouchListener(new View.OnTouchListener() {
+     *         public boolean onTouch(View v, MotionEvent event) {
+     *             if (MotionEvent.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
+     *                 mItemTouchHelper.startSwipe(viewHolder);
+     *             }
+     *             return false;
+     *         }
+     *     });
+     * 
+ * + * @param viewHolder The ViewHolder to start swiping. It must be a direct child of + * RecyclerView. + */ + public void startSwipe(@NonNull ViewHolder viewHolder) { + if (!mCallback.hasSwipeFlag(mRecyclerView, viewHolder)) { + Log.e(TAG, "Start swipe has been called but swiping is not enabled"); + return; + } + if (viewHolder.itemView.getParent() != mRecyclerView) { + Log.e(TAG, "Start swipe has been called with a view holder which is not a child of " + + "the RecyclerView controlled by this ItemTouchHelper."); + return; + } + obtainVelocityTracker(); + mDx = mDy = 0f; + select(viewHolder, ACTION_STATE_SWIPE); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + RecoverAnimation findAnimation(MotionEvent event) { + if (mRecoverAnimations.isEmpty()) { + return null; + } + View target = findChildView(event); + for (int i = mRecoverAnimations.size() - 1; i >= 0; i--) { + final RecoverAnimation anim = mRecoverAnimations.get(i); + if (anim.mViewHolder.itemView == target) { + return anim; + } + } + return null; + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) { + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); + + // Calculate the distance moved + mDx = x - mInitialTouchX; + mDy = y - mInitialTouchY; + if ((directionFlags & LEFT) == 0) { + mDx = Math.max(0, mDx); + } + if ((directionFlags & RIGHT) == 0) { + mDx = Math.min(0, mDx); + } + if ((directionFlags & UP) == 0) { + mDy = Math.max(0, mDy); + } + if ((directionFlags & DOWN) == 0) { + mDy = Math.min(0, mDy); + } + } + + private int swipeIfNecessary(ViewHolder viewHolder) { + if (mActionState == ACTION_STATE_DRAG) { + return 0; + } + final int originalMovementFlags = mCallback.getMovementFlags(mRecyclerView, viewHolder); + final int absoluteMovementFlags = mCallback.convertToAbsoluteDirection( + originalMovementFlags, + ViewCompat.getLayoutDirection(mRecyclerView)); + final int flags = (absoluteMovementFlags + & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); + if (flags == 0) { + return 0; + } + final int originalFlags = (originalMovementFlags + & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE * DIRECTION_FLAG_COUNT); + int swipeDir; + if (Math.abs(mDx) > Math.abs(mDy)) { + if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { + // if swipe dir is not in original flags, it should be the relative direction + if ((originalFlags & swipeDir) == 0) { + // convert to relative + return Callback.convertToRelativeDirection(swipeDir, + ViewCompat.getLayoutDirection(mRecyclerView)); + } + return swipeDir; + } + if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { + return swipeDir; + } + } else { + if ((swipeDir = checkVerticalSwipe(viewHolder, flags)) > 0) { + return swipeDir; + } + if ((swipeDir = checkHorizontalSwipe(viewHolder, flags)) > 0) { + // if swipe dir is not in original flags, it should be the relative direction + if ((originalFlags & swipeDir) == 0) { + // convert to relative + return Callback.convertToRelativeDirection(swipeDir, + ViewCompat.getLayoutDirection(mRecyclerView)); + } + return swipeDir; + } + } + return 0; + } + + private int checkHorizontalSwipe(ViewHolder viewHolder, int flags) { + if ((flags & (LEFT | RIGHT)) != 0) { + final int dirFlag = mDx > 0 ? RIGHT : LEFT; + if (mVelocityTracker != null && mActivePointerId > -1) { + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, + mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); + final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); + final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); + final int velDirFlag = xVelocity > 0f ? RIGHT : LEFT; + final float absXVelocity = Math.abs(xVelocity); + if ((velDirFlag & flags) != 0 && dirFlag == velDirFlag + && absXVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) + && absXVelocity > Math.abs(yVelocity)) { + return velDirFlag; + } + } + + final float threshold = mRecyclerView.getWidth() * mCallback + .getSwipeThreshold(viewHolder); + + if ((flags & dirFlag) != 0 && Math.abs(mDx) > threshold) { + return dirFlag; + } + } + return 0; + } + + private int checkVerticalSwipe(ViewHolder viewHolder, int flags) { + if ((flags & (UP | DOWN)) != 0) { + final int dirFlag = mDy > 0 ? DOWN : UP; + if (mVelocityTracker != null && mActivePointerId > -1) { + mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, + mCallback.getSwipeVelocityThreshold(mMaxSwipeVelocity)); + final float xVelocity = mVelocityTracker.getXVelocity(mActivePointerId); + final float yVelocity = mVelocityTracker.getYVelocity(mActivePointerId); + final int velDirFlag = yVelocity > 0f ? DOWN : UP; + final float absYVelocity = Math.abs(yVelocity); + if ((velDirFlag & flags) != 0 && velDirFlag == dirFlag + && absYVelocity >= mCallback.getSwipeEscapeVelocity(mSwipeEscapeVelocity) + && absYVelocity > Math.abs(xVelocity)) { + return velDirFlag; + } + } + + final float threshold = mRecyclerView.getHeight() * mCallback + .getSwipeThreshold(viewHolder); + if ((flags & dirFlag) != 0 && Math.abs(mDy) > threshold) { + return dirFlag; + } + } + return 0; + } + + private void addChildDrawingOrderCallback() { + if (Build.VERSION.SDK_INT >= 21) { + return; // we use elevation on Lollipop + } + if (mChildDrawingOrderCallback == null) { + mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() { + @Override + public int onGetChildDrawingOrder(int childCount, int i) { + if (mOverdrawChild == null) { + return i; + } + int childPosition = mOverdrawChildPosition; + if (childPosition == -1) { + childPosition = mRecyclerView.indexOfChild(mOverdrawChild); + mOverdrawChildPosition = childPosition; + } + if (i == childCount - 1) { + return childPosition; + } + return i < childPosition ? i : i + 1; + } + }; + } + mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void removeChildDrawingOrderCallbackIfNecessary(View view) { + if (view == mOverdrawChild) { + mOverdrawChild = null; + // only remove if we've added + if (mChildDrawingOrderCallback != null) { + mRecyclerView.setChildDrawingOrderCallback(null); + } + } + } + + /** + * An interface which can be implemented by LayoutManager for better integration with + * {@link ItemTouchHelper}. + */ + public interface ViewDropHandler { + + /** + * Called by the {@link ItemTouchHelper} after a View is dropped over another View. + *

+ * A LayoutManager should implement this interface to get ready for the upcoming move + * operation. + *

+ * For example, LinearLayoutManager sets up a "scrollToPositionWithOffset" calls so that + * the View under drag will be used as an anchor View while calculating the next layout, + * making layout stay consistent. + * + * @param view The View which is being dragged. It is very likely that user is still + * dragging this View so there might be other calls to + * {@code prepareForDrop()} after this one. + * @param target The target view which is being dropped on. + * @param x The left offset of the View that is being dragged. This value + * includes the movement caused by the user. + * @param y The top offset of the View that is being dragged. This value + * includes the movement caused by the user. + */ + void prepareForDrop(@NonNull View view, @NonNull View target, int x, int y); + } + + /** + * This class is the contract between ItemTouchHelper and your application. It lets you control + * which touch behaviors are enabled per each ViewHolder and also receive callbacks when user + * performs these actions. + *

+ * To control which actions user can take on each view, you should override + * {@link #getMovementFlags(RecyclerView, ViewHolder)} and return appropriate set + * of direction flags. ({@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link #END}, + * {@link #UP}, {@link #DOWN}). You can use + * {@link #makeMovementFlags(int, int)} to easily construct it. Alternatively, you can use + * {@link SimpleCallback}. + *

+ * If user drags an item, ItemTouchHelper will call + * {@link Callback#onMove(RecyclerView, ViewHolder, ViewHolder) + * onMove(recyclerView, dragged, target)}. + * Upon receiving this callback, you should move the item from the old position + * ({@code dragged.getAdapterPosition()}) to new position ({@code target.getAdapterPosition()}) + * in your adapter and also call {@link RecyclerView.Adapter#notifyItemMoved(int, int)}. + * To control where a View can be dropped, you can override + * {@link #canDropOver(RecyclerView, ViewHolder, ViewHolder)}. When a + * dragging View overlaps multiple other views, Callback chooses the closest View with which + * dragged View might have changed positions. Although this approach works for many use cases, + * if you have a custom LayoutManager, you can override + * {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)} to select a + * custom drop target. + *

+ * When a View is swiped, ItemTouchHelper animates it until it goes out of bounds, then calls + * {@link #onSwiped(ViewHolder, int)}. At this point, you should update your + * adapter (e.g. remove the item) and call related Adapter#notify event. + */ + @SuppressWarnings("UnusedParameters") + public abstract static class Callback { + + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; + + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_SWIPE_ANIMATION_DURATION = 250; + + static final int RELATIVE_DIR_FLAGS = START | END + | ((START | END) << DIRECTION_FLAG_COUNT) + | ((START | END) << (2 * DIRECTION_FLAG_COUNT)); + + private static final int ABS_HORIZONTAL_DIR_FLAGS = LEFT | RIGHT + | ((LEFT | RIGHT) << DIRECTION_FLAG_COUNT) + | ((LEFT | RIGHT) << (2 * DIRECTION_FLAG_COUNT)); + + private static final Interpolator sDragScrollInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + return t * t * t * t * t; + } + }; + + private static final Interpolator sDragViewScrollCapInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + /** + * Drag scroll speed keeps accelerating until this many milliseconds before being capped. + */ + private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; + + private int mCachedMaxScrollSpeed = -1; + + /** + * Returns the {@link ItemTouchUIUtil} that is used by the {@link Callback} class for + * visual + * changes on Views in response to user interactions. {@link ItemTouchUIUtil} has different + * implementations for different platform versions. + *

+ * By default, {@link Callback} applies these changes on + * {@link RecyclerView.ViewHolder#itemView}. + *

+ * For example, if you have a use case where you only want the text to move when user + * swipes over the view, you can do the following: + *

+         *     public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder){
+         *         getDefaultUIUtil().clearView(((ItemTouchViewHolder) viewHolder).textView);
+         *     }
+         *     public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+         *         if (viewHolder != null){
+         *             getDefaultUIUtil().onSelected(((ItemTouchViewHolder) viewHolder).textView);
+         *         }
+         *     }
+         *     public void onChildDraw(Canvas c, RecyclerView recyclerView,
+         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+         *             boolean isCurrentlyActive) {
+         *         getDefaultUIUtil().onDraw(c, recyclerView,
+         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+         *                 actionState, isCurrentlyActive);
+         *         return true;
+         *     }
+         *     public void onChildDrawOver(Canvas c, RecyclerView recyclerView,
+         *             RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState,
+         *             boolean isCurrentlyActive) {
+         *         getDefaultUIUtil().onDrawOver(c, recyclerView,
+         *                 ((ItemTouchViewHolder) viewHolder).textView, dX, dY,
+         *                 actionState, isCurrentlyActive);
+         *         return true;
+         *     }
+         * 
+ * + * @return The {@link ItemTouchUIUtil} instance that is used by the {@link Callback} + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public static ItemTouchUIUtil getDefaultUIUtil() { + return ItemTouchUIUtilImpl.INSTANCE; + } + + /** + * Replaces a movement direction with its relative version by taking layout direction into + * account. + * + * @param flags The flag value that include any number of movement flags. + * @param layoutDirection The layout direction of the View. Can be obtained from + * {@link ViewCompat#getLayoutDirection(android.view.View)}. + * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead + * of {@link #LEFT}, {@link #RIGHT}. + * @see #convertToAbsoluteDirection(int, int) + */ + @SuppressWarnings("WeakerAccess") + public static int convertToRelativeDirection(int flags, int layoutDirection) { + int masked = flags & ABS_HORIZONTAL_DIR_FLAGS; + if (masked == 0) { + return flags; // does not have any abs flags, good. + } + flags &= ~masked; //remove left / right. + if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { + // no change. just OR with 2 bits shifted mask and return + flags |= masked << 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. + return flags; + } else { + // add RIGHT flag as START + flags |= ((masked << 1) & ~ABS_HORIZONTAL_DIR_FLAGS); + // first clean RIGHT bit then add LEFT flag as END + flags |= ((masked << 1) & ABS_HORIZONTAL_DIR_FLAGS) << 2; + } + return flags; + } + + /** + * Convenience method to create movement flags. + *

+ * For instance, if you want to let your items be drag & dropped vertically and swiped + * left to be dismissed, you can call this method with: + * makeMovementFlags(UP | DOWN, LEFT); + * + * @param dragFlags The directions in which the item can be dragged. + * @param swipeFlags The directions in which the item can be swiped. + * @return Returns an integer composed of the given drag and swipe flags. + */ + public static int makeMovementFlags(int dragFlags, int swipeFlags) { + return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags) + | makeFlag(ACTION_STATE_SWIPE, swipeFlags) + | makeFlag(ACTION_STATE_DRAG, dragFlags); + } + + /** + * Shifts the given direction flags to the offset of the given action state. + * + * @param actionState The action state you want to get flags in. Should be one of + * {@link #ACTION_STATE_IDLE}, {@link #ACTION_STATE_SWIPE} or + * {@link #ACTION_STATE_DRAG}. + * @param directions The direction flags. Can be composed from {@link #UP}, {@link #DOWN}, + * {@link #RIGHT}, {@link #LEFT} {@link #START} and {@link #END}. + * @return And integer that represents the given directions in the provided actionState. + */ + @SuppressWarnings("WeakerAccess") + public static int makeFlag(int actionState, int directions) { + return directions << (actionState * DIRECTION_FLAG_COUNT); + } + + /** + * Should return a composite flag which defines the enabled move directions in each state + * (idle, swiping, dragging). + *

+ * Instead of composing this flag manually, you can use {@link #makeMovementFlags(int, + * int)} + * or {@link #makeFlag(int, int)}. + *

+ * This flag is composed of 3 sets of 8 bits, where first 8 bits are for IDLE state, next + * 8 bits are for SWIPE state and third 8 bits are for DRAG state. + * Each 8 bit sections can be constructed by simply OR'ing direction flags defined in + * {@link ItemTouchHelper}. + *

+ * For example, if you want it to allow swiping LEFT and RIGHT but only allow starting to + * swipe by swiping RIGHT, you can return: + *

+         *      makeFlag(ACTION_STATE_IDLE, RIGHT) | makeFlag(ACTION_STATE_SWIPE, LEFT | RIGHT);
+         * 
+ * This means, allow right movement while IDLE and allow right and left movement while + * swiping. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached. + * @param viewHolder The ViewHolder for which the movement information is necessary. + * @return flags specifying which movements are allowed on this ViewHolder. + * @see #makeMovementFlags(int, int) + * @see #makeFlag(int, int) + */ + public abstract int getMovementFlags(@NonNull RecyclerView recyclerView, + @NonNull ViewHolder viewHolder); + + /** + * Converts a given set of flags to absolution direction which means {@link #START} and + * {@link #END} are replaced with {@link #LEFT} and {@link #RIGHT} depending on the layout + * direction. + * + * @param flags The flag value that include any number of movement flags. + * @param layoutDirection The layout direction of the RecyclerView. + * @return Updated flags which includes only absolute direction values. + */ + @SuppressWarnings("WeakerAccess") + public int convertToAbsoluteDirection(int flags, int layoutDirection) { + int masked = flags & RELATIVE_DIR_FLAGS; + if (masked == 0) { + return flags; // does not have any relative flags, good. + } + flags &= ~masked; //remove start / end + if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { + // no change. just OR with 2 bits shifted mask and return + flags |= masked >> 2; // START is 2 bits after LEFT, END is 2 bits after RIGHT. + return flags; + } else { + // add START flag as RIGHT + flags |= ((masked >> 1) & ~RELATIVE_DIR_FLAGS); + // first clean start bit then add END flag as LEFT + flags |= ((masked >> 1) & RELATIVE_DIR_FLAGS) >> 2; + } + return flags; + } + + final int getAbsoluteMovementFlags(RecyclerView recyclerView, + ViewHolder viewHolder) { + final int flags = getMovementFlags(recyclerView, viewHolder); + return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView)); + } + + boolean hasDragFlag(RecyclerView recyclerView, ViewHolder viewHolder) { + final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); + return (flags & ACTION_MODE_DRAG_MASK) != 0; + } + + boolean hasSwipeFlag(RecyclerView recyclerView, + ViewHolder viewHolder) { + final int flags = getAbsoluteMovementFlags(recyclerView, viewHolder); + return (flags & ACTION_MODE_SWIPE_MASK) != 0; + } + + /** + * Return true if the current ViewHolder can be dropped over the the target ViewHolder. + *

+ * This method is used when selecting drop target for the dragged View. After Views are + * eliminated either via bounds check or via this method, resulting set of views will be + * passed to {@link #chooseDropTarget(ViewHolder, java.util.List, int, int)}. + *

+ * Default implementation returns true. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. + * @param current The ViewHolder that user is dragging. + * @param target The ViewHolder which is below the dragged ViewHolder. + * @return True if the dragged ViewHolder can be replaced with the target ViewHolder, false + * otherwise. + */ + @SuppressWarnings("WeakerAccess") + public boolean canDropOver(@NonNull RecyclerView recyclerView, @NonNull ViewHolder current, + @NonNull ViewHolder target) { + return true; + } + + /** + * Called when ItemTouchHelper wants to move the dragged item from its old position to + * the new position. + *

+ * If this method returns true, ItemTouchHelper assumes {@code viewHolder} has been moved + * to the adapter position of {@code target} ViewHolder + * ({@link ViewHolder#getAbsoluteAdapterPosition() + * ViewHolder#getAdapterPositionInRecyclerView()}). + *

+ * If you don't support drag & drop, this method will never be called. + * + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to. + * @param viewHolder The ViewHolder which is being dragged by the user. + * @param target The ViewHolder over which the currently active item is being + * dragged. + * @return True if the {@code viewHolder} has been moved to the adapter position of + * {@code target}. + * @see #onMoved(RecyclerView, ViewHolder, int, ViewHolder, int, int, int) + */ + public abstract boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull ViewHolder viewHolder, @NonNull ViewHolder target); + + /** + * Returns whether ItemTouchHelper should start a drag and drop operation if an item is + * long pressed. + *

+ * Default value returns true but you may want to disable this if you want to start + * dragging on a custom view touch using {@link #startDrag(ViewHolder)}. + * + * @return True if ItemTouchHelper should start dragging an item when it is long pressed, + * false otherwise. Default value is true. + * @see #startDrag(ViewHolder) + */ + public boolean isLongPressDragEnabled() { + return true; + } + + /** + * Returns whether ItemTouchHelper should start a swipe operation if a pointer is swiped + * over the View. + *

+ * Default value returns true but you may want to disable this if you want to start + * swiping on a custom view touch using {@link #startSwipe(ViewHolder)}. + * + * @return True if ItemTouchHelper should start swiping an item when user swipes a pointer + * over the View, false otherwise. Default value is true. + * @see #startSwipe(ViewHolder) + */ + public boolean isItemViewSwipeEnabled() { + return true; + } + + /** + * When finding views under a dragged view, by default, ItemTouchHelper searches for views + * that overlap with the dragged View. By overriding this method, you can extend or shrink + * the search box. + * + * @return The extra margin to be added to the hit box of the dragged View. + */ + @SuppressWarnings("WeakerAccess") + public int getBoundingBoxMargin() { + return 0; + } + + /** + * Returns the fraction that the user should move the View to be considered as swiped. + * The fraction is calculated with respect to RecyclerView's bounds. + *

+ * Default value is .5f, which means, to swipe a View, user must move the View at least + * half of RecyclerView's width or height, depending on the swipe direction. + * + * @param viewHolder The ViewHolder that is being dragged. + * @return A float value that denotes the fraction of the View size. Default value + * is .5f . + */ + @SuppressWarnings("WeakerAccess") + public float getSwipeThreshold(@NonNull ViewHolder viewHolder) { + return .5f; + } + + /** + * Returns the fraction that the user should move the View to be considered as it is + * dragged. After a view is moved this amount, ItemTouchHelper starts checking for Views + * below it for a possible drop. + * + * @param viewHolder The ViewHolder that is being dragged. + * @return A float value that denotes the fraction of the View size. Default value is + * .5f . + */ + @SuppressWarnings("WeakerAccess") + public float getMoveThreshold(@NonNull ViewHolder viewHolder) { + return .5f; + } + + /** + * Defines the minimum velocity which will be considered as a swipe action by the user. + *

+ * You can increase this value to make it harder to swipe or decrease it to make it easier. + * Keep in mind that ItemTouchHelper also checks the perpendicular velocity and makes sure + * current direction velocity is larger then the perpendicular one. Otherwise, user's + * movement is ambiguous. You can change the threshold by overriding + * {@link #getSwipeVelocityThreshold(float)}. + *

+ * The velocity is calculated in pixels per second. + *

+ * The default framework value is passed as a parameter so that you can modify it with a + * multiplier. + * + * @param defaultValue The default value (in pixels per second) used by the + * ItemTouchHelper. + * @return The minimum swipe velocity. The default implementation returns the + * defaultValue parameter. + * @see #getSwipeVelocityThreshold(float) + * @see #getSwipeThreshold(ViewHolder) + */ + @SuppressWarnings("WeakerAccess") + public float getSwipeEscapeVelocity(float defaultValue) { + return defaultValue; + } + + /** + * Defines the maximum velocity ItemTouchHelper will ever calculate for pointer movements. + *

+ * To consider a movement as swipe, ItemTouchHelper requires it to be larger than the + * perpendicular movement. If both directions reach to the max threshold, none of them will + * be considered as a swipe because it is usually an indication that user rather tried to + * scroll then swipe. + *

+ * The velocity is calculated in pixels per second. + *

+ * You can customize this behavior by changing this method. If you increase the value, it + * will be easier for the user to swipe diagonally and if you decrease the value, user will + * need to make a rather straight finger movement to trigger a swipe. + * + * @param defaultValue The default value(in pixels per second) used by the ItemTouchHelper. + * @return The velocity cap for pointer movements. The default implementation returns the + * defaultValue parameter. + * @see #getSwipeEscapeVelocity(float) + */ + @SuppressWarnings("WeakerAccess") + public float getSwipeVelocityThreshold(float defaultValue) { + return defaultValue; + } + + /** + * Called by ItemTouchHelper to select a drop target from the list of ViewHolders that + * are under the dragged View. + *

+ * Default implementation filters the View with which dragged item have changed position + * in the drag direction. For instance, if the view is dragged UP, it compares the + * view.getTop() of the two views before and after drag started. If that value + * is different, the target view passes the filter. + *

+ * Among these Views which pass the test, the one closest to the dragged view is chosen. + *

+ * This method is called on the main thread every time user moves the View. If you want to + * override it, make sure it does not do any expensive operations. + * + * @param selected The ViewHolder being dragged by the user. + * @param dropTargets The list of ViewHolder that are under the dragged View and + * candidate as a drop. + * @param curX The updated left value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @param curY The updated top value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @return A ViewHolder to whose position the dragged ViewHolder should be + * moved to. + */ + @SuppressWarnings("WeakerAccess") + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public ViewHolder chooseDropTarget(@NonNull ViewHolder selected, + @NonNull List dropTargets, int curX, int curY) { + int right = curX + selected.itemView.getWidth(); + int bottom = curY + selected.itemView.getHeight(); + ViewHolder winner = null; + int winnerScore = -1; + final int dx = curX - selected.itemView.getLeft(); + final int dy = curY - selected.itemView.getTop(); + final int targetsSize = dropTargets.size(); + for (int i = 0; i < targetsSize; i++) { + final ViewHolder target = dropTargets.get(i); + if (dx > 0) { + int diff = target.itemView.getRight() - right; + if (diff < 0 && target.itemView.getRight() > selected.itemView.getRight()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + if (dx < 0) { + int diff = target.itemView.getLeft() - curX; + if (diff > 0 && target.itemView.getLeft() < selected.itemView.getLeft()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + if (dy < 0) { + int diff = target.itemView.getTop() - curY; + if (diff > 0 && target.itemView.getTop() < selected.itemView.getTop()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + + if (dy > 0) { + int diff = target.itemView.getBottom() - bottom; + if (diff < 0 && target.itemView.getBottom() > selected.itemView.getBottom()) { + final int score = Math.abs(diff); + if (score > winnerScore) { + winnerScore = score; + winner = target; + } + } + } + } + return winner; + } + + /** + * Called when a ViewHolder is swiped by the user. + *

+ * If you are returning relative directions ({@link #START} , {@link #END}) from the + * {@link #getMovementFlags(RecyclerView, ViewHolder)} method, this method + * will also use relative directions. Otherwise, it will use absolute directions. + *

+ * If you don't support swiping, this method will never be called. + *

+ * ItemTouchHelper will keep a reference to the View until it is detached from + * RecyclerView. + * As soon as it is detached, ItemTouchHelper will call + * {@link #clearView(RecyclerView, ViewHolder)}. + * + * @param viewHolder The ViewHolder which has been swiped by the user. + * @param direction The direction to which the ViewHolder is swiped. It is one of + * {@link #UP}, {@link #DOWN}, + * {@link #LEFT} or {@link #RIGHT}. If your + * {@link #getMovementFlags(RecyclerView, ViewHolder)} + * method + * returned relative flags instead of {@link #LEFT} / {@link #RIGHT}; + * `direction` will be relative as well. ({@link #START} or {@link + * #END}). + */ + public abstract void onSwiped(@NonNull ViewHolder viewHolder, int direction); + + /** + * Called when the ViewHolder swiped or dragged by the ItemTouchHelper is changed. + *

+ * If you override this method, you should call super. + * + * @param viewHolder The new ViewHolder that is being swiped or dragged. Might be null if + * it is cleared. + * @param actionState One of {@link ItemTouchHelper#ACTION_STATE_IDLE}, + * {@link ItemTouchHelper#ACTION_STATE_SWIPE} or + * {@link ItemTouchHelper#ACTION_STATE_DRAG}. + * @see #clearView(RecyclerView, RecyclerView.ViewHolder) + */ + public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) { + if (viewHolder != null) { + ItemTouchUIUtilImpl.INSTANCE.onSelected(viewHolder.itemView); + } + } + + private int getMaxDragScroll(RecyclerView recyclerView) { + if (mCachedMaxScrollSpeed == -1) { + mCachedMaxScrollSpeed = recyclerView.getResources().getDimensionPixelSize( + R.dimen.item_touch_helper_max_drag_scroll_per_frame); + } + return mCachedMaxScrollSpeed; + } + + /** + * Called when {@link #onMove(RecyclerView, ViewHolder, ViewHolder)} returns true. + *

+ * ItemTouchHelper does not create an extra Bitmap or View while dragging, instead, it + * modifies the existing View. Because of this reason, it is important that the View is + * still part of the layout after it is moved. This may not work as intended when swapped + * Views are close to RecyclerView bounds or there are gaps between them (e.g. other Views + * which were not eligible for dropping over). + *

+ * This method is responsible to give necessary hint to the LayoutManager so that it will + * keep the View in visible area. For example, for LinearLayoutManager, this is as simple + * as calling {@link LinearLayoutManager#scrollToPositionWithOffset(int, int)}. + * + * Default implementation calls {@link RecyclerView#scrollToPosition(int)} if the View's + * new position is likely to be out of bounds. + *

+ * It is important to ensure the ViewHolder will stay visible as otherwise, it might be + * removed by the LayoutManager if the move causes the View to go out of bounds. In that + * case, drag will end prematurely. + * + * @param recyclerView The RecyclerView controlled by the ItemTouchHelper. + * @param viewHolder The ViewHolder under user's control. + * @param fromPos The previous adapter position of the dragged item (before it was + * moved). + * @param target The ViewHolder on which the currently active item has been dropped. + * @param toPos The new adapter position of the dragged item. + * @param x The updated left value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + * @param y The updated top value of the dragged View after drag translations + * are applied. This value does not include margins added by + * {@link RecyclerView.ItemDecoration}s. + */ + public void onMoved(@NonNull final RecyclerView recyclerView, + @NonNull final ViewHolder viewHolder, int fromPos, @NonNull final ViewHolder target, + int toPos, int x, int y) { + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof ViewDropHandler) { + ((ViewDropHandler) layoutManager).prepareForDrop(viewHolder.itemView, + target.itemView, x, y); + return; + } + + // if layout manager cannot handle it, do some guesswork + if (layoutManager.canScrollHorizontally()) { + final int minLeft = layoutManager.getDecoratedLeft(target.itemView); + if (minLeft <= recyclerView.getPaddingLeft()) { + recyclerView.scrollToPosition(toPos); + } + final int maxRight = layoutManager.getDecoratedRight(target.itemView); + if (maxRight >= recyclerView.getWidth() - recyclerView.getPaddingRight()) { + recyclerView.scrollToPosition(toPos); + } + } + + if (layoutManager.canScrollVertically()) { + final int minTop = layoutManager.getDecoratedTop(target.itemView); + if (minTop <= recyclerView.getPaddingTop()) { + recyclerView.scrollToPosition(toPos); + } + final int maxBottom = layoutManager.getDecoratedBottom(target.itemView); + if (maxBottom >= recyclerView.getHeight() - recyclerView.getPaddingBottom()) { + recyclerView.scrollToPosition(toPos); + } + } + } + + void onDraw(Canvas c, RecyclerView parent, ViewHolder selected, + List recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); + for (int i = 0; i < recoverAnimSize; i++) { + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + anim.update(); + final int count = c.save(); + onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, + false); + c.restoreToCount(count); + } + if (selected != null) { + final int count = c.save(); + onChildDraw(c, parent, selected, dX, dY, actionState, true); + c.restoreToCount(count); + } + } + + void onDrawOver(Canvas c, RecyclerView parent, ViewHolder selected, + List recoverAnimationList, + int actionState, float dX, float dY) { + final int recoverAnimSize = recoverAnimationList.size(); + for (int i = 0; i < recoverAnimSize; i++) { + final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i); + final int count = c.save(); + onChildDrawOver(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState, + false); + c.restoreToCount(count); + } + if (selected != null) { + final int count = c.save(); + onChildDrawOver(c, parent, selected, dX, dY, actionState, true); + c.restoreToCount(count); + } + boolean hasRunningAnimation = false; + for (int i = recoverAnimSize - 1; i >= 0; i--) { + final RecoverAnimation anim = recoverAnimationList.get(i); + if (anim.mEnded && !anim.mIsPendingCleanup) { + recoverAnimationList.remove(i); + } else if (!anim.mEnded) { + hasRunningAnimation = true; + } + } + if (hasRunningAnimation) { + parent.invalidate(); + } + } + + /** + * Called by the ItemTouchHelper when the user interaction with an element is over and it + * also completed its animation. + *

+ * This is a good place to clear all changes on the View that was done in + * {@link #onSelectedChanged(RecyclerView.ViewHolder, int)}, + * {@link #onChildDraw(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean)} or + * {@link #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, boolean)}. + * + * @param recyclerView The RecyclerView which is controlled by the ItemTouchHelper. + * @param viewHolder The View that was interacted by the user. + */ + public void clearView(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { + ItemTouchUIUtilImpl.INSTANCE.clearView(viewHolder.itemView); + } + + /** + * Called by ItemTouchHelper on RecyclerView's onDraw callback. + *

+ * If you would like to customize how your View's respond to user interactions, this is + * a good place to override. + *

+ * Default implementation translates the child by the given dX, + * dY. + * ItemTouchHelper also takes care of drawing the child after other children if it is being + * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this + * is + * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L + * and after, it changes View's elevation value to be greater than all other children.) + * + * @param c The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was + * interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either {@link + * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. + * @param isCurrentlyActive True if this view is currently being controlled by the user or + * false it is simply animating back to its original state. + * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean) + */ + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, + @NonNull ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY, + actionState, isCurrentlyActive); + } + + /** + * Called by ItemTouchHelper on RecyclerView's onDraw callback. + *

+ * If you would like to customize how your View's respond to user interactions, this is + * a good place to override. + *

+ * Default implementation translates the child by the given dX, + * dY. + * ItemTouchHelper also takes care of drawing the child after other children if it is being + * dragged. This is done using child re-ordering mechanism. On platforms prior to L, this + * is + * achieved via {@link android.view.ViewGroup#getChildDrawingOrder(int, int)} and on L + * and after, it changes View's elevation value to be greater than all other children.) + * + * @param c The canvas which RecyclerView is drawing its children + * @param recyclerView The RecyclerView to which ItemTouchHelper is attached to + * @param viewHolder The ViewHolder which is being interacted by the User or it was + * interacted and simply animating to its original position + * @param dX The amount of horizontal displacement caused by user's action + * @param dY The amount of vertical displacement caused by user's action + * @param actionState The type of interaction on the View. Is either {@link + * #ACTION_STATE_DRAG} or {@link #ACTION_STATE_SWIPE}. + * @param isCurrentlyActive True if this view is currently being controlled by the user or + * false it is simply animating back to its original state. + * @see #onChildDrawOver(Canvas, RecyclerView, ViewHolder, float, float, int, + * boolean) + */ + public void onChildDrawOver(@NonNull Canvas c, @NonNull RecyclerView recyclerView, + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + ItemTouchUIUtilImpl.INSTANCE.onDrawOver(c, recyclerView, viewHolder.itemView, dX, dY, + actionState, isCurrentlyActive); + } + + /** + * Called by the ItemTouchHelper when user action finished on a ViewHolder and now the View + * will be animated to its final position. + *

+ * Default implementation uses ItemAnimator's duration values. If + * animationType is {@link #ANIMATION_TYPE_DRAG}, it returns + * {@link RecyclerView.ItemAnimator#getMoveDuration()}, otherwise, it returns + * {@link RecyclerView.ItemAnimator#getRemoveDuration()}. If RecyclerView does not have + * any {@link RecyclerView.ItemAnimator} attached, this method returns + * {@code DEFAULT_DRAG_ANIMATION_DURATION} or {@code DEFAULT_SWIPE_ANIMATION_DURATION} + * depending on the animation type. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param animationType The type of animation. Is one of {@link #ANIMATION_TYPE_DRAG}, + * {@link #ANIMATION_TYPE_SWIPE_CANCEL} or + * {@link #ANIMATION_TYPE_SWIPE_SUCCESS}. + * @param animateDx The horizontal distance that the animation will offset + * @param animateDy The vertical distance that the animation will offset + * @return The duration for the animation + */ + @SuppressWarnings("WeakerAccess") + public long getAnimationDuration(@NonNull RecyclerView recyclerView, int animationType, + float animateDx, float animateDy) { + final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator(); + if (itemAnimator == null) { + return animationType == ANIMATION_TYPE_DRAG ? DEFAULT_DRAG_ANIMATION_DURATION + : DEFAULT_SWIPE_ANIMATION_DURATION; + } else { + return animationType == ANIMATION_TYPE_DRAG ? itemAnimator.getMoveDuration() + : itemAnimator.getRemoveDuration(); + } + } + + /** + * Called by the ItemTouchHelper when user is dragging a view out of bounds. + *

+ * You can override this method to decide how much RecyclerView should scroll in response + * to this action. Default implementation calculates a value based on the amount of View + * out of bounds and the time it spent there. The longer user keeps the View out of bounds, + * the faster the list will scroll. Similarly, the larger portion of the View is out of + * bounds, the faster the RecyclerView will scroll. + * + * @param recyclerView The RecyclerView instance to which ItemTouchHelper is + * attached to. + * @param viewSize The total size of the View in scroll direction, excluding + * item decorations. + * @param viewSizeOutOfBounds The total size of the View that is out of bounds. This value + * is negative if the View is dragged towards left or top edge. + * @param totalSize The total size of RecyclerView in the scroll direction. + * @param msSinceStartScroll The time passed since View is kept out of bounds. + * @return The amount that RecyclerView should scroll. Keep in mind that this value will + * be passed to {@link RecyclerView#scrollBy(int, int)} method. + */ + @SuppressWarnings("WeakerAccess") + public int interpolateOutOfBoundsScroll(@NonNull RecyclerView recyclerView, + int viewSize, int viewSizeOutOfBounds, + int totalSize, long msSinceStartScroll) { + final int maxScroll = getMaxDragScroll(recyclerView); + final int absOutOfBounds = Math.abs(viewSizeOutOfBounds); + final int direction = (int) Math.signum(viewSizeOutOfBounds); + // might be negative if other direction + float outOfBoundsRatio = Math.min(1f, 1f * absOutOfBounds / viewSize); + final int cappedScroll = (int) (direction * maxScroll + * sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); + final float timeRatio; + if (msSinceStartScroll > DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS) { + timeRatio = 1f; + } else { + timeRatio = (float) msSinceStartScroll / DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; + } + final int value = (int) (cappedScroll * sDragScrollInterpolator + .getInterpolation(timeRatio)); + if (value == 0) { + return viewSizeOutOfBounds > 0 ? 1 : -1; + } + return value; + } + } + + /** + * A simple wrapper to the default Callback which you can construct with drag and swipe + * directions and this class will handle the flag callbacks. You should still override onMove + * or + * onSwiped depending on your use case. + * + *

+     * ItemTouchHelper mIth = new ItemTouchHelper(
+     *     new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
+     *         ItemTouchHelper.LEFT) {
+     *         public boolean onMove(RecyclerView recyclerView,
+     *             ViewHolder viewHolder, ViewHolder target) {
+     *             final int fromPos = viewHolder.getAdapterPosition();
+     *             final int toPos = target.getAdapterPosition();
+     *             // move item in `fromPos` to `toPos` in adapter.
+     *             return true;// true if moved, false otherwise
+     *         }
+     *         public void onSwiped(ViewHolder viewHolder, int direction) {
+     *             // remove from adapter
+     *         }
+     * });
+     * 
+ */ + public abstract static class SimpleCallback extends Callback { + + private int mDefaultSwipeDirs; + + private int mDefaultDragDirs; + + /** + * Creates a Callback for the given drag and swipe allowance. These values serve as + * defaults + * and if you want to customize behavior per ViewHolder, you can override + * {@link #getSwipeDirs(RecyclerView, ViewHolder)} + * and / or {@link #getDragDirs(RecyclerView, ViewHolder)}. + * + * @param dragDirs Binary OR of direction flags in which the Views can be dragged. Must be + * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link + * #END}, + * {@link #UP} and {@link #DOWN}. + * @param swipeDirs Binary OR of direction flags in which the Views can be swiped. Must be + * composed of {@link #LEFT}, {@link #RIGHT}, {@link #START}, {@link + * #END}, + * {@link #UP} and {@link #DOWN}. + */ + public SimpleCallback(int dragDirs, int swipeDirs) { + mDefaultSwipeDirs = swipeDirs; + mDefaultDragDirs = dragDirs; + } + + /** + * Updates the default swipe directions. For example, you can use this method to toggle + * certain directions depending on your use case. + * + * @param defaultSwipeDirs Binary OR of directions in which the ViewHolders can be swiped. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public void setDefaultSwipeDirs(@SuppressWarnings("unused") int defaultSwipeDirs) { + mDefaultSwipeDirs = defaultSwipeDirs; + } + + /** + * Updates the default drag directions. For example, you can use this method to toggle + * certain directions depending on your use case. + * + * @param defaultDragDirs Binary OR of directions in which the ViewHolders can be dragged. + */ + @SuppressWarnings({"WeakerAccess", "unused"}) + public void setDefaultDragDirs(@SuppressWarnings("unused") int defaultDragDirs) { + mDefaultDragDirs = defaultDragDirs; + } + + /** + * Returns the swipe directions for the provided ViewHolder. + * Default implementation returns the swipe directions that was set via constructor or + * {@link #setDefaultSwipeDirs(int)}. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param viewHolder The ViewHolder for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + @SuppressWarnings("WeakerAccess") + public int getSwipeDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, + @NonNull @SuppressWarnings("unused") ViewHolder viewHolder) { + return mDefaultSwipeDirs; + } + + /** + * Returns the drag directions for the provided ViewHolder. + * Default implementation returns the drag directions that was set via constructor or + * {@link #setDefaultDragDirs(int)}. + * + * @param recyclerView The RecyclerView to which the ItemTouchHelper is attached to. + * @param viewHolder The ViewHolder for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + @SuppressWarnings("WeakerAccess") + public int getDragDirs(@SuppressWarnings("unused") @NonNull RecyclerView recyclerView, + @SuppressWarnings("unused") @NonNull ViewHolder viewHolder) { + return mDefaultDragDirs; + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, + @NonNull ViewHolder viewHolder) { + return makeMovementFlags(getDragDirs(recyclerView, viewHolder), + getSwipeDirs(recyclerView, viewHolder)); + } + } + + private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { + + /** + * Whether to execute code in response to the the invoking of + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)}. + * + * It is necessary to control this here because + * {@link GestureDetector.SimpleOnGestureListener} can only be set on a + * {@link GestureDetector} in a GestureDetector's constructor, a GestureDetector will call + * onLongPress if an {@link MotionEvent#ACTION_DOWN} event is not followed by another event + * that would cancel it (like {@link MotionEvent#ACTION_UP} or + * {@link MotionEvent#ACTION_CANCEL}), the long press responding to the long press event + * needs to be cancellable to prevent unexpected behavior. + * + * @see #doNotReactToLongPress() + */ + private boolean mShouldReactToLongPress = true; + + ItemTouchHelperGestureListener() { + } + + /** + * Call to prevent executing code in response to + * {@link ItemTouchHelperGestureListener#onLongPress(MotionEvent)} being called. + */ + void doNotReactToLongPress() { + mShouldReactToLongPress = false; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (!mShouldReactToLongPress) { + return; + } + View child = findChildView(e); + if (child != null) { + ViewHolder vh = mRecyclerView.getChildViewHolder(child); + if (vh != null) { + if (!mCallback.hasDragFlag(mRecyclerView, vh)) { + return; + } + int pointerId = e.getPointerId(0); + // Long press is deferred. + // Check w/ active pointer id to avoid selecting after motion + // event is canceled. + if (pointerId == mActivePointerId) { + final int index = e.findPointerIndex(mActivePointerId); + final float x = e.getX(index); + final float y = e.getY(index); + mInitialTouchX = x; + mInitialTouchY = y; + mDx = mDy = 0f; + if (DEBUG) { + Log.d(TAG, + "onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY); + } + if (mCallback.isLongPressDragEnabled()) { + select(vh, ACTION_STATE_DRAG); + } + } + } + } + } + } + + @VisibleForTesting + static class RecoverAnimation implements Animator.AnimatorListener { + + final float mStartDx; + + final float mStartDy; + + final float mTargetX; + + final float mTargetY; + + final ViewHolder mViewHolder; + + final int mActionState; + + @VisibleForTesting + final ValueAnimator mValueAnimator; + + final int mAnimationType; + + boolean mIsPendingCleanup; + + float mX; + + float mY; + + // if user starts touching a recovering view, we put it into interaction mode again, + // instantly. + boolean mOverridden = false; + + boolean mEnded = false; + + private float mFraction; + + RecoverAnimation(ViewHolder viewHolder, int animationType, + int actionState, float startDx, float startDy, float targetX, float targetY) { + mActionState = actionState; + mAnimationType = animationType; + mViewHolder = viewHolder; + mStartDx = startDx; + mStartDy = startDy; + mTargetX = targetX; + mTargetY = targetY; + mValueAnimator = ValueAnimator.ofFloat(0f, 1f); + mValueAnimator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setFraction(animation.getAnimatedFraction()); + } + }); + mValueAnimator.setTarget(viewHolder.itemView); + mValueAnimator.addListener(this); + setFraction(0f); + } + + public void setDuration(long duration) { + mValueAnimator.setDuration(duration); + } + + public void start() { + mViewHolder.setIsRecyclable(false); + mValueAnimator.start(); + } + + public void cancel() { + mValueAnimator.cancel(); + } + + public void setFraction(float fraction) { + mFraction = fraction; + } + + /** + * We run updates on onDraw method but use the fraction from animator callback. + * This way, we can sync translate x/y values w/ the animators to avoid one-off frames. + */ + public void update() { + if (mStartDx == mTargetX) { + mX = mViewHolder.itemView.getTranslationX(); + } else { + mX = mStartDx + mFraction * (mTargetX - mStartDx); + } + if (mStartDy == mTargetY) { + mY = mViewHolder.itemView.getTranslationY(); + } else { + mY = mStartDy + mFraction * (mTargetY - mStartDy); + } + } + + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mEnded) { + mViewHolder.setIsRecyclable(true); + } + mEnded = true; + } + + @Override + public void onAnimationCancel(Animator animation) { + setFraction(1f); //make sure we recover the view's state. + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java b/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java new file mode 100644 index 0000000000..8f4f8f0616 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.graphics.Canvas; +import android.view.View; + +/** + * Utility class for {@link ItemTouchHelper} which handles item transformations for different + * API versions. + *

+ * This class has methods that map to {@link ItemTouchHelper.Callback}'s drawing methods. Default + * implementations in {@link ItemTouchHelper.Callback} call these methods with + * {@link RecyclerView.ViewHolder#itemView} and {@link ItemTouchUIUtil} makes necessary changes + * on the View depending on the API level. You can access the instance of {@link ItemTouchUIUtil} + * via {@link ItemTouchHelper.Callback#getDefaultUIUtil()} and call its methods with the children + * of ViewHolder that you want to apply default effects. + * + * @see ItemTouchHelper.Callback#getDefaultUIUtil() + */ +public interface ItemTouchUIUtil { + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas, + * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void onDraw(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas, + * RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)} + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void onDrawOver(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView, + * RecyclerView.ViewHolder)} + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void clearView(View view); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged( + * RecyclerView.ViewHolder, int)} + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void onSelected(View view); +} + diff --git a/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java b/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java new file mode 100644 index 0000000000..c592e3e526 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ItemTouchUIUtilImpl.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.graphics.Canvas; +import android.os.Build; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.core.view.ViewCompat; +import androidx.recyclerview.R; + +/** + * Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them + * public API, which is not desired in this case. + */ +class ItemTouchUIUtilImpl implements ItemTouchUIUtil { + static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl(); + + @Override + public void onDraw( + @NonNull Canvas c, + @NonNull RecyclerView recyclerView, + @NonNull View view, + float dX, + float dY, + int actionState, + boolean isCurrentlyActive + ) { + if (Build.VERSION.SDK_INT >= 21) { + if (isCurrentlyActive) { + Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation); + if (originalElevation == null) { + originalElevation = ViewCompat.getElevation(view); + float newElevation = 1f + findMaxElevation(recyclerView, view); + ViewCompat.setElevation(view, newElevation); + view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation); + } + } + } + + view.setTranslationX(dX); + view.setTranslationY(dY); + } + + private static float findMaxElevation(RecyclerView recyclerView, View itemView) { + final int childCount = recyclerView.getChildCount(); + float max = 0; + for (int i = 0; i < childCount; i++) { + final View child = recyclerView.getChildAt(i); + if (child == itemView) { + continue; + } + final float elevation = ViewCompat.getElevation(child); + if (elevation > max) { + max = elevation; + } + } + return max; + } + + @Override + public void onDrawOver( + @NonNull Canvas c, + @NonNull RecyclerView recyclerView, + @NonNull View view, + float dX, + float dY, + int actionState, + boolean isCurrentlyActive + ) { + } + + @Override + public void clearView(@NonNull View view) { + if (Build.VERSION.SDK_INT >= 21) { + final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation); + if (tag instanceof Float) { + ViewCompat.setElevation(view, (Float) tag); + } + view.setTag(R.id.item_touch_helper_previous_elevation, null); + } + + view.setTranslationX(0f); + view.setTranslationY(0f); + } + + @Override + public void onSelected(@NonNull View view) { + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/LayoutState.java b/app/src/main/java/androidx/recyclerview/widget/LayoutState.java new file mode 100644 index 0000000000..8805c1cc93 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/LayoutState.java @@ -0,0 +1,114 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.view.View; + +/** + * Helper class that keeps temporary state while {LayoutManager} is filling out the empty + * space. + */ +class LayoutState { + + static final int LAYOUT_START = -1; + + static final int LAYOUT_END = 1; + + static final int INVALID_LAYOUT = Integer.MIN_VALUE; + + static final int ITEM_DIRECTION_HEAD = -1; + + static final int ITEM_DIRECTION_TAIL = 1; + + /** + * We may not want to recycle children in some cases (e.g. layout) + */ + boolean mRecycle = true; + + /** + * Number of pixels that we should fill, in the layout direction. + */ + int mAvailable; + + /** + * Current position on the adapter to get the next item. + */ + int mCurrentPosition; + + /** + * Defines the direction in which the data adapter is traversed. + * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} + */ + int mItemDirection; + + /** + * Defines the direction in which the layout is filled. + * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} + */ + int mLayoutDirection; + + /** + * This is the target pixel closest to the start of the layout that we are trying to fill + */ + int mStartLine = 0; + + /** + * This is the target pixel closest to the end of the layout that we are trying to fill + */ + int mEndLine = 0; + + /** + * If true, layout should stop if a focusable view is added + */ + boolean mStopInFocusable; + + /** + * If the content is not wrapped with any value + */ + boolean mInfinite; + + /** + * @return true if there are more items in the data adapter + */ + boolean hasMore(RecyclerView.State state) { + return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); + } + + /** + * Gets the view for the next element that we should render. + * Also updates current item index to the next item, based on {@link #mItemDirection} + * + * @return The next element that we should render. + */ + View next(RecyclerView.Recycler recycler) { + final View view = recycler.getViewForPosition(mCurrentPosition); + mCurrentPosition += mItemDirection; + return view; + } + + @Override + public String toString() { + return "LayoutState{" + + "mAvailable=" + mAvailable + + ", mCurrentPosition=" + mCurrentPosition + + ", mItemDirection=" + mItemDirection + + ", mLayoutDirection=" + mLayoutDirection + + ", mStartLine=" + mStartLine + + ", mEndLine=" + mEndLine + + '}'; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/app/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java new file mode 100644 index 0000000000..22fd533731 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java @@ -0,0 +1,2624 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.core.os.TraceCompat; +import androidx.core.view.ViewCompat; + +import java.util.List; + +/** + * A {@link RecyclerView.LayoutManager} implementation which provides + * similar functionality to {@link android.widget.ListView}. + */ +public class LinearLayoutManager extends RecyclerView.LayoutManager implements + ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { + + private static final String TAG = "LinearLayoutManager"; + + static final boolean DEBUG = false; + + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + + public static final int VERTICAL = RecyclerView.VERTICAL; + + public static final int INVALID_OFFSET = Integer.MIN_VALUE; + + + /** + * While trying to find next view to focus, LayoutManager will not try to scroll more + * than this factor times the total space of the list. If layout is vertical, total space is the + * height minus padding, if layout is horizontal, total space is the width minus padding. + */ + private static final float MAX_SCROLL_FACTOR = 1 / 3f; + + /** + * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} + */ + @RecyclerView.Orientation + int mOrientation = RecyclerView.DEFAULT_ORIENTATION; + + /** + * Helper class that keeps temporary layout state. + * It does not keep state after layout is complete but we still keep a reference to re-use + * the same object. + */ + private LayoutState mLayoutState; + + /** + * Many calculations are made depending on orientation. To keep it clean, this interface + * helps {@link LinearLayoutManager} make those decisions. + */ + OrientationHelper mOrientationHelper; + + /** + * We need to track this so that we can ignore current position when it changes. + */ + private boolean mLastStackFromEnd; + + + /** + * Defines if layout should be calculated from end to start. + * + * @see #mShouldReverseLayout + */ + private boolean mReverseLayout = false; + + /** + * This keeps the final value for how LayoutManager should start laying out views. + * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. + * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. + */ + boolean mShouldReverseLayout = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and + * it supports both orientations. + * see {@link android.widget.AbsListView#setStackFromBottom(boolean)} + */ + private boolean mStackFromEnd = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + int mPendingScrollPosition = RecyclerView.NO_POSITION; + + /** + * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is + * called. + */ + int mPendingScrollPositionOffset = INVALID_OFFSET; + + private boolean mRecycleChildrenOnDetach; + + SavedState mPendingSavedState = null; + + /** + * Re-used variable to keep anchor information on re-layout. + * Anchor position and coordinate defines the reference point for LLM while doing a layout. + */ + final AnchorInfo mAnchorInfo = new AnchorInfo(); + + /** + * Stashed to avoid allocation, currently only used in #fill() + */ + private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult(); + + /** + * Number of items to prefetch when first coming on screen with new data. + */ + private int mInitialPrefetchItemCount = 2; + + // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. + // This should only be used used transiently and should not be used to retain any state over + // time. + private int[] mReusableIntPair = new int[2]; + + /** + * Creates a vertical LinearLayoutManager + * + * @param context Current context, will be used to access resources. + */ + public LinearLayoutManager( + // Suppressed because fixing it requires a source-incompatible change to a very + // commonly used constructor, for no benefit: the context parameter is unused + @SuppressLint("UnknownNullness") Context context + ) { + this(context, RecyclerView.DEFAULT_ORIENTATION, false); + } + + /** + * @param context Current context, will be used to access resources. + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link + * #VERTICAL}. + * @param reverseLayout When set to true, layouts from end to start. + */ + public LinearLayoutManager( + // Suppressed because fixing it requires a source-incompatible change to a very + // commonly used constructor, for no benefit: the context parameter is unused + @SuppressLint("UnknownNullness") Context context, + @RecyclerView.Orientation int orientation, + boolean reverseLayout + ) { + setOrientation(orientation); + setReverseLayout(reverseLayout); + } + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to vertical orientation. + * + * {@link android.R.attr#orientation} + * {@link androidx.recyclerview.R.attr#reverseLayout} + * {@link androidx.recyclerview.R.attr#stackFromEnd} + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setOrientation(properties.orientation); + setReverseLayout(properties.reverseLayout); + setStackFromEnd(properties.stackFromEnd); + } + + @Override + public boolean isAutoMeasureEnabled() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + *

+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to true so that views will be available to other RecyclerViews + * immediately. + *

+ * Note that, setting this flag will result in a performance drop if RecyclerView + * is restored. + * + * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. + */ + public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { + mRecycleChildrenOnDetach = recycleChildrenOnDetach; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + if (mRecycleChildrenOnDetach) { + removeAndRecycleAllViews(recycler); + recycler.clear(); + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (getChildCount() > 0) { + event.setFromIndex(findFirstVisibleItemPosition()); + event.setToIndex(findLastVisibleItemPosition()); + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public Parcelable onSaveInstanceState() { + if (mPendingSavedState != null) { + return new SavedState(mPendingSavedState); + } + SavedState state = new SavedState(); + if (getChildCount() > 0) { + ensureLayoutState(); + boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; + state.mAnchorLayoutFromEnd = didLayoutFromEnd; + if (didLayoutFromEnd) { + final View refChild = getChildClosestToEnd(); + state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(refChild); + state.mAnchorPosition = getPosition(refChild); + } else { + final View refChild = getChildClosestToStart(); + state.mAnchorPosition = getPosition(refChild); + state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) + - mOrientationHelper.getStartAfterPadding(); + } + } else { + state.invalidateAnchor(); + } + return state; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; + if (mPendingScrollPosition != RecyclerView.NO_POSITION) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + if (DEBUG) { + Log.d(TAG, "loaded saved state"); + } + } else if (DEBUG) { + Log.d(TAG, "invalid saved state class"); + } + } + + /** + * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} + */ + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + /** + * @return true if {@link #getOrientation()} is {@link #VERTICAL} + */ + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + /** + * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} + */ + public void setStackFromEnd(boolean stackFromEnd) { + assertNotInLayoutOrScroll(null); + if (mStackFromEnd == stackFromEnd) { + return; + } + mStackFromEnd = stackFromEnd; + requestLayout(); + } + + public boolean getStackFromEnd() { + return mStackFromEnd; + } + + /** + * Returns the current orientation of the layout. + * + * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL} + * @see #setOrientation(int) + */ + @RecyclerView.Orientation + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation of the layout. {@link LinearLayoutManager} + * will do its best to keep scroll position. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(@RecyclerView.Orientation int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation:" + orientation); + } + + assertNotInLayoutOrScroll(null); + + if (orientation != mOrientation || mOrientationHelper == null) { + mOrientationHelper = + OrientationHelper.createOrientationHelper(this, orientation); + mAnchorInfo.mOrientationHelper = mOrientationHelper; + mOrientation = orientation; + requestLayout(); + } + } + + /** + * Calculates the view layout order. (e.g. from end to start or start to end) + * RTL layout support is applied automatically. So if layout is RTL and + * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. + */ + private void resolveShouldLayoutReverse() { + // A == B is the same result, but we rather keep it readable + if (mOrientation == VERTICAL || !isLayoutRTL()) { + mShouldReverseLayout = mReverseLayout; + } else { + mShouldReverseLayout = !mReverseLayout; + } + } + + /** + * Returns if views are laid out from the opposite direction of the layout. + * + * @return If layout is reversed or not. + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + + /** + * Used to reverse item traversal and layout order. + * This behaves similar to the layout change for RTL views. When set to true, first item is + * laid out at the end of the UI, second item is laid out before it etc. + * + * For horizontal layouts, it depends on the layout direction. + * When set to true, If {@link RecyclerView} is LTR, than it will + * layout from RTL, if {@link RecyclerView}} is RTL, it will layout + * from LTR. + * + * If you are looking for the exact same behavior of + * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use + * {@link #setStackFromEnd(boolean)} + */ + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (reverseLayout == mReverseLayout) { + return; + } + mReverseLayout = reverseLayout; + requestLayout(); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public View findViewByPosition(int position) { + final int childCount = getChildCount(); + if (childCount == 0) { + return null; + } + final int firstChild = getPosition(getChildAt(0)); + final int viewPosition = position - firstChild; + if (viewPosition >= 0 && viewPosition < childCount) { + final View child = getChildAt(viewPosition); + if (getPosition(child) == position) { + return child; // in pre-layout, this may not match + } + } + // fallback to traversal. This might be necessary in pre-layout. + return super.findViewByPosition(position); + } + + /** + *

Returns the amount of extra space that should be laid out by LayoutManager.

+ * + *

By default, {@link LinearLayoutManager} lays out 1 extra page + * of items while smooth scrolling and 0 otherwise. You can override this method to implement + * your custom layout pre-cache logic.

+ * + *

Note:Laying out invisible elements generally comes with significant + * performance cost. It's typically only desirable in places like smooth scrolling to an unknown + * location, where 1) the extra content helps LinearLayoutManager know in advance when its + * target is approaching, so it can decelerate early and smoothly and 2) while motion is + * continuous.

+ * + *

Extending the extra layout space is especially expensive if done while the user may change + * scrolling direction. Changing direction will cause the extra layout space to swap to the + * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large + * enough to handle it.

+ * + * @return The extra space that should be laid out (in pixels). + * @deprecated Use {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} instead. + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + protected int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return mOrientationHelper.getTotalSpace(); + } else { + return 0; + } + } + + /** + *

Calculates the amount of extra space (in pixels) that should be laid out by {@link + * LinearLayoutManager} and stores the result in {@code extraLayoutSpace}. {@code + * extraLayoutSpace[0]} should be used for the extra space at the top/left, and {@code + * extraLayoutSpace[1]} should be used for the extra space at the bottom/right (depending on the + * orientation). Thus, the side where it is applied is unaffected by {@link + * #getLayoutDirection()} (LTR vs RTL), {@link #getStackFromEnd()} and {@link + * #getReverseLayout()}. Negative values are ignored.

+ * + *

By default, {@code LinearLayoutManager} lays out 1 extra page of items while smooth + * scrolling, in the direction of the scroll, and no extra space is laid out in all other + * situations. You can override this method to implement your own custom pre-cache logic. Use + * {@link RecyclerView.State#hasTargetScrollPosition()} to find out if a smooth scroll to a + * position is in progress, and {@link RecyclerView.State#getTargetScrollPosition()} to find out + * which item it is scrolling to.

+ * + *

Note:Laying out extra items generally comes with significant performance + * cost. It's typically only desirable in places like smooth scrolling to an unknown location, + * where 1) the extra content helps LinearLayoutManager know in advance when its target is + * approaching, so it can decelerate early and smoothly and 2) while motion is continuous.

+ * + *

Extending the extra layout space is especially expensive if done while the user may change + * scrolling direction. In the default implementation, changing direction will cause the extra + * layout space to swap to the opposite side of the viewport, incurring many rebinds/recycles, + * unless the cache is large enough to handle it.

+ */ + protected void calculateExtraLayoutSpace(@NonNull RecyclerView.State state, + @NonNull int[] extraLayoutSpace) { + int extraLayoutSpaceStart = 0; + int extraLayoutSpaceEnd = 0; + + // If calculateExtraLayoutSpace is not overridden, call the + // deprecated getExtraLayoutSpace for backwards compatibility + @SuppressWarnings("deprecation") + int extraScrollSpace = getExtraLayoutSpace(state); + if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + extraLayoutSpaceStart = extraScrollSpace; + } else { + extraLayoutSpaceEnd = extraScrollSpace; + } + + extraLayoutSpace[0] = extraLayoutSpaceStart; + extraLayoutSpace[1] = extraLayoutSpaceEnd; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { + LinearSmoothScroller linearSmoothScroller = + new LinearSmoothScroller(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public PointF computeScrollVectorForPosition(int targetPosition) { + if (getChildCount() == 0) { + return null; + } + final int firstChildPos = getPosition(getChildAt(0)); + final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; + if (mOrientation == HORIZONTAL) { + return new PointF(direction, 0); + } else { + return new PointF(0, direction); + } + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + // layout algorithm: + // 1) by checking children and other variables, find an anchor coordinate and an anchor + // item position. + // 2) fill towards start, stacking from bottom + // 3) fill towards end, stacking from top + // 4) scroll to fulfill requirements like stack from bottom. + // create layout state + if (DEBUG) { + Log.d(TAG, "is pre layout:" + state.isPreLayout()); + } + if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { + if (state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + return; + } + } + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + } + + ensureLayoutState(); + mLayoutState.mRecycle = false; + // resolve layout direction + resolveShouldLayoutReverse(); + + final View focused = getFocusedChild(); + if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION + || mPendingSavedState != null) { + mAnchorInfo.reset(); + mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; + // calculate anchor position and coordinate + updateAnchorInfoForLayout(recycler, state, mAnchorInfo); + mAnchorInfo.mValid = true; + } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused) + >= mOrientationHelper.getEndAfterPadding() + || mOrientationHelper.getDecoratedEnd(focused) + <= mOrientationHelper.getStartAfterPadding())) { + // This case relates to when the anchor child is the focused view and due to layout + // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows + // up after tapping an EditText which shrinks RV causing the focused view (The tapped + // EditText which is the anchor child) to get kicked out of the screen. Will update the + // anchor coordinate in order to make sure that the focused view is laid out. Otherwise, + // the available space in layoutState will be calculated as negative preventing the + // focused view from being laid out in fill. + // Note that we won't update the anchor position between layout passes (refer to + // TestResizingRelayoutWithAutoMeasure), which happens if we were to call + // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference + // child which can change between layout passes). + mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); + } + if (DEBUG) { + Log.d(TAG, "Anchor info:" + mAnchorInfo); + } + + // LLM may decide to layout items for "extra" pixels to account for scrolling target, + // caching or predictive animations. + + mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0 + ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + calculateExtraLayoutSpace(state, mReusableIntPair); + int extraForStart = Math.max(0, mReusableIntPair[0]) + + mOrientationHelper.getStartAfterPadding(); + int extraForEnd = Math.max(0, mReusableIntPair[1]) + + mOrientationHelper.getEndPadding(); + if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION + && mPendingScrollPositionOffset != INVALID_OFFSET) { + // if the child is visible and we are going to move it around, we should layout + // extra items in the opposite direction to make sure new items animate nicely + // instead of just fading in + final View existing = findViewByPosition(mPendingScrollPosition); + if (existing != null) { + final int current; + final int upcomingOffset; + if (mShouldReverseLayout) { + current = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(existing); + upcomingOffset = current - mPendingScrollPositionOffset; + } else { + current = mOrientationHelper.getDecoratedStart(existing) + - mOrientationHelper.getStartAfterPadding(); + upcomingOffset = mPendingScrollPositionOffset - current; + } + if (upcomingOffset > 0) { + extraForStart += upcomingOffset; + } else { + extraForEnd -= upcomingOffset; + } + } + } + int startOffset; + int endOffset; + final int firstLayoutDirection; + if (mAnchorInfo.mLayoutFromEnd) { + firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + } else { + firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + } + + onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); + detachAndScrapAttachedViews(recycler); + mLayoutState.mInfinite = resolveIsInfinite(); + mLayoutState.mIsPreLayout = state.isPreLayout(); + // noRecycleSpace not needed: recycling doesn't happen in below's fill + // invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN + mLayoutState.mNoRecycleSpace = 0; + if (mAnchorInfo.mLayoutFromEnd) { + // fill towards start + updateLayoutStateToFillStart(mAnchorInfo); + mLayoutState.mExtraFillSpace = extraForStart; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + final int firstElement = mLayoutState.mCurrentPosition; + if (mLayoutState.mAvailable > 0) { + extraForEnd += mLayoutState.mAvailable; + } + // fill towards end + updateLayoutStateToFillEnd(mAnchorInfo); + mLayoutState.mExtraFillSpace = extraForEnd; + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + + if (mLayoutState.mAvailable > 0) { + // end could not consume all. add more items towards start + extraForStart = mLayoutState.mAvailable; + updateLayoutStateToFillStart(firstElement, startOffset); + mLayoutState.mExtraFillSpace = extraForStart; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + } + } else { + // fill towards end + updateLayoutStateToFillEnd(mAnchorInfo); + mLayoutState.mExtraFillSpace = extraForEnd; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + final int lastElement = mLayoutState.mCurrentPosition; + if (mLayoutState.mAvailable > 0) { + extraForStart += mLayoutState.mAvailable; + } + // fill towards start + updateLayoutStateToFillStart(mAnchorInfo); + mLayoutState.mExtraFillSpace = extraForStart; + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + + if (mLayoutState.mAvailable > 0) { + extraForEnd = mLayoutState.mAvailable; + // start could not consume all it should. add more items towards end + updateLayoutStateToFillEnd(lastElement, endOffset); + mLayoutState.mExtraFillSpace = extraForEnd; + fill(recycler, mLayoutState, state, false); + endOffset = mLayoutState.mOffset; + } + } + + // changes may cause gaps on the UI, try to fix them. + // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have + // changed + if (getChildCount() > 0) { + // because layout from end may be changed by scroll to position + // we re-calculate it. + // find which side we should check for gaps. + if (mShouldReverseLayout ^ mStackFromEnd) { + int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); + startOffset += fixOffset; + endOffset += fixOffset; + fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); + startOffset += fixOffset; + endOffset += fixOffset; + } else { + int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); + startOffset += fixOffset; + endOffset += fixOffset; + fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); + startOffset += fixOffset; + endOffset += fixOffset; + } + } + layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); + if (!state.isPreLayout()) { + mOrientationHelper.onLayoutComplete(); + } else { + mAnchorInfo.reset(); + } + mLastStackFromEnd = mStackFromEnd; + if (DEBUG) { + validateChildOrder(); + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; // we don't need this anymore + mPendingScrollPosition = RecyclerView.NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + mAnchorInfo.reset(); + } + + /** + * Method called when Anchor position is decided. Extending class can setup accordingly or + * even update anchor info if necessary. + * + * @param recycler The recycler for the layout + * @param state The layout state + * @param anchorInfo The mutable POJO that keeps the position and offset. + * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter + * indices. + */ + void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, + AnchorInfo anchorInfo, int firstLayoutItemDirection) { + } + + /** + * If necessary, layouts new items for predictive animations + */ + private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, + RecyclerView.State state, int startOffset, + int endOffset) { + // If there are scrap children that we did not layout, we need to find where they did go + // and layout them accordingly so that animations can work as expected. + // This case may happen if new views are added or an existing view expands and pushes + // another view out of bounds. + if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout() + || !supportsPredictiveItemAnimations()) { + return; + } + // to make the logic simpler, we calculate the size of children and call fill. + int scrapExtraStart = 0, scrapExtraEnd = 0; + final List scrapList = recycler.getScrapList(); + final int scrapSize = scrapList.size(); + final int firstChildPos = getPosition(getChildAt(0)); + for (int i = 0; i < scrapSize; i++) { + RecyclerView.ViewHolder scrap = scrapList.get(i); + if (scrap.isRemoved()) { + continue; + } + final int position = scrap.getLayoutPosition(); + final int direction = position < firstChildPos != mShouldReverseLayout + ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; + if (direction == LayoutState.LAYOUT_START) { + scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); + } else { + scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); + } + } + + if (DEBUG) { + Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart + + " towards start and " + scrapExtraEnd + " towards end"); + } + mLayoutState.mScrapList = scrapList; + if (scrapExtraStart > 0) { + View anchor = getChildClosestToStart(); + updateLayoutStateToFillStart(getPosition(anchor), startOffset); + mLayoutState.mExtraFillSpace = scrapExtraStart; + mLayoutState.mAvailable = 0; + mLayoutState.assignPositionFromScrapList(); + fill(recycler, mLayoutState, state, false); + } + + if (scrapExtraEnd > 0) { + View anchor = getChildClosestToEnd(); + updateLayoutStateToFillEnd(getPosition(anchor), endOffset); + mLayoutState.mExtraFillSpace = scrapExtraEnd; + mLayoutState.mAvailable = 0; + mLayoutState.assignPositionFromScrapList(); + fill(recycler, mLayoutState, state, false); + } + mLayoutState.mScrapList = null; + } + + private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, + AnchorInfo anchorInfo) { + if (updateAnchorFromPendingData(state, anchorInfo)) { + if (DEBUG) { + Log.d(TAG, "updated anchor info from pending information"); + } + return; + } + + if (updateAnchorFromChildren(recycler, state, anchorInfo)) { + if (DEBUG) { + Log.d(TAG, "updated anchor info from existing children"); + } + return; + } + if (DEBUG) { + Log.d(TAG, "deciding anchor info for fresh state"); + } + anchorInfo.assignCoordinateFromPadding(); + anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; + } + + /** + * Finds an anchor child from existing Views. Most of the time, this is the view closest to + * start or end that has a valid position (e.g. not removed). + *

+ * If a child has focus, it is given priority. + */ + private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, + RecyclerView.State state, AnchorInfo anchorInfo) { + if (getChildCount() == 0) { + return false; + } + final View focused = getFocusedChild(); + if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) { + anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused)); + return true; + } + if (mLastStackFromEnd != mStackFromEnd) { + return false; + } + View referenceChild = + findReferenceChild( + recycler, + state, + anchorInfo.mLayoutFromEnd, + mStackFromEnd); + if (referenceChild != null) { + anchorInfo.assignFromView(referenceChild, getPosition(referenceChild)); + // If all visible views are removed in 1 pass, reference child might be out of bounds. + // If that is the case, offset it back to 0 so that we use these pre-layout children. + if (!state.isPreLayout() && supportsPredictiveItemAnimations()) { + // validate this child is at least partially visible. if not, offset it to start + final int childStart = mOrientationHelper.getDecoratedStart(referenceChild); + final int childEnd = mOrientationHelper.getDecoratedEnd(referenceChild); + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); + // b/148869110: usually if childStart >= boundsEnd the child is out of + // bounds, except if the child is 0 pixels! + boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart; + boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd; + if (outOfBoundsBefore || outOfBoundsAfter) { + anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? boundsEnd : boundsStart; + } + } + return true; + } + return false; + } + + /** + * If there is a pending scroll position or saved states, updates the anchor info from that + * data and returns true + */ + private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { + if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { + return false; + } + // validate scroll position + if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { + mPendingScrollPosition = RecyclerView.NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + if (DEBUG) { + Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); + } + return false; + } + + // if child is visible, try to make it a reference child and ensure it is fully visible. + // if child is not visible, align it depending on its virtual position. + anchorInfo.mPosition = mPendingScrollPosition; + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + // Anchor offset depends on how that child was laid out. Here, we update it + // according to our current view bounds + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + if (anchorInfo.mLayoutFromEnd) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() + - mPendingSavedState.mAnchorOffset; + } else { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + + mPendingSavedState.mAnchorOffset; + } + return true; + } + + if (mPendingScrollPositionOffset == INVALID_OFFSET) { + View child = findViewByPosition(mPendingScrollPosition); + if (child != null) { + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); + if (childSize > mOrientationHelper.getTotalSpace()) { + // item does not fit. fix depending on layout direction + anchorInfo.assignCoordinateFromPadding(); + return true; + } + final int startGap = mOrientationHelper.getDecoratedStart(child) + - mOrientationHelper.getStartAfterPadding(); + if (startGap < 0) { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); + anchorInfo.mLayoutFromEnd = false; + return true; + } + final int endGap = mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(child); + if (endGap < 0) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); + anchorInfo.mLayoutFromEnd = true; + return true; + } + anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd + ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper + .getTotalSpaceChange()) + : mOrientationHelper.getDecoratedStart(child); + } else { // item is not visible. + if (getChildCount() > 0) { + // get position of any child, does not matter + int pos = getPosition(getChildAt(0)); + anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos + == mShouldReverseLayout; + } + anchorInfo.assignCoordinateFromPadding(); + } + return true; + } + // override layout from end values for consistency + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + // if this changes, we should update prepareForDrop as well + if (mShouldReverseLayout) { + anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() + - mPendingScrollPositionOffset; + } else { + anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + + mPendingScrollPositionOffset; + } + return true; + } + + /** + * @return The final offset amount for children + */ + private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, + RecyclerView.State state, boolean canOffsetChildren) { + int gap = mOrientationHelper.getEndAfterPadding() - endOffset; + int fixOffset = 0; + if (gap > 0) { + fixOffset = -scrollBy(-gap, recycler, state); + } else { + return 0; // nothing to fix + } + // move offset according to scroll amount + endOffset += fixOffset; + if (canOffsetChildren) { + // re-calculate gap, see if we could fix it + gap = mOrientationHelper.getEndAfterPadding() - endOffset; + if (gap > 0) { + mOrientationHelper.offsetChildren(gap); + return gap + fixOffset; + } + } + return fixOffset; + } + + /** + * @return The final offset amount for children + */ + private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, + RecyclerView.State state, boolean canOffsetChildren) { + int gap = startOffset - mOrientationHelper.getStartAfterPadding(); + int fixOffset = 0; + if (gap > 0) { + // check if we should fix this gap. + fixOffset = -scrollBy(gap, recycler, state); + } else { + return 0; // nothing to fix + } + startOffset += fixOffset; + if (canOffsetChildren) { + // re-calculate gap, see if we could fix it + gap = startOffset - mOrientationHelper.getStartAfterPadding(); + if (gap > 0) { + mOrientationHelper.offsetChildren(-gap); + return fixOffset - gap; + } + } + return fixOffset; + } + + private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) { + updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); + } + + private void updateLayoutStateToFillEnd(int itemPosition, int offset) { + mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : + LayoutState.ITEM_DIRECTION_TAIL; + mLayoutState.mCurrentPosition = itemPosition; + mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END; + mLayoutState.mOffset = offset; + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + } + + private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) { + updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); + } + + private void updateLayoutStateToFillStart(int itemPosition, int offset) { + mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); + mLayoutState.mCurrentPosition = itemPosition; + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : + LayoutState.ITEM_DIRECTION_HEAD; + mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START; + mLayoutState.mOffset = offset; + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + + } + + protected boolean isLayoutRTL() { + return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; + } + + void ensureLayoutState() { + if (mLayoutState == null) { + mLayoutState = createLayoutState(); + } + } + + /** + * Test overrides this to plug some tracking and verification. + * + * @return A new LayoutState + */ + LayoutState createLayoutState() { + return new LayoutState(); + } + + /** + *

Scroll the RecyclerView to make the position visible.

+ * + *

RecyclerView will scroll the minimum amount that is necessary to make the + * target position visible. If you are looking for a similar behavior to + * {@link android.widget.ListView#setSelection(int)} or + * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use + * {@link #scrollToPositionWithOffset(int, int)}.

+ * + *

Note that scroll position change will not be reflected until the next layout call.

+ * + * @param position Scroll to this adapter position + * @see #scrollToPositionWithOffset(int, int) + */ + @Override + public void scrollToPosition(int position) { + mPendingScrollPosition = position; + mPendingScrollPositionOffset = INVALID_OFFSET; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + /** + * Scroll to the specified adapter position with the given offset from resolved layout + * start. Resolved layout start depends on {@link #getReverseLayout()}, + * {@link ViewCompat#getLayoutDirection(android.view.View)} and {@link #getStackFromEnd()}. + *

+ * For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling + * scrollToPositionWithOffset(10, 20) will layout such that + * item[10]'s bottom is 20 pixels above the RecyclerView's bottom. + *

+ * Note that scroll position change will not be reflected until the next layout call. + *

+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. + * + * @param position Index (starting at 0) of the reference item. + * @param offset The distance (in pixels) between the start edge of the item view and + * start edge of the RecyclerView. + * @see #setReverseLayout(boolean) + * @see #scrollToPosition(int) + */ + public void scrollToPositionWithOffset(int position, int offset) { + mPendingScrollPosition = position; + mPendingScrollPositionOffset = offset; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + private int computeScrollOffset(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled, mShouldReverseLayout); + } + + private int computeScrollExtent(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled); + } + + private int computeScrollRange(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + ensureLayoutState(); + return ScrollbarHelper.computeScrollRange(state, mOrientationHelper, + findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), + findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), + this, mSmoothScrollbarEnabled); + } + + /** + * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed + * based on the number of visible pixels in the visible items. This however assumes that all + * list items have similar or equal widths or heights (depending on list orientation). + * If you use a list in which items have different dimensions, the scrollbar will change + * appearance as the user scrolls through the list. To avoid this issue, you need to disable + * this property. + * + * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based + * solely on the number of items in the adapter and the position of the visible items inside + * the adapter. This provides a stable scrollbar as the user navigates through a list of items + * with varying widths / heights. + * + * @param enabled Whether or not to enable smooth scrollbar. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public void setSmoothScrollbarEnabled(boolean enabled) { + mSmoothScrollbarEnabled = enabled; + } + + /** + * Returns the current state of the smooth scrollbar feature. It is enabled by default. + * + * @return True if smooth scrollbar is enabled, false otherwise. + * @see #setSmoothScrollbarEnabled(boolean) + */ + public boolean isSmoothScrollbarEnabled() { + return mSmoothScrollbarEnabled; + } + + private void updateLayoutState(int layoutDirection, int requiredSpace, + boolean canUseExistingSpace, RecyclerView.State state) { + // If parent provides a hint, don't measure unlimited. + mLayoutState.mInfinite = resolveIsInfinite(); + mLayoutState.mLayoutDirection = layoutDirection; + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + calculateExtraLayoutSpace(state, mReusableIntPair); + int extraForStart = Math.max(0, mReusableIntPair[0]); + int extraForEnd = Math.max(0, mReusableIntPair[1]); + boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END; + mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart; + mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd; + int scrollingOffset; + if (layoutToEnd) { + mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding(); + // get the first child in the direction we are going + final View child = getChildClosestToEnd(); + // the direction in which we are traversing children + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; + mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); + // calculate how much we can scroll without adding new children (independent of layout) + scrollingOffset = mOrientationHelper.getDecoratedEnd(child) + - mOrientationHelper.getEndAfterPadding(); + + } else { + final View child = getChildClosestToStart(); + mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding(); + mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; + mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); + scrollingOffset = -mOrientationHelper.getDecoratedStart(child) + + mOrientationHelper.getStartAfterPadding(); + } + mLayoutState.mAvailable = requiredSpace; + if (canUseExistingSpace) { + mLayoutState.mAvailable -= scrollingOffset; + } + mLayoutState.mScrollingOffset = scrollingOffset; + } + + boolean resolveIsInfinite() { + return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED + && mOrientationHelper.getEnd() == 0; + } + + void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final int pos = layoutState.mCurrentPosition; + if (pos >= 0 && pos < state.getItemCount()) { + layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset)); + } + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectInitialPrefetchPositions(int adapterItemCount, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + final boolean fromEnd; + final int anchorPos; + if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { + // use restored state, since it hasn't been resolved yet + fromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + anchorPos = mPendingSavedState.mAnchorPosition; + } else { + resolveShouldLayoutReverse(); + fromEnd = mShouldReverseLayout; + if (mPendingScrollPosition == RecyclerView.NO_POSITION) { + anchorPos = fromEnd ? adapterItemCount - 1 : 0; + } else { + anchorPos = mPendingScrollPosition; + } + } + + final int direction = fromEnd + ? LayoutState.ITEM_DIRECTION_HEAD + : LayoutState.ITEM_DIRECTION_TAIL; + int targetPos = anchorPos; + for (int i = 0; i < mInitialPrefetchItemCount; i++) { + if (targetPos >= 0 && targetPos < adapterItemCount) { + layoutPrefetchRegistry.addPosition(targetPos, 0); + } else { + break; // no more to prefetch + } + targetPos += direction; + } + } + + /** + * Sets the number of items to prefetch in + * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines + * how many inner items should be prefetched when this LayoutManager's RecyclerView + * is nested inside another RecyclerView. + * + *

Set this value to the number of items this inner LayoutManager will display when it is + * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items + * so they are ready, avoiding jank as the inner RecyclerView is scrolled into the viewport.

+ * + *

For example, take a vertically scrolling RecyclerView with horizontally scrolling inner + * RecyclerViews. The rows always have 4 items visible in them (or 5 if not aligned). Passing + * 4 to this method for each inner RecyclerView's LinearLayoutManager will enable + * RecyclerView's prefetching feature to do create/bind work for 4 views within a row early, + * before it is scrolled on screen, instead of just the default 2.

+ * + *

Calling this method does nothing unless the LayoutManager is in a RecyclerView + * nested in another RecyclerView.

+ * + *

Note: Setting this value to be larger than the number of + * views that will be visible in this view can incur unnecessary bind work, and an increase to + * the number of Views created and in active use.

+ * + * @param itemCount Number of items to prefetch + * @see #isItemPrefetchEnabled() + * @see #getInitialPrefetchItemCount() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public void setInitialPrefetchItemCount(int itemCount) { + mInitialPrefetchItemCount = itemCount; + } + + /** + * Gets the number of items to prefetch in + * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines + * how many inner items should be prefetched when this LayoutManager's RecyclerView + * is nested inside another RecyclerView. + * + * @return number of items to prefetch. + * @see #isItemPrefetchEnabled() + * @see #setInitialPrefetchItemCount(int) + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public int getInitialPrefetchItemCount() { + return mInitialPrefetchItemCount; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + int delta = (mOrientation == HORIZONTAL) ? dx : dy; + if (getChildCount() == 0 || delta == 0) { + // can't support this scroll, so don't bother prefetching + return; + } + + ensureLayoutState(); + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDelta = Math.abs(delta); + updateLayoutState(layoutDirection, absDelta, true, state); + collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry); + } + + int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || delta == 0) { + return 0; + } + ensureLayoutState(); + mLayoutState.mRecycle = true; + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDelta = Math.abs(delta); + updateLayoutState(layoutDirection, absDelta, true, state); + final int consumed = mLayoutState.mScrollingOffset + + fill(recycler, mLayoutState, state, false); + if (consumed < 0) { + if (DEBUG) { + Log.d(TAG, "Don't have any more elements to scroll"); + } + return 0; + } + final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta; + mOrientationHelper.offsetChildren(-scrolled); + if (DEBUG) { + Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled); + } + mLayoutState.mLastScrollDelta = scrolled; + return scrolled; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void assertNotInLayoutOrScroll(String message) { + if (mPendingSavedState == null) { + super.assertNotInLayoutOrScroll(message); + } + } + + /** + * Recycles children between given indices. + * + * @param startIndex inclusive + * @param endIndex exclusive + */ + private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { + if (startIndex == endIndex) { + return; + } + if (DEBUG) { + Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); + } + if (endIndex > startIndex) { + for (int i = endIndex - 1; i >= startIndex; i--) { + removeAndRecycleViewAt(i, recycler); + } + } else { + for (int i = startIndex; i > endIndex; i--) { + removeAndRecycleViewAt(i, recycler); + } + } + } + + /** + * Recycles views that went out of bounds after scrolling towards the end of the layout. + *

+ * Checks both layout position and visible position to guarantee that the view is not visible. + * + * @param recycler Recycler instance of {@link RecyclerView} + * @param scrollingOffset This can be used to add additional padding to the visible area. This + * is used to detect children that will go out of bounds after scrolling, + * without actually moving them. + * @param noRecycleSpace Extra space that should be excluded from recycling. This is the space + * from {@code extraLayoutSpace[0]}, calculated in {@link + * #calculateExtraLayoutSpace}. + */ + private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset, + int noRecycleSpace) { + if (scrollingOffset < 0) { + if (DEBUG) { + Log.d(TAG, "Called recycle from start with a negative value. This might happen" + + " during layout changes but may be sign of a bug"); + } + return; + } + // ignore padding, ViewGroup may not clip children. + final int limit = scrollingOffset - noRecycleSpace; + final int childCount = getChildCount(); + if (mShouldReverseLayout) { + for (int i = childCount - 1; i >= 0; i--) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedEnd(child) > limit + || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { + // stop here + recycleChildren(recycler, childCount - 1, i); + return; + } + } + } else { + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedEnd(child) > limit + || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { + // stop here + recycleChildren(recycler, 0, i); + return; + } + } + } + } + + + /** + * Recycles views that went out of bounds after scrolling towards the start of the layout. + *

+ * Checks both layout position and visible position to guarantee that the view is not visible. + * + * @param recycler Recycler instance of {@link RecyclerView} + * @param scrollingOffset This can be used to add additional padding to the visible area. This + * is used to detect children that will go out of bounds after scrolling, + * without actually moving them. + * @param noRecycleSpace Extra space that should be excluded from recycling. This is the space + * from {@code extraLayoutSpace[1]}, calculated in {@link + * #calculateExtraLayoutSpace}. + */ + private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset, + int noRecycleSpace) { + final int childCount = getChildCount(); + if (scrollingOffset < 0) { + if (DEBUG) { + Log.d(TAG, "Called recycle from end with a negative value. This might happen" + + " during layout changes but may be sign of a bug"); + } + return; + } + final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace; + if (mShouldReverseLayout) { + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedStart(child) < limit + || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { + // stop here + recycleChildren(recycler, 0, i); + return; + } + } + } else { + for (int i = childCount - 1; i >= 0; i--) { + View child = getChildAt(i); + if (mOrientationHelper.getDecoratedStart(child) < limit + || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { + // stop here + recycleChildren(recycler, childCount - 1, i); + return; + } + } + } + } + + /** + * Helper method to call appropriate recycle method depending on current layout direction + * + * @param recycler Current recycler that is attached to RecyclerView + * @param layoutState Current layout state. Right now, this object does not change but + * we may consider moving it out of this view so passing around as a + * parameter for now, rather than accessing {@link #mLayoutState} + * @see #recycleViewsFromStart(RecyclerView.Recycler, int, int) + * @see #recycleViewsFromEnd(RecyclerView.Recycler, int, int) + * @see LinearLayoutManager.LayoutState#mLayoutDirection + */ + private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (!layoutState.mRecycle || layoutState.mInfinite) { + return; + } + int scrollingOffset = layoutState.mScrollingOffset; + int noRecycleSpace = layoutState.mNoRecycleSpace; + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace); + } else { + recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace); + } + } + + /** + * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly + * independent from the rest of the {@link LinearLayoutManager} + * and with little change, can be made publicly available as a helper class. + * + * @param recycler Current recycler that is attached to RecyclerView + * @param layoutState Configuration on how we should fill out the available space. + * @param state Context passed by the RecyclerView to control scroll steps. + * @param stopOnFocusable If true, filling stops in the first focusable new child + * @return Number of pixels that it added. Useful for scroll functions. + */ + int fill(RecyclerView.Recycler recycler, LayoutState layoutState, + RecyclerView.State state, boolean stopOnFocusable) { + // max offset we should set is mFastScroll + available + final int start = layoutState.mAvailable; + if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { + // TODO ugly bug fix. should not happen + if (layoutState.mAvailable < 0) { + layoutState.mScrollingOffset += layoutState.mAvailable; + } + recycleByLayoutState(recycler, layoutState); + } + int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; + LayoutChunkResult layoutChunkResult = mLayoutChunkResult; + while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { + layoutChunkResult.resetInternal(); + if (RecyclerView.VERBOSE_TRACING) { + TraceCompat.beginSection("LLM LayoutChunk"); + } + layoutChunk(recycler, state, layoutState, layoutChunkResult); + if (RecyclerView.VERBOSE_TRACING) { + TraceCompat.endSection(); + } + if (layoutChunkResult.mFinished) { + break; + } + layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; + /** + * Consume the available space if: + * * layoutChunk did not request to be ignored + * * OR we are laying out scrap children + * * OR we are not doing pre-layout + */ + if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null + || !state.isPreLayout()) { + layoutState.mAvailable -= layoutChunkResult.mConsumed; + // we keep a separate remaining space because mAvailable is important for recycling + remainingSpace -= layoutChunkResult.mConsumed; + } + + if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { + layoutState.mScrollingOffset += layoutChunkResult.mConsumed; + if (layoutState.mAvailable < 0) { + layoutState.mScrollingOffset += layoutState.mAvailable; + } + recycleByLayoutState(recycler, layoutState); + } + if (stopOnFocusable && layoutChunkResult.mFocusable) { + break; + } + } + if (DEBUG) { + validateChildOrder(); + } + return start - layoutState.mAvailable; + } + + void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, + LayoutState layoutState, LayoutChunkResult result) { + View view = layoutState.next(recycler); + if (view == null) { + if (DEBUG && layoutState.mScrapList == null) { + throw new RuntimeException("received null view when unexpected"); + } + // if we are laying out views in scrap, this may return null which means there is + // no more items to layout. + result.mFinished = true; + return; + } + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); + if (layoutState.mScrapList == null) { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addView(view); + } else { + addView(view, 0); + } + } else { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addDisappearingView(view); + } else { + addDisappearingView(view, 0); + } + } + measureChildWithMargins(view, 0, 0); + result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); + int left, top, right, bottom; + if (mOrientation == VERTICAL) { + if (isLayoutRTL()) { + right = getWidth() - getPaddingRight(); + left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); + } else { + left = getPaddingLeft(); + right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + bottom = layoutState.mOffset; + top = layoutState.mOffset - result.mConsumed; + } else { + top = layoutState.mOffset; + bottom = layoutState.mOffset + result.mConsumed; + } + } else { + top = getPaddingTop(); + bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); + + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + right = layoutState.mOffset; + left = layoutState.mOffset - result.mConsumed; + } else { + left = layoutState.mOffset; + right = layoutState.mOffset + result.mConsumed; + } + } + // We calculate everything with View's bounding box (which includes decor and margins) + // To calculate correct layout position, we subtract margins. + layoutDecoratedWithMargins(view, left, top, right, bottom); + if (DEBUG) { + Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); + } + // Consume the available space if the view is not removed OR changed + if (params.isItemRemoved() || params.isItemChanged()) { + result.mIgnoreConsumed = true; + } + result.mFocusable = view.hasFocusable(); + } + + @Override + boolean shouldMeasureTwice() { + return getHeightMode() != View.MeasureSpec.EXACTLY + && getWidthMode() != View.MeasureSpec.EXACTLY + && hasFlexibleChildInBothOrientations(); + } + + /** + * Converts a focusDirection to orientation. + * + * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction + * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. + */ + int convertFocusDirectionToLayoutDirection(int focusDirection) { + switch (focusDirection) { + case View.FOCUS_BACKWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_START; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_END; + } else { + return LayoutState.LAYOUT_START; + } + case View.FOCUS_FORWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_END; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_START; + } else { + return LayoutState.LAYOUT_END; + } + case View.FOCUS_UP: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_DOWN: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_LEFT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_RIGHT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + default: + if (DEBUG) { + Log.d(TAG, "Unknown focus request:" + focusDirection); + } + return LayoutState.INVALID_LAYOUT; + } + + } + + /** + * Convenience method to find the child closes to start. Caller should check it has enough + * children. + * + * @return The child closes to start of the layout from user's perspective. + */ + private View getChildClosestToStart() { + return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0); + } + + /** + * Convenience method to find the child closes to end. Caller should check it has enough + * children. + * + * @return The child closes to end of the layout from user's perspective. + */ + private View getChildClosestToEnd() { + return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1); + } + + /** + * Convenience method to find the visible child closes to start. Caller should check if it has + * enough children. + * + * @param completelyVisible Whether child should be completely visible or not + * @return The first visible child closest to start of the layout from user's perspective. + */ + View findFirstVisibleChildClosestToStart(boolean completelyVisible, + boolean acceptPartiallyVisible) { + if (mShouldReverseLayout) { + return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, + acceptPartiallyVisible); + } else { + return findOneVisibleChild(0, getChildCount(), completelyVisible, + acceptPartiallyVisible); + } + } + + /** + * Convenience method to find the visible child closes to end. Caller should check if it has + * enough children. + * + * @param completelyVisible Whether child should be completely visible or not + * @return The first visible child closest to end of the layout from user's perspective. + */ + View findFirstVisibleChildClosestToEnd(boolean completelyVisible, + boolean acceptPartiallyVisible) { + if (mShouldReverseLayout) { + return findOneVisibleChild(0, getChildCount(), completelyVisible, + acceptPartiallyVisible); + } else { + return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, + acceptPartiallyVisible); + } + } + + // overridden by GridLayoutManager + + /** + * Finds a suitable anchor child. + *

+ * Due to ambiguous adapter updates or children being removed, some children's positions may be + * invalid. This method is a best effort to find a position within adapter bounds if possible. + *

+ * It also prioritizes children from best to worst in this order: + *

    + *
  1. An in bounds child. + *
  2. An out of bounds child. + *
  3. An invalid child. + *
+ * + * @param layoutFromEnd True if the RV scrolls in the reverse direction, which is the same as + * (reverseLayout ^ stackFromEnd). + * @param traverseChildrenInReverseOrder True if the children should be traversed in reverse + * order (stackFromEnd). + * @return A View that can be used an an anchor View. + */ + View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean layoutFromEnd, boolean traverseChildrenInReverseOrder) { + ensureLayoutState(); + + // Determine which direction through the view children we are going iterate. + int start = 0; + int end = getChildCount(); + int diff = 1; + if (traverseChildrenInReverseOrder) { + start = getChildCount() - 1; + end = -1; + diff = -1; + } + + int itemCount = state.getItemCount(); + + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); + + View invalidMatch = null; + View bestFirstFind = null; + View bestSecondFind = null; + + for (int i = start; i != end; i += diff) { + final View view = getChildAt(i); + final int position = getPosition(view); + final int childStart = mOrientationHelper.getDecoratedStart(view); + final int childEnd = mOrientationHelper.getDecoratedEnd(view); + if (position >= 0 && position < itemCount) { + if (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { + if (invalidMatch == null) { + invalidMatch = view; // removed item, least preferred + } + } else { + // b/148869110: usually if childStart >= boundsEnd the child is out of + // bounds, except if the child is 0 pixels! + boolean outOfBoundsBefore = childEnd <= boundsStart && childStart < boundsStart; + boolean outOfBoundsAfter = childStart >= boundsEnd && childEnd > boundsEnd; + if (outOfBoundsBefore || outOfBoundsAfter) { + // The item is out of bounds. + // We want to find the items closest to the in bounds items and because we + // are always going through the items linearly, the 2 items we want are the + // last out of bounds item on the side we start searching on, and the first + // out of bounds item on the side we are ending on. The side that we are + // ending on ultimately takes priority because we want items later in the + // layout to move forward if no in bounds anchors are found. + if (layoutFromEnd) { + if (outOfBoundsAfter) { + bestFirstFind = view; + } else if (bestSecondFind == null) { + bestSecondFind = view; + } + } else { + if (outOfBoundsBefore) { + bestFirstFind = view; + } else if (bestSecondFind == null) { + bestSecondFind = view; + } + } + } else { + // We found an in bounds item, greedily return it. + return view; + } + } + } + } + // We didn't find an in bounds item so we will settle for an item in this order: + // 1. bestSecondFind + // 2. bestFirstFind + // 3. invalidMatch + return bestSecondFind != null ? bestSecondFind : + (bestFirstFind != null ? bestFirstFind : invalidMatch); + } + + // returns the out-of-bound child view closest to RV's end bounds. An out-of-bound child is + // defined as a child that's either partially or fully invisible (outside RV's padding area). + private View findPartiallyOrCompletelyInvisibleChildClosestToEnd() { + return mShouldReverseLayout ? findFirstPartiallyOrCompletelyInvisibleChild() + : findLastPartiallyOrCompletelyInvisibleChild(); + } + + // returns the out-of-bound child view closest to RV's starting bounds. An out-of-bound child is + // defined as a child that's either partially or fully invisible (outside RV's padding area). + private View findPartiallyOrCompletelyInvisibleChildClosestToStart() { + return mShouldReverseLayout ? findLastPartiallyOrCompletelyInvisibleChild() : + findFirstPartiallyOrCompletelyInvisibleChild(); + } + + private View findFirstPartiallyOrCompletelyInvisibleChild() { + return findOnePartiallyOrCompletelyInvisibleChild(0, getChildCount()); + } + + private View findLastPartiallyOrCompletelyInvisibleChild() { + return findOnePartiallyOrCompletelyInvisibleChild(getChildCount() - 1, -1); + } + + /** + * Returns the adapter position of the first visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * LayoutManager may pre-cache some views that are not necessarily visible. Those views + * are ignored in this method. + * + * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items. + * @see #findFirstCompletelyVisibleItemPosition() + * @see #findLastVisibleItemPosition() + */ + public int findFirstVisibleItemPosition() { + final View child = findOneVisibleChild(0, getChildCount(), false, true); + return child == null ? RecyclerView.NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the first fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + *

+ * Note that bounds check is only performed in the current orientation. That means, if + * LayoutManager is horizontal, it will only check the view's left and right edges. + * + * @return The adapter position of the first fully visible item or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + * @see #findFirstVisibleItemPosition() + * @see #findLastCompletelyVisibleItemPosition() + */ + public int findFirstCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(0, getChildCount(), true, false); + return child == null ? RecyclerView.NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the last visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * LayoutManager may pre-cache some views that are not necessarily visible. Those views + * are ignored in this method. + * + * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items. + * @see #findLastCompletelyVisibleItemPosition() + * @see #findFirstVisibleItemPosition() + */ + public int findLastVisibleItemPosition() { + final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true); + return child == null ? RecyclerView.NO_POSITION : getPosition(child); + } + + /** + * Returns the adapter position of the last fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + *

+ * Note that bounds check is only performed in the current orientation. That means, if + * LayoutManager is horizontal, it will only check the view's left and right edges. + * + * @return The adapter position of the last fully visible view or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + * @see #findLastVisibleItemPosition() + * @see #findFirstCompletelyVisibleItemPosition() + */ + public int findLastCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false); + return child == null ? RecyclerView.NO_POSITION : getPosition(child); + } + + // Returns the first child that is visible in the provided index range, i.e. either partially or + // fully visible depending on the arguments provided. Completely invisible children are not + // acceptable by this method, but could be returned + // using #findOnePartiallyOrCompletelyInvisibleChild + View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, + boolean acceptPartiallyVisible) { + ensureLayoutState(); + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; + @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; + if (completelyVisible) { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); + } else { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); + } + if (acceptPartiallyVisible) { + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); + } + return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag) : mVerticalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag); + } + + View findOnePartiallyOrCompletelyInvisibleChild(int fromIndex, int toIndex) { + ensureLayoutState(); + final int next = toIndex > fromIndex ? 1 : (toIndex < fromIndex ? -1 : 0); + if (next == 0) { + return getChildAt(fromIndex); + } + @ViewBoundsCheck.ViewBounds int preferredBoundsFlag = 0; + @ViewBoundsCheck.ViewBounds int acceptableBoundsFlag = 0; + if (mOrientationHelper.getDecoratedStart(getChildAt(fromIndex)) + < mOrientationHelper.getStartAfterPadding()) { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS | ViewBoundsCheck.FLAG_CVE_LT_PVE + | ViewBoundsCheck.FLAG_CVE_GT_PVS); + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVS_LT_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE); + } else { + preferredBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE | ViewBoundsCheck.FLAG_CVS_GT_PVS + | ViewBoundsCheck.FLAG_CVS_LT_PVE); + acceptableBoundsFlag = (ViewBoundsCheck.FLAG_CVE_GT_PVE + | ViewBoundsCheck.FLAG_CVS_GT_PVS); + } + return (mOrientation == HORIZONTAL) ? mHorizontalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag) : mVerticalBoundCheck + .findOneViewWithinBoundFlags(fromIndex, toIndex, preferredBoundsFlag, + acceptableBoundsFlag); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public View onFocusSearchFailed(View focused, int direction, + RecyclerView.Recycler recycler, RecyclerView.State state) { + resolveShouldLayoutReverse(); + if (getChildCount() == 0) { + return null; + } + + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); + if (layoutDir == LayoutState.INVALID_LAYOUT) { + return null; + } + ensureLayoutState(); + final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); + updateLayoutState(layoutDir, maxScroll, false, state); + mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; + mLayoutState.mRecycle = false; + fill(recycler, mLayoutState, state, true); + + // nextCandidate is the first child view in the layout direction that's partially + // within RV's bounds, i.e. part of it is visible or it's completely invisible but still + // touching RV's bounds. This will be the unfocusable candidate view to become visible onto + // the screen if no focusable views are found in the given layout direction. + final View nextCandidate; + if (layoutDir == LayoutState.LAYOUT_START) { + nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToStart(); + } else { + nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToEnd(); + } + // nextFocus is meaningful only if it refers to a focusable child, in which case it + // indicates the next view to gain focus. + final View nextFocus; + if (layoutDir == LayoutState.LAYOUT_START) { + nextFocus = getChildClosestToStart(); + } else { + nextFocus = getChildClosestToEnd(); + } + if (nextFocus.hasFocusable()) { + if (nextCandidate == null) { + return null; + } + return nextFocus; + } + return nextCandidate; + } + + /** + * Used for debugging. + * Logs the internal representation of children to default logger. + */ + private void logChildren() { + Log.d(TAG, "internal representation of views on the screen"); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + Log.d(TAG, "item " + getPosition(child) + ", coord:" + + mOrientationHelper.getDecoratedStart(child)); + } + Log.d(TAG, "=============="); + } + + /** + * Used for debugging. + * Validates that child views are laid out in correct order. This is important because rest of + * the algorithm relies on this constraint. + * + * In default layout, child 0 should be closest to screen position 0 and last child should be + * closest to position WIDTH or HEIGHT. + * In reverse layout, last child should be closes to screen position 0 and first child should + * be closest to position WIDTH or HEIGHT + */ + void validateChildOrder() { + Log.d(TAG, "validating child count " + getChildCount()); + if (getChildCount() < 1) { + return; + } + int lastPos = getPosition(getChildAt(0)); + int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0)); + if (mShouldReverseLayout) { + for (int i = 1; i < getChildCount(); i++) { + View child = getChildAt(i); + int pos = getPosition(child); + int screenLoc = mOrientationHelper.getDecoratedStart(child); + if (pos < lastPos) { + logChildren(); + throw new RuntimeException("detected invalid position. loc invalid? " + + (screenLoc < lastScreenLoc)); + } + if (screenLoc > lastScreenLoc) { + logChildren(); + throw new RuntimeException("detected invalid location"); + } + } + } else { + for (int i = 1; i < getChildCount(); i++) { + View child = getChildAt(i); + int pos = getPosition(child); + int screenLoc = mOrientationHelper.getDecoratedStart(child); + if (pos < lastPos) { + logChildren(); + throw new RuntimeException("detected invalid position. loc invalid? " + + (screenLoc < lastScreenLoc)); + } + if (screenLoc < lastScreenLoc) { + logChildren(); + throw new RuntimeException("detected invalid location"); + } + } + } + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd; + } + + /** + * {@inheritDoc} + */ + // This method is only intended to be called (and should only ever be called) by + // ItemTouchHelper. + @Override + public void prepareForDrop(@NonNull View view, @NonNull View target, int x, int y) { + assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation"); + ensureLayoutState(); + resolveShouldLayoutReverse(); + final int myPos = getPosition(view); + final int targetPos = getPosition(target); + final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL + : LayoutState.ITEM_DIRECTION_HEAD; + if (mShouldReverseLayout) { + if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() + - (mOrientationHelper.getDecoratedStart(target) + + mOrientationHelper.getDecoratedMeasurement(view))); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getEndAfterPadding() + - mOrientationHelper.getDecoratedEnd(target)); + } + } else { + if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) { + scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target)); + } else { + scrollToPositionWithOffset(targetPos, + mOrientationHelper.getDecoratedEnd(target) + - mOrientationHelper.getDecoratedMeasurement(view)); + } + } + } + + /** + * Helper class that keeps temporary state while {LayoutManager} is filling out the empty + * space. + */ + static class LayoutState { + + static final String TAG = "LLM#LayoutState"; + + static final int LAYOUT_START = -1; + + static final int LAYOUT_END = 1; + + static final int INVALID_LAYOUT = Integer.MIN_VALUE; + + static final int ITEM_DIRECTION_HEAD = -1; + + static final int ITEM_DIRECTION_TAIL = 1; + + static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE; + + /** + * We may not want to recycle children in some cases (e.g. layout) + */ + boolean mRecycle = true; + + /** + * Pixel offset where layout should start + */ + int mOffset; + + /** + * Number of pixels that we should fill, in the layout direction. + */ + int mAvailable; + + /** + * Current position on the adapter to get the next item. + */ + int mCurrentPosition; + + /** + * Defines the direction in which the data adapter is traversed. + * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} + */ + int mItemDirection; + + /** + * Defines the direction in which the layout is filled. + * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} + */ + int mLayoutDirection; + + /** + * Used when LayoutState is constructed in a scrolling state. + * It should be set the amount of scrolling we can make without creating a new view. + * Settings this is required for efficient view recycling. + */ + int mScrollingOffset; + + /** + * Used if you want to pre-layout items that are not yet visible. + * The difference with {@link #mAvailable} is that, when recycling, distance laid out for + * {@link #mExtraFillSpace} is not considered to avoid recycling visible children. + */ + int mExtraFillSpace = 0; + + /** + * Contains the {@link #calculateExtraLayoutSpace(RecyclerView.State, int[])} extra layout + * space} that should be excluded for recycling when cleaning up the tail of the list during + * a smooth scroll. + */ + int mNoRecycleSpace = 0; + + /** + * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value + * is set to true, we skip removed views since they should not be laid out in post layout + * step. + */ + boolean mIsPreLayout = false; + + /** + * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)} + * amount. + */ + int mLastScrollDelta; + + /** + * When LLM needs to layout particular views, it sets this list in which case, LayoutState + * will only return views from this list and return null if it cannot find an item. + */ + List mScrapList = null; + + /** + * Used when there is no limit in how many views can be laid out. + */ + boolean mInfinite; + + /** + * @return true if there are more items in the data adapter + */ + boolean hasMore(RecyclerView.State state) { + return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); + } + + /** + * Gets the view for the next element that we should layout. + * Also updates current item index to the next item, based on {@link #mItemDirection} + * + * @return The next element that we should layout. + */ + View next(RecyclerView.Recycler recycler) { + if (mScrapList != null) { + return nextViewFromScrapList(); + } + final View view = recycler.getViewForPosition(mCurrentPosition); + mCurrentPosition += mItemDirection; + return view; + } + + /** + * Returns the next item from the scrap list. + *

+ * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection + * + * @return View if an item in the current position or direction exists if not null. + */ + private View nextViewFromScrapList() { + final int size = mScrapList.size(); + for (int i = 0; i < size; i++) { + final View view = mScrapList.get(i).itemView; + final RecyclerView.LayoutParams lp = + (RecyclerView.LayoutParams) view.getLayoutParams(); + if (lp.isItemRemoved()) { + continue; + } + if (mCurrentPosition == lp.getViewLayoutPosition()) { + assignPositionFromScrapList(view); + return view; + } + } + return null; + } + + public void assignPositionFromScrapList() { + assignPositionFromScrapList(null); + } + + public void assignPositionFromScrapList(View ignore) { + final View closest = nextViewInLimitedList(ignore); + if (closest == null) { + mCurrentPosition = RecyclerView.NO_POSITION; + } else { + mCurrentPosition = ((RecyclerView.LayoutParams) closest.getLayoutParams()) + .getViewLayoutPosition(); + } + } + + public View nextViewInLimitedList(View ignore) { + int size = mScrapList.size(); + View closest = null; + int closestDistance = Integer.MAX_VALUE; + if (DEBUG && mIsPreLayout) { + throw new IllegalStateException("Scrap list cannot be used in pre layout"); + } + for (int i = 0; i < size; i++) { + View view = mScrapList.get(i).itemView; + final RecyclerView.LayoutParams lp = + (RecyclerView.LayoutParams) view.getLayoutParams(); + if (view == ignore || lp.isItemRemoved()) { + continue; + } + final int distance = (lp.getViewLayoutPosition() - mCurrentPosition) + * mItemDirection; + if (distance < 0) { + continue; // item is not in current direction + } + if (distance < closestDistance) { + closest = view; + closestDistance = distance; + if (distance == 0) { + break; + } + } + } + return closest; + } + + void log() { + Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); + } + } + + /** + * @hide + */ + @RestrictTo(LIBRARY) + @SuppressLint("BanParcelableUsage") + public static class SavedState implements Parcelable { + + int mAnchorPosition; + + int mAnchorOffset; + + boolean mAnchorLayoutFromEnd; + + public SavedState() { + + } + + SavedState(Parcel in) { + mAnchorPosition = in.readInt(); + mAnchorOffset = in.readInt(); + mAnchorLayoutFromEnd = in.readInt() == 1; + } + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public SavedState(SavedState other) { + mAnchorPosition = other.mAnchorPosition; + mAnchorOffset = other.mAnchorOffset; + mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; + } + + boolean hasValidAnchor() { + return mAnchorPosition >= 0; + } + + void invalidateAnchor() { + mAnchorPosition = RecyclerView.NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorPosition); + dest.writeInt(mAnchorOffset); + dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Simple data class to keep Anchor information + */ + static class AnchorInfo { + OrientationHelper mOrientationHelper; + int mPosition; + int mCoordinate; + boolean mLayoutFromEnd; + boolean mValid; + + AnchorInfo() { + reset(); + } + + void reset() { + mPosition = RecyclerView.NO_POSITION; + mCoordinate = INVALID_OFFSET; + mLayoutFromEnd = false; + mValid = false; + } + + /** + * assigns anchor coordinate from the RecyclerView's padding depending on current + * layoutFromEnd value + */ + void assignCoordinateFromPadding() { + mCoordinate = mLayoutFromEnd + ? mOrientationHelper.getEndAfterPadding() + : mOrientationHelper.getStartAfterPadding(); + } + + @Override + public String toString() { + return "AnchorInfo{" + + "mPosition=" + mPosition + + ", mCoordinate=" + mCoordinate + + ", mLayoutFromEnd=" + mLayoutFromEnd + + ", mValid=" + mValid + + '}'; + } + + boolean isViewValidAsAnchor(View child, RecyclerView.State state) { + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0 + && lp.getViewLayoutPosition() < state.getItemCount(); + } + + public void assignFromViewAndKeepVisibleRect(View child, int position) { + final int spaceChange = mOrientationHelper.getTotalSpaceChange(); + if (spaceChange >= 0) { + assignFromView(child, position); + return; + } + mPosition = position; + if (mLayoutFromEnd) { + final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; + final int childEnd = mOrientationHelper.getDecoratedEnd(child); + final int previousEndMargin = prevLayoutEnd - childEnd; + mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin; + // ensure we did not push child's top out of bounds because of this + if (previousEndMargin > 0) { // we have room to shift bottom if necessary + final int childSize = mOrientationHelper.getDecoratedMeasurement(child); + final int estimatedChildStart = mCoordinate - childSize; + final int layoutStart = mOrientationHelper.getStartAfterPadding(); + final int previousStartMargin = mOrientationHelper.getDecoratedStart(child) + - layoutStart; + final int startReference = layoutStart + Math.min(previousStartMargin, 0); + final int startMargin = estimatedChildStart - startReference; + if (startMargin < 0) { + // offset to make top visible but not too much + mCoordinate += Math.min(previousEndMargin, -startMargin); + } + } + } else { + final int childStart = mOrientationHelper.getDecoratedStart(child); + final int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); + mCoordinate = childStart; + if (startMargin > 0) { // we have room to fix end as well + final int estimatedEnd = childStart + + mOrientationHelper.getDecoratedMeasurement(child); + final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() + - spaceChange; + final int previousEndMargin = previousLayoutEnd + - mOrientationHelper.getDecoratedEnd(child); + final int endReference = mOrientationHelper.getEndAfterPadding() + - Math.min(0, previousEndMargin); + final int endMargin = endReference - estimatedEnd; + if (endMargin < 0) { + mCoordinate -= Math.min(startMargin, -endMargin); + } + } + } + } + + public void assignFromView(View child, int position) { + if (mLayoutFromEnd) { + mCoordinate = mOrientationHelper.getDecoratedEnd(child) + + mOrientationHelper.getTotalSpaceChange(); + } else { + mCoordinate = mOrientationHelper.getDecoratedStart(child); + } + + mPosition = position; + } + } + + protected static class LayoutChunkResult { + public int mConsumed; + public boolean mFinished; + public boolean mIgnoreConsumed; + public boolean mFocusable; + + void resetInternal() { + mConsumed = 0; + mFinished = false; + mIgnoreConsumed = false; + mFocusable = false; + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java b/app/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java new file mode 100644 index 0000000000..b4ba75fcdf --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/LinearSmoothScroller.java @@ -0,0 +1,360 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PointF; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +/** + * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until + * the target position becomes a child of the RecyclerView and then uses a + * {@link DecelerateInterpolator} to slowly approach to target position. + *

+ * If the {@link RecyclerView.LayoutManager} you are using does not implement the + * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the + * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with + * the support library implement this interface. + */ +public class LinearSmoothScroller extends RecyclerView.SmoothScroller { + + private static final boolean DEBUG = false; + + private static final float MILLISECONDS_PER_INCH = 25f; + + private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000; + + /** + * Align child view's left or top with parent view's left or top + * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_START = -1; + + /** + * Align child view's right or bottom with parent view's right or bottom + * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_END = 1; + + /** + *

Decides if the child should be snapped from start or end, depending on where it + * currently is in relation to its parent.

+ *

For instance, if the view is virtually on the left of RecyclerView, using + * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}

+ * + * @see #calculateDtToFit(int, int, int, int, int) + * @see #calculateDxToMakeVisible(android.view.View, int) + * @see #calculateDyToMakeVisible(android.view.View, int) + */ + public static final int SNAP_TO_ANY = 0; + + // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target + // view is not laid out until interim target position is reached, we can detect the case before + // scrolling slows down and reschedule another interim target scroll + private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f; + + protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator(); + + protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + protected PointF mTargetVector; + + private final DisplayMetrics mDisplayMetrics; + private boolean mHasCalculatedMillisPerPixel = false; + private float mMillisPerPixel; + + // Temporary variables to keep track of the interim scroll target. These values do not + // point to a real item position, rather point to an estimated location pixels. + protected int mInterimTargetDx = 0, mInterimTargetDy = 0; + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LinearSmoothScroller(Context context) { + mDisplayMetrics = context.getResources().getDisplayMetrics(); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onStart() { + + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference()); + final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference()); + final int distance = (int) Math.sqrt(dx * dx + dy * dy); + final int time = calculateTimeForDeceleration(distance); + if (time > 0) { + action.update(-dx, -dy, time, mDecelerateInterpolator); + } + } + + /** + * {@inheritDoc} + */ + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { + // TODO(b/72745539): Is there ever a time when onSeekTargetStep should be called when + // getChildCount returns 0? Should this logic be extracted out of this method such that + // this method is not called if getChildCount() returns 0? + if (getChildCount() == 0) { + stop(); + return; + } + //noinspection PointlessBooleanExpression + if (DEBUG && mTargetVector != null + && (mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0)) { + throw new IllegalStateException("Scroll happened in the opposite direction" + + " of the target. Some calculations are wrong"); + } + mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx); + mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy); + + if (mInterimTargetDx == 0 && mInterimTargetDy == 0) { + updateActionForInterimTarget(action); + } // everything is valid, keep going + + } + + /** + * {@inheritDoc} + */ + @Override + protected void onStop() { + mInterimTargetDx = mInterimTargetDy = 0; + mTargetVector = null; + } + + /** + * Calculates the scroll speed. + * + *

By default, LinearSmoothScroller assumes this method always returns the same value and + * caches the result of calling it. + * + * @param displayMetrics DisplayMetrics to be used for real dimension calculations + * @return The time (in ms) it should take for each pixel. For instance, if returned value is + * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + private float getSpeedPerPixel() { + if (!mHasCalculatedMillisPerPixel) { + mMillisPerPixel = calculateSpeedPerPixel(mDisplayMetrics); + mHasCalculatedMillisPerPixel = true; + } + return mMillisPerPixel; + } + + /** + *

Calculates the time for deceleration so that transition from LinearInterpolator to + * DecelerateInterpolator looks smooth.

+ * + * @param dx Distance to scroll + * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning + * from LinearInterpolation + */ + protected int calculateTimeForDeceleration(int dx) { + // we want to cover same area with the linear interpolator for the first 10% of the + // interpolation. After that, deceleration will take control. + // area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x + // which gives 0.100028 when x = .3356 + // this is why we divide linear scrolling time with .3356 + return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356); + } + + /** + * Calculates the time it should take to scroll the given distance (in pixels) + * + * @param dx Distance in pixels that we want to scroll + * @return Time in milliseconds + * @see #calculateSpeedPerPixel(android.util.DisplayMetrics) + */ + protected int calculateTimeForScrolling(int dx) { + // In a case where dx is very small, rounding may return 0 although dx > 0. + // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive + // time. + return (int) Math.ceil(Math.abs(dx) * getSpeedPerPixel()); + } + + /** + * When scrolling towards a child view, this method defines whether we should align the left + * or the right edge of the child with the parent RecyclerView. + * + * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector + * @see #SNAP_TO_START + * @see #SNAP_TO_END + * @see #SNAP_TO_ANY + */ + protected int getHorizontalSnapPreference() { + return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY : + mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START; + } + + /** + * When scrolling towards a child view, this method defines whether we should align the top + * or the bottom edge of the child with the parent RecyclerView. + * + * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector + * @see #SNAP_TO_START + * @see #SNAP_TO_END + * @see #SNAP_TO_ANY + */ + protected int getVerticalSnapPreference() { + return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY : + mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START; + } + + /** + * When the target scroll position is not a child of the RecyclerView, this method calculates + * a direction vector towards that child and triggers a smooth scroll. + * + * @see #computeScrollVectorForPosition(int) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + protected void updateActionForInterimTarget(Action action) { + // find an interim target position + PointF scrollVector = computeScrollVectorForPosition(getTargetPosition()); + if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) { + final int target = getTargetPosition(); + action.jumpTo(target); + stop(); + return; + } + normalize(scrollVector); + mTargetVector = scrollVector; + + mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x); + mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y); + final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX); + // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the + // interim target. Since we track the distance travelled in onSeekTargetStep callback, it + // won't actually scroll more than what we need. + action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO), + (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO), + (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator); + } + + private int clampApplyScroll(int tmpDt, int dt) { + final int before = tmpDt; + tmpDt -= dt; + if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset + return 0; + } + return tmpDt; + } + + /** + * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and + * {@link #calculateDyToMakeVisible(android.view.View, int)} + */ + public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int + snapPreference) { + switch (snapPreference) { + case SNAP_TO_START: + return boxStart - viewStart; + case SNAP_TO_END: + return boxEnd - viewEnd; + case SNAP_TO_ANY: + final int dtStart = boxStart - viewStart; + if (dtStart > 0) { + return dtStart; + } + final int dtEnd = boxEnd - viewEnd; + if (dtEnd < 0) { + return dtEnd; + } + break; + default: + throw new IllegalArgumentException("snap preference should be one of the" + + " constants defined in SmoothScroller, starting with SNAP_"); + } + return 0; + } + + /** + * Calculates the vertical scroll amount necessary to make the given view fully visible + * inside the RecyclerView. + * + * @param view The view which we want to make fully visible + * @param snapPreference The edge which the view should snap to when entering the visible + * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or + * {@link #SNAP_TO_ANY}. + * @return The vertical scroll amount necessary to make the view visible with the given + * snap preference. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int calculateDyToMakeVisible(View view, int snapPreference) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollVertically()) { + return 0; + } + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + final int top = layoutManager.getDecoratedTop(view) - params.topMargin; + final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin; + final int start = layoutManager.getPaddingTop(); + final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom(); + return calculateDtToFit(top, bottom, start, end, snapPreference); + } + + /** + * Calculates the horizontal scroll amount necessary to make the given view fully visible + * inside the RecyclerView. + * + * @param view The view which we want to make fully visible + * @param snapPreference The edge which the view should snap to when entering the visible + * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or + * {@link #SNAP_TO_END} + * @return The vertical scroll amount necessary to make the view visible with the given + * snap preference. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int calculateDxToMakeVisible(View view, int snapPreference) { + final RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null || !layoutManager.canScrollHorizontally()) { + return 0; + } + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin; + final int right = layoutManager.getDecoratedRight(view) + params.rightMargin; + final int start = layoutManager.getPaddingLeft(); + final int end = layoutManager.getWidth() - layoutManager.getPaddingRight(); + return calculateDtToFit(left, right, start, end, snapPreference); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java b/app/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java new file mode 100644 index 0000000000..6481b877e5 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/LinearSnapHelper.java @@ -0,0 +1,276 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.graphics.PointF; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Implementation of the {@link SnapHelper} supporting snapping in either vertical or horizontal + * orientation. + *

+ * The implementation will snap the center of the target child view to the center of + * the attached {@link RecyclerView}. If you intend to change this behavior then override + * {@link SnapHelper#calculateDistanceToFinalSnap}. + */ +public class LinearSnapHelper extends SnapHelper { + + private static final float INVALID_DISTANCE = 1f; + + // Orientation helpers are lazily created per LayoutManager. + @Nullable + private OrientationHelper mVerticalHelper; + @Nullable + private OrientationHelper mHorizontalHelper; + + @Override + public int[] calculateDistanceToFinalSnap( + @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { + int[] out = new int[2]; + if (layoutManager.canScrollHorizontally()) { + out[0] = distanceToCenter(targetView, + getHorizontalHelper(layoutManager)); + } else { + out[0] = 0; + } + + if (layoutManager.canScrollVertically()) { + out[1] = distanceToCenter(targetView, + getVerticalHelper(layoutManager)); + } else { + out[1] = 0; + } + return out; + } + + @Override + public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return RecyclerView.NO_POSITION; + } + + final int itemCount = layoutManager.getItemCount(); + if (itemCount == 0) { + return RecyclerView.NO_POSITION; + } + + final View currentView = findSnapView(layoutManager); + if (currentView == null) { + return RecyclerView.NO_POSITION; + } + + final int currentPosition = layoutManager.getPosition(currentView); + if (currentPosition == RecyclerView.NO_POSITION) { + return RecyclerView.NO_POSITION; + } + + RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = + (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; + // deltaJumps sign comes from the velocity which may not match the order of children in + // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to + // get the direction. + PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); + if (vectorForEnd == null) { + // cannot get a vector for the given position. + return RecyclerView.NO_POSITION; + } + + int vDeltaJump, hDeltaJump; + if (layoutManager.canScrollHorizontally()) { + hDeltaJump = estimateNextPositionDiffForFling(layoutManager, + getHorizontalHelper(layoutManager), velocityX, 0); + if (vectorForEnd.x < 0) { + hDeltaJump = -hDeltaJump; + } + } else { + hDeltaJump = 0; + } + if (layoutManager.canScrollVertically()) { + vDeltaJump = estimateNextPositionDiffForFling(layoutManager, + getVerticalHelper(layoutManager), 0, velocityY); + if (vectorForEnd.y < 0) { + vDeltaJump = -vDeltaJump; + } + } else { + vDeltaJump = 0; + } + + int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump; + if (deltaJump == 0) { + return RecyclerView.NO_POSITION; + } + + int targetPos = currentPosition + deltaJump; + if (targetPos < 0) { + targetPos = 0; + } + if (targetPos >= itemCount) { + targetPos = itemCount - 1; + } + return targetPos; + } + + @Override + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + if (layoutManager.canScrollVertically()) { + return findCenterView(layoutManager, getVerticalHelper(layoutManager)); + } else if (layoutManager.canScrollHorizontally()) { + return findCenterView(layoutManager, getHorizontalHelper(layoutManager)); + } + return null; + } + + private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) { + final int childCenter = helper.getDecoratedStart(targetView) + + (helper.getDecoratedMeasurement(targetView) / 2); + final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + return childCenter - containerCenter; + } + + /** + * Estimates a position to which SnapHelper will try to scroll to in response to a fling. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * @param helper The {@link OrientationHelper} that is created from the LayoutManager. + * @param velocityX The velocity on the x axis. + * @param velocityY The velocity on the y axis. + * + * @return The diff between the target scroll position and the current position. + */ + private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper, int velocityX, int velocityY) { + int[] distances = calculateScrollDistance(velocityX, velocityY); + float distancePerChild = computeDistancePerChild(layoutManager, helper); + if (distancePerChild <= 0) { + return 0; + } + int distance = + Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1]; + return (int) Math.round(distance / distancePerChild); + } + + /** + * Return the child view that is currently closest to the center of this parent. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * + * @return the child view that is currently closest to the center of this parent. + */ + @Nullable + private View findCenterView(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper) { + int childCount = layoutManager.getChildCount(); + if (childCount == 0) { + return null; + } + + View closestChild = null; + final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + int absClosest = Integer.MAX_VALUE; + + for (int i = 0; i < childCount; i++) { + final View child = layoutManager.getChildAt(i); + int childCenter = helper.getDecoratedStart(child) + + (helper.getDecoratedMeasurement(child) / 2); + int absDistance = Math.abs(childCenter - center); + + /** if child center is closer than previous closest, set it as closest **/ + if (absDistance < absClosest) { + absClosest = absDistance; + closestChild = child; + } + } + return closestChild; + } + + /** + * Computes an average pixel value to pass a single child. + *

+ * Returns a negative value if it cannot be calculated. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * @param helper The relevant {@link OrientationHelper} for the attached + * {@link RecyclerView.LayoutManager}. + * + * @return A float value that is the average number of pixels needed to scroll by one view in + * the relevant direction. + */ + private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper) { + View minPosView = null; + View maxPosView = null; + int minPos = Integer.MAX_VALUE; + int maxPos = Integer.MIN_VALUE; + int childCount = layoutManager.getChildCount(); + if (childCount == 0) { + return INVALID_DISTANCE; + } + + for (int i = 0; i < childCount; i++) { + View child = layoutManager.getChildAt(i); + final int pos = layoutManager.getPosition(child); + if (pos == RecyclerView.NO_POSITION) { + continue; + } + if (pos < minPos) { + minPos = pos; + minPosView = child; + } + if (pos > maxPos) { + maxPos = pos; + maxPosView = child; + } + } + if (minPosView == null || maxPosView == null) { + return INVALID_DISTANCE; + } + int start = Math.min(helper.getDecoratedStart(minPosView), + helper.getDecoratedStart(maxPosView)); + int end = Math.max(helper.getDecoratedEnd(minPosView), + helper.getDecoratedEnd(maxPosView)); + int distance = end - start; + if (distance == 0) { + return INVALID_DISTANCE; + } + return 1f * distance / ((maxPos - minPos) + 1); + } + + @NonNull + private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { + if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) { + mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); + } + return mVerticalHelper; + } + + @NonNull + private OrientationHelper getHorizontalHelper( + @NonNull RecyclerView.LayoutManager layoutManager) { + if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) { + mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); + } + return mHorizontalHelper; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ListAdapter.java b/app/src/main/java/androidx/recyclerview/widget/ListAdapter.java new file mode 100644 index 0000000000..6b1ad73c13 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ListAdapter.java @@ -0,0 +1,190 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; + +/** + * {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting List data in a + * {@link RecyclerView}, including computing diffs between Lists on a background thread. + *

+ * This class is a convenience wrapper around {@link AsyncListDiffer} that implements Adapter common + * default behavior for item access and counting. + *

+ * While using a LiveData<List> is an easy way to provide data to the adapter, it isn't required + * - you can use {@link #submitList(List)} when new lists are available. + *

+ * A complete usage pattern with Room would look like this: + *

+ * {@literal @}Dao
+ * interface UserDao {
+ *     {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
+ *     public abstract LiveData<List<User>> usersByLastName();
+ * }
+ *
+ * class MyViewModel extends ViewModel {
+ *     public final LiveData<List<User>> usersList;
+ *     public MyViewModel(UserDao userDao) {
+ *         usersList = userDao.usersByLastName();
+ *     }
+ * }
+ *
+ * class MyActivity extends AppCompatActivity {
+ *     {@literal @}Override
+ *     public void onCreate(Bundle savedState) {
+ *         super.onCreate(savedState);
+ *         MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
+ *         RecyclerView recyclerView = findViewById(R.id.user_list);
+ *         UserAdapter<User> adapter = new UserAdapter();
+ *         viewModel.usersList.observe(this, list -> adapter.submitList(list));
+ *         recyclerView.setAdapter(adapter);
+ *     }
+ * }
+ *
+ * class UserAdapter extends ListAdapter<User, UserViewHolder> {
+ *     public UserAdapter() {
+ *         super(User.DIFF_CALLBACK);
+ *     }
+ *     {@literal @}Override
+ *     public void onBindViewHolder(UserViewHolder holder, int position) {
+ *         holder.bindTo(getItem(position));
+ *     }
+ *     public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
+ *             new DiffUtil.ItemCallback<User>() {
+ *         {@literal @}Override
+ *         public boolean areItemsTheSame(
+ *                 {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ *             // User properties may have changed if reloaded from the DB, but ID is fixed
+ *             return oldUser.getId() == newUser.getId();
+ *         }
+ *         {@literal @}Override
+ *         public boolean areContentsTheSame(
+ *                 {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
+ *             // NOTE: if you use equals, your object must properly override Object#equals()
+ *             // Incorrectly returning false here will result in too many animations.
+ *             return oldUser.equals(newUser);
+ *         }
+ *     }
+ * }
+ * + * Advanced users that wish for more control over adapter behavior, or to provide a specific base + * class should refer to {@link AsyncListDiffer}, which provides custom mapping from diff events + * to adapter positions. + * + * @param Type of the Lists this Adapter will receive. + * @param A class that extends ViewHolder that will be used by the adapter. + */ +public abstract class ListAdapter + extends RecyclerView.Adapter { + final AsyncListDiffer mDiffer; + private final AsyncListDiffer.ListListener mListener = + new AsyncListDiffer.ListListener() { + @Override + public void onCurrentListChanged( + @NonNull List previousList, @NonNull List currentList) { + ListAdapter.this.onCurrentListChanged(previousList, currentList); + } + }; + + @SuppressWarnings("unused") + protected ListAdapter(@NonNull DiffUtil.ItemCallback diffCallback) { + mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), + new AsyncDifferConfig.Builder<>(diffCallback).build()); + mDiffer.addListListener(mListener); + } + + @SuppressWarnings("unused") + protected ListAdapter(@NonNull AsyncDifferConfig config) { + mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config); + mDiffer.addListListener(mListener); + } + + /** + * Submits a new list to be diffed, and displayed. + *

+ * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + * + * @param list The new list to be displayed. + */ + public void submitList(@Nullable List list) { + mDiffer.submitList(list); + } + + /** + * Set the new list to be displayed. + *

+ * If a List is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + *

+ * The commit callback can be used to know when the List is committed, but note that it + * may not be executed. If List B is submitted immediately after List A, and is + * committed directly, the callback associated with List A will not be run. + * + * @param list The new list to be displayed. + * @param commitCallback Optional runnable that is executed when the List is committed, if + * it is committed. + */ + public void submitList(@Nullable List list, @Nullable final Runnable commitCallback) { + mDiffer.submitList(list, commitCallback); + } + + protected T getItem(int position) { + return mDiffer.getCurrentList().get(position); + } + + @Override + public int getItemCount() { + return mDiffer.getCurrentList().size(); + } + + /** + * Get the current List - any diffing to present this list has already been computed and + * dispatched via the ListUpdateCallback. + *

+ * If a null List, or no List has been submitted, an empty list will be returned. + *

+ * The returned list may not be mutated - mutations to content must be done through + * {@link #submitList(List)}. + * + * @return The list currently being displayed. + * + * @see #onCurrentListChanged(List, List) + */ + @NonNull + public List getCurrentList() { + return mDiffer.getCurrentList(); + } + + /** + * Called when the current List is updated. + *

+ * If a null List is passed to {@link #submitList(List)}, or no List has been + * submitted, the current List is represented as an empty List. + * + * @param previousList List that was displayed previously. + * @param currentList new List being displayed, will be empty if {@code null} was passed to + * {@link #submitList(List)}. + * + * @see #getCurrentList() + */ + public void onCurrentListChanged(@NonNull List previousList, @NonNull List currentList) { + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ListUpdateCallback.java b/app/src/main/java/androidx/recyclerview/widget/ListUpdateCallback.java new file mode 100644 index 0000000000..ed8e7fc676 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ListUpdateCallback.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import androidx.annotation.Nullable; + +/** + * An interface that can receive Update operations that are applied to a list. + *

+ * This class can be used together with DiffUtil to detect changes between two lists. + */ +public interface ListUpdateCallback { + /** + * Called when {@code count} number of items are inserted at the given position. + * + * @param position The position of the new item. + * @param count The number of items that have been added. + */ + void onInserted(int position, int count); + + /** + * Called when {@code count} number of items are removed from the given position. + * + * @param position The position of the item which has been removed. + * @param count The number of items which have been removed. + */ + void onRemoved(int position, int count); + + /** + * Called when an item changes its position in the list. + * + * @param fromPosition The previous position of the item before the move. + * @param toPosition The new position of the item. + */ + void onMoved(int fromPosition, int toPosition); + + /** + * Called when {@code count} number of items are updated at the given position. + * + * @param position The position of the item which has been updated. + * @param count The number of items which has changed. + */ + void onChanged(int position, int count, @Nullable Object payload); +} diff --git a/app/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java b/app/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java new file mode 100644 index 0000000000..4286cd639c --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/MessageThreadUtil.java @@ -0,0 +1,294 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +class MessageThreadUtil implements ThreadUtil { + + @Override + public MainThreadCallback getMainThreadProxy(final MainThreadCallback callback) { + return new MainThreadCallback() { + final MessageQueue mQueue = new MessageQueue(); + final private Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + + static final int UPDATE_ITEM_COUNT = 1; + static final int ADD_TILE = 2; + static final int REMOVE_TILE = 3; + + @Override + public void updateItemCount(int generation, int itemCount) { + sendMessage(SyncQueueItem.obtainMessage(UPDATE_ITEM_COUNT, generation, itemCount)); + } + + @Override + public void addTile(int generation, TileList.Tile tile) { + sendMessage(SyncQueueItem.obtainMessage(ADD_TILE, generation, tile)); + } + + @Override + public void removeTile(int generation, int position) { + sendMessage(SyncQueueItem.obtainMessage(REMOVE_TILE, generation, position)); + } + + private void sendMessage(SyncQueueItem msg) { + mQueue.sendMessage(msg); + mMainThreadHandler.post(mMainThreadRunnable); + } + + private Runnable mMainThreadRunnable = new Runnable() { + @Override + public void run() { + SyncQueueItem msg = mQueue.next(); + while (msg != null) { + switch (msg.what) { + case UPDATE_ITEM_COUNT: + callback.updateItemCount(msg.arg1, msg.arg2); + break; + case ADD_TILE: + @SuppressWarnings("unchecked") + TileList.Tile tile = (TileList.Tile) msg.data; + callback.addTile(msg.arg1, tile); + break; + case REMOVE_TILE: + callback.removeTile(msg.arg1, msg.arg2); + break; + default: + Log.e("ThreadUtil", "Unsupported message, what=" + msg.what); + } + msg = mQueue.next(); + } + } + }; + }; + } + + @SuppressWarnings("deprecation") /* AsyncTask */ + @Override + public BackgroundCallback getBackgroundProxy(final BackgroundCallback callback) { + return new BackgroundCallback() { + final MessageQueue mQueue = new MessageQueue(); + private final Executor mExecutor = android.os.AsyncTask.THREAD_POOL_EXECUTOR; + AtomicBoolean mBackgroundRunning = new AtomicBoolean(false); + + static final int REFRESH = 1; + static final int UPDATE_RANGE = 2; + static final int LOAD_TILE = 3; + static final int RECYCLE_TILE = 4; + + @Override + public void refresh(int generation) { + sendMessageAtFrontOfQueue(SyncQueueItem.obtainMessage(REFRESH, generation, null)); + } + + @Override + public void updateRange(int rangeStart, int rangeEnd, + int extRangeStart, int extRangeEnd, int scrollHint) { + sendMessageAtFrontOfQueue(SyncQueueItem.obtainMessage(UPDATE_RANGE, + rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint, null)); + } + + @Override + public void loadTile(int position, int scrollHint) { + sendMessage(SyncQueueItem.obtainMessage(LOAD_TILE, position, scrollHint)); + } + + @Override + public void recycleTile(TileList.Tile tile) { + sendMessage(SyncQueueItem.obtainMessage(RECYCLE_TILE, 0, tile)); + } + + private void sendMessage(SyncQueueItem msg) { + mQueue.sendMessage(msg); + maybeExecuteBackgroundRunnable(); + } + + private void sendMessageAtFrontOfQueue(SyncQueueItem msg) { + mQueue.sendMessageAtFrontOfQueue(msg); + maybeExecuteBackgroundRunnable(); + } + + private void maybeExecuteBackgroundRunnable() { + if (mBackgroundRunning.compareAndSet(false, true)) { + mExecutor.execute(mBackgroundRunnable); + } + } + + private Runnable mBackgroundRunnable = new Runnable() { + @Override + public void run() { + while (true) { + SyncQueueItem msg = mQueue.next(); + if (msg == null) { + break; + } + switch (msg.what) { + case REFRESH: + mQueue.removeMessages(REFRESH); + callback.refresh(msg.arg1); + break; + case UPDATE_RANGE: + mQueue.removeMessages(UPDATE_RANGE); + mQueue.removeMessages(LOAD_TILE); + callback.updateRange( + msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5); + break; + case LOAD_TILE: + callback.loadTile(msg.arg1, msg.arg2); + break; + case RECYCLE_TILE: + @SuppressWarnings("unchecked") + TileList.Tile tile = (TileList.Tile) msg.data; + callback.recycleTile(tile); + break; + default: + Log.e("ThreadUtil", "Unsupported message, what=" + msg.what); + } + } + mBackgroundRunning.set(false); + } + }; + }; + } + + /** + * Replica of android.os.Message. Unfortunately, cannot use it without a Handler and don't want + * to create a thread just for this component. + */ + static class SyncQueueItem { + + private static SyncQueueItem sPool; + private static final Object sPoolLock = new Object(); + SyncQueueItem next; + public int what; + public int arg1; + public int arg2; + public int arg3; + public int arg4; + public int arg5; + public Object data; + + void recycle() { + next = null; + what = arg1 = arg2 = arg3 = arg4 = arg5 = 0; + data = null; + synchronized (sPoolLock) { + if (sPool != null) { + next = sPool; + } + sPool = this; + } + } + + static SyncQueueItem obtainMessage(int what, int arg1, int arg2, int arg3, int arg4, + int arg5, Object data) { + synchronized (sPoolLock) { + final SyncQueueItem item; + if (sPool == null) { + item = new SyncQueueItem(); + } else { + item = sPool; + sPool = sPool.next; + item.next = null; + } + item.what = what; + item.arg1 = arg1; + item.arg2 = arg2; + item.arg3 = arg3; + item.arg4 = arg4; + item.arg5 = arg5; + item.data = data; + return item; + } + } + + static SyncQueueItem obtainMessage(int what, int arg1, int arg2) { + return obtainMessage(what, arg1, arg2, 0, 0, 0, null); + } + + static SyncQueueItem obtainMessage(int what, int arg1, Object data) { + return obtainMessage(what, arg1, 0, 0, 0, 0, data); + } + } + + static class MessageQueue { + + private SyncQueueItem mRoot; + private final Object mLock = new Object(); + + SyncQueueItem next() { + synchronized (mLock) { + if (mRoot == null) { + return null; + } + final SyncQueueItem next = mRoot; + mRoot = mRoot.next; + return next; + } + } + + void sendMessageAtFrontOfQueue(SyncQueueItem item) { + synchronized (mLock) { + item.next = mRoot; + mRoot = item; + } + } + + void sendMessage(SyncQueueItem item) { + synchronized (mLock) { + if (mRoot == null) { + mRoot = item; + return; + } + SyncQueueItem last = mRoot; + while (last.next != null) { + last = last.next; + } + last.next = item; + } + } + + void removeMessages(int what) { + synchronized (mLock) { + while (mRoot != null && mRoot.what == what) { + SyncQueueItem item = mRoot; + mRoot = mRoot.next; + item.recycle(); + } + if (mRoot != null) { + SyncQueueItem prev = mRoot; + SyncQueueItem item = prev.next; + while (item != null) { + SyncQueueItem next = item.next; + if (item.what == what) { + prev.next = next; + item.recycle(); + } else { + prev = item; + } + item = next; + } + } + } + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java b/app/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java new file mode 100644 index 0000000000..a6b9a300e3 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/NestedAdapterWrapper.java @@ -0,0 +1,201 @@ +/* + * Copyright 2020 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 androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Preconditions; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * Wrapper for each adapter in {@link ConcatAdapter}. + */ +class NestedAdapterWrapper { + @NonNull + private final ViewTypeStorage.ViewTypeLookup mViewTypeLookup; + @NonNull + private final StableIdStorage.StableIdLookup mStableIdLookup; + public final Adapter adapter; + @SuppressWarnings("WeakerAccess") + final Callback mCallback; + // we cache this value so that we can know the previous size when change happens + // this is also important as getting real size while an adapter is dispatching possibly a + // a chain of events might create inconsistencies (as it happens in DiffUtil). + // Instead, we always calculate this value based on notify events. + @SuppressWarnings("WeakerAccess") + int mCachedItemCount; + + private RecyclerView.AdapterDataObserver mAdapterObserver = + new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + mCachedItemCount = adapter.getItemCount(); + mCallback.onChanged(NestedAdapterWrapper.this); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + mCallback.onItemRangeChanged( + NestedAdapterWrapper.this, + positionStart, + itemCount, + null + ); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + mCallback.onItemRangeChanged( + NestedAdapterWrapper.this, + positionStart, + itemCount, + payload + ); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + mCachedItemCount += itemCount; + mCallback.onItemRangeInserted( + NestedAdapterWrapper.this, + positionStart, + itemCount); + if (mCachedItemCount > 0 + && adapter.getStateRestorationPolicy() == PREVENT_WHEN_EMPTY) { + mCallback.onStateRestorationPolicyChanged(NestedAdapterWrapper.this); + } + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + mCachedItemCount -= itemCount; + mCallback.onItemRangeRemoved( + NestedAdapterWrapper.this, + positionStart, + itemCount + ); + if (mCachedItemCount < 1 + && adapter.getStateRestorationPolicy() == PREVENT_WHEN_EMPTY) { + mCallback.onStateRestorationPolicyChanged(NestedAdapterWrapper.this); + } + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + Preconditions.checkArgument(itemCount == 1, + "moving more than 1 item is not supported in RecyclerView"); + mCallback.onItemRangeMoved( + NestedAdapterWrapper.this, + fromPosition, + toPosition + ); + } + + @Override + public void onStateRestorationPolicyChanged() { + mCallback.onStateRestorationPolicyChanged( + NestedAdapterWrapper.this + ); + } + }; + + NestedAdapterWrapper( + Adapter adapter, + final Callback callback, + ViewTypeStorage viewTypeStorage, + StableIdStorage.StableIdLookup stableIdLookup) { + this.adapter = adapter; + mCallback = callback; + mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this); + mStableIdLookup = stableIdLookup; + mCachedItemCount = this.adapter.getItemCount(); + this.adapter.registerAdapterDataObserver(mAdapterObserver); + } + + + void dispose() { + adapter.unregisterAdapterDataObserver(mAdapterObserver); + mViewTypeLookup.dispose(); + } + + int getCachedItemCount() { + return mCachedItemCount; + } + + int getItemViewType(int localPosition) { + return mViewTypeLookup.localToGlobal(adapter.getItemViewType(localPosition)); + } + + ViewHolder onCreateViewHolder( + ViewGroup parent, + int globalViewType) { + int localType = mViewTypeLookup.globalToLocal(globalViewType); + return adapter.onCreateViewHolder(parent, localType); + } + + void onBindViewHolder(ViewHolder viewHolder, int localPosition) { + adapter.bindViewHolder(viewHolder, localPosition); + } + + public long getItemId(int localPosition) { + long localItemId = adapter.getItemId(localPosition); + return mStableIdLookup.localToGlobal(localItemId); + } + + interface Callback { + void onChanged(@NonNull NestedAdapterWrapper wrapper); + + void onItemRangeChanged( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount + ); + + void onItemRangeChanged( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount, + @Nullable Object payload + ); + + void onItemRangeInserted( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount); + + void onItemRangeRemoved( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int positionStart, + int itemCount + ); + + void onItemRangeMoved( + @NonNull NestedAdapterWrapper nestedAdapterWrapper, + int fromPosition, + int toPosition + ); + + void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper); + } + +} diff --git a/app/src/main/java/androidx/recyclerview/widget/OpReorderer.java b/app/src/main/java/androidx/recyclerview/widget/OpReorderer.java new file mode 100644 index 0000000000..722960c82e --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/OpReorderer.java @@ -0,0 +1,233 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import java.util.List; + +class OpReorderer { + + final Callback mCallback; + + OpReorderer(Callback callback) { + mCallback = callback; + } + + void reorderOps(List ops) { + // since move operations breaks continuity, their effects on ADD/RM are hard to handle. + // we push them to the end of the list so that they can be handled easily. + int badMove; + while ((badMove = getLastMoveOutOfOrder(ops)) != -1) { + swapMoveOp(ops, badMove, badMove + 1); + } + } + + private void swapMoveOp(List list, int badMove, int next) { + final AdapterHelper.UpdateOp moveOp = list.get(badMove); + final AdapterHelper.UpdateOp nextOp = list.get(next); + switch (nextOp.cmd) { + case AdapterHelper.UpdateOp.REMOVE: + swapMoveRemove(list, badMove, moveOp, next, nextOp); + break; + case AdapterHelper.UpdateOp.ADD: + swapMoveAdd(list, badMove, moveOp, next, nextOp); + break; + case AdapterHelper.UpdateOp.UPDATE: + swapMoveUpdate(list, badMove, moveOp, next, nextOp); + break; + } + } + + void swapMoveRemove(List list, int movePos, AdapterHelper.UpdateOp moveOp, + int removePos, AdapterHelper.UpdateOp removeOp) { + AdapterHelper.UpdateOp extraRm = null; + // check if move is nulled out by remove + boolean revertedMove = false; + final boolean moveIsBackwards; + + if (moveOp.positionStart < moveOp.itemCount) { + moveIsBackwards = false; + if (removeOp.positionStart == moveOp.positionStart + && removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) { + revertedMove = true; + } + } else { + moveIsBackwards = true; + if (removeOp.positionStart == moveOp.itemCount + 1 + && removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) { + revertedMove = true; + } + } + + // going in reverse, first revert the effect of add + if (moveOp.itemCount < removeOp.positionStart) { + removeOp.positionStart--; + } else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) { + // move is removed. + removeOp.itemCount--; + moveOp.cmd = AdapterHelper.UpdateOp.REMOVE; + moveOp.itemCount = 1; + if (removeOp.itemCount == 0) { + list.remove(removePos); + mCallback.recycleUpdateOp(removeOp); + } + // no need to swap, it is already a remove + return; + } + + // now affect of add is consumed. now apply effect of first remove + if (moveOp.positionStart <= removeOp.positionStart) { + removeOp.positionStart++; + } else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) { + final int remaining = removeOp.positionStart + removeOp.itemCount + - moveOp.positionStart; + extraRm = mCallback.obtainUpdateOp(AdapterHelper.UpdateOp.REMOVE, moveOp.positionStart + 1, remaining, null); + removeOp.itemCount = moveOp.positionStart - removeOp.positionStart; + } + + // if effects of move is reverted by remove, we are done. + if (revertedMove) { + list.set(movePos, removeOp); + list.remove(removePos); + mCallback.recycleUpdateOp(moveOp); + return; + } + + // now find out the new locations for move actions + if (moveIsBackwards) { + if (extraRm != null) { + if (moveOp.positionStart > extraRm.positionStart) { + moveOp.positionStart -= extraRm.itemCount; + } + if (moveOp.itemCount > extraRm.positionStart) { + moveOp.itemCount -= extraRm.itemCount; + } + } + if (moveOp.positionStart > removeOp.positionStart) { + moveOp.positionStart -= removeOp.itemCount; + } + if (moveOp.itemCount > removeOp.positionStart) { + moveOp.itemCount -= removeOp.itemCount; + } + } else { + if (extraRm != null) { + if (moveOp.positionStart >= extraRm.positionStart) { + moveOp.positionStart -= extraRm.itemCount; + } + if (moveOp.itemCount >= extraRm.positionStart) { + moveOp.itemCount -= extraRm.itemCount; + } + } + if (moveOp.positionStart >= removeOp.positionStart) { + moveOp.positionStart -= removeOp.itemCount; + } + if (moveOp.itemCount >= removeOp.positionStart) { + moveOp.itemCount -= removeOp.itemCount; + } + } + + list.set(movePos, removeOp); + if (moveOp.positionStart != moveOp.itemCount) { + list.set(removePos, moveOp); + } else { + list.remove(removePos); + } + if (extraRm != null) { + list.add(movePos, extraRm); + } + } + + private void swapMoveAdd(List list, int move, AdapterHelper.UpdateOp moveOp, int add, + AdapterHelper.UpdateOp addOp) { + int offset = 0; + // going in reverse, first revert the effect of add + if (moveOp.itemCount < addOp.positionStart) { + offset--; + } + if (moveOp.positionStart < addOp.positionStart) { + offset++; + } + if (addOp.positionStart <= moveOp.positionStart) { + moveOp.positionStart += addOp.itemCount; + } + if (addOp.positionStart <= moveOp.itemCount) { + moveOp.itemCount += addOp.itemCount; + } + addOp.positionStart += offset; + list.set(move, addOp); + list.set(add, moveOp); + } + + void swapMoveUpdate(List list, int move, AdapterHelper.UpdateOp moveOp, int update, + AdapterHelper.UpdateOp updateOp) { + AdapterHelper.UpdateOp extraUp1 = null; + AdapterHelper.UpdateOp extraUp2 = null; + // going in reverse, first revert the effect of add + if (moveOp.itemCount < updateOp.positionStart) { + updateOp.positionStart--; + } else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) { + // moved item is updated. add an update for it + updateOp.itemCount--; + extraUp1 = mCallback.obtainUpdateOp(AdapterHelper.UpdateOp.UPDATE, moveOp.positionStart, 1, updateOp.payload); + } + // now affect of add is consumed. now apply effect of first remove + if (moveOp.positionStart <= updateOp.positionStart) { + updateOp.positionStart++; + } else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) { + final int remaining = updateOp.positionStart + updateOp.itemCount + - moveOp.positionStart; + extraUp2 = mCallback.obtainUpdateOp( + AdapterHelper.UpdateOp.UPDATE, moveOp.positionStart + 1, remaining, + updateOp.payload); + updateOp.itemCount -= remaining; + } + list.set(update, moveOp); + if (updateOp.itemCount > 0) { + list.set(move, updateOp); + } else { + list.remove(move); + mCallback.recycleUpdateOp(updateOp); + } + if (extraUp1 != null) { + list.add(move, extraUp1); + } + if (extraUp2 != null) { + list.add(move, extraUp2); + } + } + + private int getLastMoveOutOfOrder(List list) { + boolean foundNonMove = false; + for (int i = list.size() - 1; i >= 0; i--) { + final AdapterHelper.UpdateOp op1 = list.get(i); + if (op1.cmd == AdapterHelper.UpdateOp.MOVE) { + if (foundNonMove) { + return i; + } + } else { + foundNonMove = true; + } + } + return -1; + } + + interface Callback { + + AdapterHelper.UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload); + + void recycleUpdateOp(AdapterHelper.UpdateOp op); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/OrientationHelper.java b/app/src/main/java/androidx/recyclerview/widget/OrientationHelper.java new file mode 100644 index 0000000000..f94e0dd162 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/OrientationHelper.java @@ -0,0 +1,446 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.graphics.Rect; +import android.view.View; + +/** + * Helper class for LayoutManagers to abstract measurements depending on the View's orientation. + *

+ * It is developed to easily support vertical and horizontal orientations in a LayoutManager but + * can also be used to abstract calls around view bounds and child measurements with margins and + * decorations. + * + * @see #createHorizontalHelper(RecyclerView.LayoutManager) + * @see #createVerticalHelper(RecyclerView.LayoutManager) + */ +public abstract class OrientationHelper { + + private static final int INVALID_SIZE = Integer.MIN_VALUE; + + protected final RecyclerView.LayoutManager mLayoutManager; + + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + + public static final int VERTICAL = RecyclerView.VERTICAL; + + private int mLastTotalSpace = INVALID_SIZE; + + final Rect mTmpRect = new Rect(); + + private OrientationHelper(RecyclerView.LayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + /** + * Returns the {@link RecyclerView.LayoutManager LayoutManager} that + * is associated with this OrientationHelper. + */ + public RecyclerView.LayoutManager getLayoutManager() { + return mLayoutManager; + } + + /** + * Call this method after onLayout method is complete if state is NOT pre-layout. + * This method records information like layout bounds that might be useful in the next layout + * calculations. + */ + public void onLayoutComplete() { + mLastTotalSpace = getTotalSpace(); + } + + /** + * Returns the layout space change between the previous layout pass and current layout pass. + *

+ * Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's + * {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler, + * RecyclerView.State)} method. + * + * @return The difference between the current total space and previous layout's total space. + * @see #onLayoutComplete() + */ + public int getTotalSpaceChange() { + return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace; + } + + /** + * Returns the start of the view including its decoration and margin. + *

+ * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left + * decoration and 3px left margin, returned value will be 15px. + * + * @param view The view element to check + * @return The first pixel of the element + * @see #getDecoratedEnd(android.view.View) + */ + public abstract int getDecoratedStart(View view); + + /** + * Returns the end of the view including its decoration and margin. + *

+ * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right + * decoration and 3px right margin, returned value will be 205. + * + * @param view The view element to check + * @return The last pixel of the element + * @see #getDecoratedStart(android.view.View) + */ + public abstract int getDecoratedEnd(View view); + + /** + * Returns the end of the View after its matrix transformations are applied to its layout + * position. + *

+ * This method is useful when trying to detect the visible edge of a View. + *

+ * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed end will be returned + * @return The end of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedEndWithDecoration(View view); + + /** + * Returns the start of the View after its matrix transformations are applied to its layout + * position. + *

+ * This method is useful when trying to detect the visible edge of a View. + *

+ * It includes the decorations but does not include the margins. + * + * @param view The view whose transformed start will be returned + * @return The start of the View after its decor insets and transformation matrix is applied to + * its position + * + * @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect) + */ + public abstract int getTransformedStartWithDecoration(View view); + + /** + * Returns the space occupied by this View in the current orientation including decorations and + * margins. + * + * @param view The view element to check + * @return Total space occupied by this view + * @see #getDecoratedMeasurementInOther(View) + */ + public abstract int getDecoratedMeasurement(View view); + + /** + * Returns the space occupied by this View in the perpendicular orientation including + * decorations and margins. + * + * @param view The view element to check + * @return Total space occupied by this view in the perpendicular orientation to current one + * @see #getDecoratedMeasurement(View) + */ + public abstract int getDecoratedMeasurementInOther(View view); + + /** + * Returns the start position of the layout after the start padding is added. + * + * @return The very first pixel we can draw. + */ + public abstract int getStartAfterPadding(); + + /** + * Returns the end position of the layout after the end padding is removed. + * + * @return The end boundary for this layout. + */ + public abstract int getEndAfterPadding(); + + /** + * Returns the end position of the layout without taking padding into account. + * + * @return The end boundary for this layout without considering padding. + */ + public abstract int getEnd(); + + /** + * Offsets all children's positions by the given amount. + * + * @param amount Value to add to each child's layout parameters + */ + public abstract void offsetChildren(int amount); + + /** + * Returns the total space to layout. This number is the difference between + * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}. + * + * @return Total space to layout children + */ + public abstract int getTotalSpace(); + + /** + * Offsets the child in this orientation. + * + * @param view View to offset + * @param offset offset amount + */ + public abstract void offsetChild(View view, int offset); + + /** + * Returns the padding at the end of the layout. For horizontal helper, this is the right + * padding and for vertical helper, this is the bottom padding. This method does not check + * whether the layout is RTL or not. + * + * @return The padding at the end of the layout. + */ + public abstract int getEndPadding(); + + /** + * Returns the MeasureSpec mode for the current orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getMode(); + + /** + * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager. + * + * @return The current measure spec mode. + * + * @see View.MeasureSpec + * @see RecyclerView.LayoutManager#getWidthMode() + * @see RecyclerView.LayoutManager#getHeightMode() + */ + public abstract int getModeInOther(); + + /** + * Creates an OrientationHelper for the given LayoutManager and orientation. + * + * @param layoutManager LayoutManager to attach to + * @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL} + * @return A new OrientationHelper + */ + public static OrientationHelper createOrientationHelper( + RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) { + switch (orientation) { + case HORIZONTAL: + return createHorizontalHelper(layoutManager); + case VERTICAL: + return createVerticalHelper(layoutManager); + } + throw new IllegalArgumentException("invalid orientation"); + } + + /** + * Creates a horizontal OrientationHelper for the given LayoutManager. + * + * @param layoutManager The LayoutManager to attach to. + * @return A new OrientationHelper + */ + public static OrientationHelper createHorizontalHelper( + RecyclerView.LayoutManager layoutManager) { + return new OrientationHelper(layoutManager) { + @Override + public int getEndAfterPadding() { + return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight(); + } + + @Override + public int getEnd() { + return mLayoutManager.getWidth(); + } + + @Override + public void offsetChildren(int amount) { + mLayoutManager.offsetChildrenHorizontal(amount); + } + + @Override + public int getStartAfterPadding() { + return mLayoutManager.getPaddingLeft(); + } + + @Override + public int getDecoratedMeasurement(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + + params.rightMargin; + } + + @Override + public int getDecoratedMeasurementInOther(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + + params.bottomMargin; + } + + @Override + public int getDecoratedEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedRight(view) + params.rightMargin; + } + + @Override + public int getDecoratedStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedLeft(view) - params.leftMargin; + } + + @Override + public int getTransformedEndWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.right; + } + + @Override + public int getTransformedStartWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.left; + } + + @Override + public int getTotalSpace() { + return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft() + - mLayoutManager.getPaddingRight(); + } + + @Override + public void offsetChild(View view, int offset) { + view.offsetLeftAndRight(offset); + } + + @Override + public int getEndPadding() { + return mLayoutManager.getPaddingRight(); + } + + @Override + public int getMode() { + return mLayoutManager.getWidthMode(); + } + + @Override + public int getModeInOther() { + return mLayoutManager.getHeightMode(); + } + }; + } + + /** + * Creates a vertical OrientationHelper for the given LayoutManager. + * + * @param layoutManager The LayoutManager to attach to. + * @return A new OrientationHelper + */ + public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) { + return new OrientationHelper(layoutManager) { + @Override + public int getEndAfterPadding() { + return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom(); + } + + @Override + public int getEnd() { + return mLayoutManager.getHeight(); + } + + @Override + public void offsetChildren(int amount) { + mLayoutManager.offsetChildrenVertical(amount); + } + + @Override + public int getStartAfterPadding() { + return mLayoutManager.getPaddingTop(); + } + + @Override + public int getDecoratedMeasurement(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + + params.bottomMargin; + } + + @Override + public int getDecoratedMeasurementInOther(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + + params.rightMargin; + } + + @Override + public int getDecoratedEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin; + } + + @Override + public int getDecoratedStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return mLayoutManager.getDecoratedTop(view) - params.topMargin; + } + + @Override + public int getTransformedEndWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.bottom; + } + + @Override + public int getTransformedStartWithDecoration(View view) { + mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect); + return mTmpRect.top; + } + + @Override + public int getTotalSpace() { + return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop() + - mLayoutManager.getPaddingBottom(); + } + + @Override + public void offsetChild(View view, int offset) { + view.offsetTopAndBottom(offset); + } + + @Override + public int getEndPadding() { + return mLayoutManager.getPaddingBottom(); + } + + @Override + public int getMode() { + return mLayoutManager.getHeightMode(); + } + + @Override + public int getModeInOther() { + return mLayoutManager.getWidthMode(); + } + }; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java b/app/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java new file mode 100644 index 0000000000..3d97cdf552 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/PagerSnapHelper.java @@ -0,0 +1,273 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.graphics.PointF; +import android.util.DisplayMetrics; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or + * horizontal orientation. + * + *

+ * + * PagerSnapHelper can help achieve a similar behavior to + * {@link androidx.viewpager.widget.ViewPager}. Set both {@link RecyclerView} and the items of the + * {@link RecyclerView.Adapter} to have + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach + * PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}. + */ +public class PagerSnapHelper extends SnapHelper { + private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms + + // Orientation helpers are lazily created per LayoutManager. + @Nullable + private OrientationHelper mVerticalHelper; + @Nullable + private OrientationHelper mHorizontalHelper; + + @Nullable + @Override + public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull View targetView) { + int[] out = new int[2]; + if (layoutManager.canScrollHorizontally()) { + out[0] = distanceToCenter(targetView, + getHorizontalHelper(layoutManager)); + } else { + out[0] = 0; + } + + if (layoutManager.canScrollVertically()) { + out[1] = distanceToCenter(targetView, + getVerticalHelper(layoutManager)); + } else { + out[1] = 0; + } + return out; + } + + @Nullable + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public View findSnapView(RecyclerView.LayoutManager layoutManager) { + if (layoutManager.canScrollVertically()) { + return findCenterView(layoutManager, getVerticalHelper(layoutManager)); + } else if (layoutManager.canScrollHorizontally()) { + return findCenterView(layoutManager, getHorizontalHelper(layoutManager)); + } + return null; + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { + final int itemCount = layoutManager.getItemCount(); + if (itemCount == 0) { + return RecyclerView.NO_POSITION; + } + + final OrientationHelper orientationHelper = getOrientationHelper(layoutManager); + if (orientationHelper == null) { + return RecyclerView.NO_POSITION; + } + + // A child that is exactly in the center is eligible for both before and after + View closestChildBeforeCenter = null; + int distanceBefore = Integer.MIN_VALUE; + View closestChildAfterCenter = null; + int distanceAfter = Integer.MAX_VALUE; + + // Find the first view before the center, and the first view after the center + final int childCount = layoutManager.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = layoutManager.getChildAt(i); + if (child == null) { + continue; + } + final int distance = distanceToCenter(child, orientationHelper); + + if (distance <= 0 && distance > distanceBefore) { + // Child is before the center and closer then the previous best + distanceBefore = distance; + closestChildBeforeCenter = child; + } + if (distance >= 0 && distance < distanceAfter) { + // Child is after the center and closer then the previous best + distanceAfter = distance; + closestChildAfterCenter = child; + } + } + + // Return the position of the first child from the center, in the direction of the fling + final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY); + if (forwardDirection && closestChildAfterCenter != null) { + return layoutManager.getPosition(closestChildAfterCenter); + } else if (!forwardDirection && closestChildBeforeCenter != null) { + return layoutManager.getPosition(closestChildBeforeCenter); + } + + // There is no child in the direction of the fling. Either it doesn't exist (start/end of + // the list), or it is not yet attached (very rare case when children are larger then the + // viewport). Extrapolate from the child that is visible to get the position of the view to + // snap to. + View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter; + if (visibleView == null) { + return RecyclerView.NO_POSITION; + } + int visiblePosition = layoutManager.getPosition(visibleView); + int snapToPosition = visiblePosition + + (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1); + + if (snapToPosition < 0 || snapToPosition >= itemCount) { + return RecyclerView.NO_POSITION; + } + return snapToPosition; + } + + private boolean isForwardFling(RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { + if (layoutManager.canScrollHorizontally()) { + return velocityX > 0; + } else { + return velocityY > 0; + } + } + + private boolean isReverseLayout(RecyclerView.LayoutManager layoutManager) { + final int itemCount = layoutManager.getItemCount(); + if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider = + (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager; + PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1); + if (vectorForEnd != null) { + return vectorForEnd.x < 0 || vectorForEnd.y < 0; + } + } + return false; + } + + @Nullable + @Override + protected RecyclerView.SmoothScroller createScroller( + @NonNull RecyclerView.LayoutManager layoutManager) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return null; + } + return new LinearSmoothScroller(mRecyclerView.getContext()) { + @Override + protected void onTargetFound(@NonNull View targetView, + @NonNull RecyclerView.State state, @NonNull Action action) { + int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), + targetView); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); + if (time > 0) { + action.update(dx, dy, time, mDecelerateInterpolator); + } + } + + @Override + protected float calculateSpeedPerPixel(@NonNull DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + @Override + protected int calculateTimeForScrolling(int dx) { + return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx)); + } + }; + } + + private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) { + final int childCenter = helper.getDecoratedStart(targetView) + + (helper.getDecoratedMeasurement(targetView) / 2); + final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + return childCenter - containerCenter; + } + + /** + * Return the child view that is currently closest to the center of this parent. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}. + * + * @return the child view that is currently closest to the center of this parent. + */ + @Nullable + private View findCenterView(RecyclerView.LayoutManager layoutManager, + OrientationHelper helper) { + int childCount = layoutManager.getChildCount(); + if (childCount == 0) { + return null; + } + + View closestChild = null; + final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2; + int absClosest = Integer.MAX_VALUE; + + for (int i = 0; i < childCount; i++) { + final View child = layoutManager.getChildAt(i); + int childCenter = helper.getDecoratedStart(child) + + (helper.getDecoratedMeasurement(child) / 2); + int absDistance = Math.abs(childCenter - center); + + /* if child center is closer than previous closest, set it as closest */ + if (absDistance < absClosest) { + absClosest = absDistance; + closestChild = child; + } + } + return closestChild; + } + + @Nullable + private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) { + if (layoutManager.canScrollVertically()) { + return getVerticalHelper(layoutManager); + } else if (layoutManager.canScrollHorizontally()) { + return getHorizontalHelper(layoutManager); + } else { + return null; + } + } + + @NonNull + private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) { + if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) { + mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager); + } + return mVerticalHelper; + } + + @NonNull + private OrientationHelper getHorizontalHelper( + @NonNull RecyclerView.LayoutManager layoutManager) { + if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) { + mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager); + } + return mHorizontalHelper; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/app/src/main/java/androidx/recyclerview/widget/RecyclerView.java new file mode 100644 index 0000000000..9265d56d01 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/RecyclerView.java @@ -0,0 +1,14454 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; +import static androidx.core.util.Preconditions.checkArgument; +import static androidx.core.view.ViewCompat.TYPE_NON_TOUCH; +import static androidx.core.view.ViewCompat.TYPE_TOUCH; + +import android.animation.LayoutTransition; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.Observable; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.Display; +import android.view.FocusFinder; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.LinearLayout; +import android.widget.OverScroller; + +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.Px; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.core.os.TraceCompat; +import androidx.core.util.Preconditions; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.InputDeviceCompat; +import androidx.core.view.MotionEventCompat; +import androidx.core.view.NestedScrollingChild2; +import androidx.core.view.NestedScrollingChild3; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.ScrollingView; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewConfigurationCompat; +import androidx.core.view.accessibility.AccessibilityEventCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.widget.EdgeEffectCompat; +import androidx.customview.poolingcontainer.PoolingContainer; +import androidx.customview.poolingcontainer.PoolingContainerListener; +import androidx.customview.view.AbsSavedState; +import androidx.recyclerview.R; +import androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemHolderInfo; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Set; + +/** + * A flexible view for providing a limited window into a large data set. + * + *

Glossary of terms:

+ * + *
    + *
  • Adapter: A subclass of {@link Adapter} responsible for providing views + * that represent items in a data set.
  • + *
  • Position: The position of a data item within an Adapter.
  • + *
  • Index: The index of an attached child view as used in a call to + * {@link ViewGroup#getChildAt}. Contrast with Position.
  • + *
  • Binding: The process of preparing a child view to display data corresponding + * to a position within the adapter.
  • + *
  • Recycle (view): A view previously used to display data for a specific adapter + * position may be placed in a cache for later reuse to display the same type of data again + * later. This can drastically improve performance by skipping initial layout inflation + * or construction.
  • + *
  • Scrap (view): A child view that has entered into a temporarily detached + * state during layout. Scrap views may be reused without becoming fully detached + * from the parent RecyclerView, either unmodified if no rebinding is required or modified + * by the adapter if the view was considered dirty.
  • + *
  • Dirty (view): A child view that must be rebound by the adapter before + * being displayed.
  • + *
+ * + *

Positions in RecyclerView:

+ *

+ * RecyclerView introduces an additional level of abstraction between the {@link Adapter} and + * {@link LayoutManager} to be able to detect data set changes in batches during a layout + * calculation. This saves LayoutManager from tracking adapter changes to calculate animations. + * It also helps with performance because all view bindings happen at the same time and unnecessary + * bindings are avoided. + *

+ * For this reason, there are two types of position related methods in RecyclerView: + *

    + *
  • layout position: Position of an item in the latest layout calculation. This is the + * position from the LayoutManager's perspective.
  • + *
  • adapter position: Position of an item in the adapter. This is the position from + * the Adapter's perspective.
  • + *
+ *

+ * These two positions are the same except the time between dispatching adapter.notify* + * events and calculating the updated layout. + *

+ * Methods that return or receive *LayoutPosition* use position as of the latest + * layout calculation (e.g. {@link ViewHolder#getLayoutPosition()}, + * {@link #findViewHolderForLayoutPosition(int)}). These positions include all changes until the + * last layout calculation. You can rely on these positions to be consistent with what user is + * currently seeing on the screen. For example, if you have a list of items on the screen and user + * asks for the 5th element, you should use these methods as they'll match what user + * is seeing. + *

+ * The other set of position related methods are in the form of + * *AdapterPosition*. (e.g. {@link ViewHolder#getAbsoluteAdapterPosition()}, + * {@link ViewHolder#getBindingAdapterPosition()}, + * {@link #findViewHolderForAdapterPosition(int)}) You should use these methods when you need to + * work with up-to-date adapter positions even if they may not have been reflected to layout yet. + * For example, if you want to access the item in the adapter on a ViewHolder click, you should use + * {@link ViewHolder#getBindingAdapterPosition()}. Beware that these methods may not be able to + * calculate adapter positions if {@link Adapter#notifyDataSetChanged()} has been called and new + * layout has not yet been calculated. For this reasons, you should carefully handle + * {@link #NO_POSITION} or null results from these methods. + *

+ * When writing a {@link LayoutManager} you almost always want to use layout positions whereas when + * writing an {@link Adapter}, you probably want to use adapter positions. + *

+ *

Presenting Dynamic Data

+ * To display updatable data in a RecyclerView, your adapter needs to signal inserts, moves, and + * deletions to RecyclerView. You can build this yourself by manually calling + * {@code adapter.notify*} methods when content changes, or you can use one of the easier solutions + * RecyclerView provides: + *

+ *

List diffing with DiffUtil

+ * If your RecyclerView is displaying a list that is re-fetched from scratch for each update (e.g. + * from the network, or from a database), {@link DiffUtil} can calculate the difference between + * versions of the list. {@code DiffUtil} takes both lists as input and computes the difference, + * which can be passed to RecyclerView to trigger minimal animations and updates to keep your UI + * performant, and animations meaningful. This approach requires that each list is represented in + * memory with immutable content, and relies on receiving updates as new instances of lists. This + * approach is also ideal if your UI layer doesn't implement sorting, it just presents the data in + * the order it's given. + *

+ * The best part of this approach is that it extends to any arbitrary changes - item updates, + * moves, addition and removal can all be computed and handled the same way. Though you do have + * to keep two copies of the list in memory while diffing, and must avoid mutating them, it's + * possible to share unmodified elements between list versions. + *

+ * There are three primary ways to do this for RecyclerView. We recommend you start with + * {@link ListAdapter}, the higher-level API that builds in {@link List} diffing on a background + * thread, with minimal code. {@link AsyncListDiffer} also provides this behavior, but without + * defining an Adapter to subclass. If you want more control, {@link DiffUtil} is the lower-level + * API you can use to compute the diffs yourself. Each approach allows you to specify how diffs + * should be computed based on item data. + *

+ *

List mutation with SortedList

+ * If your RecyclerView receives updates incrementally, e.g. item X is inserted, or item Y is + * removed, you can use {@link SortedList} to manage your list. You define how to order items, + * and it will automatically trigger update signals that RecyclerView can use. SortedList works + * if you only need to handle insert and remove events, and has the benefit that you only ever + * need to have a single copy of the list in memory. It can also compute differences with + * {@link SortedList#replaceAll(Object[])}, but this method is more limited than the list diffing + * behavior above. + *

+ *

Paging Library

+ * The Paging + * library extends the diff-based approach to additionally support paged loading. It provides + * the {@link androidx.paging.PagedList} class that operates as a self-loading list, provided a + * source of data like a database, or paginated network API. It provides convenient list diffing + * support out of the box, similar to {@code ListAdapter} and {@code AsyncListDiffer}. For more + * information about the Paging library, see the + * library + * documentation. + * + * {@link androidx.recyclerview.R.attr#layoutManager} + */ +public class RecyclerView extends ViewGroup implements ScrollingView, + NestedScrollingChild2, NestedScrollingChild3 { + + static final String TAG = "RecyclerView"; + + static boolean sDebugAssertionsEnabled = false; + static boolean sVerboseLoggingEnabled = false; + + static final boolean VERBOSE_TRACING = false; + + private static final int[] NESTED_SCROLLING_ATTRS = + {16843830 /* android.R.attr.nestedScrollingEnabled */}; + + /** + * The following are copied from OverScroller to determine how far a fling will go. + */ + private static final float SCROLL_FRICTION = 0.015f; + private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1) + private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9)); + private final float mPhysicalCoef; + + /** + * On Kitkat and JB MR2, there is a bug which prevents DisplayList from being invalidated if + * a View is two levels deep(wrt to ViewHolder.itemView). DisplayList can be invalidated by + * setting View's visibility to INVISIBLE when View is detached. On Kitkat and JB MR2, Recycler + * recursively traverses itemView and invalidates display list for each ViewGroup that matches + * this criteria. + */ + static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18 + || Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20; + /** + * On M+, an unspecified measure spec may include a hint which we can use. On older platforms, + * this value might be garbage. To save LayoutManagers from it, RecyclerView sets the size to + * 0 when mode is unspecified. + */ + static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23; + + static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16; + + /** + * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to + * RenderThread but before the next frame begins. We schedule prefetch work in this window. + */ + static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21; + + /** + * FocusFinder#findNextFocus is broken on ICS MR1 and older for View.FOCUS_BACKWARD direction. + * We convert it to an absolute direction such as FOCUS_DOWN or FOCUS_LEFT. + */ + private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15; + + /** + * on API 15-, a focused child can still be considered a focused child of RV even after + * it's being removed or its focusable flag is set to false. This is because when this focused + * child is detached, the reference to this child is not removed in clearFocus. API 16 and above + * properly handle this case by calling ensureInputFocusOnFirstFocusable or rootViewRequestFocus + * to request focus on a new child, which will clear the focus on the old (detached) child as a + * side-effect. + */ + private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15; + + /** + * When flinging the stretch towards scrolling content, it should destretch quicker than the + * fling would normally do. The visual effect of flinging the stretch looks strange as little + * appears to happen at first and then when the stretch disappears, the content starts + * scrolling quickly. + */ + private static final float FLING_DESTRETCH_FACTOR = 4f; + + static final boolean DISPATCH_TEMP_DETACH = false; + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @IntDef({HORIZONTAL, VERTICAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface Orientation { + } + + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + public static final int VERTICAL = LinearLayout.VERTICAL; + + static final int DEFAULT_ORIENTATION = VERTICAL; + public static final int NO_POSITION = -1; + public static final long NO_ID = -1; + public static final int INVALID_TYPE = -1; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for smooth, + * continuous scrolling. + */ + public static final int TOUCH_SLOP_DEFAULT = 0; + + /** + * Constant for use with {@link #setScrollingTouchSlop(int)}. Indicates + * that the RecyclerView should use the standard touch slop for scrolling + * widgets that snap to a page or other coarse-grained barrier. + */ + public static final int TOUCH_SLOP_PAGING = 1; + + /** + * Constant that represents that a duration has not been defined. + */ + public static final int UNDEFINED_DURATION = Integer.MIN_VALUE; + + static final int MAX_SCROLL_DURATION = 2000; + + /** + * RecyclerView is calculating a scroll. + * If there are too many of these in Systrace, some Views inside RecyclerView might be causing + * it. Try to avoid using EditText, focusable views or handle them with care. + */ + static final String TRACE_SCROLL_TAG = "RV Scroll"; + + /** + * OnLayout has been called by the View system. + * If this shows up too many times in Systrace, make sure the children of RecyclerView do not + * update themselves directly. This will cause a full re-layout but when it happens via the + * Adapter notifyItemChanged, RecyclerView can avoid full layout calculation. + */ + private static final String TRACE_ON_LAYOUT_TAG = "RV OnLayout"; + + /** + * NotifyDataSetChanged or equal has been called. + * If this is taking a long time, try sending granular notify adapter changes instead of just + * calling notifyDataSetChanged or setAdapter / swapAdapter. Adding stable ids to your adapter + * might help. + */ + private static final String TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG = "RV FullInvalidate"; + + /** + * RecyclerView is doing a layout for partial adapter updates (we know what has changed) + * If this is taking a long time, you may have dispatched too many Adapter updates causing too + * many Views being rebind. Make sure all are necessary and also prefer using notify*Range + * methods. + */ + private static final String TRACE_HANDLE_ADAPTER_UPDATES_TAG = "RV PartialInvalidate"; + + /** + * RecyclerView is rebinding a View. + * If this is taking a lot of time, consider optimizing your layout or make sure you are not + * doing extra operations in onBindViewHolder call. + */ + static final String TRACE_BIND_VIEW_TAG = "RV OnBindView"; + + /** + * RecyclerView is attempting to pre-populate off screen views. + */ + static final String TRACE_PREFETCH_TAG = "RV Prefetch"; + + /** + * RecyclerView is attempting to pre-populate off screen itemviews within an off screen + * RecyclerView. + */ + static final String TRACE_NESTED_PREFETCH_TAG = "RV Nested Prefetch"; + + /** + * RecyclerView is creating a new View. + * If too many of these present in Systrace: + * - There might be a problem in Recycling (e.g. custom Animations that set transient state and + * prevent recycling or ItemAnimator not implementing the contract properly. ({@link + * > Adapter#onFailedToRecycleView(ViewHolder)}) + * + * - There might be too many item view types. + * > Try merging them + * + * - There might be too many itemChange animations and not enough space in RecyclerPool. + * >Try increasing your pool size and item cache size. + */ + static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; + private static final Class[] LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE = + new Class[]{Context.class, AttributeSet.class, int.class, int.class}; + + /** + * Enable internal assertions about RecyclerView's state and throw exceptions if the + * assertions are violated. + *

+ * This is primarily intended to diagnose problems with RecyclerView, and + * should not be enabled in production unless you have a specific reason to + * do so. + *

+ * Enabling this may negatively affect performance and/or stability. + * + * @param debugAssertionsEnabled true to enable assertions; false to disable them + */ + public static void setDebugAssertionsEnabled(boolean debugAssertionsEnabled) { + RecyclerView.sDebugAssertionsEnabled = debugAssertionsEnabled; + } + + /** + * Enable verbose logging within RecyclerView itself. + *

+ * Enabling this may negatively affect performance and reduce the utility of logcat due to + * high-volume logging. This generally should not be enabled in production + * unless you have a specific reason for doing so. + * + * @param verboseLoggingEnabled true to enable logging; false to disable it + */ + public static void setVerboseLoggingEnabled(boolean verboseLoggingEnabled) { + RecyclerView.sVerboseLoggingEnabled = verboseLoggingEnabled; + } + + private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); + + final Recycler mRecycler = new Recycler(); + + SavedState mPendingSavedState; + + /** + * Handles adapter updates + */ + AdapterHelper mAdapterHelper; + + /** + * Handles abstraction between LayoutManager children and RecyclerView children + */ + ChildHelper mChildHelper; + + /** + * Keeps data about views to be used for animations + */ + final ViewInfoStore mViewInfoStore = new ViewInfoStore(); + + /** + * Prior to L, there is no way to query this variable which is why we override the setter and + * track it here. + */ + boolean mClipToPadding; + + /** + * Note: this Runnable is only ever posted if: + * 1) We've been through first layout + * 2) We know we have a fixed size (mHasFixedSize) + * 3) We're attached + */ + final Runnable mUpdateChildViewsRunnable = new Runnable() { + @Override + public void run() { + if (!mFirstLayoutComplete || isLayoutRequested()) { + // a layout request will happen, we should not do layout here. + return; + } + if (!mIsAttached) { + requestLayout(); + // if we are not attached yet, mark us as requiring layout and skip + return; + } + if (mLayoutSuppressed) { + mLayoutWasDefered = true; + return; //we'll process updates when ice age ends. + } + consumePendingUpdateOperations(); + } + }; + + final Rect mTempRect = new Rect(); + private final Rect mTempRect2 = new Rect(); + final RectF mTempRectF = new RectF(); + Adapter mAdapter; + @VisibleForTesting + LayoutManager mLayout; + // TODO: Remove this once setRecyclerListener has been removed. + RecyclerListener mRecyclerListener; + // default access to avoid the need for synthetic accessors for Recycler inner class. + final List mRecyclerListeners = new ArrayList<>(); + final ArrayList mItemDecorations = new ArrayList<>(); + private final ArrayList mOnItemTouchListeners = + new ArrayList<>(); + private OnItemTouchListener mInterceptingOnItemTouchListener; + boolean mIsAttached; + boolean mHasFixedSize; + boolean mEnableFastScroller; + @VisibleForTesting + boolean mFirstLayoutComplete; + + /** + * The current depth of nested calls to {@link #startInterceptRequestLayout()} (number of + * calls to {@link #startInterceptRequestLayout()} - number of calls to + * {@link #stopInterceptRequestLayout(boolean)} . This is used to signal whether we + * should defer layout operations caused by layout requests from children of + * {@link RecyclerView}. + */ + private int mInterceptRequestLayoutDepth = 0; + + /** + * True if a call to requestLayout was intercepted and prevented from executing like normal and + * we plan on continuing with normal execution later. + */ + boolean mLayoutWasDefered; + + boolean mLayoutSuppressed; + private boolean mIgnoreMotionEventTillDown; + + // binary OR of change events that were eaten during a layout or scroll. + private int mEatenAccessibilityChangeFlags; + boolean mAdapterUpdateDuringMeasure; + + private final AccessibilityManager mAccessibilityManager; + private List mOnChildAttachStateListeners; + + /** + * True after an event occurs that signals that the entire data set has changed. In that case, + * we cannot run any animations since we don't know what happened until layout. + * + * Attached items are invalid until next layout, at which point layout will animate/replace + * items as necessary, building up content from the (effectively) new adapter from scratch. + * + * Cached items must be discarded when setting this to true, so that the cache may be freely + * used by prefetching until the next layout occurs. + * + * @see #processDataSetCompletelyChanged(boolean) + */ + boolean mDataSetHasChangedAfterLayout = false; + + /** + * True after the data set has completely changed and + * {@link LayoutManager#onItemsChanged(RecyclerView)} should be called during the subsequent + * measure/layout. + * + * @see #processDataSetCompletelyChanged(boolean) + */ + boolean mDispatchItemsChangedEvent = false; + + /** + * This variable is incremented during a dispatchLayout and/or scroll. + * Some methods should not be called during these periods (e.g. adapter data change). + * Doing so will create hard to find bugs so we better check it and throw an exception. + * + * @see #assertInLayoutOrScroll(String) + * @see #assertNotInLayoutOrScroll(String) + */ + private int mLayoutOrScrollCounter = 0; + + /** + * Similar to mLayoutOrScrollCounter but logs a warning instead of throwing an exception + * (for API compatibility). + *

+ * It is a bad practice for a developer to update the data in a scroll callback since it is + * potentially called during a layout. + */ + private int mDispatchScrollCounter = 0; + + @NonNull + private EdgeEffectFactory mEdgeEffectFactory = sDefaultEdgeEffectFactory; + private EdgeEffect mLeftGlow, mTopGlow, mRightGlow, mBottomGlow; + + ItemAnimator mItemAnimator = new DefaultItemAnimator(); + + private static final int INVALID_POINTER = -1; + + /** + * The RecyclerView is not currently scrolling. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * The RecyclerView is currently being dragged by outside input such as user touch input. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * The RecyclerView is currently animating to a final position while not under + * outside control. + * + * @see #getScrollState() + */ + public static final int SCROLL_STATE_SETTLING = 2; + + static final long FOREVER_NS = Long.MAX_VALUE; + + // Touch/scrolling handling + + private int mScrollState = SCROLL_STATE_IDLE; + private int mScrollPointerId = INVALID_POINTER; + private VelocityTracker mVelocityTracker; + private int mInitialTouchX; + private int mInitialTouchY; + private int mLastTouchX; + private int mLastTouchY; + private int mTouchSlop; + private OnFlingListener mOnFlingListener; + private final int mMinFlingVelocity; + private final int mMaxFlingVelocity; + + // This value is used when handling rotary encoder generic motion events. + private float mScaledHorizontalScrollFactor = Float.MIN_VALUE; + private float mScaledVerticalScrollFactor = Float.MIN_VALUE; + + private boolean mPreserveFocusAfterLayout = true; + + final ViewFlinger mViewFlinger = new ViewFlinger(); + + GapWorker mGapWorker; + GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry = + ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null; + + final State mState = new State(); + + private OnScrollListener mScrollListener; + private List mScrollListeners; + + // For use in item animations + boolean mItemsAddedOrRemoved = false; + boolean mItemsChanged = false; + private ItemAnimator.ItemAnimatorListener mItemAnimatorListener = + new ItemAnimatorRestoreListener(); + boolean mPostedAnimatorRunner = false; + RecyclerViewAccessibilityDelegate mAccessibilityDelegate; + private ChildDrawingOrderCallback mChildDrawingOrderCallback; + + // simple array to keep min and max child position during a layout calculation + // preserved not to create a new one in each layout pass + private final int[] mMinMaxLayoutPositions = new int[2]; + + private NestedScrollingChildHelper mScrollingChildHelper; + private final int[] mScrollOffset = new int[2]; + private final int[] mNestedOffsets = new int[2]; + + // Reusable int array to be passed to method calls that mutate it in order to "return" two ints. + final int[] mReusableIntPair = new int[2]; + + /** + * These are views that had their a11y importance changed during a layout. We defer these events + * until the end of the layout because a11y service may make sync calls back to the RV while + * the View's state is undefined. + */ + @VisibleForTesting + final List mPendingAccessibilityImportanceChange = new ArrayList<>(); + + private Runnable mItemAnimatorRunner = new Runnable() { + @Override + public void run() { + if (mItemAnimator != null) { + mItemAnimator.runPendingAnimations(); + } + mPostedAnimatorRunner = false; + } + }; + + static final Interpolator sQuinticInterpolator = new Interpolator() { + @Override + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + static final StretchEdgeEffectFactory sDefaultEdgeEffectFactory = + new StretchEdgeEffectFactory(); + + // These fields are only used to track whether we need to layout and measure RV children in + // onLayout. + // + // We track this information because there is an optimized path such that when + // LayoutManager#isAutoMeasureEnabled() returns true and we are measured with + // MeasureSpec.EXACTLY in both dimensions, we skip measuring and layout children till the + // layout phase. + // + // However, there are times when we are first measured with something other than + // MeasureSpec.EXACTLY in both dimensions, in which case we measure and layout children during + // onMeasure. Then if we are measured again with EXACTLY, and we skip measurement, we will + // get laid out with a different size than we were last aware of being measured with. If + // that happens and we don't check for it, we may not remeasure children, which would be a bug. + // + // mLastAutoMeasureNonExactMeasureResult tracks our last known measurements in this case, and + // mLastAutoMeasureSkippedDueToExact tracks whether or not we skipped. So, whenever we + // layout, we can see if our last known measurement information is different from our actual + // laid out size, and if it is, only then do we remeasure and relayout children. + private boolean mLastAutoMeasureSkippedDueToExact; + private int mLastAutoMeasureNonExactMeasuredWidth = 0; + private int mLastAutoMeasureNonExactMeasuredHeight = 0; + + /** + * The callback to convert view info diffs into animations. + */ + private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback = + new ViewInfoStore.ProcessCallback() { + @Override + public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info, + @Nullable ItemHolderInfo postInfo) { + mRecycler.unscrapView(viewHolder); + animateDisappearance(viewHolder, info, postInfo); + } + + @Override + public void processAppeared(ViewHolder viewHolder, + ItemHolderInfo preInfo, ItemHolderInfo info) { + animateAppearance(viewHolder, preInfo, info); + } + + @Override + public void processPersistent(ViewHolder viewHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) { + viewHolder.setIsRecyclable(false); + if (mDataSetHasChangedAfterLayout) { + // since it was rebound, use change instead as we'll be mapping them from + // stable ids. If stable ids were false, we would not be running any + // animations + if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo, + postInfo)) { + postAnimationRunner(); + } + } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + + @Override + public void unused(ViewHolder viewHolder) { + mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler); + } + }; + + public RecyclerView(@NonNull Context context) { + this(context, null); + } + + public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, R.attr.recyclerViewStyle); + } + + public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setScrollContainer(true); + setFocusableInTouchMode(true); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mScaledHorizontalScrollFactor = + ViewConfigurationCompat.getScaledHorizontalScrollFactor(vc, context); + mScaledVerticalScrollFactor = + ViewConfigurationCompat.getScaledVerticalScrollFactor(vc, context); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + final float ppi = context.getResources().getDisplayMetrics().density * 160.0f; + mPhysicalCoef = SensorManager.GRAVITY_EARTH // g (m/s^2) + * 39.37f // inch/meter + * ppi + * 0.84f; // look and feel tuning + setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); + + mItemAnimator.setListener(mItemAnimatorListener); + initAdapterManager(); + initChildrenHelper(); + initAutofill(); + // If not explicitly specified this view is important for accessibility. + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + mAccessibilityManager = (AccessibilityManager) getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyleAttr, 0); + + ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.RecyclerView, + attrs, a, defStyleAttr, 0); + String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager); + int descendantFocusability = a.getInt( + R.styleable.RecyclerView_android_descendantFocusability, -1); + if (descendantFocusability == -1) { + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + } + mClipToPadding = a.getBoolean(R.styleable.RecyclerView_android_clipToPadding, true); + mEnableFastScroller = a.getBoolean(R.styleable.RecyclerView_fastScrollEnabled, false); + if (mEnableFastScroller) { + StateListDrawable verticalThumbDrawable = (StateListDrawable) a + .getDrawable(R.styleable.RecyclerView_fastScrollVerticalThumbDrawable); + Drawable verticalTrackDrawable = a + .getDrawable(R.styleable.RecyclerView_fastScrollVerticalTrackDrawable); + StateListDrawable horizontalThumbDrawable = (StateListDrawable) a + .getDrawable(R.styleable.RecyclerView_fastScrollHorizontalThumbDrawable); + Drawable horizontalTrackDrawable = a + .getDrawable(R.styleable.RecyclerView_fastScrollHorizontalTrackDrawable); + initFastScroller(verticalThumbDrawable, verticalTrackDrawable, + horizontalThumbDrawable, horizontalTrackDrawable); + } + a.recycle(); + + // Create the layoutManager if specified. + createLayoutManager(context, layoutManagerName, attrs, defStyleAttr, 0); + + boolean nestedScrollingEnabled = true; + if (Build.VERSION.SDK_INT >= 21) { + a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, + defStyleAttr, 0); + ViewCompat.saveAttributeDataForStyleable(this, + context, NESTED_SCROLLING_ATTRS, attrs, a, defStyleAttr, 0); + nestedScrollingEnabled = a.getBoolean(0, true); + a.recycle(); + } + // Re-set whether nested scrolling is enabled so that it is set on all API levels + setNestedScrollingEnabled(nestedScrollingEnabled); + PoolingContainer.setPoolingContainer(this, true); + } + + /** + * Label appended to all public exception strings, used to help find which RV in an app is + * hitting an exception. + */ + String exceptionLabel() { + return " " + super.toString() + + ", adapter:" + mAdapter + + ", layout:" + mLayout + + ", context:" + getContext(); + } + + /** + * If not explicitly specified, this view and its children don't support autofill. + *

+ * This is done because autofill's means of uniquely identifying views doesn't work out of the + * box with View recycling. + */ + @SuppressLint("InlinedApi") + private void initAutofill() { + if (ViewCompat.getImportantForAutofill(this) == View.IMPORTANT_FOR_AUTOFILL_AUTO) { + ViewCompat.setImportantForAutofill(this, + View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS); + } + } + + /** + * Returns the accessibility delegate compatibility implementation used by the RecyclerView. + * + * @return An instance of AccessibilityDelegateCompat used by RecyclerView + */ + @Nullable + public RecyclerViewAccessibilityDelegate getCompatAccessibilityDelegate() { + return mAccessibilityDelegate; + } + + /** + * Sets the accessibility delegate compatibility implementation used by RecyclerView. + * + * @param accessibilityDelegate The accessibility delegate to be used by RecyclerView. + */ + public void setAccessibilityDelegateCompat( + @Nullable RecyclerViewAccessibilityDelegate accessibilityDelegate) { + mAccessibilityDelegate = accessibilityDelegate; + ViewCompat.setAccessibilityDelegate(this, mAccessibilityDelegate); + } + + @Override + public CharSequence getAccessibilityClassName() { + return "androidx.recyclerview.widget.RecyclerView"; + } + + /** + * Instantiate and set a LayoutManager, if specified in the attributes. + */ + private void createLayoutManager(Context context, String className, AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + if (className != null) { + className = className.trim(); + if (!className.isEmpty()) { + className = getFullClassName(context, className); + try { + ClassLoader classLoader; + if (isInEditMode()) { + // Stupid layoutlib cannot handle simple class loaders. + classLoader = this.getClass().getClassLoader(); + } else { + classLoader = context.getClassLoader(); + } + Class layoutManagerClass = + Class.forName(className, false, classLoader) + .asSubclass(LayoutManager.class); + Constructor constructor; + Object[] constructorArgs = null; + try { + constructor = layoutManagerClass + .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE); + constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes}; + } catch (NoSuchMethodException e) { + try { + constructor = layoutManagerClass.getConstructor(); + } catch (NoSuchMethodException e1) { + e1.initCause(e); + throw new IllegalStateException(attrs.getPositionDescription() + + ": Error creating LayoutManager " + className, e1); + } + } + constructor.setAccessible(true); + setLayoutManager(constructor.newInstance(constructorArgs)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Unable to find LayoutManager " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (InstantiationException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Cannot access non-public constructor " + className, e); + } catch (ClassCastException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Class is not a LayoutManager " + className, e); + } + } + } + } + + private String getFullClassName(Context context, String className) { + if (className.charAt(0) == '.') { + return context.getPackageName() + className; + } + if (className.contains(".")) { + return className; + } + return RecyclerView.class.getPackage().getName() + '.' + className; + } + + private void initChildrenHelper() { + mChildHelper = new ChildHelper(new ChildHelper.Callback() { + @Override + public int getChildCount() { + return RecyclerView.this.getChildCount(); + } + + @Override + public void addView(View child, int index) { + if (VERBOSE_TRACING) { + TraceCompat.beginSection("RV addView"); + } + RecyclerView.this.addView(child, index); + if (VERBOSE_TRACING) { + TraceCompat.endSection(); + } + dispatchChildAttached(child); + } + + @Override + public int indexOfChild(View view) { + return RecyclerView.this.indexOfChild(view); + } + + @Override + public void removeViewAt(int index) { + final View child = RecyclerView.this.getChildAt(index); + if (child != null) { + dispatchChildDetached(child); + + // Clear any android.view.animation.Animation that may prevent the item from + // detaching when being removed. If a child is re-added before the + // lazy detach occurs, it will receive invalid attach/detach sequencing. + child.clearAnimation(); + } + if (VERBOSE_TRACING) { + TraceCompat.beginSection("RV removeViewAt"); + } + RecyclerView.this.removeViewAt(index); + if (VERBOSE_TRACING) { + TraceCompat.endSection(); + } + } + + @Override + public View getChildAt(int offset) { + return RecyclerView.this.getChildAt(offset); + } + + @Override + public void removeAllViews() { + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + dispatchChildDetached(child); + + // Clear any android.view.animation.Animation that may prevent the item from + // detaching when being removed. If a child is re-added before the + // lazy detach occurs, it will receive invalid attach/detach sequencing. + child.clearAnimation(); + } + RecyclerView.this.removeAllViews(); + } + + @Override + public ViewHolder getChildViewHolder(View view) { + return getChildViewHolderInt(view); + } + + @Override + public void attachViewToParent(View child, int index, + ViewGroup.LayoutParams layoutParams) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + if (!vh.isTmpDetached() && !vh.shouldIgnore()) { + throw new IllegalArgumentException("Called attach on a child which is not" + + " detached: " + vh + exceptionLabel()); + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "reAttach " + vh); + } + vh.clearTmpDetachFlag(); + } else { + if (sDebugAssertionsEnabled) { + throw new IllegalArgumentException( + "No ViewHolder found for child: " + child + ", index: " + index + + exceptionLabel()); + } + } + RecyclerView.this.attachViewToParent(child, index, layoutParams); + } + + @Override + public void detachViewFromParent(int offset) { + final View view = getChildAt(offset); + if (view != null) { + final ViewHolder vh = getChildViewHolderInt(view); + if (vh != null) { + if (vh.isTmpDetached() && !vh.shouldIgnore()) { + throw new IllegalArgumentException("called detach on an already" + + " detached child " + vh + exceptionLabel()); + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "tmpDetach " + vh); + } + vh.addFlags(ViewHolder.FLAG_TMP_DETACHED); + } + } else { + if (sDebugAssertionsEnabled) { + throw new IllegalArgumentException( + "No view at offset " + offset + exceptionLabel()); + } + } + RecyclerView.this.detachViewFromParent(offset); + } + + @Override + public void onEnteredHiddenState(View child) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + vh.onEnteredHiddenState(RecyclerView.this); + } + } + + @Override + public void onLeftHiddenState(View child) { + final ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + vh.onLeftHiddenState(RecyclerView.this); + } + } + }); + } + + void initAdapterManager() { + mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() { + @Override + public ViewHolder findViewHolder(int position) { + final ViewHolder vh = findViewHolderForPosition(position, true); + if (vh == null) { + return null; + } + // ensure it is not hidden because for adapter helper, the only thing matter is that + // LM thinks view is a child. + if (mChildHelper.isHidden(vh.itemView)) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "assuming view holder cannot be find because it is hidden"); + } + return null; + } + return vh; + } + + @Override + public void offsetPositionsForRemovingInvisible(int start, int count) { + offsetPositionRecordsForRemove(start, count, true); + mItemsAddedOrRemoved = true; + mState.mDeletedInvisibleItemCountSincePreviousLayout += count; + } + + @Override + public void offsetPositionsForRemovingLaidOutOrNewView( + int positionStart, int itemCount) { + offsetPositionRecordsForRemove(positionStart, itemCount, false); + mItemsAddedOrRemoved = true; + } + + + @Override + public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) { + viewRangeUpdate(positionStart, itemCount, payload); + mItemsChanged = true; + } + + @Override + public void onDispatchFirstPass(AdapterHelper.UpdateOp op) { + dispatchUpdate(op); + } + + void dispatchUpdate(AdapterHelper.UpdateOp op) { + switch (op.cmd) { + case AdapterHelper.UpdateOp.ADD: + mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount); + break; + case AdapterHelper.UpdateOp.REMOVE: + mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount); + break; + case AdapterHelper.UpdateOp.UPDATE: + mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount, + op.payload); + break; + case AdapterHelper.UpdateOp.MOVE: + mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1); + break; + } + } + + @Override + public void onDispatchSecondPass(AdapterHelper.UpdateOp op) { + dispatchUpdate(op); + } + + @Override + public void offsetPositionsForAdd(int positionStart, int itemCount) { + offsetPositionRecordsForInsert(positionStart, itemCount); + mItemsAddedOrRemoved = true; + } + + @Override + public void offsetPositionsForMove(int from, int to) { + offsetPositionRecordsForMove(from, to); + // should we create mItemsMoved ? + mItemsAddedOrRemoved = true; + } + }); + } + + /** + * RecyclerView can perform several optimizations if it can know in advance that RecyclerView's + * size is not affected by the adapter contents. RecyclerView can still change its size based + * on other factors (e.g. its parent's size) but this size calculation cannot depend on the + * size of its children or contents of its adapter (except the number of items in the adapter). + *

+ * If your use of RecyclerView falls into this category, set this to {@code true}. It will allow + * RecyclerView to avoid invalidating the whole layout when its adapter contents change. + * + * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView. + */ + public void setHasFixedSize(boolean hasFixedSize) { + mHasFixedSize = hasFixedSize; + } + + /** + * @return true if the app has specified that changes in adapter content cannot change + * the size of the RecyclerView itself. + */ + public boolean hasFixedSize() { + return mHasFixedSize; + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + if (clipToPadding != mClipToPadding) { + invalidateGlows(); + } + mClipToPadding = clipToPadding; + super.setClipToPadding(clipToPadding); + if (mFirstLayoutComplete) { + requestLayout(); + } + } + + /** + * Returns whether this RecyclerView will clip its children to its padding, and resize (but + * not clip) any EdgeEffect to the padded region, if padding is present. + *

+ * By default, children are clipped to the padding of their parent + * RecyclerView. This clipping behavior is only enabled if padding is non-zero. + * + * @return true if this RecyclerView clips children to its padding and resizes (but doesn't + * clip) any EdgeEffect to the padded region, false otherwise. + * @attr name android:clipToPadding + */ + @Override + public boolean getClipToPadding() { + return mClipToPadding; + } + + /** + * Configure the scrolling touch slop for a specific use case. + * + * Set up the RecyclerView's scrolling motion threshold based on common usages. + * Valid arguments are {@link #TOUCH_SLOP_DEFAULT} and {@link #TOUCH_SLOP_PAGING}. + * + * @param slopConstant One of the TOUCH_SLOP_ constants representing + * the intended usage of this RecyclerView + */ + public void setScrollingTouchSlop(int slopConstant) { + final ViewConfiguration vc = ViewConfiguration.get(getContext()); + switch (slopConstant) { + default: + Log.w(TAG, "setScrollingTouchSlop(): bad argument constant " + + slopConstant + "; using default value"); + // fall-through + case TOUCH_SLOP_DEFAULT: + mTouchSlop = vc.getScaledTouchSlop(); + break; + + case TOUCH_SLOP_PAGING: + mTouchSlop = vc.getScaledPagingTouchSlop(); + break; + } + } + + /** + * Swaps the current adapter with the provided one. It is similar to + * {@link #setAdapter(Adapter)} but assumes existing adapter and the new adapter uses the same + * {@link ViewHolder} and does not clear the RecycledViewPool. + *

+ * Note that it still calls onAdapterChanged callbacks. + * + * @param adapter The new adapter to set, or null to set no adapter. + * @param removeAndRecycleExistingViews If set to true, RecyclerView will recycle all existing + * Views. If adapters have stable ids and/or you want to + * animate the disappearing views, you may prefer to set + * this to false. + * @see #setAdapter(Adapter) + */ + public void swapAdapter(@Nullable Adapter adapter, boolean removeAndRecycleExistingViews) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, true, removeAndRecycleExistingViews); + processDataSetCompletelyChanged(true); + requestLayout(); + } + + /** + * Set a new adapter to provide child views on demand. + *

+ * When adapter is changed, all existing views are recycled back to the pool. If the pool has + * only one adapter, it will be cleared. + * + * @param adapter The new adapter to set, or null to set no adapter. + * @see #swapAdapter(Adapter, boolean) + */ + public void setAdapter(@Nullable Adapter adapter) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, false, true); + processDataSetCompletelyChanged(false); + requestLayout(); + } + + /** + * Removes and recycles all views - both those currently attached, and those in the Recycler. + */ + void removeAndRecycleViews() { + // end all running animations + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + // Since animations are ended, mLayout.children should be equal to + // recyclerView.children. This may not be true if item animator's end does not work as + // expected. (e.g. not release children instantly). It is safer to use mLayout's child + // count. + if (mLayout != null) { + mLayout.removeAndRecycleAllViews(mRecycler); + mLayout.removeAndRecycleScrapInt(mRecycler); + } + // we should clear it here before adapters are swapped to ensure correct callbacks. + mRecycler.clear(); + } + + /** + * Replaces the current adapter with the new one and triggers listeners. + * + * @param adapter The new adapter + * @param compatibleWithPrevious If true, the new adapter is using the same View Holders and + * item types with the current adapter (helps us avoid cache + * invalidation). + * @param removeAndRecycleViews If true, we'll remove and recycle all existing views. If + * compatibleWithPrevious is false, this parameter is ignored. + */ + private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, + boolean removeAndRecycleViews) { + if (mAdapter != null) { + mAdapter.unregisterAdapterDataObserver(mObserver); + mAdapter.onDetachedFromRecyclerView(this); + } + if (!compatibleWithPrevious || removeAndRecycleViews) { + removeAndRecycleViews(); + } + mAdapterHelper.reset(); + final Adapter oldAdapter = mAdapter; + mAdapter = adapter; + if (adapter != null) { + adapter.registerAdapterDataObserver(mObserver); + adapter.onAttachedToRecyclerView(this); + } + if (mLayout != null) { + mLayout.onAdapterChanged(oldAdapter, mAdapter); + } + mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious); + mState.mStructureChanged = true; + } + + /** + * Retrieves the previously set adapter or null if no adapter is set. + * + * @return The previously set adapter + * @see #setAdapter(Adapter) + */ + @Nullable + public Adapter getAdapter() { + return mAdapter; + } + + /** + * Register a listener that will be notified whenever a child view is recycled. + * + *

This listener will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates expensive + * or heavyweight data with item views, this may be a good place to release + * or free those resources.

+ * + * @param listener Listener to register, or null to clear + * @deprecated Use {@link #addRecyclerListener(RecyclerListener)} and + * {@link #removeRecyclerListener(RecyclerListener)} + */ + @Deprecated + public void setRecyclerListener(@Nullable RecyclerListener listener) { + mRecyclerListener = listener; + } + + /** + * Register a listener that will be notified whenever a child view is recycled. + * + *

The listeners will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates data with + * the item views being recycled, this may be a good place to release + * or free those resources.

+ * + * @param listener Listener to register. + */ + public void addRecyclerListener(@NonNull RecyclerListener listener) { + checkArgument(listener != null, "'listener' arg cannot " + + "be null."); + mRecyclerListeners.add(listener); + } + + /** + * Removes the provided listener from RecyclerListener list. + * + * @param listener Listener to unregister. + */ + public void removeRecyclerListener(@NonNull RecyclerListener listener) { + mRecyclerListeners.remove(listener); + } + + /** + *

Return the offset of the RecyclerView's text baseline from the its top + * boundary. If the LayoutManager of this RecyclerView does not support baseline alignment, + * this method returns -1.

+ * + * @return the offset of the baseline within the RecyclerView's bounds or -1 + * if baseline alignment is not supported + */ + @Override + public int getBaseline() { + if (mLayout != null) { + return mLayout.getBaseline(); + } else { + return super.getBaseline(); + } + } + + /** + * Register a listener that will be notified whenever a child view is attached to or detached + * from RecyclerView. + * + *

This listener will be called when a LayoutManager or the RecyclerView decides + * that a child view is no longer needed. If an application associates expensive + * or heavyweight data with item views, this may be a good place to release + * or free those resources.

+ * + * @param listener Listener to register + */ + public void addOnChildAttachStateChangeListener( + @NonNull OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + mOnChildAttachStateListeners = new ArrayList<>(); + } + mOnChildAttachStateListeners.add(listener); + } + + /** + * Removes the provided listener from child attached state listeners list. + * + * @param listener Listener to unregister + */ + public void removeOnChildAttachStateChangeListener( + @NonNull OnChildAttachStateChangeListener listener) { + if (mOnChildAttachStateListeners == null) { + return; + } + mOnChildAttachStateListeners.remove(listener); + } + + /** + * Removes all listeners that were added via + * {@link #addOnChildAttachStateChangeListener(OnChildAttachStateChangeListener)}. + */ + public void clearOnChildAttachStateChangeListeners() { + if (mOnChildAttachStateListeners != null) { + mOnChildAttachStateListeners.clear(); + } + } + + /** + * Set the {@link LayoutManager} that this RecyclerView will use. + * + *

In contrast to other adapter-backed views such as {@link android.widget.ListView} + * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom + * layout arrangements for child views. These arrangements are controlled by the + * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.

+ * + *

Several default strategies are provided for common uses such as lists and grids.

+ * + * @param layout LayoutManager to use + */ + public void setLayoutManager(@Nullable LayoutManager layout) { + if (layout == mLayout) { + return; + } + stopScroll(); + // TODO We should do this switch a dispatchLayout pass and animate children. There is a good + // chance that LayoutManagers will re-use views. + if (mLayout != null) { + // end all running animations + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + mLayout.removeAndRecycleAllViews(mRecycler); + mLayout.removeAndRecycleScrapInt(mRecycler); + mRecycler.clear(); + + if (mIsAttached) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mLayout.setRecyclerView(null); + mLayout = null; + } else { + mRecycler.clear(); + } + // this is just a defensive measure for faulty item animators. + mChildHelper.removeAllViewsUnfiltered(); + mLayout = layout; + if (layout != null) { + if (layout.mRecyclerView != null) { + throw new IllegalArgumentException("LayoutManager " + layout + + " is already attached to a RecyclerView:" + + layout.mRecyclerView.exceptionLabel()); + } + mLayout.setRecyclerView(this); + if (mIsAttached) { + mLayout.dispatchAttachedToWindow(this); + } + } + mRecycler.updateViewCacheSize(); + requestLayout(); + } + + /** + * Set a {@link OnFlingListener} for this {@link RecyclerView}. + *

+ * If the {@link OnFlingListener} is set then it will receive + * calls to {@link #fling(int, int)} and will be able to intercept them. + * + * @param onFlingListener The {@link OnFlingListener} instance. + */ + public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) { + mOnFlingListener = onFlingListener; + } + + /** + * Get the current {@link OnFlingListener} from this {@link RecyclerView}. + * + * @return The {@link OnFlingListener} instance currently set (can be null). + */ + @Nullable + public OnFlingListener getOnFlingListener() { + return mOnFlingListener; + } + + @Override + protected Parcelable onSaveInstanceState() { + SavedState state = new SavedState(super.onSaveInstanceState()); + if (mPendingSavedState != null) { + state.copyFrom(mPendingSavedState); + } else if (mLayout != null) { + state.mLayoutState = mLayout.onSaveInstanceState(); + } else { + state.mLayoutState = null; + } + + return state; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + mPendingSavedState = (SavedState) state; + super.onRestoreInstanceState(mPendingSavedState.getSuperState()); + // Historically, some app developers have used onRestoreInstanceState(State) in ways it + // was never intended. For example, some devs have used it to manually set a state they + // updated themselves such that passing the state here would cause a LayoutManager to + // receive it and update its internal state accordingly, even if state was already + // previously restored. Therefore, it is necessary to always call requestLayout to retain + // the functionality even if it otherwise seems like a strange thing to do. + // ¯\_(ツ)_/¯ + requestLayout(); + } + + /** + * Override to prevent freezing of any views created by the adapter. + */ + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any views created by the adapter. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + dispatchThawSelfOnly(container); + } + + /** + * Adds a view to the animatingViews list. + * mAnimatingViews holds the child views that are currently being kept around + * purely for the purpose of being animated out of view. They are drawn as a regular + * part of the child list of the RecyclerView, but they are invisible to the LayoutManager + * as they are managed separately from the regular child views. + * + * @param viewHolder The ViewHolder to be removed + */ + private void addAnimatingView(ViewHolder viewHolder) { + final View view = viewHolder.itemView; + final boolean alreadyParented = view.getParent() == this; + mRecycler.unscrapView(getChildViewHolder(view)); + if (viewHolder.isTmpDetached()) { + // re-attach + mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true); + } else if (!alreadyParented) { + mChildHelper.addView(view, true); + } else { + mChildHelper.hide(view); + } + } + + /** + * Removes a view from the animatingViews list. + * + * @param view The view to be removed + * @return true if an animating view is removed + * @see #addAnimatingView(RecyclerView.ViewHolder) + */ + boolean removeAnimatingView(View view) { + startInterceptRequestLayout(); + final boolean removed = mChildHelper.removeViewIfHidden(view); + if (removed) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + mRecycler.unscrapView(viewHolder); + mRecycler.recycleViewHolderInternal(viewHolder); + if (sVerboseLoggingEnabled) { + Log.d(TAG, "after removing animated view: " + view + ", " + this); + } + } + // only clear request eaten flag if we removed the view. + stopInterceptRequestLayout(!removed); + return removed; + } + + /** + * Return the {@link LayoutManager} currently responsible for + * layout policy for this RecyclerView. + * + * @return The currently bound LayoutManager + */ + @Nullable + public LayoutManager getLayoutManager() { + return mLayout; + } + + /** + * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null; + * if no pool is set for this view a new one will be created. See + * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information. + * + * @return The pool used to store recycled item views for reuse. + * @see #setRecycledViewPool(RecycledViewPool) + */ + @NonNull + public RecycledViewPool getRecycledViewPool() { + return mRecycler.getRecycledViewPool(); + } + + /** + * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. + * This can be useful if you have multiple RecyclerViews with adapters that use the same + * view types, for example if you have several data sets with the same kinds of item views + * displayed by a {@link androidx.viewpager.widget.ViewPager}. + * + * @param pool Pool to set. If this parameter is null a new pool will be created and used. + */ + public void setRecycledViewPool(@Nullable RecycledViewPool pool) { + mRecycler.setRecycledViewPool(pool); + } + + /** + * Sets a new {@link ViewCacheExtension} to be used by the Recycler. + * + * @param extension ViewCacheExtension to be used or null if you want to clear the existing one. + * @see ViewCacheExtension#getViewForPositionAndType(Recycler, int, int) + */ + public void setViewCacheExtension(@Nullable ViewCacheExtension extension) { + mRecycler.setViewCacheExtension(extension); + } + + /** + * Set the number of offscreen views to retain before adding them to the potentially shared + * {@link #getRecycledViewPool() recycled view pool}. + * + *

The offscreen view cache stays aware of changes in the attached adapter, allowing + * a LayoutManager to reuse those views unmodified without needing to return to the adapter + * to rebind them.

+ * + * @param size Number of views to cache offscreen before returning them to the general + * recycled view pool + */ + public void setItemViewCacheSize(int size) { + mRecycler.setViewCacheSize(size); + } + + /** + * Return the current scrolling state of the RecyclerView. + * + * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or + * {@link #SCROLL_STATE_SETTLING} + */ + public int getScrollState() { + return mScrollState; + } + + void setScrollState(int state) { + if (state == mScrollState) { + return; + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, + new Exception()); + } + mScrollState = state; + if (state != SCROLL_STATE_SETTLING) { + stopScrollersInternal(); + } + dispatchOnScrollStateChanged(state); + } + + /** + * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can + * affect both measurement and drawing of individual item views. + * + *

Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.

+ * + * @param decor Decoration to add + * @param index Position in the decoration chain to insert this decoration at. If this value + * is negative the decoration will be added at the end. + */ + public void addItemDecoration(@NonNull ItemDecoration decor, int index) { + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or" + + " layout"); + } + if (mItemDecorations.isEmpty()) { + setWillNotDraw(false); + } + if (index < 0) { + mItemDecorations.add(decor); + } else { + mItemDecorations.add(index, decor); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can + * affect both measurement and drawing of individual item views. + * + *

Item decorations are ordered. Decorations placed earlier in the list will + * be run/queried/drawn first for their effects on item views. Padding added to views + * will be nested; a padding added by an earlier decoration will mean further + * item decorations in the list will be asked to draw/pad within the previous decoration's + * given area.

+ * + * @param decor Decoration to add + */ + public void addItemDecoration(@NonNull ItemDecoration decor) { + addItemDecoration(decor, -1); + } + + /** + * Returns an {@link ItemDecoration} previously added to this RecyclerView. + * + * @param index The index position of the desired ItemDecoration. + * @return the ItemDecoration at index position + * @throws IndexOutOfBoundsException on invalid index + */ + @NonNull + public ItemDecoration getItemDecorationAt(int index) { + final int size = getItemDecorationCount(); + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size); + } + + return mItemDecorations.get(index); + } + + /** + * Returns the number of {@link ItemDecoration} currently added to this RecyclerView. + * + * @return number of ItemDecorations currently added added to this RecyclerView. + */ + public int getItemDecorationCount() { + return mItemDecorations.size(); + } + + /** + * Removes the {@link ItemDecoration} associated with the supplied index position. + * + * @param index The index position of the ItemDecoration to be removed. + */ + public void removeItemDecorationAt(int index) { + final int size = getItemDecorationCount(); + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(index + " is an invalid index for size " + size); + } + + removeItemDecoration(getItemDecorationAt(index)); + } + + /** + * Remove an {@link ItemDecoration} from this RecyclerView. + * + *

The given decoration will no longer impact the measurement and drawing of + * item views.

+ * + * @param decor Decoration to remove + * @see #addItemDecoration(ItemDecoration) + */ + public void removeItemDecoration(@NonNull ItemDecoration decor) { + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot remove item decoration during a scroll or" + + " layout"); + } + mItemDecorations.remove(decor); + if (mItemDecorations.isEmpty()) { + setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Sets the {@link ChildDrawingOrderCallback} to be used for drawing children. + *

+ * See {@link ViewGroup#getChildDrawingOrder(int, int)} for details. Calling this method will + * always call {@link ViewGroup#setChildrenDrawingOrderEnabled(boolean)}. The parameter will be + * true if childDrawingOrderCallback is not null, false otherwise. + *

+ * Note that child drawing order may be overridden by View's elevation. + * + * @param childDrawingOrderCallback The ChildDrawingOrderCallback to be used by the drawing + * system. + */ + public void setChildDrawingOrderCallback( + @Nullable ChildDrawingOrderCallback childDrawingOrderCallback) { + if (childDrawingOrderCallback == mChildDrawingOrderCallback) { + return; + } + mChildDrawingOrderCallback = childDrawingOrderCallback; + setChildrenDrawingOrderEnabled(mChildDrawingOrderCallback != null); + } + + /** + * Set a listener that will be notified of any changes in scroll state or position. + * + * @param listener Listener to set or null to clear + * @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and + * {@link #removeOnScrollListener(OnScrollListener)} + */ + @Deprecated + public void setOnScrollListener(@Nullable OnScrollListener listener) { + mScrollListener = listener; + } + + /** + * Add a listener that will be notified of any changes in scroll state or position. + * + *

Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnScrollListeners()} + * to remove all attached listeners.

+ * + * @param listener listener to set + */ + public void addOnScrollListener(@NonNull OnScrollListener listener) { + if (mScrollListeners == null) { + mScrollListeners = new ArrayList<>(); + } + mScrollListeners.add(listener); + } + + /** + * Remove a listener that was notified of any changes in scroll state or position. + * + * @param listener listener to set or null to clear + */ + public void removeOnScrollListener(@NonNull OnScrollListener listener) { + if (mScrollListeners != null) { + mScrollListeners.remove(listener); + } + } + + /** + * Remove all secondary listener that were notified of any changes in scroll state or position. + */ + public void clearOnScrollListeners() { + if (mScrollListeners != null) { + mScrollListeners.clear(); + } + } + + /** + * Convenience method to scroll to a certain position. + * + * RecyclerView does not implement scrolling logic, rather forwards the call to + * {@link RecyclerView.LayoutManager#scrollToPosition(int)} + * + * @param position Scroll to this adapter position + * @see RecyclerView.LayoutManager#scrollToPosition(int) + */ + public void scrollToPosition(int position) { + if (mLayoutSuppressed) { + return; + } + stopScroll(); + if (mLayout == null) { + Log.e(TAG, "Cannot scroll to position a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + mLayout.scrollToPosition(position); + awakenScrollBars(); + } + + void jumpToPositionForSmoothScroller(int position) { + if (mLayout == null) { + return; + } + + // If we are jumping to a position, we are in fact scrolling the contents of the RV, so + // we should be sure that we are in the settling state. + setScrollState(SCROLL_STATE_SETTLING); + mLayout.scrollToPosition(position); + awakenScrollBars(); + } + + /** + * Starts a smooth scroll to an adapter position. + *

+ * To support smooth scrolling, you must override + * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a + * {@link SmoothScroller}. + *

+ * {@link LayoutManager} is responsible for creating the actual scroll action. If you want to + * provide a custom smooth scroll logic, override + * {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your + * LayoutManager. + * + * @param position The adapter position to scroll to + * @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int) + */ + public void smoothScrollToPosition(int position) { + if (mLayoutSuppressed) { + return; + } + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + mLayout.smoothScrollToPosition(this, mState, position); + } + + @Override + public void scrollTo(int x, int y) { + Log.w(TAG, "RecyclerView does not support scrolling to an absolute position. " + + "Use scrollToPosition instead"); + } + + @Override + public void scrollBy(int x, int y) { + if (mLayout == null) { + Log.e(TAG, "Cannot scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutSuppressed) { + return; + } + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + if (canScrollHorizontal || canScrollVertical) { + scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null, + TYPE_TOUCH); + } + } + + /** + * Same as {@link RecyclerView#scrollBy(int, int)}, but also participates in nested scrolling. + * @param x The amount of horizontal scroll requested + * @param y The amount of vertical scroll requested + * @see androidx.core.view.NestedScrollingChild + */ + public void nestedScrollBy(int x, int y) { + nestedScrollByInternal(x, y, null, TYPE_NON_TOUCH); + } + + /** + * Similar to {@link RecyclerView#scrollByInternal(int, int, MotionEvent, int)}, but fully + * participates in nested scrolling "end to end", meaning that it will start nested scrolling, + * participate in nested scrolling, and then end nested scrolling all within one call. + * @param x The amount of horizontal scroll requested. + * @param y The amount of vertical scroll requested. + * @param motionEvent The originating MotionEvent if any. + * @param type The type of nested scrolling to engage in (TYPE_TOUCH or TYPE_NON_TOUCH). + */ + @SuppressWarnings("SameParameterValue") + private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) { + + if (mLayout == null) { + Log.e(TAG, "Cannot scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutSuppressed) { + return; + } + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontal) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertical) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + + // If there is no MotionEvent, treat it as center-aligned edge effect: + float verticalDisplacement = motionEvent == null ? getHeight() / 2f : motionEvent.getY(); + float horizontalDisplacement = motionEvent == null ? getWidth() / 2f : motionEvent.getX(); + x -= releaseHorizontalGlow(x, verticalDisplacement); + y -= releaseVerticalGlow(y, horizontalDisplacement); + startNestedScroll(nestedScrollAxis, type); + if (dispatchNestedPreScroll( + canScrollHorizontal ? x : 0, + canScrollVertical ? y : 0, + mReusableIntPair, mScrollOffset, type + )) { + x -= mReusableIntPair[0]; + y -= mReusableIntPair[1]; + } + + scrollByInternal( + canScrollHorizontal ? x : 0, + canScrollVertical ? y : 0, + motionEvent, type); + if (mGapWorker != null && (x != 0 || y != 0)) { + mGapWorker.postFromTraversal(this, x, y); + } + stopNestedScroll(type); + } + + /** + * Scrolls the RV by 'dx' and 'dy' via calls to + * {@link LayoutManager#scrollHorizontallyBy(int, Recycler, State)} and + * {@link LayoutManager#scrollVerticallyBy(int, Recycler, State)}. + * + * Also sets how much of the scroll was actually consumed in 'consumed' parameter (indexes 0 and + * 1 for the x axis and y axis, respectively). + * + * This method should only be called in the context of an existing scroll operation such that + * any other necessary operations (such as a call to {@link #consumePendingUpdateOperations()}) + * is already handled. + */ + void scrollStep(int dx, int dy, @Nullable int[] consumed) { + startInterceptRequestLayout(); + onEnterLayoutOrScroll(); + + TraceCompat.beginSection(TRACE_SCROLL_TAG); + fillRemainingScrollValues(mState); + + int consumedX = 0; + int consumedY = 0; + if (dx != 0) { + consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); + } + if (dy != 0) { + consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); + } + + TraceCompat.endSection(); + repositionShadowingViews(); + + onExitLayoutOrScroll(); + stopInterceptRequestLayout(false); + + if (consumed != null) { + consumed[0] = consumedX; + consumed[1] = consumedY; + } + } + + /** + * Helper method reflect data changes to the state. + *

+ * Adapter changes during a scroll may trigger a crash because scroll assumes no data change + * but data actually changed. + *

+ * This method consumes all deferred changes to avoid that case. + */ + void consumePendingUpdateOperations() { + if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) { + TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + TraceCompat.endSection(); + return; + } + if (!mAdapterHelper.hasPendingUpdates()) { + return; + } + + // if it is only an item change (no add-remove-notifyDataSetChanged) we can check if any + // of the visible items is affected and if not, just ignore the change. + if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper + .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE + | AdapterHelper.UpdateOp.MOVE)) { + TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); + startInterceptRequestLayout(); + onEnterLayoutOrScroll(); + mAdapterHelper.preProcess(); + if (!mLayoutWasDefered) { + if (hasUpdatedView()) { + dispatchLayout(); + } else { + // no need to layout, clean state + mAdapterHelper.consumePostponedUpdates(); + } + } + stopInterceptRequestLayout(true); + onExitLayoutOrScroll(); + TraceCompat.endSection(); + } else if (mAdapterHelper.hasPendingUpdates()) { + TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + TraceCompat.endSection(); + } + } + + /** + * @return True if an existing view holder needs to be updated + */ + private boolean hasUpdatedView() { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder == null || holder.shouldIgnore()) { + continue; + } + if (holder.isUpdated()) { + return true; + } + } + return false; + } + + /** + * Does not perform bounds checking. Used by internal methods that have already validated input. + *

+ * It also reports any unused scroll request to the related EdgeEffect. + * + * @param x The amount of horizontal scroll request + * @param y The amount of vertical scroll request + * @param ev The originating MotionEvent, or null if not from a touch event. + * @param type NestedScrollType, TOUCH or NON_TOUCH. + * @return Whether any scroll was consumed in either direction. + */ + boolean scrollByInternal(int x, int y, MotionEvent ev, int type) { + int unconsumedX = 0; + int unconsumedY = 0; + int consumedX = 0; + int consumedY = 0; + + consumePendingUpdateOperations(); + if (mAdapter != null) { + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + scrollStep(x, y, mReusableIntPair); + consumedX = mReusableIntPair[0]; + consumedY = mReusableIntPair[1]; + unconsumedX = x - consumedX; + unconsumedY = y - consumedY; + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset, + type, mReusableIntPair); + unconsumedX -= mReusableIntPair[0]; + unconsumedY -= mReusableIntPair[1]; + boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0; + + // Update the last touch co-ords, taking any scroll offset into account + mLastTouchX -= mScrollOffset[0]; + mLastTouchY -= mScrollOffset[1]; + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) { + pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); + } + considerReleasingGlowsOnScroll(x, y); + } + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); + } + if (!awakenScrollBars()) { + invalidate(); + } + return consumedNestedScroll || consumedX != 0 || consumedY != 0; + } + + /** + * If either of the horizontal edge glows are currently active, this consumes part or all of + * deltaX on the edge glow. + * + * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive + * for moving down and negative for moving up. + * @param y The vertical position of the pointer. + * @return The amount of deltaX that has been consumed by the + * edge glow. + */ + private int releaseHorizontalGlow(int deltaX, float y) { + // First allow releasing existing overscroll effect: + float consumed = 0; + float displacement = y / getHeight(); + float pullDistance = (float) deltaX / getWidth(); + if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) { + if (canScrollHorizontally(-1)) { + mLeftGlow.onRelease(); + } else { + consumed = -EdgeEffectCompat.onPullDistance(mLeftGlow, -pullDistance, + 1 - displacement); + if (EdgeEffectCompat.getDistance(mLeftGlow) == 0) { + mLeftGlow.onRelease(); + } + } + invalidate(); + } else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) { + if (canScrollHorizontally(1)) { + mRightGlow.onRelease(); + } else { + consumed = EdgeEffectCompat.onPullDistance(mRightGlow, pullDistance, displacement); + if (EdgeEffectCompat.getDistance(mRightGlow) == 0) { + mRightGlow.onRelease(); + } + } + invalidate(); + } + return Math.round(consumed * getWidth()); + } + + /** + * If either of the vertical edge glows are currently active, this consumes part or all of + * deltaY on the edge glow. + * + * @param deltaY The pointer motion, in pixels, in the vertical direction, positive + * for moving down and negative for moving up. + * @param x The vertical position of the pointer. + * @return The amount of deltaY that has been consumed by the + * edge glow. + */ + private int releaseVerticalGlow(int deltaY, float x) { + // First allow releasing existing overscroll effect: + float consumed = 0; + float displacement = x / getWidth(); + float pullDistance = (float) deltaY / getHeight(); + if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) { + if (canScrollVertically(-1)) { + mTopGlow.onRelease(); + } else { + consumed = -EdgeEffectCompat.onPullDistance(mTopGlow, -pullDistance, displacement); + if (EdgeEffectCompat.getDistance(mTopGlow) == 0) { + mTopGlow.onRelease(); + } + } + invalidate(); + } else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) { + if (canScrollVertically(1)) { + mBottomGlow.onRelease(); + } else { + consumed = EdgeEffectCompat.onPullDistance(mBottomGlow, pullDistance, + 1 - displacement); + if (EdgeEffectCompat.getDistance(mBottomGlow) == 0) { + mBottomGlow.onRelease(); + } + } + invalidate(); + } + return Math.round(consumed * getHeight()); + } + + /** + *

Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal + * range. This value is used to compute the length of the thumb within the scrollbar's track. + *

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollOffset(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The horizontal offset of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeHorizontalScrollOffset + * (RecyclerView.State) + */ + @Override + public int computeHorizontalScrollOffset() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mState) : 0; + } + + /** + *

Compute the horizontal extent of the horizontal scrollbar's thumb within the + * horizontal range. This value is used to compute the length of the thumb within the + * scrollbar's track.

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The horizontal extent of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeHorizontalScrollExtent(RecyclerView.State) + */ + @Override + public int computeHorizontalScrollExtent() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mState) : 0; + } + + /** + *

Compute the horizontal range that the horizontal scrollbar represents.

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The total horizontal range represented by the vertical scrollbar + * @see RecyclerView.LayoutManager#computeHorizontalScrollRange(RecyclerView.State) + */ + @Override + public int computeHorizontalScrollRange() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mState) : 0; + } + + /** + *

Compute the vertical offset of the vertical scrollbar's thumb within the vertical range. + * This value is used to compute the length of the thumb within the scrollbar's track.

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollOffset(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The vertical offset of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeVerticalScrollOffset + * (RecyclerView.State) + */ + @Override + public int computeVerticalScrollOffset() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mState) : 0; + } + + /** + *

Compute the vertical extent of the vertical scrollbar's thumb within the vertical range. + * This value is used to compute the length of the thumb within the scrollbar's track.

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The vertical extent of the scrollbar's thumb + * @see RecyclerView.LayoutManager#computeVerticalScrollExtent(RecyclerView.State) + */ + @Override + public int computeVerticalScrollExtent() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mState) : 0; + } + + /** + *

Compute the vertical range that the vertical scrollbar represents.

+ * + *

The range is expressed in arbitrary units that must be the same as the units used by + * {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.

+ * + *

Default implementation returns 0.

+ * + *

If you want to support scroll bars, override + * {@link RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State)} in your + * LayoutManager.

+ * + * @return The total vertical range represented by the vertical scrollbar + * @see RecyclerView.LayoutManager#computeVerticalScrollRange(RecyclerView.State) + */ + @Override + public int computeVerticalScrollRange() { + if (mLayout == null) { + return 0; + } + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0; + } + + /** + * This method should be called before any code that may trigger a child view to cause a call to + * {@link RecyclerView#requestLayout()}. Doing so enables {@link RecyclerView} to avoid + * reacting to additional redundant calls to {@link #requestLayout()}. + *

+ * A call to this method must always be accompanied by a call to + * {@link #stopInterceptRequestLayout(boolean)} that follows the code that may trigger a + * child View to cause a call to {@link RecyclerView#requestLayout()}. + * + * @see #stopInterceptRequestLayout(boolean) + */ + void startInterceptRequestLayout() { + mInterceptRequestLayoutDepth++; + if (mInterceptRequestLayoutDepth == 1 && !mLayoutSuppressed) { + mLayoutWasDefered = false; + } + } + + /** + * This method should be called after any code that may trigger a child view to cause a call to + * {@link RecyclerView#requestLayout()}. + *

+ * A call to this method must always be accompanied by a call to + * {@link #startInterceptRequestLayout()} that precedes the code that may trigger a child + * View to cause a call to {@link RecyclerView#requestLayout()}. + * + * @see #startInterceptRequestLayout() + */ + void stopInterceptRequestLayout(boolean performLayoutChildren) { + if (mInterceptRequestLayoutDepth < 1) { + //noinspection PointlessBooleanExpression + if (sDebugAssertionsEnabled) { + throw new IllegalStateException("stopInterceptRequestLayout was called more " + + "times than startInterceptRequestLayout." + + exceptionLabel()); + } + mInterceptRequestLayoutDepth = 1; + } + if (!performLayoutChildren && !mLayoutSuppressed) { + // Reset the layout request eaten counter. + // This is necessary since eatRequest calls can be nested in which case the other + // call will override the inner one. + // for instance: + // eat layout for process adapter updates + // eat layout for dispatchLayout + // a bunch of req layout calls arrive + + mLayoutWasDefered = false; + } + if (mInterceptRequestLayoutDepth == 1) { + // when layout is frozen we should delay dispatchLayout() + if (performLayoutChildren && mLayoutWasDefered && !mLayoutSuppressed + && mLayout != null && mAdapter != null) { + dispatchLayout(); + } + if (!mLayoutSuppressed) { + mLayoutWasDefered = false; + } + } + mInterceptRequestLayoutDepth--; + } + + /** + * Tells this RecyclerView to suppress all layout and scroll calls until layout + * suppression is disabled with a later call to suppressLayout(false). + * When layout suppression is disabled, a requestLayout() call is sent + * if requestLayout() was attempted while layout was being suppressed. + *

+ * In addition to the layout suppression {@link #smoothScrollBy(int, int)}, + * {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and + * {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are + * dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be + * called. + * + *

+ * suppressLayout(true) does not prevent app from directly calling {@link + * LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition( + *RecyclerView, State, int)}. + *

+ * {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically + * stop suppressing. + *

+ * Note: Running ItemAnimator is not stopped automatically, it's caller's + * responsibility to call ItemAnimator.end(). + * + * @param suppress true to suppress layout and scroll, false to re-enable. + */ + @Override + public final void suppressLayout(boolean suppress) { + if (suppress != mLayoutSuppressed) { + assertNotInLayoutOrScroll("Do not suppressLayout in layout or scroll"); + if (!suppress) { + mLayoutSuppressed = false; + if (mLayoutWasDefered && mLayout != null && mAdapter != null) { + requestLayout(); + } + mLayoutWasDefered = false; + } else { + final long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + onTouchEvent(cancelEvent); + mLayoutSuppressed = true; + mIgnoreMotionEventTillDown = true; + stopScroll(); + } + } + } + + /** + * Returns whether layout and scroll calls on this container are currently being + * suppressed, due to an earlier call to {@link #suppressLayout(boolean)}. + * + * @return true if layout and scroll are currently suppressed, false otherwise. + */ + @Override + public final boolean isLayoutSuppressed() { + return mLayoutSuppressed; + } + + /** + * Enable or disable layout and scroll. After setLayoutFrozen(true) is called, + * Layout requests will be postponed until setLayoutFrozen(false) is called; + * child views are not updated when RecyclerView is frozen, {@link #smoothScrollBy(int, int)}, + * {@link #scrollBy(int, int)}, {@link #scrollToPosition(int)} and + * {@link #smoothScrollToPosition(int)} are dropped; TouchEvents and GenericMotionEvents are + * dropped; {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} will not be + * called. + * + *

+ * setLayoutFrozen(true) does not prevent app from directly calling {@link + * LayoutManager#scrollToPosition(int)}, {@link LayoutManager#smoothScrollToPosition( + *RecyclerView, State, int)}. + *

+ * {@link #setAdapter(Adapter)} and {@link #swapAdapter(Adapter, boolean)} will automatically + * stop frozen. + *

+ * Note: Running ItemAnimator is not stopped automatically, it's caller's + * responsibility to call ItemAnimator.end(). + * + * @param frozen true to freeze layout and scroll, false to re-enable. + * @deprecated Use {@link #suppressLayout(boolean)}. + */ + @Deprecated + public void setLayoutFrozen(boolean frozen) { + suppressLayout(frozen); + } + + /** + * @return true if layout and scroll are frozen + * @deprecated Use {@link #isLayoutSuppressed()}. + */ + @Deprecated + public boolean isLayoutFrozen() { + return isLayoutSuppressed(); + } + + /** + * @deprecated Use {@link #setItemAnimator(ItemAnimator)} ()}. + */ + @Deprecated + @Override + public void setLayoutTransition(LayoutTransition transition) { + if (Build.VERSION.SDK_INT < 18) { + // Transitions on APIs below 18 are using an empty LayoutTransition as a replacement + // for suppressLayout(true) and null LayoutTransition to then unsuppress it. + // We can detect this cases and use our suppressLayout() implementation instead. + if (transition == null) { + suppressLayout(false); + return; + } else { + int layoutTransitionChanging = 4; // LayoutTransition.CHANGING (Added in API 16) + if (transition.getAnimator(LayoutTransition.CHANGE_APPEARING) == null + && transition.getAnimator(LayoutTransition.CHANGE_DISAPPEARING) == null + && transition.getAnimator(LayoutTransition.APPEARING) == null + && transition.getAnimator(LayoutTransition.DISAPPEARING) == null + && transition.getAnimator(layoutTransitionChanging) == null) { + suppressLayout(true); + return; + } + } + } + + if (transition == null) { + super.setLayoutTransition(null); + } else { + throw new IllegalArgumentException("Providing a LayoutTransition into RecyclerView is " + + "not supported. Please use setItemAnimator() instead for animating changes " + + "to the items in this RecyclerView"); + } + } + + /** + * Animate a scroll by the given amount of pixels along either axis. + * + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + */ + public void smoothScrollBy(@Px int dx, @Px int dy) { + smoothScrollBy(dx, dy, null); + } + + /** + * Animate a scroll by the given amount of pixels along either axis. + * + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param interpolator {@link Interpolator} to be used for scrolling. If it is + * {@code null}, RecyclerView will use an internal default interpolator. + */ + public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator) { + smoothScrollBy(dx, dy, interpolator, UNDEFINED_DURATION); + } + + /** + * Smooth scrolls the RecyclerView by a given distance. + * + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null}, + * RecyclerView will use an internal default interpolator. + * @param duration Duration of the animation in milliseconds. Set to + * {@link #UNDEFINED_DURATION} + * to have the duration be automatically calculated based on an internally + * defined standard initial velocity. A duration less than 1 (that does not + * equal UNDEFINED_DURATION), will result in a call to + * {@link #scrollBy(int, int)}. + */ + public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator, + int duration) { + smoothScrollBy(dx, dy, interpolator, duration, false); + } + + /** + * Internal smooth scroll by implementation that currently has some tricky logic related to it's + * parameters. + *

    + *
  • For scrolling to occur, on either dimension, dx or dy must not be equal to 0 and the + * {@link LayoutManager} must support scrolling in a direction for which the value is not 0. + *
  • For smooth scrolling to occur, scrolling must occur and the duration must be equal to + * {@link #UNDEFINED_DURATION} or greater than 0. + *
  • For scrolling to occur with nested scrolling, smooth scrolling must occur and + * {@code withNestedScrolling} must be {@code true}. This could be updated, but it would + * require that {@link #scrollBy(int, int)} be implemented such that it too can handle nested + * scrolling. + *
+ * + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code + * null}, + * RecyclerView will use an internal default interpolator. + * @param duration Duration of the animation in milliseconds. Set to + * {@link #UNDEFINED_DURATION} + * to have the duration be automatically calculated based on an + * internally + * defined standard initial velocity. A duration less than 1 (that + * does not + * equal UNDEFINED_DURATION), will result in a call to + * {@link #scrollBy(int, int)}. + * @param withNestedScrolling True to perform the smooth scroll with nested scrolling. If + * {@code duration} is less than 0 and not equal to + * {@link #UNDEFINED_DURATION}, smooth scrolling will not occur and + * thus no nested scrolling will occur. + */ + // Should be considered private. Not private to avoid synthetic accessor. + void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator, + int duration, boolean withNestedScrolling) { + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutSuppressed) { + return; + } + if (!mLayout.canScrollHorizontally()) { + dx = 0; + } + if (!mLayout.canScrollVertically()) { + dy = 0; + } + if (dx != 0 || dy != 0) { + boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0; + if (durationSuggestsAnimation) { + if (withNestedScrolling) { + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (dx != 0) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (dy != 0) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); + } + mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator); + } else { + scrollBy(dx, dy); + } + } + } + + /** + * Begin a standard fling with an initial velocity along each axis in pixels per second. + * If the velocity given is below the system-defined minimum this method will return false + * and no fling will occur. + * + * @param velocityX Initial horizontal velocity in pixels per second + * @param velocityY Initial vertical velocity in pixels per second + * @return true if the fling was started, false if the velocity was too low to fling or + * LayoutManager does not support scrolling in the axis fling is issued. + * @see LayoutManager#canScrollVertically() + * @see LayoutManager#canScrollHorizontally() + */ + public boolean fling(int velocityX, int velocityY) { + if (mLayout == null) { + Log.e(TAG, "Cannot fling without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return false; + } + if (mLayoutSuppressed) { + return false; + } + + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + + if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) { + velocityX = 0; + } + if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) { + velocityY = 0; + } + if (velocityX == 0 && velocityY == 0) { + // If we don't have any velocity, return false + return false; + } + + // Flinging while the edge effect is active should affect the edge effect, + // not scrolling. + int flingX = 0; + int flingY = 0; + if (velocityX != 0) { + if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0) { + if (shouldAbsorb(mLeftGlow, -velocityX, getWidth())) { + mLeftGlow.onAbsorb(-velocityX); + } else { + flingX = velocityX; + } + velocityX = 0; + } else if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0) { + if (shouldAbsorb(mRightGlow, velocityX, getWidth())) { + mRightGlow.onAbsorb(velocityX); + } else { + flingX = velocityX; + } + velocityX = 0; + } + } + if (velocityY != 0) { + if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0) { + if (shouldAbsorb(mTopGlow, -velocityY, getHeight())) { + mTopGlow.onAbsorb(-velocityY); + } else { + flingY = velocityY; + } + velocityY = 0; + } else if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0) { + if (shouldAbsorb(mBottomGlow, velocityY, getHeight())) { + mBottomGlow.onAbsorb(velocityY); + } else { + flingY = velocityY; + } + velocityY = 0; + } + } + if (flingX != 0 || flingY != 0) { + flingX = Math.max(-mMaxFlingVelocity, Math.min(flingX, mMaxFlingVelocity)); + flingY = Math.max(-mMaxFlingVelocity, Math.min(flingY, mMaxFlingVelocity)); + mViewFlinger.fling(flingX, flingY); + } + if (velocityX == 0 && velocityY == 0) { + return flingX != 0 || flingY != 0; + } + + if (!dispatchNestedPreFling(velocityX, velocityY)) { + final boolean canScroll = canScrollHorizontal || canScrollVertical; + dispatchNestedFling(velocityX, velocityY, canScroll); + + if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { + return true; + } + + if (canScroll) { + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontal) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertical) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); + + velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); + velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); + mViewFlinger.fling(velocityX, velocityY); + return true; + } + } + return false; + } + + /** + * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should + * animate with a fling. It will animate with a fling if the velocity will remove the + * EdgeEffect through its normal operation. + * + * @param edgeEffect The EdgeEffect that might absorb the velocity. + * @param velocity The velocity of the fling motion + * @param size The width or height of the RecyclerView, depending on the edge that the + * EdgeEffect is on. + * @return true if the velocity should be absorbed or false if it should be flung. + */ + private boolean shouldAbsorb(@NonNull EdgeEffect edgeEffect, int velocity, int size) { + if (velocity > 0) { + return true; + } + float distance = EdgeEffectCompat.getDistance(edgeEffect) * size; + + // This is flinging without the spring, so let's see if it will fling past the overscroll + float flingDistance = getSplineFlingDistance(-velocity); + + return flingDistance < distance; + } + + /** + * If mLeftGlow or mRightGlow is currently active and the motion will remove some of the + * stretch, this will consume any of unconsumedX that the glow can. If the motion would + * increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed. + * + * @param unconsumedX The horizontal delta that might be consumed by the horizontal EdgeEffects + * @return The remaining unconsumed delta after the edge effects have consumed. + */ + int consumeFlingInHorizontalStretch(int unconsumedX) { + return consumeFlingInStretch(unconsumedX, mLeftGlow, mRightGlow, getWidth()); + } + + /** + * If mTopGlow or mBottomGlow is currently active and the motion will remove some of the + * stretch, this will consume any of unconsumedY that the glow can. If the motion would + * increase the stretch, or the EdgeEffect isn't a stretch, then nothing will be consumed. + * + * @param unconsumedY The vertical delta that might be consumed by the vertical EdgeEffects + * @return The remaining unconsumed delta after the edge effects have consumed. + */ + int consumeFlingInVerticalStretch(int unconsumedY) { + return consumeFlingInStretch(unconsumedY, mTopGlow, mBottomGlow, getHeight()); + } + + /** + * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for + * consuming deltas from EdgeEffects + * @param unconsumed The unconsumed delta that the EdgeEffets may consume + * @param startGlow The start (top or left) EdgeEffect + * @param endGlow The end (bottom or right) EdgeEffect + * @param size The width or height of the container, depending on whether this is for + * horizontal or vertical EdgeEffects + * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. + */ + private int consumeFlingInStretch( + int unconsumed, + EdgeEffect startGlow, + EdgeEffect endGlow, + int size + ) { + if (unconsumed > 0 && startGlow != null && EdgeEffectCompat.getDistance(startGlow) != 0f) { + float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR + * EdgeEffectCompat.onPullDistance(startGlow, deltaDistance, 0.5f)); + if (consumed != unconsumed) { + startGlow.finish(); + } + return unconsumed - consumed; + } + if (unconsumed < 0 && endGlow != null && EdgeEffectCompat.getDistance(endGlow) != 0f) { + float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size; + int consumed = Math.round(size / FLING_DESTRETCH_FACTOR + * EdgeEffectCompat.onPullDistance(endGlow, deltaDistance, 0.5f)); + if (consumed != unconsumed) { + endGlow.finish(); + } + return unconsumed - consumed; + } + return unconsumed; + } + + /** + * Stop any current scroll in progress, such as one started by + * {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling. + */ + public void stopScroll() { + setScrollState(SCROLL_STATE_IDLE); + stopScrollersInternal(); + } + + /** + * Similar to {@link #stopScroll()} but does not set the state. + */ + private void stopScrollersInternal() { + mViewFlinger.stop(); + if (mLayout != null) { + mLayout.stopSmoothScroller(); + } + } + + /** + * Returns the minimum velocity to start a fling. + * + * @return The minimum velocity to start a fling + */ + public int getMinFlingVelocity() { + return mMinFlingVelocity; + } + + + /** + * Returns the maximum fling velocity used by this RecyclerView. + * + * @return The maximum fling velocity used by this RecyclerView. + */ + public int getMaxFlingVelocity() { + return mMaxFlingVelocity; + } + + /** + * Apply a pull to relevant overscroll glow effects + */ + private void pullGlows(float x, float overscrollX, float y, float overscrollY) { + boolean invalidate = false; + if (overscrollX < 0) { + ensureLeftGlow(); + EdgeEffectCompat.onPullDistance(mLeftGlow, -overscrollX / getWidth(), + 1f - y / getHeight()); + invalidate = true; + } else if (overscrollX > 0) { + ensureRightGlow(); + EdgeEffectCompat.onPullDistance(mRightGlow, overscrollX / getWidth(), y / getHeight()); + invalidate = true; + } + + if (overscrollY < 0) { + ensureTopGlow(); + EdgeEffectCompat.onPullDistance(mTopGlow, -overscrollY / getHeight(), x / getWidth()); + invalidate = true; + } else if (overscrollY > 0) { + ensureBottomGlow(); + EdgeEffectCompat.onPullDistance(mBottomGlow, overscrollY / getHeight(), + 1f - x / getWidth()); + invalidate = true; + } + + if (invalidate || overscrollX != 0 || overscrollY != 0) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void releaseGlows() { + boolean needsInvalidate = false; + if (mLeftGlow != null) { + mLeftGlow.onRelease(); + needsInvalidate = mLeftGlow.isFinished(); + } + if (mTopGlow != null) { + mTopGlow.onRelease(); + needsInvalidate |= mTopGlow.isFinished(); + } + if (mRightGlow != null) { + mRightGlow.onRelease(); + needsInvalidate |= mRightGlow.isFinished(); + } + if (mBottomGlow != null) { + mBottomGlow.onRelease(); + needsInvalidate |= mBottomGlow.isFinished(); + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + void considerReleasingGlowsOnScroll(int dx, int dy) { + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) { + mLeftGlow.onRelease(); + needsInvalidate = mLeftGlow.isFinished(); + } + if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) { + mRightGlow.onRelease(); + needsInvalidate |= mRightGlow.isFinished(); + } + if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) { + mTopGlow.onRelease(); + needsInvalidate |= mTopGlow.isFinished(); + } + if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) { + mBottomGlow.onRelease(); + needsInvalidate |= mBottomGlow.isFinished(); + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + void absorbGlows(int velocityX, int velocityY) { + if (velocityX < 0) { + ensureLeftGlow(); + if (mLeftGlow.isFinished()) { + mLeftGlow.onAbsorb(-velocityX); + } + } else if (velocityX > 0) { + ensureRightGlow(); + if (mRightGlow.isFinished()) { + mRightGlow.onAbsorb(velocityX); + } + } + + if (velocityY < 0) { + ensureTopGlow(); + if (mTopGlow.isFinished()) { + mTopGlow.onAbsorb(-velocityY); + } + } else if (velocityY > 0) { + ensureBottomGlow(); + if (mBottomGlow.isFinished()) { + mBottomGlow.onAbsorb(velocityY); + } + } + + if (velocityX != 0 || velocityY != 0) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + void ensureLeftGlow() { + if (mLeftGlow != null) { + return; + } + mLeftGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_LEFT); + if (mClipToPadding) { + mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureRightGlow() { + if (mRightGlow != null) { + return; + } + mRightGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_RIGHT); + if (mClipToPadding) { + mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureTopGlow() { + if (mTopGlow != null) { + return; + } + mTopGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_TOP); + if (mClipToPadding) { + mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + + } + + void ensureBottomGlow() { + if (mBottomGlow != null) { + return; + } + mBottomGlow = mEdgeEffectFactory.createEdgeEffect(this, EdgeEffectFactory.DIRECTION_BOTTOM); + if (mClipToPadding) { + mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + } + + void invalidateGlows() { + mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null; + } + + /** + * Set a {@link EdgeEffectFactory} for this {@link RecyclerView}. + *

+ * When a new {@link EdgeEffectFactory} is set, any existing over-scroll effects are cleared + * and new effects are created as needed using + * {@link EdgeEffectFactory#createEdgeEffect(RecyclerView, int)} + * + * @param edgeEffectFactory The {@link EdgeEffectFactory} instance. + */ + public void setEdgeEffectFactory(@NonNull EdgeEffectFactory edgeEffectFactory) { + Preconditions.checkNotNull(edgeEffectFactory); + mEdgeEffectFactory = edgeEffectFactory; + invalidateGlows(); + } + + /** + * Retrieves the previously set {@link EdgeEffectFactory} or the default factory if nothing + * was set. + * + * @return The previously set {@link EdgeEffectFactory} + * @see #setEdgeEffectFactory(EdgeEffectFactory) + */ + @NonNull + public EdgeEffectFactory getEdgeEffectFactory() { + return mEdgeEffectFactory; + } + + /** + * Since RecyclerView is a collection ViewGroup that includes virtual children (items that are + * in the Adapter but not visible in the UI), it employs a more involved focus search strategy + * that differs from other ViewGroups. + *

+ * It first does a focus search within the RecyclerView. If this search finds a View that is in + * the focus direction with respect to the currently focused View, RecyclerView returns that + * child as the next focus target. When it cannot find such child, it calls + * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} to layout more Views + * in the focus search direction. If LayoutManager adds a View that matches the + * focus search criteria, it will be returned as the focus search result. Otherwise, + * RecyclerView will call parent to handle the focus search like a regular ViewGroup. + *

+ * When the direction is {@link View#FOCUS_FORWARD} or {@link View#FOCUS_BACKWARD}, a View that + * is not in the focus direction is still valid focus target which may not be the desired + * behavior if the Adapter has more children in the focus direction. To handle this case, + * RecyclerView converts the focus direction to an absolute direction and makes a preliminary + * focus search in that direction. If there are no Views to gain focus, it will call + * {@link LayoutManager#onFocusSearchFailed(View, int, Recycler, State)} before running a + * focus search with the original (relative) direction. This allows RecyclerView to provide + * better candidates to the focus search while still allowing the view system to take focus from + * the RecyclerView and give it to a more suitable child if such child exists. + * + * @param focused The view that currently has focus + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD} or 0 for not applicable. + * @return A new View that can be the next focus after the focused View + */ + @Override + public View focusSearch(View focused, int direction) { + View result = mLayout.onInterceptFocusSearch(focused, direction); + if (result != null) { + return result; + } + final boolean canRunFocusFailure = mAdapter != null && mLayout != null + && !isComputingLayout() && !mLayoutSuppressed; + + final FocusFinder ff = FocusFinder.getInstance(); + if (canRunFocusFailure + && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) { + // convert direction to absolute direction and see if we have a view there and if not + // tell LayoutManager to add if it can. + boolean needsFocusFailureLayout = false; + if (mLayout.canScrollVertically()) { + final int absDir = + direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP; + final View found = ff.findNextFocus(this, focused, absDir); + needsFocusFailureLayout = found == null; + if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { + // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. + direction = absDir; + } + } + if (!needsFocusFailureLayout && mLayout.canScrollHorizontally()) { + boolean rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; + final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl + ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + final View found = ff.findNextFocus(this, focused, absDir); + needsFocusFailureLayout = found == null; + if (FORCE_ABS_FOCUS_SEARCH_DIRECTION) { + // Workaround for broken FOCUS_BACKWARD in API 15 and older devices. + direction = absDir; + } + } + if (needsFocusFailureLayout) { + consumePendingUpdateOperations(); + final View focusedItemView = findContainingItemView(focused); + if (focusedItemView == null) { + // panic, focused view is not a child anymore, cannot call super. + return null; + } + startInterceptRequestLayout(); + mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + stopInterceptRequestLayout(false); + } + result = ff.findNextFocus(this, focused, direction); + } else { + result = ff.findNextFocus(this, focused, direction); + if (result == null && canRunFocusFailure) { + consumePendingUpdateOperations(); + final View focusedItemView = findContainingItemView(focused); + if (focusedItemView == null) { + // panic, focused view is not a child anymore, cannot call super. + return null; + } + startInterceptRequestLayout(); + result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + stopInterceptRequestLayout(false); + } + } + if (result != null && !result.hasFocusable()) { + if (getFocusedChild() == null) { + // Scrolling to this unfocusable view is not meaningful since there is no currently + // focused view which RV needs to keep visible. + return super.focusSearch(focused, direction); + } + // If the next view returned by onFocusSearchFailed in layout manager has no focusable + // views, we still scroll to that view in order to make it visible on the screen. + // If it's focusable, framework already calls RV's requestChildFocus which handles + // bringing this newly focused item onto the screen. + requestChildOnScreen(result, null); + return focused; + } + return isPreferredNextFocus(focused, result, direction) + ? result : super.focusSearch(focused, direction); + } + + /** + * Checks if the new focus candidate is a good enough candidate such that RecyclerView will + * assign it as the next focus View instead of letting view hierarchy decide. + * A good candidate means a View that is aligned in the focus direction wrt the focused View + * and is not the RecyclerView itself. + * When this method returns false, RecyclerView will let the parent make the decision so the + * same View may still get the focus as a result of that search. + */ + private boolean isPreferredNextFocus(View focused, View next, int direction) { + if (next == null || next == this || next == focused) { + return false; + } + // panic, result view is not a child anymore, maybe workaround b/37864393 + if (findContainingItemView(next) == null) { + return false; + } + if (focused == null) { + return true; + } + // panic, focused view is not a child anymore, maybe workaround b/37864393 + if (findContainingItemView(focused) == null) { + return true; + } + + mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); + mTempRect2.set(0, 0, next.getWidth(), next.getHeight()); + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetDescendantRectToMyCoords(next, mTempRect2); + final int rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL ? -1 : 1; + int rightness = 0; + if ((mTempRect.left < mTempRect2.left + || mTempRect.right <= mTempRect2.left) + && mTempRect.right < mTempRect2.right) { + rightness = 1; + } else if ((mTempRect.right > mTempRect2.right + || mTempRect.left >= mTempRect2.right) + && mTempRect.left > mTempRect2.left) { + rightness = -1; + } + int downness = 0; + if ((mTempRect.top < mTempRect2.top + || mTempRect.bottom <= mTempRect2.top) + && mTempRect.bottom < mTempRect2.bottom) { + downness = 1; + } else if ((mTempRect.bottom > mTempRect2.bottom + || mTempRect.top >= mTempRect2.bottom) + && mTempRect.top > mTempRect2.top) { + downness = -1; + } + switch (direction) { + case View.FOCUS_LEFT: + return rightness < 0; + case View.FOCUS_RIGHT: + return rightness > 0; + case View.FOCUS_UP: + return downness < 0; + case View.FOCUS_DOWN: + return downness > 0; + case View.FOCUS_FORWARD: + return downness > 0 || (downness == 0 && rightness * rtl > 0); + case View.FOCUS_BACKWARD: + return downness < 0 || (downness == 0 && rightness * rtl < 0); + } + throw new IllegalArgumentException("Invalid direction: " + direction + exceptionLabel()); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) { + requestChildOnScreen(child, focused); + } + super.requestChildFocus(child, focused); + } + + /** + * Requests that the given child of the RecyclerView be positioned onto the screen. This method + * can be called for both unfocusable and focusable child views. For unfocusable child views, + * the {@param focused} parameter passed is null, whereas for a focusable child, this parameter + * indicates the actual descendant view within this child view that holds the focus. + * + * @param child The child view of this RecyclerView that wants to come onto the screen. + * @param focused The descendant view that actually has the focus if child is focusable, null + * otherwise. + */ + private void requestChildOnScreen(@NonNull View child, @Nullable View focused) { + View rectView = (focused != null) ? focused : child; + mTempRect.set(0, 0, rectView.getWidth(), rectView.getHeight()); + + // get item decor offsets w/o refreshing. If they are invalid, there will be another + // layout pass to fix them, then it is LayoutManager's responsibility to keep focused + // View in viewport. + final ViewGroup.LayoutParams focusedLayoutParams = rectView.getLayoutParams(); + if (focusedLayoutParams instanceof LayoutParams) { + // if focused child has item decors, use them. Otherwise, ignore. + final LayoutParams lp = (LayoutParams) focusedLayoutParams; + if (!lp.mInsetsDirty) { + final Rect insets = lp.mDecorInsets; + mTempRect.left -= insets.left; + mTempRect.right += insets.right; + mTempRect.top -= insets.top; + mTempRect.bottom += insets.bottom; + } + } + + if (focused != null) { + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetRectIntoDescendantCoords(child, mTempRect); + } + mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete, + (focused == null)); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) { + return mLayout.requestChildRectangleOnScreen(this, child, rect, immediate); + } + + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) { + super.addFocusables(views, direction, focusableMode); + } + } + + @Override + protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + if (isComputingLayout()) { + // if we are in the middle of a layout calculation, don't let any child take focus. + // RV will handle it after layout calculation is finished. + return false; + } + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mLayoutOrScrollCounter = 0; + mIsAttached = true; + mFirstLayoutComplete = mFirstLayoutComplete && !isLayoutRequested(); + + mRecycler.onAttachedToWindow(); + + if (mLayout != null) { + mLayout.dispatchAttachedToWindow(this); + } + mPostedAnimatorRunner = false; + + if (ALLOW_THREAD_GAP_WORK) { + // Register with gap worker + mGapWorker = GapWorker.sGapWorker.get(); + if (mGapWorker == null) { + mGapWorker = new GapWorker(); + + // break 60 fps assumption if data from display appears valid + // NOTE: we only do this query once, statically, because it's very expensive (> 1ms) + Display display = ViewCompat.getDisplay(this); + float refreshRate = 60.0f; + if (!isInEditMode() && display != null) { + float displayRefreshRate = display.getRefreshRate(); + if (displayRefreshRate >= 30.0f) { + refreshRate = displayRefreshRate; + } + } + mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate); + GapWorker.sGapWorker.set(mGapWorker); + } + mGapWorker.add(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + stopScroll(); + mIsAttached = false; + if (mLayout != null) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mPendingAccessibilityImportanceChange.clear(); + removeCallbacks(mItemAnimatorRunner); + mViewInfoStore.onDetach(); + mRecycler.onDetachedFromWindow(); + + PoolingContainer.callPoolingContainerOnReleaseForChildren(this); + + if (ALLOW_THREAD_GAP_WORK && mGapWorker != null) { + // Unregister with gap worker + mGapWorker.remove(this); + mGapWorker = null; + } + } + + /** + * Returns true if RecyclerView is attached to window. + */ + @Override + public boolean isAttachedToWindow() { + return mIsAttached; + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is not. + * + * @param message The message for the exception. Can be null. + * @see #assertNotInLayoutOrScroll(String) + */ + void assertInLayoutOrScroll(String message) { + if (!isComputingLayout()) { + if (message == null) { + throw new IllegalStateException("Cannot call this method unless RecyclerView is " + + "computing a layout or scrolling" + exceptionLabel()); + } + throw new IllegalStateException(message + exceptionLabel()); + + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is. + * + * @param message The message for the exception. Can be null. + * @see #assertInLayoutOrScroll(String) + */ + void assertNotInLayoutOrScroll(String message) { + if (isComputingLayout()) { + if (message == null) { + throw new IllegalStateException("Cannot call this method while RecyclerView is " + + "computing a layout or scrolling" + exceptionLabel()); + } + throw new IllegalStateException(message); + } + if (mDispatchScrollCounter > 0) { + Log.w(TAG, "Cannot call this method in a scroll callback. Scroll callbacks might" + + "be run during a measure & layout pass where you cannot change the" + + "RecyclerView data. Any method call that might change the structure" + + "of the RecyclerView or the adapter contents should be postponed to" + + "the next frame.", + new IllegalStateException("" + exceptionLabel())); + } + } + + /** + * Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched + * to child views or this view's standard scrolling behavior. + * + *

Client code may use listeners to implement item manipulation behavior. Once a listener + * returns true from + * {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its + * {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called + * for each incoming MotionEvent until the end of the gesture.

+ * + * @param listener Listener to add + * @see SimpleOnItemTouchListener + */ + public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) { + mOnItemTouchListeners.add(listener); + } + + /** + * Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events. + * + * @param listener Listener to remove + */ + public void removeOnItemTouchListener(@NonNull OnItemTouchListener listener) { + mOnItemTouchListeners.remove(listener); + if (mInterceptingOnItemTouchListener == listener) { + mInterceptingOnItemTouchListener = null; + } + } + + /** + * Dispatches the motion event to the intercepting OnItemTouchListener or provides opportunity + * for OnItemTouchListeners to intercept. + * + * @param e The MotionEvent + * @return True if handled by an intercepting OnItemTouchListener. + */ + private boolean dispatchToOnItemTouchListeners(MotionEvent e) { + + // OnItemTouchListeners should receive calls to their methods in the same pattern that + // ViewGroups do. That pattern is a bit confusing, which in turn makes the below code a + // bit confusing. Here are rules for the pattern: + // + // 1. A single MotionEvent should not be passed to either OnInterceptTouchEvent or + // OnTouchEvent twice. + // 2. ACTION_DOWN MotionEvents may be passed to both onInterceptTouchEvent and + // onTouchEvent. + // 3. All other MotionEvents should be passed to either onInterceptTouchEvent or + // onTouchEvent, not both. + + // Side Note: We don't currently perfectly mimic how MotionEvents work in the view system. + // If we were to do so, for every MotionEvent, any OnItemTouchListener that is before the + // intercepting OnItemTouchEvent should still have a chance to intercept, and if it does, + // the previously intercepting OnItemTouchEvent should get an ACTION_CANCEL event. + + if (mInterceptingOnItemTouchListener == null) { + if (e.getAction() == MotionEvent.ACTION_DOWN) { + return false; + } + return findInterceptingOnItemTouchListener(e); + } else { + mInterceptingOnItemTouchListener.onTouchEvent(this, e); + final int action = e.getAction(); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mInterceptingOnItemTouchListener = null; + } + return true; + } + } + + /** + * Looks for an OnItemTouchListener that wants to intercept. + * + *

Calls {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} on each + * of the registered {@link OnItemTouchListener}s, passing in the + * MotionEvent. If one returns true and the action is not ACTION_CANCEL, saves the intercepting + * OnItemTouchListener to be called for future {@link RecyclerView#onTouchEvent(MotionEvent)} + * and immediately returns true. If none want to intercept or the action is ACTION_CANCEL, + * returns false. + * + * @param e The MotionEvent + * @return true if an OnItemTouchListener is saved as intercepting. + */ + private boolean findInterceptingOnItemTouchListener(MotionEvent e) { + int action = e.getAction(); + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) { + mInterceptingOnItemTouchListener = listener; + return true; + } + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + if (mLayoutSuppressed) { + // When layout is suppressed, RV does not intercept the motion event. + // A child view e.g. a button may still get the click. + return false; + } + + // Clear the active onInterceptTouchListener. None should be set at this time, and if one + // is, it's because some other code didn't follow the standard contract. + mInterceptingOnItemTouchListener = null; + if (findInterceptingOnItemTouchListener(e)) { + cancelScroll(); + return true; + } + + if (mLayout == null) { + return false; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(e); + + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mIgnoreMotionEventTillDown) { + mIgnoreMotionEventTillDown = false; + } + mScrollPointerId = e.getPointerId(0); + mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + + if (stopGlowAnimations(e) || mScrollState == SCROLL_STATE_SETTLING) { + getParent().requestDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + stopNestedScroll(TYPE_NON_TOUCH); + } + + // Clear the nested offsets + mNestedOffsets[0] = mNestedOffsets[1] = 0; + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis, TYPE_TOUCH); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + mScrollPointerId = e.getPointerId(actionIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f); + break; + + case MotionEvent.ACTION_MOVE: { + final int index = e.findPointerIndex(mScrollPointerId); + if (index < 0) { + Log.e(TAG, "Error processing scroll; pointer index for id " + + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); + return false; + } + + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); + if (mScrollState != SCROLL_STATE_DRAGGING) { + final int dx = x - mInitialTouchX; + final int dy = y - mInitialTouchY; + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + mLastTouchX = x; + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + mLastTouchY = y; + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + } + break; + + case MotionEvent.ACTION_POINTER_UP: { + onPointerUp(e); + } + break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.clear(); + stopNestedScroll(TYPE_TOUCH); + } + break; + + case MotionEvent.ACTION_CANCEL: { + cancelScroll(); + } + } + return mScrollState == SCROLL_STATE_DRAGGING; + } + + /** + * This stops any edge glow animation that is currently running by applying a + * 0 length pull at the displacement given by the provided MotionEvent. On pre-S devices, + * this method does nothing, allowing any animating edge effect to continue animating and + * returning false always. + * + * @param e The motion event to use to indicate the finger position for the displacement of + * the current pull. + * @return true if any edge effect had an existing effect to be drawn ond the + * animation was stopped or false if no edge effect had a value to display. + */ + private boolean stopGlowAnimations(MotionEvent e) { + boolean stopped = false; + if (mLeftGlow != null && EdgeEffectCompat.getDistance(mLeftGlow) != 0 + && !canScrollHorizontally(-1)) { + EdgeEffectCompat.onPullDistance(mLeftGlow, 0, 1 - (e.getY() / getHeight())); + stopped = true; + } + if (mRightGlow != null && EdgeEffectCompat.getDistance(mRightGlow) != 0 + && !canScrollHorizontally(1)) { + EdgeEffectCompat.onPullDistance(mRightGlow, 0, e.getY() / getHeight()); + stopped = true; + } + if (mTopGlow != null && EdgeEffectCompat.getDistance(mTopGlow) != 0 + && !canScrollVertically(-1)) { + EdgeEffectCompat.onPullDistance(mTopGlow, 0, e.getX() / getWidth()); + stopped = true; + } + if (mBottomGlow != null && EdgeEffectCompat.getDistance(mBottomGlow) != 0 + && !canScrollVertically(1)) { + EdgeEffectCompat.onPullDistance(mBottomGlow, 0, 1 - e.getX() / getWidth()); + stopped = true; + } + return stopped; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + listener.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + if (mLayoutSuppressed || mIgnoreMotionEventTillDown) { + return false; + } + if (dispatchToOnItemTouchListeners(e)) { + cancelScroll(); + return true; + } + + if (mLayout == null) { + return false; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + boolean eventAddedToVelocityTracker = false; + + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); + + if (action == MotionEvent.ACTION_DOWN) { + mNestedOffsets[0] = mNestedOffsets[1] = 0; + } + final MotionEvent vtev = MotionEvent.obtain(e); + vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mScrollPointerId = e.getPointerId(0); + mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + + int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis, TYPE_TOUCH); + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: { + mScrollPointerId = e.getPointerId(actionIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f); + } + break; + + case MotionEvent.ACTION_MOVE: { + final int index = e.findPointerIndex(mScrollPointerId); + if (index < 0) { + Log.e(TAG, "Error processing scroll; pointer index for id " + + mScrollPointerId + " not found. Did any MotionEvents get skipped?"); + return false; + } + + final int x = (int) (e.getX(index) + 0.5f); + final int y = (int) (e.getY(index) + 0.5f); + int dx = mLastTouchX - x; + int dy = mLastTouchY - y; + + if (mScrollState != SCROLL_STATE_DRAGGING) { + boolean startScroll = false; + if (canScrollHorizontally) { + if (dx > 0) { + dx = Math.max(0, dx - mTouchSlop); + } else { + dx = Math.min(0, dx + mTouchSlop); + } + if (dx != 0) { + startScroll = true; + } + } + if (canScrollVertically) { + if (dy > 0) { + dy = Math.max(0, dy - mTouchSlop); + } else { + dy = Math.min(0, dy + mTouchSlop); + } + if (dy != 0) { + startScroll = true; + } + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + + if (mScrollState == SCROLL_STATE_DRAGGING) { + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + dx -= releaseHorizontalGlow(dx, e.getY()); + dy -= releaseVerticalGlow(dy, e.getX()); + + if (dispatchNestedPreScroll( + canScrollHorizontally ? dx : 0, + canScrollVertically ? dy : 0, + mReusableIntPair, mScrollOffset, TYPE_TOUCH + )) { + dx -= mReusableIntPair[0]; + dy -= mReusableIntPair[1]; + // Updated the nested offsets + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + // Scroll has initiated, prevent parents from intercepting + getParent().requestDisallowInterceptTouchEvent(true); + } + + mLastTouchX = x - mScrollOffset[0]; + mLastTouchY = y - mScrollOffset[1]; + + if (scrollByInternal( + canScrollHorizontally ? dx : 0, + canScrollVertically ? dy : 0, + e, TYPE_TOUCH)) { + getParent().requestDisallowInterceptTouchEvent(true); + } + if (mGapWorker != null && (dx != 0 || dy != 0)) { + mGapWorker.postFromTraversal(this, dx, dy); + } + } + } + break; + + case MotionEvent.ACTION_POINTER_UP: { + onPointerUp(e); + } + break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.addMovement(vtev); + eventAddedToVelocityTracker = true; + mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); + final float xvel = canScrollHorizontally + ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0; + final float yvel = canScrollVertically + ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0; + if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { + setScrollState(SCROLL_STATE_IDLE); + } + resetScroll(); + } + break; + + case MotionEvent.ACTION_CANCEL: { + cancelScroll(); + } + break; + } + + if (!eventAddedToVelocityTracker) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + + return true; + } + + private void resetScroll() { + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + stopNestedScroll(TYPE_TOUCH); + releaseGlows(); + } + + private void cancelScroll() { + resetScroll(); + setScrollState(SCROLL_STATE_IDLE); + } + + private void onPointerUp(MotionEvent e) { + final int actionIndex = e.getActionIndex(); + if (e.getPointerId(actionIndex) == mScrollPointerId) { + // Pick a new pointer to pick up the slack. + final int newIndex = actionIndex == 0 ? 1 : 0; + mScrollPointerId = e.getPointerId(newIndex); + mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f); + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if (mLayout == null) { + return false; + } + if (mLayoutSuppressed) { + return false; + } + if (event.getAction() == MotionEvent.ACTION_SCROLL) { + final float vScroll, hScroll; + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + if (mLayout.canScrollVertically()) { + // Inverse the sign of the vertical scroll to align the scroll orientation + // with AbsListView. + vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vScroll = 0f; + } + if (mLayout.canScrollHorizontally()) { + hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } else { + hScroll = 0f; + } + } else if ((event.getSource() & InputDeviceCompat.SOURCE_ROTARY_ENCODER) != 0) { + final float axisScroll = event.getAxisValue(MotionEventCompat.AXIS_SCROLL); + if (mLayout.canScrollVertically()) { + // Invert the sign of the vertical scroll to align the scroll orientation + // with AbsListView. + vScroll = -axisScroll; + hScroll = 0f; + } else if (mLayout.canScrollHorizontally()) { + vScroll = 0f; + hScroll = axisScroll; + } else { + vScroll = 0f; + hScroll = 0f; + } + } else { + vScroll = 0f; + hScroll = 0f; + } + + if (vScroll != 0 || hScroll != 0) { + nestedScrollByInternal((int) (hScroll * mScaledHorizontalScrollFactor), + (int) (vScroll * mScaledVerticalScrollFactor), event, TYPE_NON_TOUCH); + } + } + return false; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (mLayout == null) { + defaultOnMeasure(widthSpec, heightSpec); + return; + } + if (mLayout.isAutoMeasureEnabled()) { + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); + + /** + * This specific call should be considered deprecated and replaced with + * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could + * break existing third party code but all documentation directs developers to not + * override {@link LayoutManager#onMeasure(int, int)} when + * {@link LayoutManager#isAutoMeasureEnabled()} returns true. + */ + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + + // Calculate and track whether we should skip measurement here because the MeasureSpec + // modes in both dimensions are EXACTLY. + mLastAutoMeasureSkippedDueToExact = + widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; + if (mLastAutoMeasureSkippedDueToExact || mAdapter == null) { + return; + } + + if (mState.mLayoutStep == State.STEP_START) { + dispatchLayoutStep1(); + } + // set dimensions in 2nd step. Pre-layout should happen with old dimensions for + // consistency + mLayout.setMeasureSpecs(widthSpec, heightSpec); + mState.mIsMeasuring = true; + dispatchLayoutStep2(); + + // now we can get the width and height from the children. + mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); + + // if RecyclerView has non-exact width and height and if there is at least one child + // which also has non-exact width & height, we have to re-measure. + if (mLayout.shouldMeasureTwice()) { + mLayout.setMeasureSpecs( + MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); + mState.mIsMeasuring = true; + dispatchLayoutStep2(); + // now we can get the width and height from the children. + mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); + } + + mLastAutoMeasureNonExactMeasuredWidth = getMeasuredWidth(); + mLastAutoMeasureNonExactMeasuredHeight = getMeasuredHeight(); + } else { + if (mHasFixedSize) { + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + return; + } + // custom onMeasure + if (mAdapterUpdateDuringMeasure) { + startInterceptRequestLayout(); + onEnterLayoutOrScroll(); + processAdapterUpdatesAndSetAnimationFlags(); + onExitLayoutOrScroll(); + + if (mState.mRunPredictiveAnimations) { + mState.mInPreLayout = true; + } else { + // consume remaining updates to provide a consistent state with the layout pass. + mAdapterHelper.consumeUpdatesInOnePass(); + mState.mInPreLayout = false; + } + mAdapterUpdateDuringMeasure = false; + stopInterceptRequestLayout(false); + } else if (mState.mRunPredictiveAnimations) { + // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true: + // this means there is already an onMeasure() call performed to handle the pending + // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout + // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time + // because getViewForPosition() will crash when LM uses a child to measure. + setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); + return; + } + + if (mAdapter != null) { + mState.mItemCount = mAdapter.getItemCount(); + } else { + mState.mItemCount = 0; + } + startInterceptRequestLayout(); + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + stopInterceptRequestLayout(false); + mState.mInPreLayout = false; // clear + } + } + + /** + * An implementation of {@link View#onMeasure(int, int)} to fall back to in various scenarios + * where this RecyclerView is otherwise lacking better information. + */ + void defaultOnMeasure(int widthSpec, int heightSpec) { + // calling LayoutManager here is not pretty but that API is already public and it is better + // than creating another method since this is internal. + final int width = LayoutManager.chooseSize(widthSpec, + getPaddingLeft() + getPaddingRight(), + ViewCompat.getMinimumWidth(this)); + final int height = LayoutManager.chooseSize(heightSpec, + getPaddingTop() + getPaddingBottom(), + ViewCompat.getMinimumHeight(this)); + + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (w != oldw || h != oldh) { + invalidateGlows(); + // layout's w/h are updated during measure/layout steps. + } + } + + /** + * Sets the {@link ItemAnimator} that will handle animations involving changes + * to the items in this RecyclerView. By default, RecyclerView instantiates and + * uses an instance of {@link DefaultItemAnimator}. Whether item animations are + * enabled for the RecyclerView depends on the ItemAnimator and whether + * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations() + * supports item animations}. + * + * @param animator The ItemAnimator being set. If null, no animations will occur + * when changes occur to the items in this RecyclerView. + */ + public void setItemAnimator(@Nullable ItemAnimator animator) { + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + mItemAnimator.setListener(null); + } + mItemAnimator = animator; + if (mItemAnimator != null) { + mItemAnimator.setListener(mItemAnimatorListener); + } + } + + void onEnterLayoutOrScroll() { + mLayoutOrScrollCounter++; + } + + void onExitLayoutOrScroll() { + onExitLayoutOrScroll(true); + } + + void onExitLayoutOrScroll(boolean enableChangeEvents) { + mLayoutOrScrollCounter--; + if (mLayoutOrScrollCounter < 1) { + if (sDebugAssertionsEnabled && mLayoutOrScrollCounter < 0) { + throw new IllegalStateException("layout or scroll counter cannot go below zero." + + "Some calls are not matching" + exceptionLabel()); + } + mLayoutOrScrollCounter = 0; + if (enableChangeEvents) { + dispatchContentChangedIfNecessary(); + dispatchPendingImportantForAccessibilityChanges(); + } + } + } + + boolean isAccessibilityEnabled() { + return mAccessibilityManager != null && mAccessibilityManager.isEnabled(); + } + + private void dispatchContentChangedIfNecessary() { + final int flags = mEatenAccessibilityChangeFlags; + mEatenAccessibilityChangeFlags = 0; + if (flags != 0 && isAccessibilityEnabled()) { + final AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + AccessibilityEventCompat.setContentChangeTypes(event, flags); + sendAccessibilityEventUnchecked(event); + } + } + + /** + * Returns whether RecyclerView is currently computing a layout. + *

+ * If this method returns true, it means that RecyclerView is in a lockdown state and any + * attempt to update adapter contents will result in an exception because adapter contents + * cannot be changed while RecyclerView is trying to compute the layout. + *

+ * It is very unlikely that your code will be running during this state as it is + * called by the framework when a layout traversal happens or RecyclerView starts to scroll + * in response to system events (touch, accessibility etc). + *

+ * This case may happen if you have some custom logic to change adapter contents in + * response to a View callback (e.g. focus change callback) which might be triggered during a + * layout calculation. In these cases, you should just postpone the change using a Handler or a + * similar mechanism. + * + * @return true if RecyclerView is currently computing a layout, false + * otherwise + */ + public boolean isComputingLayout() { + return mLayoutOrScrollCounter > 0; + } + + /** + * Returns true if an accessibility event should not be dispatched now. This happens when an + * accessibility request arrives while RecyclerView does not have a stable state which is very + * hard to handle for a LayoutManager. Instead, this method records necessary information about + * the event and dispatches a window change event after the critical section is finished. + * + * @return True if the accessibility event should be postponed. + */ + boolean shouldDeferAccessibilityEvent(AccessibilityEvent event) { + if (isComputingLayout()) { + int type = 0; + if (event != null) { + type = AccessibilityEventCompat.getContentChangeTypes(event); + } + if (type == 0) { + type = AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED; + } + mEatenAccessibilityChangeFlags |= type; + return true; + } + return false; + } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + if (shouldDeferAccessibilityEvent(event)) { + return; + } + super.sendAccessibilityEventUnchecked(event); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } + + /** + * Gets the current ItemAnimator for this RecyclerView. A null return value + * indicates that there is no animator and that item changes will happen without + * any animations. By default, RecyclerView instantiates and + * uses an instance of {@link DefaultItemAnimator}. + * + * @return ItemAnimator The current ItemAnimator. If null, no animations will occur + * when changes occur to the items in this RecyclerView. + */ + @Nullable + public ItemAnimator getItemAnimator() { + return mItemAnimator; + } + + /** + * Post a runnable to the next frame to run pending item animations. Only the first such + * request will be posted, governed by the mPostedAnimatorRunner flag. + */ + void postAnimationRunner() { + if (!mPostedAnimatorRunner && mIsAttached) { + ViewCompat.postOnAnimation(this, mItemAnimatorRunner); + mPostedAnimatorRunner = true; + } + } + + private boolean predictiveItemAnimationsEnabled() { + return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations()); + } + + /** + * Consumes adapter updates and calculates which type of animations we want to run. + * Called in onMeasure and dispatchLayout. + *

+ * This method may process only the pre-layout state of updates or all of them. + */ + private void processAdapterUpdatesAndSetAnimationFlags() { + if (mDataSetHasChangedAfterLayout) { + // Processing these items have no value since data set changed unexpectedly. + // Instead, we just reset it. + mAdapterHelper.reset(); + if (mDispatchItemsChangedEvent) { + mLayout.onItemsChanged(this); + } + } + // simple animations are a subset of advanced animations (which will cause a + // pre-layout step) + // If layout supports predictive animations, pre-process to decide if we want to run them + if (predictiveItemAnimationsEnabled()) { + mAdapterHelper.preProcess(); + } else { + mAdapterHelper.consumeUpdatesInOnePass(); + } + boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged; + mState.mRunSimpleAnimations = mFirstLayoutComplete + && mItemAnimator != null + && (mDataSetHasChangedAfterLayout + || animationTypeSupported + || mLayout.mRequestedSimpleAnimations) + && (!mDataSetHasChangedAfterLayout + || mAdapter.hasStableIds()); + mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations + && animationTypeSupported + && !mDataSetHasChangedAfterLayout + && predictiveItemAnimationsEnabled(); + } + + /** + * Wrapper around layoutChildren() that handles animating changes caused by layout. + * Animations work on the assumption that there are five different kinds of items + * in play: + * PERSISTENT: items are visible before and after layout + * REMOVED: items were visible before layout and were removed by the app + * ADDED: items did not exist before layout and were added by the app + * DISAPPEARING: items exist in the data set before/after, but changed from + * visible to non-visible in the process of layout (they were moved off + * screen as a side-effect of other changes) + * APPEARING: items exist in the data set before/after, but changed from + * non-visible to visible in the process of layout (they were moved on + * screen as a side-effect of other changes) + * The overall approach figures out what items exist before/after layout and + * infers one of the five above states for each of the items. Then the animations + * are set up accordingly: + * PERSISTENT views are animated via + * {@link ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * DISAPPEARING views are animated via + * {@link ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * APPEARING views are animated via + * {@link ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} + * and changed views are animated via + * {@link ItemAnimator#animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)}. + */ + void dispatchLayout() { + if (mAdapter == null) { + Log.w(TAG, "No adapter attached; skipping layout"); + // leave the state in START + return; + } + if (mLayout == null) { + Log.e(TAG, "No layout manager attached; skipping layout"); + // leave the state in START + return; + } + mState.mIsMeasuring = false; + + // If the last time we measured children in onMeasure, we skipped the measurement and layout + // of RV children because the MeasureSpec in both dimensions was EXACTLY, and current + // dimensions of the RV are not equal to the last measured dimensions of RV, we need to + // measure and layout children one last time. + boolean needsRemeasureDueToExactSkip = mLastAutoMeasureSkippedDueToExact + && (mLastAutoMeasureNonExactMeasuredWidth != getWidth() + || mLastAutoMeasureNonExactMeasuredHeight != getHeight()); + mLastAutoMeasureNonExactMeasuredWidth = 0; + mLastAutoMeasureNonExactMeasuredHeight = 0; + mLastAutoMeasureSkippedDueToExact = false; + + if (mState.mLayoutStep == State.STEP_START) { + dispatchLayoutStep1(); + mLayout.setExactMeasureSpecsFrom(this); + dispatchLayoutStep2(); + } else if (mAdapterHelper.hasUpdates() + || needsRemeasureDueToExactSkip + || mLayout.getWidth() != getWidth() + || mLayout.getHeight() != getHeight()) { + // First 2 steps are done in onMeasure but looks like we have to run again due to + // changed size. + + // TODO(shepshapard): Worth a note that I believe + // "mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()" above is + // not actually correct, causes unnecessary work to be done, and should be + // removed. Removing causes many tests to fail and I didn't have the time to + // investigate. Just a note for the a future reader or bug fixer. + mLayout.setExactMeasureSpecsFrom(this); + dispatchLayoutStep2(); + } else { + // always make sure we sync them (to ensure mode is exact) + mLayout.setExactMeasureSpecsFrom(this); + } + dispatchLayoutStep3(); + } + + private void saveFocusInfo() { + View child = null; + if (mPreserveFocusAfterLayout && hasFocus() && mAdapter != null) { + child = getFocusedChild(); + } + + final ViewHolder focusedVh = child == null ? null : findContainingViewHolder(child); + if (focusedVh == null) { + resetFocusInfo(); + } else { + mState.mFocusedItemId = mAdapter.hasStableIds() ? focusedVh.getItemId() : NO_ID; + // mFocusedItemPosition should hold the current adapter position of the previously + // focused item. If the item is removed, we store the previous adapter position of the + // removed item. + mState.mFocusedItemPosition = mDataSetHasChangedAfterLayout ? NO_POSITION + : (focusedVh.isRemoved() ? focusedVh.mOldPosition + : focusedVh.getAbsoluteAdapterPosition()); + mState.mFocusedSubChildId = getDeepestFocusedViewWithId(focusedVh.itemView); + } + } + + private void resetFocusInfo() { + mState.mFocusedItemId = NO_ID; + mState.mFocusedItemPosition = NO_POSITION; + mState.mFocusedSubChildId = View.NO_ID; + } + + /** + * Finds the best view candidate to request focus on using mFocusedItemPosition index of the + * previously focused item. It first traverses the adapter forward to find a focusable candidate + * and if no such candidate is found, it reverses the focus search direction for the items + * before the mFocusedItemPosition'th index; + * + * @return The best candidate to request focus on, or null if no such candidate exists. Null + * indicates all the existing adapter items are unfocusable. + */ + @Nullable + private View findNextViewToFocus() { + int startFocusSearchIndex = mState.mFocusedItemPosition != -1 ? mState.mFocusedItemPosition + : 0; + ViewHolder nextFocus; + final int itemCount = mState.getItemCount(); + for (int i = startFocusSearchIndex; i < itemCount; i++) { + nextFocus = findViewHolderForAdapterPosition(i); + if (nextFocus == null) { + break; + } + if (nextFocus.itemView.hasFocusable()) { + return nextFocus.itemView; + } + } + final int limit = Math.min(itemCount, startFocusSearchIndex); + for (int i = limit - 1; i >= 0; i--) { + nextFocus = findViewHolderForAdapterPosition(i); + if (nextFocus == null) { + return null; + } + if (nextFocus.itemView.hasFocusable()) { + return nextFocus.itemView; + } + } + return null; + } + + private void recoverFocusFromState() { + if (!mPreserveFocusAfterLayout || mAdapter == null || !hasFocus() + || getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS + || (getDescendantFocusability() == FOCUS_BEFORE_DESCENDANTS && isFocused())) { + // No-op if either of these cases happens: + // 1. RV has no focus, or 2. RV blocks focus to its children, or 3. RV takes focus + // before its children and is focused (i.e. it already stole the focus away from its + // descendants). + return; + } + // only recover focus if RV itself has the focus or the focused view is hidden + if (!isFocused()) { + final View focusedChild = getFocusedChild(); + if (IGNORE_DETACHED_FOCUSED_CHILD + && (focusedChild.getParent() == null || !focusedChild.hasFocus())) { + // Special handling of API 15-. A focused child can be invalid because mFocus is not + // cleared when the child is detached (mParent = null), + // This happens because clearFocus on API 15- does not invalidate mFocus of its + // parent when this child is detached. + // For API 16+, this is not an issue because requestFocus takes care of clearing the + // prior detached focused child. For API 15- the problem happens in 2 cases because + // clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called + // for the current focused item which calls clearChild or 2. when the prior focused + // child is removed, removeDetachedView called in layout step 3 which calls + // clearChild. We should ignore this invalid focused child in all our calculations + // for the next view to receive focus, and apply the focus recovery logic instead. + if (mChildHelper.getChildCount() == 0) { + // No children left. Request focus on the RV itself since one of its children + // was holding focus previously. + requestFocus(); + return; + } + } else if (!mChildHelper.isHidden(focusedChild)) { + // If the currently focused child is hidden, apply the focus recovery logic. + // Otherwise return, i.e. the currently (unhidden) focused child is good enough :/. + return; + } + } + ViewHolder focusTarget = null; + // RV first attempts to locate the previously focused item to request focus on using + // mFocusedItemId. If such an item no longer exists, it then makes a best-effort attempt to + // find the next best candidate to request focus on based on mFocusedItemPosition. + if (mState.mFocusedItemId != NO_ID && mAdapter.hasStableIds()) { + focusTarget = findViewHolderForItemId(mState.mFocusedItemId); + } + View viewToFocus = null; + if (focusTarget == null || mChildHelper.isHidden(focusTarget.itemView) + || !focusTarget.itemView.hasFocusable()) { + if (mChildHelper.getChildCount() > 0) { + // At this point, RV has focus and either of these conditions are true: + // 1. There's no previously focused item either because RV received focused before + // layout, or the previously focused item was removed, or RV doesn't have stable IDs + // 2. Previous focus child is hidden, or 3. Previous focused child is no longer + // focusable. In either of these cases, we make sure that RV still passes down the + // focus to one of its focusable children using a best-effort algorithm. + viewToFocus = findNextViewToFocus(); + } + } else { + // looks like the focused item has been replaced with another view that represents the + // same item in the adapter. Request focus on that. + viewToFocus = focusTarget.itemView; + } + + if (viewToFocus != null) { + if (mState.mFocusedSubChildId != NO_ID) { + View child = viewToFocus.findViewById(mState.mFocusedSubChildId); + if (child != null && child.isFocusable()) { + viewToFocus = child; + } + } + viewToFocus.requestFocus(); + } + } + + private int getDeepestFocusedViewWithId(View view) { + int lastKnownId = view.getId(); + while (!view.isFocused() && view instanceof ViewGroup && view.hasFocus()) { + view = ((ViewGroup) view).getFocusedChild(); + final int id = view.getId(); + if (id != View.NO_ID) { + lastKnownId = view.getId(); + } + } + return lastKnownId; + } + + final void fillRemainingScrollValues(State state) { + if (getScrollState() == SCROLL_STATE_SETTLING) { + final OverScroller scroller = mViewFlinger.mOverScroller; + state.mRemainingScrollHorizontal = scroller.getFinalX() - scroller.getCurrX(); + state.mRemainingScrollVertical = scroller.getFinalY() - scroller.getCurrY(); + } else { + state.mRemainingScrollHorizontal = 0; + state.mRemainingScrollVertical = 0; + } + } + + /** + * The first step of a layout where we; + * - process adapter updates + * - decide which animation should run + * - save information about current views + * - If necessary, run predictive layout and save its information + */ + private void dispatchLayoutStep1() { + mState.assertLayoutStep(State.STEP_START); + fillRemainingScrollValues(mState); + mState.mIsMeasuring = false; + startInterceptRequestLayout(); + mViewInfoStore.clear(); + onEnterLayoutOrScroll(); + processAdapterUpdatesAndSetAnimationFlags(); + saveFocusInfo(); + mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; + mItemsAddedOrRemoved = mItemsChanged = false; + mState.mInPreLayout = mState.mRunPredictiveAnimations; + mState.mItemCount = mAdapter.getItemCount(); + findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); + + if (mState.mRunSimpleAnimations) { + // Step 0: Find out where all non-removed items are, pre-layout + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) { + continue; + } + final ItemHolderInfo animationInfo = mItemAnimator + .recordPreLayoutInformation(mState, holder, + ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), + holder.getUnmodifiedPayloads()); + mViewInfoStore.addToPreLayout(holder, animationInfo); + if (mState.mTrackOldChangeHolders && holder.isUpdated() && !holder.isRemoved() + && !holder.shouldIgnore() && !holder.isInvalid()) { + long key = getChangedHolderKey(holder); + // This is NOT the only place where a ViewHolder is added to old change holders + // list. There is another case where: + // * A VH is currently hidden but not deleted + // * The hidden item is changed in the adapter + // * Layout manager decides to layout the item in the pre-Layout pass (step1) + // When this case is detected, RV will un-hide that view and add to the old + // change holders list. + mViewInfoStore.addToOldChangeHolders(key, holder); + } + } + } + if (mState.mRunPredictiveAnimations) { + // Step 1: run prelayout: This will use the old positions of items. The layout manager + // is expected to layout everything, even removed items (though not to add removed + // items back to the container). This gives the pre-layout position of APPEARING views + // which come into existence as part of the real layout. + + // Save old positions so that LayoutManager can run its mapping logic. + saveOldPositions(); + final boolean didStructureChange = mState.mStructureChanged; + mState.mStructureChanged = false; + // temporarily disable flag because we are asking for previous layout + mLayout.onLayoutChildren(mRecycler, mState); + mState.mStructureChanged = didStructureChange; + + for (int i = 0; i < mChildHelper.getChildCount(); ++i) { + final View child = mChildHelper.getChildAt(i); + final ViewHolder viewHolder = getChildViewHolderInt(child); + if (viewHolder.shouldIgnore()) { + continue; + } + if (!mViewInfoStore.isInPreLayout(viewHolder)) { + int flags = ItemAnimator.buildAdapterChangeFlagsForAnimations(viewHolder); + boolean wasHidden = viewHolder + .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (!wasHidden) { + flags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; + } + final ItemHolderInfo animationInfo = mItemAnimator.recordPreLayoutInformation( + mState, viewHolder, flags, viewHolder.getUnmodifiedPayloads()); + if (wasHidden) { + recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo); + } else { + mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo); + } + } + } + // we don't process disappearing list because they may re-appear in post layout pass. + clearOldPositions(); + } else { + clearOldPositions(); + } + onExitLayoutOrScroll(); + stopInterceptRequestLayout(false); + mState.mLayoutStep = State.STEP_LAYOUT; + } + + /** + * The second layout step where we do the actual layout of the views for the final state. + * This step might be run multiple times if necessary (e.g. measure). + */ + private void dispatchLayoutStep2() { + startInterceptRequestLayout(); + onEnterLayoutOrScroll(); + mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); + mAdapterHelper.consumeUpdatesInOnePass(); + mState.mItemCount = mAdapter.getItemCount(); + mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; + if (mPendingSavedState != null && mAdapter.canRestoreState()) { + if (mPendingSavedState.mLayoutState != null) { + mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState); + } + mPendingSavedState = null; + } + // Step 2: Run layout + mState.mInPreLayout = false; + mLayout.onLayoutChildren(mRecycler, mState); + + mState.mStructureChanged = false; + + // onLayoutChildren may have caused client code to disable item animations; re-check + mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; + mState.mLayoutStep = State.STEP_ANIMATIONS; + onExitLayoutOrScroll(); + stopInterceptRequestLayout(false); + } + + /** + * The final step of the layout where we save the information about views for animations, + * trigger animations and do any necessary cleanup. + */ + private void dispatchLayoutStep3() { + mState.assertLayoutStep(State.STEP_ANIMATIONS); + startInterceptRequestLayout(); + onEnterLayoutOrScroll(); + mState.mLayoutStep = State.STEP_START; + if (mState.mRunSimpleAnimations) { + // Step 3: Find out where things are now, and process change animations. + // traverse list in reverse because we may call animateChange in the loop which may + // remove the target view holder. + for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { + ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + long key = getChangedHolderKey(holder); + final ItemHolderInfo animationInfo = mItemAnimator + .recordPostLayoutInformation(mState, holder); + ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); + if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) { + // run a change animation + + // If an Item is CHANGED but the updated version is disappearing, it creates + // a conflicting case. + // Since a view that is marked as disappearing is likely to be going out of + // bounds, we run a change animation. Both views will be cleaned automatically + // once their animations finish. + // On the other hand, if it is the same view holder instance, we run a + // disappearing animation instead because we are not going to rebind the updated + // VH unless it is enforced by the layout manager. + final boolean oldDisappearing = mViewInfoStore.isDisappearing( + oldChangeViewHolder); + final boolean newDisappearing = mViewInfoStore.isDisappearing(holder); + if (oldDisappearing && oldChangeViewHolder == holder) { + // run disappear animation instead of change + mViewInfoStore.addToPostLayout(holder, animationInfo); + } else { + final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout( + oldChangeViewHolder); + // we add and remove so that any post info is merged. + mViewInfoStore.addToPostLayout(holder, animationInfo); + ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); + if (preInfo == null) { + handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder); + } else { + animateChange(oldChangeViewHolder, holder, preInfo, postInfo, + oldDisappearing, newDisappearing); + } + } + } else { + mViewInfoStore.addToPostLayout(holder, animationInfo); + } + } + + // Step 4: Process view info lists and trigger animations + mViewInfoStore.process(mViewInfoProcessCallback); + } + + mLayout.removeAndRecycleScrapInt(mRecycler); + mState.mPreviousLayoutItemCount = mState.mItemCount; + mDataSetHasChangedAfterLayout = false; + mDispatchItemsChangedEvent = false; + mState.mRunSimpleAnimations = false; + + mState.mRunPredictiveAnimations = false; + mLayout.mRequestedSimpleAnimations = false; + if (mRecycler.mChangedScrap != null) { + mRecycler.mChangedScrap.clear(); + } + if (mLayout.mPrefetchMaxObservedInInitialPrefetch) { + // Initial prefetch has expanded cache, so reset until next prefetch. + // This prevents initial prefetches from expanding the cache permanently. + mLayout.mPrefetchMaxCountObserved = 0; + mLayout.mPrefetchMaxObservedInInitialPrefetch = false; + mRecycler.updateViewCacheSize(); + } + + mLayout.onLayoutCompleted(mState); + onExitLayoutOrScroll(); + stopInterceptRequestLayout(false); + mViewInfoStore.clear(); + if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { + dispatchOnScrolled(0, 0); + } + recoverFocusFromState(); + resetFocusInfo(); + } + + /** + * This handles the case where there is an unexpected VH missing in the pre-layout map. + *

+ * We might be able to detect the error in the application which will help the developer to + * resolve the issue. + *

+ * If it is not an expected error, we at least print an error to notify the developer and ignore + * the animation. + * + * https://code.google.com/p/android/issues/detail?id=193958 + * + * @param key The change key + * @param holder Current ViewHolder + * @param oldChangeViewHolder Changed ViewHolder + */ + private void handleMissingPreInfoForChangeError(long key, + ViewHolder holder, ViewHolder oldChangeViewHolder) { + // check if two VH have the same key, if so, print that as an error + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + View view = mChildHelper.getChildAt(i); + ViewHolder other = getChildViewHolderInt(view); + if (other == holder) { + continue; + } + final long otherKey = getChangedHolderKey(other); + if (otherKey == key) { + if (mAdapter != null && mAdapter.hasStableIds()) { + throw new IllegalStateException("Two different ViewHolders have the same stable" + + " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT" + + " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder + + exceptionLabel()); + } else { + throw new IllegalStateException("Two different ViewHolders have the same change" + + " ID. This might happen due to inconsistent Adapter update events or" + + " if the LayoutManager lays out the same View multiple times." + + "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder + + exceptionLabel()); + } + } + } + // Very unlikely to happen but if it does, notify the developer. + Log.e(TAG, "Problem while matching changed view holders with the new" + + "ones. The pre-layout information for the change holder " + oldChangeViewHolder + + " cannot be found but it is necessary for " + holder + exceptionLabel()); + } + + /** + * Records the animation information for a view holder that was bounced from hidden list. It + * also clears the bounce back flag. + */ + void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder, + ItemHolderInfo animationInfo) { + // looks like this view bounced back from hidden list! + viewHolder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (mState.mTrackOldChangeHolders && viewHolder.isUpdated() + && !viewHolder.isRemoved() && !viewHolder.shouldIgnore()) { + long key = getChangedHolderKey(viewHolder); + mViewInfoStore.addToOldChangeHolders(key, viewHolder); + } + mViewInfoStore.addToPreLayout(viewHolder, animationInfo); + } + + private void findMinMaxChildLayoutPositions(int[] into) { + final int count = mChildHelper.getChildCount(); + if (count == 0) { + into[0] = NO_POSITION; + into[1] = NO_POSITION; + return; + } + int minPositionPreLayout = Integer.MAX_VALUE; + int maxPositionPreLayout = Integer.MIN_VALUE; + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + final int pos = holder.getLayoutPosition(); + if (pos < minPositionPreLayout) { + minPositionPreLayout = pos; + } + if (pos > maxPositionPreLayout) { + maxPositionPreLayout = pos; + } + } + into[0] = minPositionPreLayout; + into[1] = maxPositionPreLayout; + } + + private boolean didChildRangeChange(int minPositionPreLayout, int maxPositionPreLayout) { + findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); + return mMinMaxLayoutPositions[0] != minPositionPreLayout + || mMinMaxLayoutPositions[1] != maxPositionPreLayout; + } + + @Override + protected void removeDetachedView(View child, boolean animate) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh != null) { + if (vh.isTmpDetached()) { + vh.clearTmpDetachFlag(); + } else if (!vh.shouldIgnore()) { + throw new IllegalArgumentException("Called removeDetachedView with a view which" + + " is not flagged as tmp detached." + vh + exceptionLabel()); + } + } else { + if (sDebugAssertionsEnabled) { + throw new IllegalArgumentException( + "No ViewHolder found for child: " + child + exceptionLabel()); + } + } + + // Clear any android.view.animation.Animation that may prevent the item from + // detaching when being removed. If a child is re-added before the + // lazy detach occurs, it will receive invalid attach/detach sequencing. + child.clearAnimation(); + + dispatchChildDetached(child); + super.removeDetachedView(child, animate); + } + + /** + * Returns a unique key to be used while handling change animations. + * It might be child's position or stable id depending on the adapter type. + */ + long getChangedHolderKey(ViewHolder holder) { + return mAdapter.hasStableIds() ? holder.getItemId() : holder.mPosition; + } + + void animateAppearance(@NonNull ViewHolder itemHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + itemHolder.setIsRecyclable(false); + if (mItemAnimator.animateAppearance(itemHolder, preLayoutInfo, postLayoutInfo)) { + postAnimationRunner(); + } + } + + void animateDisappearance(@NonNull ViewHolder holder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + addAnimatingView(holder); + holder.setIsRecyclable(false); + if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) { + postAnimationRunner(); + } + } + + private void animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo, + boolean oldHolderDisappearing, boolean newHolderDisappearing) { + oldHolder.setIsRecyclable(false); + if (oldHolderDisappearing) { + addAnimatingView(oldHolder); + } + if (oldHolder != newHolder) { + if (newHolderDisappearing) { + addAnimatingView(newHolder); + } + oldHolder.mShadowedHolder = newHolder; + // old holder should disappear after animation ends + addAnimatingView(oldHolder); + mRecycler.unscrapView(oldHolder); + newHolder.setIsRecyclable(false); + newHolder.mShadowingHolder = oldHolder; + } + if (mItemAnimator.animateChange(oldHolder, newHolder, preInfo, postInfo)) { + postAnimationRunner(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); + dispatchLayout(); + TraceCompat.endSection(); + mFirstLayoutComplete = true; + } + + @Override + public void requestLayout() { + if (mInterceptRequestLayoutDepth == 0 && !mLayoutSuppressed) { + super.requestLayout(); + } else { + mLayoutWasDefered = true; + } + } + + void markItemDecorInsetsDirty() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = mChildHelper.getUnfilteredChildAt(i); + ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; + } + mRecycler.markItemDecorInsetsDirty(); + } + + @Override + public void draw(Canvas c) { + super.draw(c); + + final int count = mItemDecorations.size(); + for (int i = 0; i < count; i++) { + mItemDecorations.get(i).onDrawOver(c, this, mState); + } + // TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we + // need find children closest to edges. Not sure if it is worth the effort. + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished()) { + final int restore = c.save(); + final int padding = mClipToPadding ? getPaddingBottom() : 0; + c.rotate(270); + c.translate(-getHeight() + padding, 0); + needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c); + c.restoreToCount(restore); + } + if (mTopGlow != null && !mTopGlow.isFinished()) { + final int restore = c.save(); + if (mClipToPadding) { + c.translate(getPaddingLeft(), getPaddingTop()); + } + needsInvalidate |= mTopGlow != null && mTopGlow.draw(c); + c.restoreToCount(restore); + } + if (mRightGlow != null && !mRightGlow.isFinished()) { + final int restore = c.save(); + final int width = getWidth(); + final int padding = mClipToPadding ? getPaddingTop() : 0; + c.rotate(90); + c.translate(padding, -width); + needsInvalidate |= mRightGlow != null && mRightGlow.draw(c); + c.restoreToCount(restore); + } + if (mBottomGlow != null && !mBottomGlow.isFinished()) { + final int restore = c.save(); + c.rotate(180); + if (mClipToPadding) { + c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom()); + } else { + c.translate(-getWidth(), -getHeight()); + } + needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c); + c.restoreToCount(restore); + } + + // If some views are animating, ItemDecorators are likely to move/change with them. + // Invalidate RecyclerView to re-draw decorators. This is still efficient because children's + // display lists are not invalidated. + if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0 + && mItemAnimator.isRunning()) { + needsInvalidate = true; + } + + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + + final int count = mItemDecorations.size(); + for (int i = 0; i < count; i++) { + mItemDecorations.get(i).onDraw(c, this, mState); + } + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager" + exceptionLabel()); + } + return mLayout.generateDefaultLayoutParams(); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager" + exceptionLabel()); + } + return mLayout.generateLayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager" + exceptionLabel()); + } + return mLayout.generateLayoutParams(p); + } + + /** + * Returns true if RecyclerView is currently running some animations. + *

+ * If you want to be notified when animations are finished, use + * {@link ItemAnimator#isRunning(ItemAnimator.ItemAnimatorFinishedListener)}. + * + * @return True if there are some item animations currently running or waiting to be started. + */ + public boolean isAnimating() { + return mItemAnimator != null && mItemAnimator.isRunning(); + } + + void saveOldPositions() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (sDebugAssertionsEnabled && holder.mPosition == -1 && !holder.isRemoved()) { + throw new IllegalStateException("view holder cannot have position -1 unless it" + + " is removed" + exceptionLabel()); + } + if (!holder.shouldIgnore()) { + holder.saveOldPosition(); + } + } + } + + void clearOldPositions() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (!holder.shouldIgnore()) { + holder.clearOldPosition(); + } + } + mRecycler.clearOldPositions(); + } + + void offsetPositionRecordsForMove(int from, int to) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int start, end, inBetweenOffset; + if (from < to) { + start = from; + end = to; + inBetweenOffset = -1; + } else { + start = to; + end = from; + inBetweenOffset = 1; + } + + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder == null || holder.mPosition < start || holder.mPosition > end) { + continue; + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForMove attached child " + i + " holder " + + holder); + } + if (holder.mPosition == from) { + holder.offsetPosition(to - from, false); + } else { + holder.offsetPosition(inBetweenOffset, false); + } + + mState.mStructureChanged = true; + } + mRecycler.offsetPositionRecordsForMove(from, to); + requestLayout(); + } + + void offsetPositionRecordsForInsert(int positionStart, int itemCount) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder " + + holder + " now at position " + (holder.mPosition + itemCount)); + } + holder.offsetPosition(itemCount, false); + mState.mStructureChanged = true; + } + } + mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount); + requestLayout(); + } + + void offsetPositionRecordsForRemove(int positionStart, int itemCount, + boolean applyToPreLayout) { + final int positionEnd = positionStart + itemCount; + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore()) { + if (holder.mPosition >= positionEnd) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i + + " holder " + holder + " now at position " + + (holder.mPosition - itemCount)); + } + holder.offsetPosition(-itemCount, applyToPreLayout); + mState.mStructureChanged = true; + } else if (holder.mPosition >= positionStart) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i + + " holder " + holder + " now REMOVED"); + } + holder.flagRemovedAndOffsetPosition(positionStart - 1, -itemCount, + applyToPreLayout); + mState.mStructureChanged = true; + } + } + } + mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout); + requestLayout(); + } + + /** + * Rebind existing views for the given range, or create as needed. + * + * @param positionStart Adapter position to start at + * @param itemCount Number of views that must explicitly be rebound + */ + void viewRangeUpdate(int positionStart, int itemCount, Object payload) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + final int positionEnd = positionStart + itemCount; + + for (int i = 0; i < childCount; i++) { + final View child = mChildHelper.getUnfilteredChildAt(i); + final ViewHolder holder = getChildViewHolderInt(child); + if (holder == null || holder.shouldIgnore()) { + continue; + } + if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) { + // We re-bind these view holders after pre-processing is complete so that + // ViewHolders have their final positions assigned. + holder.addFlags(ViewHolder.FLAG_UPDATE); + holder.addChangePayload(payload); + // lp cannot be null since we get ViewHolder from it. + ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; + } + } + mRecycler.viewRangeUpdate(positionStart, itemCount); + } + + boolean canReuseUpdatedViewHolder(ViewHolder viewHolder) { + return mItemAnimator == null || mItemAnimator.canReuseUpdatedViewHolder(viewHolder, + viewHolder.getUnmodifiedPayloads()); + } + + /** + * Processes the fact that, as far as we can tell, the data set has completely changed. + * + *

    + *
  • Once layout occurs, all attached items should be discarded or animated. + *
  • Attached items are labeled as invalid. + *
  • Because items may still be prefetched between a "data set completely changed" + * event and a layout event, all cached items are discarded. + *
+ * + * @param dispatchItemsChanged Whether to call + * {@link LayoutManager#onItemsChanged(RecyclerView)} during + * measure/layout. + */ + void processDataSetCompletelyChanged(boolean dispatchItemsChanged) { + mDispatchItemsChangedEvent |= dispatchItemsChanged; + mDataSetHasChangedAfterLayout = true; + markKnownViewsInvalid(); + } + + /** + * Mark all known views as invalid. Used in response to a, "the whole world might have changed" + * data change event. + */ + void markKnownViewsInvalid() { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.shouldIgnore()) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + } + } + markItemDecorInsetsDirty(); + mRecycler.markKnownViewsInvalid(); + } + + /** + * Invalidates all ItemDecorations. If RecyclerView has item decorations, calling this method + * will trigger a {@link #requestLayout()} call. + */ + public void invalidateItemDecorations() { + if (mItemDecorations.size() == 0) { + return; + } + if (mLayout != null) { + mLayout.assertNotInLayoutOrScroll("Cannot invalidate item decorations during a scroll" + + " or layout"); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * Returns true if the RecyclerView should attempt to preserve currently focused Adapter Item's + * focus even if the View representing the Item is replaced during a layout calculation. + *

+ * By default, this value is {@code true}. + * + * @return True if the RecyclerView will try to preserve focused Item after a layout if it loses + * focus. + * @see #setPreserveFocusAfterLayout(boolean) + */ + public boolean getPreserveFocusAfterLayout() { + return mPreserveFocusAfterLayout; + } + + /** + * Set whether the RecyclerView should try to keep the same Item focused after a layout + * calculation or not. + *

+ * Usually, LayoutManagers keep focused views visible before and after layout but sometimes, + * views may lose focus during a layout calculation as their state changes or they are replaced + * with another view due to type change or animation. In these cases, RecyclerView can request + * focus on the new view automatically. + * + * @param preserveFocusAfterLayout Whether RecyclerView should preserve focused Item during a + * layout calculations. Defaults to true. + * @see #getPreserveFocusAfterLayout() + */ + public void setPreserveFocusAfterLayout(boolean preserveFocusAfterLayout) { + mPreserveFocusAfterLayout = preserveFocusAfterLayout; + } + + /** + * Retrieve the {@link ViewHolder} for the given child view. + * + * @param child Child of this RecyclerView to query for its ViewHolder + * @return The child view's ViewHolder + */ + public ViewHolder getChildViewHolder(@NonNull View child) { + final ViewParent parent = child.getParent(); + if (parent != null && parent != this) { + throw new IllegalArgumentException("View " + child + " is not a direct child of " + + this); + } + return getChildViewHolderInt(child); + } + + /** + * Traverses the ancestors of the given view and returns the item view that contains it and + * also a direct child of the RecyclerView. This returned view can be used to get the + * ViewHolder by calling {@link #getChildViewHolder(View)}. + * + * @param view The view that is a descendant of the RecyclerView. + * @return The direct child of the RecyclerView which contains the given view or null if the + * provided view is not a descendant of this RecyclerView. + * @see #getChildViewHolder(View) + * @see #findContainingViewHolder(View) + */ + @Nullable + public View findContainingItemView(@NonNull View view) { + ViewParent parent = view.getParent(); + while (parent != null && parent != this && parent instanceof View) { + view = (View) parent; + parent = view.getParent(); + } + return parent == this ? view : null; + } + + /** + * Returns the ViewHolder that contains the given view. + * + * @param view The view that is a descendant of the RecyclerView. + * @return The ViewHolder that contains the given view or null if the provided view is not a + * descendant of this RecyclerView. + */ + @Nullable + public ViewHolder findContainingViewHolder(@NonNull View view) { + View itemView = findContainingItemView(view); + return itemView == null ? null : getChildViewHolder(itemView); + } + + + static ViewHolder getChildViewHolderInt(View child) { + if (child == null) { + return null; + } + return ((LayoutParams) child.getLayoutParams()).mViewHolder; + } + + /** + * @deprecated use {@link #getChildAdapterPosition(View)} or + * {@link #getChildLayoutPosition(View)}. + */ + @Deprecated + public int getChildPosition(@NonNull View child) { + return getChildAdapterPosition(child); + } + + /** + * Return the adapter position that the given child view corresponds to. + * + * @param child Child View to query + * @return Adapter position corresponding to the given view or {@link #NO_POSITION} + */ + public int getChildAdapterPosition(@NonNull View child) { + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getAbsoluteAdapterPosition() : NO_POSITION; + } + + /** + * Return the adapter position of the given child view as of the latest completed layout pass. + *

+ * This position may not be equal to Item's adapter position if there are pending changes + * in the adapter which have not been reflected to the layout yet. + * + * @param child Child View to query + * @return Adapter position of the given View as of last layout pass or {@link #NO_POSITION} if + * the View is representing a removed item. + */ + public int getChildLayoutPosition(@NonNull View child) { + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getLayoutPosition() : NO_POSITION; + } + + /** + * Return the stable item id that the given child view corresponds to. + * + * @param child Child View to query + * @return Item id corresponding to the given view or {@link #NO_ID} + */ + public long getChildItemId(@NonNull View child) { + if (mAdapter == null || !mAdapter.hasStableIds()) { + return NO_ID; + } + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getItemId() : NO_ID; + } + + /** + * @deprecated use {@link #findViewHolderForLayoutPosition(int)} or + * {@link #findViewHolderForAdapterPosition(int)} + */ + @Deprecated + @Nullable + public ViewHolder findViewHolderForPosition(int position) { + return findViewHolderForPosition(position, false); + } + + /** + * Return the ViewHolder for the item in the given position of the data set as of the latest + * layout pass. + *

+ * This method checks only the children of RecyclerView. If the item at the given + * position is not laid out, it will not create a new one. + *

+ * Note that when Adapter contents change, ViewHolder positions are not updated until the + * next layout calculation. If there are pending adapter updates, the return value of this + * method may not match your adapter contents. You can use + * #{@link ViewHolder#getBindingAdapterPosition()} to get the current adapter position + * of a ViewHolder. If the {@link Adapter} that is assigned to the RecyclerView is an adapter + * that combines other adapters (e.g. {@link ConcatAdapter}), you can use the + * {@link ViewHolder#getBindingAdapter()}) to find the position relative to the {@link Adapter} + * that bound the {@link ViewHolder}. + *

+ * When the ItemAnimator is running a change animation, there might be 2 ViewHolders + * with the same layout position representing the same Item. In this case, the updated + * ViewHolder will be returned. + * + * @param position The position of the item in the data set of the adapter + * @return The ViewHolder at position or null if there is no such item + */ + @Nullable + public ViewHolder findViewHolderForLayoutPosition(int position) { + return findViewHolderForPosition(position, false); + } + + /** + * Return the ViewHolder for the item in the given position of the data set. Unlike + * {@link #findViewHolderForLayoutPosition(int)} this method takes into account any pending + * adapter changes that may not be reflected to the layout yet. On the other hand, if + * {@link Adapter#notifyDataSetChanged()} has been called but the new layout has not been + * calculated yet, this method will return null since the new positions of views + * are unknown until the layout is calculated. + *

+ * This method checks only the children of RecyclerView. If the item at the given + * position is not laid out, it will not create a new one. + *

+ * When the ItemAnimator is running a change animation, there might be 2 ViewHolders + * representing the same Item. In this case, the updated ViewHolder will be returned. + * + * @param position The position of the item in the data set of the adapter + * @return The ViewHolder at position or null if there is no such item + */ + @Nullable + public ViewHolder findViewHolderForAdapterPosition(int position) { + if (mDataSetHasChangedAfterLayout) { + return null; + } + final int childCount = mChildHelper.getUnfilteredChildCount(); + // hidden VHs are not preferred but if that is the only one we find, we rather return it + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved() + && getAdapterPositionInRecyclerView(holder) == position) { + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + return hidden; + } + + @Nullable + ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved()) { + if (checkNewPosition) { + if (holder.mPosition != position) { + continue; + } + } else if (holder.getLayoutPosition() != position) { + continue; + } + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + // This method should not query cached views. It creates a problem during adapter updates + // when we are dealing with already laid out views. Also, for the public method, it is more + // reasonable to return null if position is not laid out. + return hidden; + } + + /** + * Return the ViewHolder for the item with the given id. The RecyclerView must + * use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to + * return a non-null value. + *

+ * This method checks only the children of RecyclerView. If the item with the given + * id is not laid out, it will not create a new one. + * + * When the ItemAnimator is running a change animation, there might be 2 ViewHolders with the + * same id. In this case, the updated ViewHolder will be returned. + * + * @param id The id for the requested item + * @return The ViewHolder with the given id or null if there is no such item + */ + public ViewHolder findViewHolderForItemId(long id) { + if (mAdapter == null || !mAdapter.hasStableIds()) { + return null; + } + final int childCount = mChildHelper.getUnfilteredChildCount(); + ViewHolder hidden = null; + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved() && holder.getItemId() == id) { + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + return hidden; + } + + /** + * Find the topmost view under the given point. + * + * @param x Horizontal position in pixels to search + * @param y Vertical position in pixels to search + * @return The child view under (x, y) or null if no matching child is found + */ + @Nullable + public View findChildViewUnder(float x, float y) { + final int count = mChildHelper.getChildCount(); + for (int i = count - 1; i >= 0; i--) { + final View child = mChildHelper.getChildAt(i); + final float translationX = child.getTranslationX(); + final float translationY = child.getTranslationY(); + if (x >= child.getLeft() + translationX + && x <= child.getRight() + translationX + && y >= child.getTop() + translationY + && y <= child.getBottom() + translationY) { + return child; + } + } + return null; + } + + @Override + public boolean drawChild(Canvas canvas, View child, long drawingTime) { + return super.drawChild(canvas, child, drawingTime); + } + + /** + * Offset the bounds of all child views by dy pixels. + * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}. + * + * @param dy Vertical pixel offset to apply to the bounds of all child views + */ + public void offsetChildrenVertical(@Px int dy) { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + mChildHelper.getChildAt(i).offsetTopAndBottom(dy); + } + } + + /** + * Called when an item view is attached to this RecyclerView. + * + *

Subclasses of RecyclerView may want to perform extra bookkeeping or modifications + * of child views as they become attached. This will be called before a + * {@link LayoutManager} measures or lays out the view and is a good time to perform these + * changes.

+ * + * @param child Child view that is now attached to this RecyclerView and its associated window + */ + public void onChildAttachedToWindow(@NonNull View child) { + } + + /** + * Called when an item view is detached from this RecyclerView. + * + *

Subclasses of RecyclerView may want to perform extra bookkeeping or modifications + * of child views as they become detached. This will be called as a + * {@link LayoutManager} fully detaches the child view from the parent and its window.

+ * + * @param child Child view that is now detached from this RecyclerView and its associated window + */ + public void onChildDetachedFromWindow(@NonNull View child) { + } + + /** + * Offset the bounds of all child views by dx pixels. + * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}. + * + * @param dx Horizontal pixel offset to apply to the bounds of all child views + */ + public void offsetChildrenHorizontal(@Px int dx) { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + mChildHelper.getChildAt(i).offsetLeftAndRight(dx); + } + } + + /** + * Returns the bounds of the view including its decoration and margins. + * + * @param view The view element to check + * @param outBounds A rect that will receive the bounds of the element including its + * decoration and margins. + */ + public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) { + getDecoratedBoundsWithMarginsInt(view, outBounds); + } + + static void getDecoratedBoundsWithMarginsInt(View view, Rect outBounds) { + final LayoutParams lp = (LayoutParams) view.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + outBounds.set(view.getLeft() - insets.left - lp.leftMargin, + view.getTop() - insets.top - lp.topMargin, + view.getRight() + insets.right + lp.rightMargin, + view.getBottom() + insets.bottom + lp.bottomMargin); + } + + Rect getItemDecorInsetsForChild(View child) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.mInsetsDirty) { + return lp.mDecorInsets; + } + + if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) { + // changed/invalid items should not be updated until they are rebound. + return lp.mDecorInsets; + } + final Rect insets = lp.mDecorInsets; + insets.set(0, 0, 0, 0); + final int decorCount = mItemDecorations.size(); + for (int i = 0; i < decorCount; i++) { + mTempRect.set(0, 0, 0, 0); + mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState); + insets.left += mTempRect.left; + insets.top += mTempRect.top; + insets.right += mTempRect.right; + insets.bottom += mTempRect.bottom; + } + lp.mInsetsDirty = false; + return insets; + } + + /** + * Called when the scroll position of this RecyclerView changes. Subclasses should use + * this method to respond to scrolling within the adapter's data set instead of an explicit + * listener. + * + *

This method will always be invoked before listeners. If a subclass needs to perform + * any additional upkeep or bookkeeping after scrolling but before listeners run, + * this is a good place to do so.

+ * + *

This differs from {@link View#onScrollChanged(int, int, int, int)} in that it receives + * the distance scrolled in either direction within the adapter's data set instead of absolute + * scroll coordinates. Since RecyclerView cannot compute the absolute scroll position from + * any arbitrary point in the data set, onScrollChanged will always receive + * the current {@link View#getScrollX()} and {@link View#getScrollY()} values which + * do not correspond to the data set scroll position. However, some subclasses may choose + * to use these fields as special offsets.

+ * + * @param dx horizontal distance scrolled in pixels + * @param dy vertical distance scrolled in pixels + */ + public void onScrolled(@Px int dx, @Px int dy) { + // Do nothing + } + + void dispatchOnScrolled(int hresult, int vresult) { + mDispatchScrollCounter++; + // Pass the current scrollX/scrollY values as current values. No actual change in these + // properties occurred. Pass negative hresult and vresult as old values so that + // postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt) in onScrollChanged + // sends the scrolled accessibility event correctly. + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + onScrollChanged(scrollX, scrollY, scrollX - hresult, scrollY - vresult); + + // Pass the real deltas to onScrolled, the RecyclerView-specific method. + onScrolled(hresult, vresult); + + // Invoke listeners last. Subclassed view methods always handle the event first. + // All internal state is consistent by the time listeners are invoked. + if (mScrollListener != null) { + mScrollListener.onScrolled(this, hresult, vresult); + } + if (mScrollListeners != null) { + for (int i = mScrollListeners.size() - 1; i >= 0; i--) { + mScrollListeners.get(i).onScrolled(this, hresult, vresult); + } + } + mDispatchScrollCounter--; + } + + /** + * Called when the scroll state of this RecyclerView changes. Subclasses should use this + * method to respond to state changes instead of an explicit listener. + * + *

This method will always be invoked before listeners, but after the LayoutManager + * responds to the scroll state change.

+ * + * @param state the new scroll state, one of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING} + */ + public void onScrollStateChanged(int state) { + // Do nothing + } + + /** + * Copied from OverScroller, this returns the distance that a fling with the given velocity + * will go. + * @param velocity The velocity of the fling + * @return The distance that will be traveled by a fling of the given velocity. + */ + private float getSplineFlingDistance(int velocity) { + final double l = + Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * mPhysicalCoef)); + final double decelMinusOne = DECELERATION_RATE - 1.0; + return (float) (SCROLL_FRICTION * mPhysicalCoef + * Math.exp(DECELERATION_RATE / decelMinusOne * l)); + } + + void dispatchOnScrollStateChanged(int state) { + // Let the LayoutManager go first; this allows it to bring any properties into + // a consistent state before the RecyclerView subclass responds. + if (mLayout != null) { + mLayout.onScrollStateChanged(state); + } + + // Let the RecyclerView subclass handle this event next; any LayoutManager property + // changes will be reflected by this time. + onScrollStateChanged(state); + + // Listeners go last. All other internal state is consistent by this point. + if (mScrollListener != null) { + mScrollListener.onScrollStateChanged(this, state); + } + if (mScrollListeners != null) { + for (int i = mScrollListeners.size() - 1; i >= 0; i--) { + mScrollListeners.get(i).onScrollStateChanged(this, state); + } + } + } + + /** + * Returns whether there are pending adapter updates which are not yet applied to the layout. + *

+ * If this method returns true, it means that what user is currently seeing may not + * reflect them adapter contents (depending on what has changed). + * You may use this information to defer or cancel some operations. + *

+ * This method returns true if RecyclerView has not yet calculated the first layout after it is + * attached to the Window or the Adapter has been replaced. + * + * @return True if there are some adapter updates which are not yet reflected to layout or false + * if layout is up to date. + */ + public boolean hasPendingAdapterUpdates() { + return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout + || mAdapterHelper.hasPendingUpdates(); + } + + // Effectively private. Set to default to avoid synthetic accessor. + class ViewFlinger implements Runnable { + private int mLastFlingX; + private int mLastFlingY; + OverScroller mOverScroller; + Interpolator mInterpolator = sQuinticInterpolator; + + // When set to true, postOnAnimation callbacks are delayed until the run method completes + private boolean mEatRunOnAnimationRequest = false; + + // Tracks if postAnimationCallback should be re-attached when it is done + private boolean mReSchedulePostAnimationCallback = false; + + ViewFlinger() { + mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); + } + + @Override + public void run() { + if (mLayout == null) { + stop(); + return; // no layout, cannot scroll. + } + + mReSchedulePostAnimationCallback = false; + mEatRunOnAnimationRequest = true; + + consumePendingUpdateOperations(); + + // TODO(72745539): After reviewing the code, it seems to me we may actually want to + // update the reference to the OverScroller after onAnimation. It looks to me like + // it is possible that a new OverScroller could be created (due to a new Interpolator + // being used), when the current OverScroller knows it's done after + // scroller.computeScrollOffset() is called. If that happens, and we don't update the + // reference, it seems to me that we could prematurely stop the newly created scroller + // due to setScrollState(SCROLL_STATE_IDLE) being called below. + + // Keep a local reference so that if it is changed during onAnimation method, it won't + // cause unexpected behaviors + final OverScroller scroller = mOverScroller; + if (scroller.computeScrollOffset()) { + final int x = scroller.getCurrX(); + final int y = scroller.getCurrY(); + int unconsumedX = x - mLastFlingX; + int unconsumedY = y - mLastFlingY; + mLastFlingX = x; + mLastFlingY = y; + + unconsumedX = consumeFlingInHorizontalStretch(unconsumedX); + unconsumedY = consumeFlingInVerticalStretch(unconsumedY); + + int consumedX = 0; + int consumedY = 0; + + // Nested Pre Scroll + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null, + TYPE_NON_TOUCH)) { + unconsumedX -= mReusableIntPair[0]; + unconsumedY -= mReusableIntPair[1]; + } + + // Based on movement, we may want to trigger the hiding of existing over scroll + // glows. + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + considerReleasingGlowsOnScroll(unconsumedX, unconsumedY); + } + + // Local Scroll + if (mAdapter != null) { + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + scrollStep(unconsumedX, unconsumedY, mReusableIntPair); + consumedX = mReusableIntPair[0]; + consumedY = mReusableIntPair[1]; + unconsumedX -= consumedX; + unconsumedY -= consumedY; + + // If SmoothScroller exists, this ViewFlinger was started by it, so we must + // report back to SmoothScroller. + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + if (smoothScroller != null && !smoothScroller.isPendingInitialRun() + && smoothScroller.isRunning()) { + final int adapterSize = mState.getItemCount(); + if (adapterSize == 0) { + smoothScroller.stop(); + } else if (smoothScroller.getTargetPosition() >= adapterSize) { + smoothScroller.setTargetPosition(adapterSize - 1); + smoothScroller.onAnimation(consumedX, consumedY); + } else { + smoothScroller.onAnimation(consumedX, consumedY); + } + } + } + + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + + // Nested Post Scroll + mReusableIntPair[0] = 0; + mReusableIntPair[1] = 0; + dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null, + TYPE_NON_TOUCH, mReusableIntPair); + unconsumedX -= mReusableIntPair[0]; + unconsumedY -= mReusableIntPair[1]; + + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); + } + + if (!awakenScrollBars()) { + invalidate(); + } + + // We are done scrolling if scroller is finished, or for both the x and y dimension, + // we are done scrolling or we can't scroll further (we know we can't scroll further + // when we have unconsumed scroll distance). It's possible that we don't need + // to also check for scroller.isFinished() at all, but no harm in doing so in case + // of old bugs in Overscroller. + boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX(); + boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY(); + final boolean doneScrolling = scroller.isFinished() + || ((scrollerFinishedX || unconsumedX != 0) + && (scrollerFinishedY || unconsumedY != 0)); + + // Get the current smoothScroller. It may have changed by this point and we need to + // make sure we don't stop scrolling if it has changed and it's pending an initial + // run. + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + boolean smoothScrollerPending = + smoothScroller != null && smoothScroller.isPendingInitialRun(); + + if (!smoothScrollerPending && doneScrolling) { + // If we are done scrolling and the layout's SmoothScroller is not pending, + // do the things we do at the end of a scroll and don't postOnAnimation. + + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + final int vel = (int) scroller.getCurrVelocity(); + int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0; + int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0; + absorbGlows(velX, velY); + } + + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } else { + // Otherwise continue the scroll. + + postOnAnimation(); + if (mGapWorker != null) { + mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY); + } + } + } + + SmoothScroller smoothScroller = mLayout.mSmoothScroller; + // call this after the onAnimation is complete not to have inconsistent callbacks etc. + if (smoothScroller != null && smoothScroller.isPendingInitialRun()) { + smoothScroller.onAnimation(0, 0); + } + + mEatRunOnAnimationRequest = false; + if (mReSchedulePostAnimationCallback) { + internalPostOnAnimation(); + } else { + setScrollState(SCROLL_STATE_IDLE); + stopNestedScroll(TYPE_NON_TOUCH); + } + } + + void postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true; + } else { + internalPostOnAnimation(); + } + } + + private void internalPostOnAnimation() { + removeCallbacks(this); + ViewCompat.postOnAnimation(RecyclerView.this, this); + } + + public void fling(int velocityX, int velocityY) { + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + // Because you can't define a custom interpolator for flinging, we should make sure we + // reset ourselves back to the teh default interpolator in case a different call + // changed our interpolator. + if (mInterpolator != sQuinticInterpolator) { + mInterpolator = sQuinticInterpolator; + mOverScroller = new OverScroller(getContext(), sQuinticInterpolator); + } + mOverScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + postOnAnimation(); + } + + /** + * Smooth scrolls the RecyclerView by a given distance. + * + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @param duration Duration of the animation in milliseconds. Set to + * {@link #UNDEFINED_DURATION} to have the duration automatically + * calculated + * based on an internally defined standard velocity. + * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null}, + * RecyclerView will use an internal default interpolator. + */ + public void smoothScrollBy(int dx, int dy, int duration, + @Nullable Interpolator interpolator) { + + // Handle cases where parameter values aren't defined. + if (duration == UNDEFINED_DURATION) { + duration = computeScrollDuration(dx, dy); + } + if (interpolator == null) { + interpolator = sQuinticInterpolator; + } + + // If the Interpolator has changed, create a new OverScroller with the new + // interpolator. + if (mInterpolator != interpolator) { + mInterpolator = interpolator; + mOverScroller = new OverScroller(getContext(), interpolator); + } + + // Reset the last fling information. + mLastFlingX = mLastFlingY = 0; + + // Set to settling state and start scrolling. + setScrollState(SCROLL_STATE_SETTLING); + mOverScroller.startScroll(0, 0, dx, dy, duration); + + if (Build.VERSION.SDK_INT < 23) { + // b/64931938 before API 23, startScroll() does not reset getCurX()/getCurY() + // to start values, which causes fillRemainingScrollValues() put in obsolete values + // for LayoutManager.onLayoutChildren(). + mOverScroller.computeScrollOffset(); + } + + postOnAnimation(); + } + + /** + * Computes of an animated scroll in milliseconds. + * @param dx x distance in pixels. + * @param dy y distance in pixels. + * @return The duration of the animated scroll in milliseconds. + */ + private int computeScrollDuration(int dx, int dy) { + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final boolean horizontal = absDx > absDy; + final int containerSize = horizontal ? getWidth() : getHeight(); + + float absDelta = (float) (horizontal ? absDx : absDy); + final int duration = (int) (((absDelta / containerSize) + 1) * 300); + + return Math.min(duration, MAX_SCROLL_DURATION); + } + + public void stop() { + removeCallbacks(this); + mOverScroller.abortAnimation(); + } + + } + + void repositionShadowingViews() { + // Fix up shadow views used by change animations + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; i++) { + View view = mChildHelper.getChildAt(i); + ViewHolder holder = getChildViewHolder(view); + if (holder != null && holder.mShadowingHolder != null) { + View shadowingView = holder.mShadowingHolder.itemView; + int left = view.getLeft(); + int top = view.getTop(); + if (left != shadowingView.getLeft() || top != shadowingView.getTop()) { + shadowingView.layout(left, top, + left + shadowingView.getWidth(), + top + shadowingView.getHeight()); + } + } + } + } + + private class RecyclerViewDataObserver extends AdapterDataObserver { + RecyclerViewDataObserver() { + } + + @Override + public void onChanged() { + assertNotInLayoutOrScroll(null); + mState.mStructureChanged = true; + + processDataSetCompletelyChanged(true); + if (!mAdapterHelper.hasPendingUpdates()) { + requestLayout(); + } + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) { + triggerUpdateProcessor(); + } + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) { + triggerUpdateProcessor(); + } + } + + void triggerUpdateProcessor() { + if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) { + ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable); + } else { + mAdapterUpdateDuringMeasure = true; + requestLayout(); + } + } + + @Override + public void onStateRestorationPolicyChanged() { + if (mPendingSavedState == null) { + return; + } + // If there is a pending saved state and the new mode requires us to restore it, + // we'll request a layout which will call the adapter to see if it can restore state + // and trigger state restoration + Adapter adapter = mAdapter; + if (adapter != null && adapter.canRestoreState()) { + requestLayout(); + } + } + } + + /** + * EdgeEffectFactory lets you customize the over-scroll edge effect for RecyclerViews. + * + * @see RecyclerView#setEdgeEffectFactory(EdgeEffectFactory) + */ + public static class EdgeEffectFactory { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM}) + public @interface EdgeDirection { + } + + /** + * Direction constant for the left edge + */ + public static final int DIRECTION_LEFT = 0; + + /** + * Direction constant for the top edge + */ + public static final int DIRECTION_TOP = 1; + + /** + * Direction constant for the right edge + */ + public static final int DIRECTION_RIGHT = 2; + + /** + * Direction constant for the bottom edge + */ + public static final int DIRECTION_BOTTOM = 3; + + /** + * Create a new EdgeEffect for the provided direction. + */ + protected @NonNull + EdgeEffect createEdgeEffect(@NonNull RecyclerView view, + @EdgeDirection int direction) { + return new EdgeEffect(view.getContext()); + } + } + + /** + * The default EdgeEffectFactory sets the edge effect type of the EdgeEffect. + */ + static class StretchEdgeEffectFactory extends EdgeEffectFactory { + @NonNull + @Override + protected EdgeEffect createEdgeEffect(@NonNull RecyclerView view, int direction) { + return new EdgeEffect(view.getContext()); + } + } + + /** + * RecycledViewPool lets you share Views between multiple RecyclerViews. + *

+ * If you want to recycle views across RecyclerViews, create an instance of RecycledViewPool + * and use {@link RecyclerView#setRecycledViewPool(RecycledViewPool)}. + *

+ * RecyclerView automatically creates a pool for itself if you don't provide one. + */ + public static class RecycledViewPool { + private static final int DEFAULT_MAX_SCRAP = 5; + + /** + * Tracks both pooled holders, as well as create/bind timing metadata for the given type. + * + * Note that this tracks running averages of create/bind time across all RecyclerViews + * (and, indirectly, Adapters) that use this pool. + * + * 1) This enables us to track average create and bind times across multiple adapters. Even + * though create (and especially bind) may behave differently for different Adapter + * subclasses, sharing the pool is a strong signal that they'll perform similarly, per type. + * + * 2) If {@link #willBindInTime(int, long, long)} returns false for one view, it will return + * false for all other views of its type for the same deadline. This prevents items + * constructed by {@link GapWorker} prefetch from being bound to a lower priority prefetch. + */ + static class ScrapData { + final ArrayList mScrapHeap = new ArrayList<>(); + int mMaxScrap = DEFAULT_MAX_SCRAP; + long mCreateRunningAverageNs = 0; + long mBindRunningAverageNs = 0; + } + + SparseArray mScrap = new SparseArray<>(); + + /** + * Attach counts for clearing (that is, emptying the pool when there are no adapters + * attached) and for PoolingContainer release are tracked separately to maintain the + * historical behavior of this functionality. + * + * The count for clearing is inaccurate in certain scenarios: for instance, if a + * RecyclerView is removed from the view hierarchy and thrown away to be GCed, the + * attach count will never be correspondingly decreased. However, it has been this way + * for years without any complaints, so we are not going to potentially increase the + * number of scenarios where the pool would be cleared. + * + * The attached adapters for PoolingContainer purposes strives to be more accurate, as + * it will be decremented whenever a RecyclerView is detached from the window. This + * could potentially be inaccurate in the unlikely event that someone is manually driving + * a detached RecyclerView by calling measure, layout, draw, etc. However, the + * implementation of {@link RecyclerView#onDetachedFromWindow()} suggests this is not the + * only unexpected behavior that doing so might provoke, so this should be acceptable. + */ + int mAttachCountForClearing = 0; + + /** + * The set of adapters for PoolingContainer release purposes + * + * @see #mAttachCountForClearing + */ + Set> mAttachedAdaptersForPoolingContainer = + Collections.newSetFromMap(new IdentityHashMap<>()); + + /** + * Discard all ViewHolders. + */ + public void clear() { + for (int i = 0; i < mScrap.size(); i++) { + ScrapData data = mScrap.valueAt(i); + for (ViewHolder scrap: data.mScrapHeap) { + PoolingContainer.callPoolingContainerOnRelease(scrap.itemView); + } + data.mScrapHeap.clear(); + } + } + + /** + * Sets the maximum number of ViewHolders to hold in the pool before discarding. + * + * @param viewType ViewHolder Type + * @param max Maximum number + */ + public void setMaxRecycledViews(int viewType, int max) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mMaxScrap = max; + final ArrayList scrapHeap = scrapData.mScrapHeap; + while (scrapHeap.size() > max) { + scrapHeap.remove(scrapHeap.size() - 1); + } + } + + /** + * Returns the current number of Views held by the RecycledViewPool of the given view type. + */ + public int getRecycledViewCount(int viewType) { + return getScrapDataForType(viewType).mScrapHeap.size(); + } + + /** + * Acquire a ViewHolder of the specified type from the pool, or {@code null} if none are + * present. + * + * @param viewType ViewHolder type. + * @return ViewHolder of the specified type acquired from the pool, or {@code null} if none + * are present. + */ + @Nullable + public ViewHolder getRecycledView(int viewType) { + final ScrapData scrapData = mScrap.get(viewType); + if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { + final ArrayList scrapHeap = scrapData.mScrapHeap; + for (int i = scrapHeap.size() - 1; i >= 0; i--) { + if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) { + return scrapHeap.remove(i); + } + } + } + return null; + } + + /** + * Total number of ViewHolders held by the pool. + * + * @return Number of ViewHolders held by the pool. + */ + int size() { + int count = 0; + for (int i = 0; i < mScrap.size(); i++) { + ArrayList viewHolders = mScrap.valueAt(i).mScrapHeap; + if (viewHolders != null) { + count += viewHolders.size(); + } + } + return count; + } + + /** + * Add a scrap ViewHolder to the pool. + *

+ * If the pool is already full for that ViewHolder's type, it will be immediately discarded. + * + * @param scrap ViewHolder to be added to the pool. + */ + public void putRecycledView(ViewHolder scrap) { + final int viewType = scrap.getItemViewType(); + final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; + if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { + PoolingContainer.callPoolingContainerOnRelease(scrap.itemView); + return; + } + if (sDebugAssertionsEnabled && scrapHeap.contains(scrap)) { + throw new IllegalArgumentException("this scrap item already exists"); + } + scrap.resetInternal(); + scrapHeap.add(scrap); + } + + long runningAverage(long oldAverage, long newValue) { + if (oldAverage == 0) { + return newValue; + } + return (oldAverage / 4 * 3) + (newValue / 4); + } + + void factorInCreateTime(int viewType, long createTimeNs) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mCreateRunningAverageNs = runningAverage( + scrapData.mCreateRunningAverageNs, createTimeNs); + } + + void factorInBindTime(int viewType, long bindTimeNs) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mBindRunningAverageNs = runningAverage( + scrapData.mBindRunningAverageNs, bindTimeNs); + } + + boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { + long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; + return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); + } + + boolean willBindInTime(int viewType, long approxCurrentNs, long deadlineNs) { + long expectedDurationNs = getScrapDataForType(viewType).mBindRunningAverageNs; + return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); + } + + void attach() { + mAttachCountForClearing++; + } + + void detach() { + mAttachCountForClearing--; + } + + /** + * Adds this adapter to the set of adapters being tracked for PoolingContainer release + * purposes. This method may validly be called multiple times for a given adapter. + * Additional calls to this method for an already-attached adapter are a no-op. + * + * @param adapter the adapter to ensure is in the set + */ + void attachForPoolingContainer(@NonNull Adapter adapter) { + mAttachedAdaptersForPoolingContainer.add(adapter); + } + + /** + * Removes this adapter from the set of adapters being tracked for PoolingContainer + * release purposes. This method may validly be called multiple times for a given adapter. + + Additional calls to this method for an already-detached adapter are a no-op. + * + * @param adapter the adapter to be removed from the set + * @param isBeingReplaced {@code true} if this detach is immediately preceding a call to + * {@link #attachForPoolingContainer(Adapter)} and + * {@link PoolingContainerListener#onRelease()} should not be triggered, or false otherwise + */ + void detachForPoolingContainer(@NonNull Adapter adapter, boolean isBeingReplaced) { + mAttachedAdaptersForPoolingContainer.remove(adapter); + if (mAttachedAdaptersForPoolingContainer.size() == 0 && !isBeingReplaced) { + for (int keyIndex = 0; keyIndex < mScrap.size(); keyIndex++) { + ArrayList scrapHeap = mScrap.get(mScrap.keyAt(keyIndex)).mScrapHeap; + for (int i = 0; i < scrapHeap.size(); i++) { + PoolingContainer.callPoolingContainerOnRelease( + scrapHeap.get(i).itemView + ); + } + } + } + } + + /** + * Detaches the old adapter and attaches the new one. + *

+ * RecycledViewPool will clear its cache if it has only one adapter attached and the new + * adapter uses a different ViewHolder than the oldAdapter. + * + * @param oldAdapter The previous adapter instance. Will be detached. + * @param newAdapter The new adapter instance. Will be attached. + * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same + * ViewHolder and view types. + */ + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + if (oldAdapter != null) { + detach(); + } + if (!compatibleWithPrevious && mAttachCountForClearing == 0) { + clear(); + } + if (newAdapter != null) { + attach(); + } + } + + private ScrapData getScrapDataForType(int viewType) { + ScrapData scrapData = mScrap.get(viewType); + if (scrapData == null) { + scrapData = new ScrapData(); + mScrap.put(viewType, scrapData); + } + return scrapData; + } + } + + /** + * Utility method for finding an internal RecyclerView, if present + */ + @Nullable + static RecyclerView findNestedRecyclerView(@NonNull View view) { + if (!(view instanceof ViewGroup)) { + return null; + } + if (view instanceof RecyclerView) { + return (RecyclerView) view; + } + final ViewGroup parent = (ViewGroup) view; + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + final RecyclerView descendant = findNestedRecyclerView(child); + if (descendant != null) { + return descendant; + } + } + return null; + } + + /** + * Utility method for clearing holder's internal RecyclerView, if present + */ + static void clearNestedRecyclerViewIfNotNested(@NonNull ViewHolder holder) { + if (holder.mNestedRecyclerView != null) { + View item = holder.mNestedRecyclerView.get(); + while (item != null) { + if (item == holder.itemView) { + return; // match found, don't need to clear + } + + ViewParent parent = item.getParent(); + if (parent instanceof View) { + item = (View) parent; + } else { + item = null; + } + } + holder.mNestedRecyclerView = null; // not nested + } + } + + /** + * Time base for deadline-aware work scheduling. Overridable for testing. + * + * Will return 0 to avoid cost of System.nanoTime where deadline-aware work scheduling + * isn't relevant. + */ + long getNanoTime() { + if (ALLOW_THREAD_GAP_WORK) { + return System.nanoTime(); + } else { + return 0; + } + } + + /** + * A Recycler is responsible for managing scrapped or detached item views for reuse. + * + *

A "scrapped" view is a view that is still attached to its parent RecyclerView but + * that has been marked for removal or reuse.

+ * + *

Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for + * an adapter's data set representing the data at a given position or item ID. + * If the view to be reused is considered "dirty" the adapter will be asked to rebind it. + * If not, the view can be quickly reused by the LayoutManager with no further work. + * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout} + * may be repositioned by a LayoutManager without remeasurement.

+ */ + public final class Recycler { + final ArrayList mAttachedScrap = new ArrayList<>(); + ArrayList mChangedScrap = null; + + final ArrayList mCachedViews = new ArrayList(); + + private final List + mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); + + private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; + int mViewCacheMax = DEFAULT_CACHE_SIZE; + + RecycledViewPool mRecyclerPool; + + private ViewCacheExtension mViewCacheExtension; + + static final int DEFAULT_CACHE_SIZE = 2; + + /** + * Clear scrap views out of this recycler. Detached views contained within a + * recycled view pool will remain. + */ + public void clear() { + mAttachedScrap.clear(); + recycleAndClearCachedViews(); + } + + /** + * Set the maximum number of detached, valid views we should retain for later use. + * + * @param viewCount Number of views to keep before sending views to the shared pool + */ + public void setViewCacheSize(int viewCount) { + mRequestedCacheMax = viewCount; + updateViewCacheSize(); + } + + void updateViewCacheSize() { + int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0; + mViewCacheMax = mRequestedCacheMax + extraCache; + + // first, try the views that can be recycled + for (int i = mCachedViews.size() - 1; + i >= 0 && mCachedViews.size() > mViewCacheMax; i--) { + recycleCachedViewAt(i); + } + } + + /** + * Returns an unmodifiable list of ViewHolders that are currently in the scrap list. + * + * @return List of ViewHolders in the scrap list. + */ + @NonNull + public List getScrapList() { + return mUnmodifiableAttachedScrap; + } + + /** + * Helper method for getViewForPosition. + *

+ * Checks whether a given view holder can be used for the provided position. + * + * @param holder ViewHolder + * @return true if ViewHolder matches the provided position, false otherwise + */ + boolean validateViewHolderForOffsetPosition(ViewHolder holder) { + // if it is a removed holder, nothing to verify since we cannot ask adapter anymore + // if it is not removed, verify the type and id. + if (holder.isRemoved()) { + if (sDebugAssertionsEnabled && !mState.isPreLayout()) { + throw new IllegalStateException("should not receive a removed view unless it" + + " is pre layout" + exceptionLabel()); + } + return mState.isPreLayout(); + } + if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + + "adapter position" + holder + exceptionLabel()); + } + if (!mState.isPreLayout()) { + // don't check type if it is pre-layout. + final int type = mAdapter.getItemViewType(holder.mPosition); + if (type != holder.getItemViewType()) { + return false; + } + } + if (mAdapter.hasStableIds()) { + return holder.getItemId() == mAdapter.getItemId(holder.mPosition); + } + return true; + } + + /** + * Attempts to bind view, and account for relevant timing information. If + * deadlineNs != FOREVER_NS, this method may fail to bind, and return false. + * + * @param holder Holder to be bound. + * @param offsetPosition Position of item to be bound. + * @param position Pre-layout position of item to be bound. + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * bind the holder. + */ + @SuppressWarnings("unchecked") + private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, + int position, long deadlineNs) { + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = RecyclerView.this; + final int viewType = holder.getItemViewType(); + long startBindNs = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) { + // abort - we have a deadline we can't meet + return false; + } + + // Holders being bound should be either fully attached or fully detached. + // We don't want to bind with views that are temporarily detached, because that + // creates a situation in which they are unable to reason about their attach state + // properly. + // For example, isAttachedToWindow will return true, but the itemView will lack a + // parent. This breaks, among other possible issues, anything involving traversing + // the view tree, such as ViewTreeLifecycleOwner. + // Thus, we temporarily reattach any temp-detached holders for the bind operation. + // See https://issuetracker.google.com/265347515 for additional details on problems + // resulting from this + boolean reattachedForBind = false; + if (holder.isTmpDetached()) { + attachViewToParent(holder.itemView, getChildCount(), + holder.itemView.getLayoutParams()); + reattachedForBind = true; + } + + mAdapter.bindViewHolder(holder, offsetPosition); + + if (reattachedForBind) { + detachViewFromParent(holder.itemView); + } + + long endBindNs = getNanoTime(); + mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs); + attachAccessibilityDelegateOnBind(holder); + if (mState.isPreLayout()) { + holder.mPreLayoutPosition = position; + } + return true; + } + + /** + * Binds the given View to the position. The View can be a View previously retrieved via + * {@link #getViewForPosition(int)} or created by + * {@link Adapter#onCreateViewHolder(ViewGroup, int)}. + *

+ * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)} + * and let the RecyclerView handle caching. This is a helper method for LayoutManager who + * wants to handle its own recycling logic. + *

+ * Note that, {@link #getViewForPosition(int)} already binds the View to the position so + * you don't need to call this method unless you want to bind this View to another position. + * + * @param view The view to update. + * @param position The position of the item to bind to this View. + */ + public void bindViewToPosition(@NonNull View view, int position) { + ViewHolder holder = getChildViewHolderInt(view); + if (holder == null) { + throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot" + + " pass arbitrary views to this method, they should be created by the " + + "Adapter" + exceptionLabel()); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount() + exceptionLabel()); + } + tryBindViewHolderByDeadline(holder, offsetPosition, position, FOREVER_NS); + + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; + } + + rvLayoutParams.mInsetsDirty = true; + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = holder.itemView.getParent() == null; + } + + /** + * RecyclerView provides artificial position range (item count) in pre-layout state and + * automatically maps these positions to {@link Adapter} positions when + * {@link #getViewForPosition(int)} or {@link #bindViewToPosition(View, int)} is called. + *

+ * Usually, LayoutManager does not need to worry about this. However, in some cases, your + * LayoutManager may need to call some custom component with item positions in which + * case you need the actual adapter position instead of the pre layout position. You + * can use this method to convert a pre-layout position to adapter (post layout) position. + *

+ * Note that if the provided position belongs to a deleted ViewHolder, this method will + * return -1. + *

+ * Calling this method in post-layout state returns the same value back. + * + * @param position The pre-layout position to convert. Must be greater or equal to 0 and + * less than {@link State#getItemCount()}. + */ + public int convertPreLayoutPositionToPostLayout(int position) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("invalid position " + position + ". State " + + "item count is " + mState.getItemCount() + exceptionLabel()); + } + if (!mState.isPreLayout()) { + return position; + } + return mAdapterHelper.findPositionOffset(position); + } + + /** + * Obtain a view initialized for the given position. + * + * This method should be used by {@link LayoutManager} implementations to obtain + * views to represent data from an {@link Adapter}. + *

+ * The Recycler may reuse a scrap or detached view from a shared pool if one is + * available for the correct view type. If the adapter has not indicated that the + * data at the given position has changed, the Recycler will attempt to hand back + * a scrap view that was previously initialized for that data without rebinding. + * + * @param position Position to obtain a view for + * @return A view representing the data at position from adapter + */ + @NonNull + public View getViewForPosition(int position) { + return getViewForPosition(position, false); + } + + View getViewForPosition(int position, boolean dryRun) { + return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; + } + + /** + * Attempts to get the ViewHolder for the given position, either from the Recycler scrap, + * cache, the RecycledViewPool, or creating it directly. + *

+ * If a deadlineNs other than {@link #FOREVER_NS} is passed, this method early return + * rather than constructing or binding a ViewHolder if it doesn't think it has time. + * If a ViewHolder must be constructed and not enough time remains, null is returned. If a + * ViewHolder is aquired and must be bound but not enough time remains, an unbound holder is + * returned. Use {@link ViewHolder#isBound()} on the returned object to check for this. + * + * @param position Position of ViewHolder to be returned. + * @param dryRun True if the ViewHolder should not be removed from scrap/cache/ + * @param deadlineNs Time, relative to getNanoTime(), by which bind/create work should + * complete. If FOREVER_NS is passed, this method will not fail to + * create/bind the holder if needed. + * @return ViewHolder for requested position + */ + @Nullable + ViewHolder tryGetViewHolderForPositionByDeadline(int position, + boolean dryRun, long deadlineNs) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("Invalid item position " + position + + "(" + position + "). Item count:" + mState.getItemCount() + + exceptionLabel()); + } + boolean fromScrapOrHiddenOrCache = false; + ViewHolder holder = null; + // 0) If there is a changed scrap, try to find from there + if (mState.isPreLayout()) { + holder = getChangedScrapViewForPosition(position); + fromScrapOrHiddenOrCache = holder != null; + } + // 1) Find by position from scrap/hidden list/cache + if (holder == null) { + holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); + if (holder != null) { + if (!validateViewHolderForOffsetPosition(holder)) { + // recycle holder (and unscrap if relevant) since it can't be used + if (!dryRun) { + // we would like to recycle this but need to make sure it is not used by + // animation logic etc. + holder.addFlags(ViewHolder.FLAG_INVALID); + if (holder.isScrap()) { + removeDetachedView(holder.itemView, false); + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + } + holder = null; + } else { + fromScrapOrHiddenOrCache = true; + } + } + } + if (holder == null) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item " + + "position " + position + "(offset:" + offsetPosition + ")." + + "state:" + mState.getItemCount() + exceptionLabel()); + } + + final int type = mAdapter.getItemViewType(offsetPosition); + // 2) Find from scrap/cache via stable ids, if exists + if (mAdapter.hasStableIds()) { + holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), + type, dryRun); + if (holder != null) { + // update position + holder.mPosition = offsetPosition; + fromScrapOrHiddenOrCache = true; + } + } + if (holder == null && mViewCacheExtension != null) { + // We are NOT sending the offsetPosition because LayoutManager does not + // know it. + final View view = mViewCacheExtension + .getViewForPositionAndType(this, position, type); + if (view != null) { + holder = getChildViewHolder(view); + if (holder == null) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view which does not have a ViewHolder" + + exceptionLabel()); + } else if (holder.shouldIgnore()) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view that is ignored. You must call stopIgnoring before" + + " returning this view." + exceptionLabel()); + } + } + } + if (holder == null) { // fallback to pool + if (sVerboseLoggingEnabled) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + + position + ") fetching from shared pool"); + } + holder = getRecycledViewPool().getRecycledView(type); + if (holder != null) { + holder.resetInternal(); + if (FORCE_INVALIDATE_DISPLAY_LIST) { + invalidateDisplayListInt(holder); + } + } + } + if (holder == null) { + long start = getNanoTime(); + if (deadlineNs != FOREVER_NS + && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { + // abort - we have a deadline we can't meet + return null; + } + holder = mAdapter.createViewHolder(RecyclerView.this, type); + if (ALLOW_THREAD_GAP_WORK) { + // only bother finding nested RV if prefetching + RecyclerView innerView = findNestedRecyclerView(holder.itemView); + if (innerView != null) { + holder.mNestedRecyclerView = new WeakReference<>(innerView); + } + } + + long end = getNanoTime(); + mRecyclerPool.factorInCreateTime(type, end - start); + if (sVerboseLoggingEnabled) { + Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); + } + } + } + + // This is very ugly but the only place we can grab this information + // before the View is rebound and returned to the LayoutManager for post layout ops. + // We don't need this in pre-layout since the VH is not updated by the LM. + if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder + .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { + holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + if (mState.mRunSimpleAnimations) { + int changeFlags = ItemAnimator + .buildAdapterChangeFlagsForAnimations(holder); + changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; + final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, + holder, changeFlags, holder.getUnmodifiedPayloads()); + recordAnimationInfoIfBouncedHiddenView(holder, info); + } + } + + boolean bound = false; + if (mState.isPreLayout() && holder.isBound()) { + // do not update unless we absolutely have to. + holder.mPreLayoutPosition = position; + } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { + if (sDebugAssertionsEnabled && holder.isRemoved()) { + throw new IllegalStateException("Removed holder should be bound and it should" + + " come here only in pre-layout. Holder: " + holder + + exceptionLabel()); + } + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); + } + + final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); + final LayoutParams rvLayoutParams; + if (lp == null) { + rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); + holder.itemView.setLayoutParams(rvLayoutParams); + } else if (!checkLayoutParams(lp)) { + rvLayoutParams = (LayoutParams) generateLayoutParams(lp); + holder.itemView.setLayoutParams(rvLayoutParams); + } else { + rvLayoutParams = (LayoutParams) lp; + } + rvLayoutParams.mViewHolder = holder; + rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; + return holder; + } + + private void attachAccessibilityDelegateOnBind(ViewHolder holder) { + if (isAccessibilityEnabled()) { + final View itemView = holder.itemView; + if (ViewCompat.getImportantForAccessibility(itemView) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(itemView, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (mAccessibilityDelegate == null) { + return; + } + AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); + if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { + // If there was already an a11y delegate set on the itemView, store it in the + // itemDelegate and then set the itemDelegate as the a11y delegate. + ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) + .saveOriginalDelegate(itemView); + } + ViewCompat.setAccessibilityDelegate(itemView, itemDelegate); + } + } + + private void invalidateDisplayListInt(ViewHolder holder) { + if (holder.itemView instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) holder.itemView, false); + } + } + + private void invalidateDisplayListInt(ViewGroup viewGroup, boolean invalidateThis) { + for (int i = viewGroup.getChildCount() - 1; i >= 0; i--) { + final View view = viewGroup.getChildAt(i); + if (view instanceof ViewGroup) { + invalidateDisplayListInt((ViewGroup) view, true); + } + } + if (!invalidateThis) { + return; + } + // we need to force it to become invisible + if (viewGroup.getVisibility() == View.INVISIBLE) { + viewGroup.setVisibility(View.VISIBLE); + viewGroup.setVisibility(View.INVISIBLE); + } else { + final int visibility = viewGroup.getVisibility(); + viewGroup.setVisibility(View.INVISIBLE); + viewGroup.setVisibility(visibility); + } + } + + /** + * Recycle a detached view. The specified view will be added to a pool of views + * for later rebinding and reuse. + * + *

A view must be fully detached (removed from parent) before it may be recycled. If the + * View is scrapped, it will be removed from scrap list.

+ * + * @param view Removed view for recycling + * @see LayoutManager#removeAndRecycleView(View, Recycler) + */ + public void recycleView(@NonNull View view) { + // This public recycle method tries to make view recycle-able since layout manager + // intended to recycle this view (e.g. even if it is in scrap or change cache) + ViewHolder holder = getChildViewHolderInt(view); + if (holder.isTmpDetached()) { + removeDetachedView(view, false); + } + if (holder.isScrap()) { + holder.unScrap(); + } else if (holder.wasReturnedFromScrap()) { + holder.clearReturnedFromScrapFlag(); + } + recycleViewHolderInternal(holder); + // In most cases we dont need call endAnimation() because when view is detached, + // ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or + // if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not + // started yet, the ItemAnimatior on the view may not be cleared. + // In b/73552923, the View is removed by scroll pass while it's waiting in + // the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in + // a post runnable, incorrectly performs postDelayed() on the detached view. + // To fix the issue, we issue endAnimation() here to make sure animation of this view + // finishes. + // + // Note the order: we must call endAnimation() after recycleViewHolderInternal() + // to avoid recycle twice. If ViewHolder isRecyclable is false, + // recycleViewHolderInternal() will not recycle it, endAnimation() will reset + // isRecyclable flag and recycle the view. + if (mItemAnimator != null && !holder.isRecyclable()) { + mItemAnimator.endAnimation(holder); + } + } + + void recycleAndClearCachedViews() { + final int count = mCachedViews.size(); + for (int i = count - 1; i >= 0; i--) { + recycleCachedViewAt(i); + } + mCachedViews.clear(); + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } + + /** + * Recycles a cached view and removes the view from the list. Views are added to cache + * if and only if they are recyclable, so this method does not check it again. + *

+ * A small exception to this rule is when the view does not have an animator reference + * but transient state is true (due to animations created outside ItemAnimator). In that + * case, adapter may choose to recycle it. From RecyclerView's perspective, the view is + * still recyclable since Adapter wants to do so. + * + * @param cachedViewIndex The index of the view in cached views list + */ + void recycleCachedViewAt(int cachedViewIndex) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); + } + ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); + if (sVerboseLoggingEnabled) { + Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder); + } + addViewHolderToRecycledViewPool(viewHolder, true); + mCachedViews.remove(cachedViewIndex); + } + + /** + * internal implementation checks if view is scrapped or attached and throws an exception + * if so. + * Public version un-scraps before calling recycle. + */ + void recycleViewHolderInternal(ViewHolder holder) { + if (holder.isScrap() || holder.itemView.getParent() != null) { + throw new IllegalArgumentException( + "Scrapped or attached views may not be recycled. isScrap:" + + holder.isScrap() + " isAttached:" + + (holder.itemView.getParent() != null) + exceptionLabel()); + } + + if (holder.isTmpDetached()) { + throw new IllegalArgumentException("Tmp detached view should be removed " + + "from RecyclerView before it can be recycled: " + holder + + exceptionLabel()); + } + + if (holder.shouldIgnore()) { + throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" + + " should first call stopIgnoringView(view) before calling recycle." + + exceptionLabel()); + } + final boolean transientStatePreventsRecycling = holder + .doesTransientStatePreventRecycling(); + @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null + && transientStatePreventsRecycling + && mAdapter.onFailedToRecycleView(holder); + boolean cached = false; + boolean recycled = false; + if (sDebugAssertionsEnabled && mCachedViews.contains(holder)) { + throw new IllegalArgumentException("cached view received recycle internal? " + + holder + exceptionLabel()); + } + if (forceRecycle || holder.isRecyclable()) { + if (mViewCacheMax > 0 + && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED + | ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { + // Retire oldest cached view + int cachedViewSize = mCachedViews.size(); + if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { + recycleCachedViewAt(0); + cachedViewSize--; + } + + int targetCacheIndex = cachedViewSize; + if (ALLOW_THREAD_GAP_WORK + && cachedViewSize > 0 + && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { + // when adding the view, skip past most recently prefetched views + int cacheIndex = cachedViewSize - 1; + while (cacheIndex >= 0) { + int cachedPos = mCachedViews.get(cacheIndex).mPosition; + if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { + break; + } + cacheIndex--; + } + targetCacheIndex = cacheIndex + 1; + } + mCachedViews.add(targetCacheIndex, holder); + cached = true; + } + if (!cached) { + addViewHolderToRecycledViewPool(holder, true); + recycled = true; + } + } else { + // NOTE: A view can fail to be recycled when it is scrolled off while an animation + // runs. In this case, the item is eventually recycled by + // ItemAnimatorRestoreListener#onAnimationFinished. + + // TODO: consider cancelling an animation when an item is removed scrollBy, + // to return it to the pool faster + if (sVerboseLoggingEnabled) { + Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + + "re-visit here. We are still removing it from animation lists" + + exceptionLabel()); + } + } + // even if the holder is not removed, we still call this method so that it is removed + // from view holder lists. + mViewInfoStore.removeViewHolder(holder); + if (!cached && !recycled && transientStatePreventsRecycling) { + PoolingContainer.callPoolingContainerOnRelease(holder.itemView); + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = null; + } + } + + /** + * Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool. + * + * Pass false to dispatchRecycled for views that have not been bound. + * + * @param holder Holder to be added to the pool. + * @param dispatchRecycled True to dispatch View recycled callbacks. + */ + void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { + clearNestedRecyclerViewIfNotNested(holder); + View itemView = holder.itemView; + if (mAccessibilityDelegate != null) { + AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate(); + AccessibilityDelegateCompat originalDelegate = null; + if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) { + originalDelegate = + ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate) + .getAndRemoveOriginalDelegateForItem(itemView); + } + // Set the a11y delegate back to whatever the original delegate was. + ViewCompat.setAccessibilityDelegate(itemView, originalDelegate); + } + if (dispatchRecycled) { + dispatchViewRecycled(holder); + } + holder.mBindingAdapter = null; + holder.mOwnerRecyclerView = null; + getRecycledViewPool().putRecycledView(holder); + } + + /** + * Used as a fast path for unscrapping and recycling a view during a bulk operation. + * The caller must call {@link #clearScrap()} when it's done to update the recycler's + * internal bookkeeping. + */ + void quickRecycleScrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + recycleViewHolderInternal(holder); + } + + /** + * Mark an attached view as scrap. + * + *

"Scrap" views are still attached to their parent RecyclerView but are eligible + * for rebinding and reuse. Requests for a view for a given position may return a + * reused or rebound scrap view instance.

+ * + * @param view View to scrap + */ + void scrapView(View view) { + final ViewHolder holder = getChildViewHolderInt(view); + if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) + || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { + if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) { + throw new IllegalArgumentException("Called scrap view with an invalid view." + + " Invalid views cannot be reused from scrap, they should rebound from" + + " recycler pool." + exceptionLabel()); + } + holder.setScrapContainer(this, false); + mAttachedScrap.add(holder); + } else { + if (mChangedScrap == null) { + mChangedScrap = new ArrayList(); + } + holder.setScrapContainer(this, true); + mChangedScrap.add(holder); + } + } + + /** + * Remove a previously scrapped view from the pool of eligible scrap. + * + *

This view will no longer be eligible for reuse until re-scrapped or + * until it is explicitly removed and recycled.

+ */ + void unscrapView(ViewHolder holder) { + if (holder.mInChangeScrap) { + mChangedScrap.remove(holder); + } else { + mAttachedScrap.remove(holder); + } + holder.mScrapContainer = null; + holder.mInChangeScrap = false; + holder.clearReturnedFromScrapFlag(); + } + + int getScrapCount() { + return mAttachedScrap.size(); + } + + View getScrapViewAt(int index) { + return mAttachedScrap.get(index).itemView; + } + + void clearScrap() { + mAttachedScrap.clear(); + if (mChangedScrap != null) { + mChangedScrap.clear(); + } + } + + ViewHolder getChangedScrapViewForPosition(int position) { + // If pre-layout, check the changed scrap for an exact match. + final int changedScrapSize; + if (mChangedScrap == null || (changedScrapSize = mChangedScrap.size()) == 0) { + return null; + } + // find by position + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + // find by id + if (mAdapter.hasStableIds()) { + final int offsetPosition = mAdapterHelper.findPositionOffset(position); + if (offsetPosition > 0 && offsetPosition < mAdapter.getItemCount()) { + final long id = mAdapter.getItemId(offsetPosition); + for (int i = 0; i < changedScrapSize; i++) { + final ViewHolder holder = mChangedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getItemId() == id) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + } + } + return null; + } + + /** + * Returns a view for the position either from attach scrap, hidden children, or cache. + * + * @param position Item position + * @param dryRun Does a dry run, finds the ViewHolder but does not remove + * @return a ViewHolder that can be re-used for this position. + */ + ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { + final int scrapCount = mAttachedScrap.size(); + + // Try first for an exact, non-invalid match from scrap. + for (int i = 0; i < scrapCount; i++) { + final ViewHolder holder = mAttachedScrap.get(i); + if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position + && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + + if (!dryRun) { + View view = mChildHelper.findHiddenNonRemovedView(position); + if (view != null) { + // This View is good to be used. We just need to unhide, detach and move to the + // scrap list. + final ViewHolder vh = getChildViewHolderInt(view); + mChildHelper.unhide(view); + int layoutIndex = mChildHelper.indexOfChild(view); + if (layoutIndex == RecyclerView.NO_POSITION) { + throw new IllegalStateException("layout index should not be -1 after " + + "unhiding a view:" + vh + exceptionLabel()); + } + mChildHelper.detachViewFromParent(layoutIndex); + scrapView(view); + vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP + | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); + return vh; + } + } + + // Search in our first-level recycled view cache. + final int cacheSize = mCachedViews.size(); + for (int i = 0; i < cacheSize; i++) { + final ViewHolder holder = mCachedViews.get(i); + // invalid view holders may be in cache if adapter has stable ids as they can be + // retrieved via getScrapOrCachedViewForId + if (!holder.isInvalid() && holder.getLayoutPosition() == position + && !holder.isAttachedToTransitionOverlay()) { + if (!dryRun) { + mCachedViews.remove(i); + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position + + ") found match in cache: " + holder); + } + return holder; + } + } + return null; + } + + ViewHolder getScrapOrCachedViewForId(long id, int type, boolean dryRun) { + // Look in our attached views first + final int count = mAttachedScrap.size(); + for (int i = count - 1; i >= 0; i--) { + final ViewHolder holder = mAttachedScrap.get(i); + if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) { + if (type == holder.getItemViewType()) { + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + if (holder.isRemoved()) { + // this might be valid in two cases: + // > item is removed but we are in pre-layout pass + // >> do nothing. return as is. make sure we don't rebind + // > item is removed then added to another position and we are in + // post layout. + // >> remove removed and invalid flags, add update flag to rebind + // because item was invisible to us and we don't know what happened in + // between. + if (!mState.isPreLayout()) { + holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE + | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED); + } + } + return holder; + } else if (!dryRun) { + // if we are running animations, it is actually better to keep it in scrap + // but this would force layout manager to lay it out which would be bad. + // Recycle this scrap. Type mismatch. + mAttachedScrap.remove(i); + removeDetachedView(holder.itemView, false); + quickRecycleScrapView(holder.itemView); + } + } + } + + // Search the first-level cache + final int cacheSize = mCachedViews.size(); + for (int i = cacheSize - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) { + if (type == holder.getItemViewType()) { + if (!dryRun) { + mCachedViews.remove(i); + } + return holder; + } else if (!dryRun) { + recycleCachedViewAt(i); + return null; + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + void dispatchViewRecycled(@NonNull ViewHolder holder) { + // TODO: Remove this once setRecyclerListener (currently deprecated) is deleted. + if (mRecyclerListener != null) { + mRecyclerListener.onViewRecycled(holder); + } + + final int listenerCount = mRecyclerListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mRecyclerListeners.get(i).onViewRecycled(holder); + } + if (mAdapter != null) { + mAdapter.onViewRecycled(holder); + } + if (mState != null) { + mViewInfoStore.removeViewHolder(holder); + } + if (sVerboseLoggingEnabled) Log.d(TAG, "dispatchViewRecycled: " + holder); + } + + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + clear(); + poolingContainerDetach(oldAdapter, true); + getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, + compatibleWithPrevious); + maybeSendPoolingContainerAttach(); + } + + void offsetPositionRecordsForMove(int from, int to) { + final int start, end, inBetweenOffset; + if (from < to) { + start = from; + end = to; + inBetweenOffset = -1; + } else { + start = to; + end = from; + inBetweenOffset = 1; + } + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null || holder.mPosition < start || holder.mPosition > end) { + continue; + } + if (holder.mPosition == from) { + holder.offsetPosition(to - from, false); + } else { + holder.offsetPosition(inBetweenOffset, false); + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForMove cached child " + i + " holder " + + holder); + } + } + } + + void offsetPositionRecordsForInsert(int insertedAt, int count) { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null && holder.mPosition >= insertedAt) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " + + holder + " now at position " + (holder.mPosition + count)); + } + // insertions only affect post layout hence don't apply them to pre-layout. + holder.offsetPosition(count, false); + } + } + } + + /** + * @param removedFrom Remove start index + * @param count Remove count + * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if + * false, they'll be applied before the second layout pass + */ + void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) { + final int removedEnd = removedFrom + count; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + if (holder.mPosition >= removedEnd) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "offsetPositionRecordsForRemove cached " + i + + " holder " + holder + " now at position " + + (holder.mPosition - count)); + } + holder.offsetPosition(-count, applyToPreLayout); + } else if (holder.mPosition >= removedFrom) { + // Item for this view was removed. Dump it from the cache. + holder.addFlags(ViewHolder.FLAG_REMOVED); + recycleCachedViewAt(i); + } + } + } + } + + void setViewCacheExtension(ViewCacheExtension extension) { + mViewCacheExtension = extension; + } + + void setRecycledViewPool(RecycledViewPool pool) { + poolingContainerDetach(mAdapter); + if (mRecyclerPool != null) { + mRecyclerPool.detach(); + } + mRecyclerPool = pool; + if (mRecyclerPool != null && getAdapter() != null) { + mRecyclerPool.attach(); + } + maybeSendPoolingContainerAttach(); + } + + private void maybeSendPoolingContainerAttach() { + if (mRecyclerPool != null + && mAdapter != null + && isAttachedToWindow()) { + mRecyclerPool.attachForPoolingContainer(mAdapter); + } + } + + private void poolingContainerDetach(Adapter adapter) { + poolingContainerDetach(adapter, false); + } + + private void poolingContainerDetach(Adapter adapter, boolean isBeingReplaced) { + if (mRecyclerPool != null) { + mRecyclerPool.detachForPoolingContainer(adapter, isBeingReplaced); + } + } + + void onAttachedToWindow() { + maybeSendPoolingContainerAttach(); + } + + void onDetachedFromWindow() { + for (int i = 0; i < mCachedViews.size(); i++) { + PoolingContainer.callPoolingContainerOnRelease(mCachedViews.get(i).itemView); + } + poolingContainerDetach(mAdapter); + } + + RecycledViewPool getRecycledViewPool() { + if (mRecyclerPool == null) { + mRecyclerPool = new RecycledViewPool(); + maybeSendPoolingContainerAttach(); + } + return mRecyclerPool; + } + + void viewRangeUpdate(int positionStart, int itemCount) { + final int positionEnd = positionStart + itemCount; + final int cachedCount = mCachedViews.size(); + for (int i = cachedCount - 1; i >= 0; i--) { + final ViewHolder holder = mCachedViews.get(i); + if (holder == null) { + continue; + } + + final int pos = holder.mPosition; + if (pos >= positionStart && pos < positionEnd) { + holder.addFlags(ViewHolder.FLAG_UPDATE); + recycleCachedViewAt(i); + // cached views should not be flagged as changed because this will cause them + // to animate when they are returned from cache. + } + } + } + + void markKnownViewsInvalid() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + if (holder != null) { + holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID); + holder.addChangePayload(null); + } + } + + if (mAdapter == null || !mAdapter.hasStableIds()) { + // we cannot re-use cached views in this case. Recycle them all + recycleAndClearCachedViews(); + } + } + + void clearOldPositions() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + holder.clearOldPosition(); + } + final int scrapCount = mAttachedScrap.size(); + for (int i = 0; i < scrapCount; i++) { + mAttachedScrap.get(i).clearOldPosition(); + } + if (mChangedScrap != null) { + final int changedScrapCount = mChangedScrap.size(); + for (int i = 0; i < changedScrapCount; i++) { + mChangedScrap.get(i).clearOldPosition(); + } + } + } + + void markItemDecorInsetsDirty() { + final int cachedCount = mCachedViews.size(); + for (int i = 0; i < cachedCount; i++) { + final ViewHolder holder = mCachedViews.get(i); + LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams(); + if (layoutParams != null) { + layoutParams.mInsetsDirty = true; + } + } + } + } + + /** + * ViewCacheExtension is a helper class to provide an additional layer of view caching that can + * be controlled by the developer. + *

+ * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and + * first level cache to find a matching View. If it cannot find a suitable View, Recycler will + * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking + * {@link RecycledViewPool}. + *

+ * Note that, Recycler never sends Views to this method to be cached. It is developers + * responsibility to decide whether they want to keep their Views in this custom cache or let + * the default recycling policy handle it. + */ + public abstract static class ViewCacheExtension { + + /** + * Returns a View that can be binded to the given Adapter position. + *

+ * This method should not create a new View. Instead, it is expected to return + * an already created View that can be re-used for the given type and position. + * If the View is marked as ignored, it should first call + * {@link LayoutManager#stopIgnoringView(View)} before returning the View. + *

+ * RecyclerView will re-bind the returned View to the position if necessary. + * + * @param recycler The Recycler that can be used to bind the View + * @param position The adapter position + * @param type The type of the View, defined by adapter + * @return A View that is bound to the given position or NULL if there is no View to re-use + * @see LayoutManager#ignoreView(View) + */ + @Nullable + public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position, + int type); + } + + /** + * Base class for an Adapter + * + *

Adapters provide a binding from an app-specific data set to views that are displayed + * within a {@link RecyclerView}.

+ * + * @param A class that extends ViewHolder that will be used by the adapter. + */ + public abstract static class Adapter { + private final AdapterDataObservable mObservable = new AdapterDataObservable(); + private boolean mHasStableIds = false; + private StateRestorationPolicy mStateRestorationPolicy = StateRestorationPolicy.ALLOW; + + /** + * Called when RecyclerView needs a new {@link ViewHolder} of the given type to represent + * an item. + *

+ * This new ViewHolder should be constructed with a new View that can represent the items + * of the given type. You can either create a new View manually or inflate it from an XML + * layout file. + *

+ * The new ViewHolder will be used to display items of the adapter using + * {@link #onBindViewHolder(ViewHolder, int, List)}. Since it will be re-used to display + * different items in the data set, it is a good idea to cache references to sub views of + * the View to avoid unnecessary {@link View#findViewById(int)} calls. + * + * @param parent The ViewGroup into which the new View will be added after it is bound to + * an adapter position. + * @param viewType The view type of the new View. + * @return A new ViewHolder that holds a View of the given view type. + * @see #getItemViewType(int) + * @see #onBindViewHolder(ViewHolder, int) + */ + @NonNull + public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType); + + /** + * Called by RecyclerView to display the data at the specified position. This method should + * update the contents of the {@link ViewHolder#itemView} to reflect the item at the given + * position. + *

+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the position parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. + * + * Override {@link #onBindViewHolder(ViewHolder, int, List)} instead if Adapter can + * handle efficient partial bind. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + */ + public abstract void onBindViewHolder(@NonNull VH holder, int position); + + /** + * Called by RecyclerView to display the data at the specified position. This method + * should update the contents of the {@link ViewHolder#itemView} to reflect the item at + * the given position. + *

+ * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method + * again if the position of the item changes in the data set unless the item itself is + * invalidated or the new position cannot be determined. For this reason, you should only + * use the position parameter while acquiring the related data item inside + * this method and should not keep a copy of it. If you need the position of an item later + * on (e.g. in a click listener), use {@link ViewHolder#getBindingAdapterPosition()} which + * will have the updated adapter position. + *

+ * Partial bind vs full bind: + *

+ * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or + * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, + * the ViewHolder is currently bound to old data and Adapter may run an efficient partial + * update using the payload info. If the payload is empty, Adapter must run a full bind. + * Adapter should not assume that the payload passed in notify methods will be received by + * onBindViewHolder(). For example when the view is not attached to the screen, the + * payload in notifyItemChange() will be simply dropped. + * + * @param holder The ViewHolder which should be updated to represent the contents of the + * item at the given position in the data set. + * @param position The position of the item within the adapter's data set. + * @param payloads A non-null list of merged payloads. Can be empty list if requires full + * update. + */ + public void onBindViewHolder(@NonNull VH holder, int position, + @NonNull List payloads) { + onBindViewHolder(holder, position); + } + + /** + * Returns the position of the given {@link ViewHolder} in the given {@link Adapter}. + * + * If the given {@link Adapter} is not part of this {@link Adapter}, + * {@link RecyclerView#NO_POSITION} is returned. + * + * @param adapter The adapter which is a sub adapter of this adapter or itself. + * @param viewHolder The ViewHolder whose local position in the given adapter will be + * returned. + * @param localPosition The position of the given {@link ViewHolder} in this + * {@link Adapter}. + * + * @return The local position of the given {@link ViewHolder} in this {@link Adapter} + * or {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item + * or the given {@link Adapter} is not part of this Adapter (if this Adapter merges other + * adapters). + */ + public int findRelativeAdapterPositionIn( + @NonNull Adapter adapter, + @NonNull ViewHolder viewHolder, + int localPosition + ) { + if (adapter == this) { + return localPosition; + } + return NO_POSITION; + } + + /** + * This method calls {@link #onCreateViewHolder(ViewGroup, int)} to create a new + * {@link ViewHolder} and initializes some private fields to be used by RecyclerView. + * + * @see #onCreateViewHolder(ViewGroup, int) + */ + @NonNull + public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) { + try { + TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); + final VH holder = onCreateViewHolder(parent, viewType); + if (holder.itemView.getParent() != null) { + throw new IllegalStateException("ViewHolder views must not be attached when" + + " created. Ensure that you are not passing 'true' to the attachToRoot" + + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)"); + } + holder.mItemViewType = viewType; + return holder; + } finally { + TraceCompat.endSection(); + } + } + + /** + * This method internally calls {@link #onBindViewHolder(ViewHolder, int)} to update the + * {@link ViewHolder} contents with the item at the given position and also sets up some + * private fields to be used by RecyclerView. + * + * Adapters that merge other adapters should use + * {@link #bindViewHolder(ViewHolder, int)} when calling nested adapters so that + * RecyclerView can track which adapter bound the {@link ViewHolder} to return the correct + * position from {@link ViewHolder#getBindingAdapterPosition()} method. + * They should also override + * the {@link #findRelativeAdapterPositionIn(Adapter, ViewHolder, int)} method. + * + * @param holder The view holder whose contents should be updated + * @param position The position of the holder with respect to this adapter + * @see #onBindViewHolder(ViewHolder, int) + */ + public final void bindViewHolder(@NonNull VH holder, int position) { + boolean rootBind = holder.mBindingAdapter == null; + if (rootBind) { + holder.mPosition = position; + if (hasStableIds()) { + holder.mItemId = getItemId(position); + } + holder.setFlags(ViewHolder.FLAG_BOUND, + ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN); + TraceCompat.beginSection(TRACE_BIND_VIEW_TAG); + } + holder.mBindingAdapter = this; + if (sDebugAssertionsEnabled) { + if (holder.itemView.getParent() == null + && (ViewCompat.isAttachedToWindow(holder.itemView) + != holder.isTmpDetached())) { + throw new IllegalStateException("Temp-detached state out of sync with reality. " + + "holder.isTmpDetached(): " + holder.isTmpDetached() + + ", attached to window: " + + ViewCompat.isAttachedToWindow(holder.itemView) + + ", holder: " + holder); + } + if (holder.itemView.getParent() == null + && ViewCompat.isAttachedToWindow(holder.itemView)) { + throw new IllegalStateException( + "Attempting to bind attached holder with no parent" + + " (AKA temp detached): " + holder); + } + } + onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); + if (rootBind) { + holder.clearPayload(); + final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); + if (layoutParams instanceof RecyclerView.LayoutParams) { + ((LayoutParams) layoutParams).mInsetsDirty = true; + } + TraceCompat.endSection(); + } + } + + /** + * Return the view type of the item at position for the purposes + * of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of + * a single view type for the adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + * + * @param position position to query + * @return integer value identifying the type of the view needed to represent the item at + * position. Type codes need not be contiguous. + */ + public int getItemViewType(int position) { + return 0; + } + + /** + * Indicates whether each item in the data set can be represented with a unique identifier + * of type {@link java.lang.Long}. + * + * @param hasStableIds Whether items in data set have unique identifiers or not. + * @see #hasStableIds() + * @see #getItemId(int) + */ + public void setHasStableIds(boolean hasStableIds) { + if (hasObservers()) { + throw new IllegalStateException("Cannot change whether this adapter has " + + "stable IDs while the adapter has registered observers."); + } + mHasStableIds = hasStableIds; + } + + /** + * Return the stable ID for the item at position. If {@link #hasStableIds()} + * would return false this method should return {@link #NO_ID}. The default implementation + * of this method returns {@link #NO_ID}. + * + * @param position Adapter position to query + * @return the stable ID of the item at position + */ + public long getItemId(int position) { + return NO_ID; + } + + /** + * Returns the total number of items in the data set held by the adapter. + * + * @return The total number of items in this adapter. + */ + public abstract int getItemCount(); + + /** + * Returns true if this adapter publishes a unique long value that can + * act as a key for the item at a given position in the data set. If that item is relocated + * in the data set, the ID returned for that item should be the same. + * + * @return true if this adapter's items have stable IDs + */ + public final boolean hasStableIds() { + return mHasStableIds; + } + + /** + * Called when a view created by this adapter has been recycled. + * + *

A view is recycled when a {@link LayoutManager} decides that it no longer + * needs to be attached to its parent {@link RecyclerView}. This can be because it has + * fallen out of visibility or a set of cached views represented by views still + * attached to the parent RecyclerView. If an item view has large or expensive data + * bound to it such as large bitmaps, this may be a good place to release those + * resources.

+ *

+ * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder for the view being recycled + */ + public void onViewRecycled(@NonNull VH holder) { + } + + /** + * Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled + * due to its transient state. Upon receiving this callback, Adapter can clear the + * animation(s) that effect the View's transient state and return true so that + * the View can be recycled. Keep in mind that the View in question is already removed from + * the RecyclerView. + *

+ * In some cases, it is acceptable to recycle a View although it has transient state. Most + * of the time, this is a case where the transient state will be cleared in + * {@link #onBindViewHolder(ViewHolder, int)} call when View is rebound to a new position. + * For this reason, RecyclerView leaves the decision to the Adapter and uses the return + * value of this method to decide whether the View should be recycled or not. + *

+ * Note that when all animations are created by {@link RecyclerView.ItemAnimator}, you + * should never receive this callback because RecyclerView keeps those Views as children + * until their animations are complete. This callback is useful when children of the item + * views create animations which may not be easy to implement using an {@link ItemAnimator}. + *

+ * You should never fix this issue by calling + * holder.itemView.setHasTransientState(false); unless you've previously called + * holder.itemView.setHasTransientState(true);. Each + * View.setHasTransientState(true) call must be matched by a + * View.setHasTransientState(false) call, otherwise, the state of the View + * may become inconsistent. You should always prefer to end or cancel animations that are + * triggering the transient state instead of handling it manually. + * + * @param holder The ViewHolder containing the View that could not be recycled due to its + * transient state. + * @return True if the View should be recycled, false otherwise. Note that if this method + * returns true, RecyclerView will ignore the transient state of + * the View and recycle it regardless. If this method returns false, + * RecyclerView will check the View's transient state again before giving a final decision. + * Default implementation returns false. + */ + public boolean onFailedToRecycleView(@NonNull VH holder) { + return false; + } + + /** + * Called when a view created by this adapter has been attached to a window. + * + *

This can be used as a reasonable signal that the view is about to be seen + * by the user. If the adapter previously freed any resources in + * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow} + * those resources should be restored here.

+ * + * @param holder Holder of the view being attached + */ + public void onViewAttachedToWindow(@NonNull VH holder) { + } + + /** + * Called when a view created by this adapter has been detached from its window. + * + *

Becoming detached from the window is not necessarily a permanent condition; + * the consumer of an Adapter's views may choose to cache views offscreen while they + * are not visible, attaching and detaching them as appropriate.

+ * + * @param holder Holder of the view being detached + */ + public void onViewDetachedFromWindow(@NonNull VH holder) { + } + + /** + * Returns true if one or more observers are attached to this adapter. + * + * @return true if this adapter has observers + */ + public final boolean hasObservers() { + return mObservable.hasObservers(); + } + + /** + * Register a new observer to listen for data changes. + * + *

The adapter may publish a variety of events describing specific changes. + * Not all adapters may support all change types and some may fall back to a generic + * {@link RecyclerView.AdapterDataObserver#onChanged() + * "something changed"} event if more specific data is not available.

+ * + *

Components registering observers with an adapter are responsible for + * {@link #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) + * unregistering} those observers when finished.

+ * + * @param observer Observer to register + * @see #unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver) + */ + public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer currently listening for data changes. + * + *

The unregistered observer will no longer receive events about changes + * to the adapter.

+ * + * @param observer Observer to unregister + * @see #registerAdapterDataObserver(RecyclerView.AdapterDataObserver) + */ + public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * Called by RecyclerView when it starts observing this Adapter. + *

+ * Keep in mind that same adapter may be observed by multiple RecyclerViews. + * + * @param recyclerView The RecyclerView instance which started observing this adapter. + * @see #onDetachedFromRecyclerView(RecyclerView) + */ + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + } + + /** + * Called by RecyclerView when it stops observing this Adapter. + * + * @param recyclerView The RecyclerView instance which stopped observing this adapter. + * @see #onAttachedToRecyclerView(RecyclerView) + */ + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + } + + /** + * Notify any registered observers that the data set has changed. + * + *

There are two different classes of data change events, item changes and structural + * changes. Item changes are when a single item has its data updated but no positional + * changes have occurred. Structural changes are when items are inserted, removed or moved + * within the data set.

+ * + *

This event does not specify what about the data set has changed, forcing + * any observers to assume that all existing items and structure may no longer be valid. + * LayoutManagers will be forced to fully rebind and relayout all visible views.

+ * + *

RecyclerView will attempt to synthesize visible structural change events + * for adapters that report that they have {@link #hasStableIds() stable IDs} when + * this method is used. This can help for the purposes of animation and visual + * object persistence but individual item views will still need to be rebound + * and relaid out.

+ * + *

If you are writing an adapter it will always be more efficient to use the more + * specific change events if you can. Rely on notifyDataSetChanged() + * as a last resort.

+ * + * @see #notifyItemChanged(int) + * @see #notifyItemInserted(int) + * @see #notifyItemRemoved(int) + * @see #notifyItemRangeChanged(int, int) + * @see #notifyItemRangeInserted(int, int) + * @see #notifyItemRangeRemoved(int, int) + */ + public final void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Notify any registered observers that the item at position has changed. + * Equivalent to calling notifyItemChanged(position, null);. + * + *

This is an item change event, not a structural change event. It indicates that any + * reflection of the data at position is out of date and should be updated. + * The item at position retains the same identity.

+ * + * @param position Position of the item that has changed + * @see #notifyItemRangeChanged(int, int) + */ + public final void notifyItemChanged(int position) { + mObservable.notifyItemRangeChanged(position, 1); + } + + /** + * Notify any registered observers that the item at position has changed with + * an optional payload object. + * + *

This is an item change event, not a structural change event. It indicates that any + * reflection of the data at position is out of date and should be updated. + * The item at position retains the same identity. + *

+ * + *

+ * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param position Position of the item that has changed + * @param payload Optional parameter, use null to identify a "full" update + * @see #notifyItemRangeChanged(int, int) + */ + public final void notifyItemChanged(int position, @Nullable Object payload) { + mObservable.notifyItemRangeChanged(position, 1, payload); + } + + /** + * Notify any registered observers that the itemCount items starting at + * position positionStart have changed. + * Equivalent to calling notifyItemRangeChanged(position, itemCount, null);. + * + *

This is an item change event, not a structural change event. It indicates that + * any reflection of the data in the given position range is out of date and should + * be updated. The items in the given range retain the same identity.

+ * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * @see #notifyItemChanged(int) + */ + public final void notifyItemRangeChanged(int positionStart, int itemCount) { + mObservable.notifyItemRangeChanged(positionStart, itemCount); + } + + /** + * Notify any registered observers that the itemCount items starting at + * position positionStart have changed. An optional payload can be + * passed to each changed item. + * + *

This is an item change event, not a structural change event. It indicates that any + * reflection of the data in the given position range is out of date and should be updated. + * The items in the given range retain the same identity. + *

+ * + *

+ * Client can optionally pass a payload for partial change. These payloads will be merged + * and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the + * item is already represented by a ViewHolder and it will be rebound to the same + * ViewHolder. A notifyItemRangeChanged() with null payload will clear all existing + * payloads on that item and prevent future payload until + * {@link #onBindViewHolder(ViewHolder, int, List)} is called. Adapter should not assume + * that the payload will always be passed to onBindViewHolder(), e.g. when the view is not + * attached, the payload will be simply dropped. + * + * @param positionStart Position of the first item that has changed + * @param itemCount Number of items that have changed + * @param payload Optional parameter, use null to identify a "full" update + * @see #notifyItemChanged(int) + */ + public final void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + mObservable.notifyItemRangeChanged(positionStart, itemCount, payload); + } + + /** + * Notify any registered observers that the item reflected at position + * has been newly inserted. The item previously at position is now at + * position position + 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param position Position of the newly inserted item in the data set + * @see #notifyItemRangeInserted(int, int) + */ + public final void notifyItemInserted(int position) { + mObservable.notifyItemRangeInserted(position, 1); + } + + /** + * Notify any registered observers that the item reflected at fromPosition + * has been moved to toPosition. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param fromPosition Previous position of the item. + * @param toPosition New position of the item. + */ + public final void notifyItemMoved(int fromPosition, int toPosition) { + mObservable.notifyItemMoved(fromPosition, toPosition); + } + + /** + * Notify any registered observers that the currently reflected itemCount + * items starting at positionStart have been newly inserted. The items + * previously located at positionStart and beyond can now be found starting + * at position positionStart + itemCount. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param positionStart Position of the first item that was inserted + * @param itemCount Number of items inserted + * @see #notifyItemInserted(int) + */ + public final void notifyItemRangeInserted(int positionStart, int itemCount) { + mObservable.notifyItemRangeInserted(positionStart, itemCount); + } + + /** + * Notify any registered observers that the item previously located at position + * has been removed from the data set. The items previously located at and after + * position may now be found at oldPosition - 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param position Position of the item that has now been removed + * @see #notifyItemRangeRemoved(int, int) + */ + public final void notifyItemRemoved(int position) { + mObservable.notifyItemRangeRemoved(position, 1); + } + + /** + * Notify any registered observers that the itemCount items previously + * located at positionStart have been removed from the data set. The items + * previously located at and after positionStart + itemCount may now be found + * at oldPosition - itemCount. + * + *

This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param positionStart Previous position of the first item that was removed + * @param itemCount Number of items removed from the data set + */ + public final void notifyItemRangeRemoved(int positionStart, int itemCount) { + mObservable.notifyItemRangeRemoved(positionStart, itemCount); + } + + /** + * Sets the state restoration strategy for the Adapter. + * + * By default, it is set to {@link StateRestorationPolicy#ALLOW} which means RecyclerView + * expects any set Adapter to be immediately capable of restoring the RecyclerView's saved + * scroll position. + *

+ * This behaviour might be undesired if the Adapter's data is loaded asynchronously, and + * thus unavailable during initial layout (e.g. after Activity rotation). To avoid losing + * scroll position, you can change this to be either + * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} or + * {@link StateRestorationPolicy#PREVENT}. + * Note that the former means your RecyclerView will restore state as soon as Adapter has + * 1 or more items while the latter requires you to call + * {@link #setStateRestorationPolicy(StateRestorationPolicy)} with either + * {@link StateRestorationPolicy#ALLOW} or + * {@link StateRestorationPolicy#PREVENT_WHEN_EMPTY} again when the Adapter is + * ready to restore its state. + *

+ * RecyclerView will still layout even when State restoration is disabled. The behavior of + * how State is restored is up to the {@link LayoutManager}. All default LayoutManagers + * will override current state with restored state when state restoration happens (unless + * an explicit call to {@link LayoutManager#scrollToPosition(int)} is made). + *

+ * Calling this method after state is restored will not have any effect other than changing + * the return value of {@link #getStateRestorationPolicy()}. + * + * @param strategy The saved state restoration strategy for this Adapter. + * @see #getStateRestorationPolicy() + */ + public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) { + mStateRestorationPolicy = strategy; + mObservable.notifyStateRestorationPolicyChanged(); + } + + /** + * Returns when this Adapter wants to restore the state. + * + * @return The current {@link StateRestorationPolicy} for this Adapter. Defaults to + * {@link StateRestorationPolicy#ALLOW}. + * @see #setStateRestorationPolicy(StateRestorationPolicy) + */ + @NonNull + public final StateRestorationPolicy getStateRestorationPolicy() { + return mStateRestorationPolicy; + } + + /** + * Called by the RecyclerView to decide whether the SavedState should be given to the + * LayoutManager or not. + * + * @return {@code true} if the Adapter is ready to restore its state, {@code false} + * otherwise. + */ + boolean canRestoreState() { + switch (mStateRestorationPolicy) { + case PREVENT: + return false; + case PREVENT_WHEN_EMPTY: + return getItemCount() > 0; + default: + return true; + } + } + + /** + * Defines how this Adapter wants to restore its state after a view reconstruction (e.g. + * configuration change). + */ + public enum StateRestorationPolicy { + /** + * Adapter is ready to restore State immediately, RecyclerView will provide the state + * to the LayoutManager in the next layout pass. + */ + ALLOW, + /** + * Adapter is ready to restore State when it has more than 0 items. RecyclerView will + * provide the state to the LayoutManager as soon as the Adapter has 1 or more items. + */ + PREVENT_WHEN_EMPTY, + /** + * RecyclerView will not restore the state for the Adapter until a call to + * {@link #setStateRestorationPolicy(StateRestorationPolicy)} is made with either + * {@link #ALLOW} or {@link #PREVENT_WHEN_EMPTY}. + */ + PREVENT + } + } + + @SuppressWarnings("unchecked") + void dispatchChildDetached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildDetachedFromWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewDetachedFromWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewDetachedFromWindow(child); + } + } + } + + @SuppressWarnings("unchecked") + void dispatchChildAttached(View child) { + final ViewHolder viewHolder = getChildViewHolderInt(child); + onChildAttachedToWindow(child); + if (mAdapter != null && viewHolder != null) { + mAdapter.onViewAttachedToWindow(viewHolder); + } + if (mOnChildAttachStateListeners != null) { + final int cnt = mOnChildAttachStateListeners.size(); + for (int i = cnt - 1; i >= 0; i--) { + mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child); + } + } + } + + /** + * A LayoutManager is responsible for measuring and positioning item views + * within a RecyclerView as well as determining the policy for when to recycle + * item views that are no longer visible to the user. By changing the LayoutManager + * a RecyclerView can be used to implement a standard vertically scrolling list, + * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock + * layout managers are provided for general use. + *

+ * If the LayoutManager specifies a default constructor or one with the signature + * ({@link Context}, {@link AttributeSet}, {@code int}, {@code int}), RecyclerView will + * instantiate and set the LayoutManager when being inflated. Most used properties can + * be then obtained from {@link #getProperties(Context, AttributeSet, int, int)}. In case + * a LayoutManager specifies both constructors, the non-default constructor will take + * precedence. + */ + public abstract static class LayoutManager { + ChildHelper mChildHelper; + RecyclerView mRecyclerView; + + /** + * The callback used for retrieving information about a RecyclerView and its children in the + * horizontal direction. + */ + private final ViewBoundsCheck.Callback mHorizontalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingLeft(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getWidth() - LayoutManager.this.getPaddingRight(); + } + + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedLeft(view) - params.leftMargin; + } + + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedRight(view) + params.rightMargin; + } + }; + + /** + * The callback used for retrieving information about a RecyclerView and its children in the + * vertical direction. + */ + private final ViewBoundsCheck.Callback mVerticalBoundCheckCallback = + new ViewBoundsCheck.Callback() { + @Override + public View getChildAt(int index) { + return LayoutManager.this.getChildAt(index); + } + + @Override + public int getParentStart() { + return LayoutManager.this.getPaddingTop(); + } + + @Override + public int getParentEnd() { + return LayoutManager.this.getHeight() + - LayoutManager.this.getPaddingBottom(); + } + + @Override + public int getChildStart(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedTop(view) - params.topMargin; + } + + @Override + public int getChildEnd(View view) { + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) + view.getLayoutParams(); + return LayoutManager.this.getDecoratedBottom(view) + params.bottomMargin; + } + }; + + /** + * Utility objects used to check the boundaries of children against their parent + * RecyclerView. + * + * @see #isViewPartiallyVisible(View, boolean, boolean), + * {@link LinearLayoutManager#findOneVisibleChild(int, int, boolean, boolean)}, + * and {@link LinearLayoutManager#findOnePartiallyOrCompletelyInvisibleChild(int, int)}. + */ + ViewBoundsCheck mHorizontalBoundCheck = new ViewBoundsCheck(mHorizontalBoundCheckCallback); + ViewBoundsCheck mVerticalBoundCheck = new ViewBoundsCheck(mVerticalBoundCheckCallback); + + @Nullable + SmoothScroller mSmoothScroller; + + boolean mRequestedSimpleAnimations = false; + + boolean mIsAttachedToWindow = false; + + /** + * This field is only set via the deprecated {@link #setAutoMeasureEnabled(boolean)} and is + * only accessed via {@link #isAutoMeasureEnabled()} for backwards compatability reasons. + */ + boolean mAutoMeasure = false; + + /** + * LayoutManager has its own more strict measurement cache to avoid re-measuring a child + * if the space that will be given to it is already larger than what it has measured before. + */ + private boolean mMeasurementCacheEnabled = true; + + private boolean mItemPrefetchEnabled = true; + + /** + * Written by {@link GapWorker} when prefetches occur to track largest number of view ever + * requested by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)} or + * {@link #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry)} call. + * + * If expanded by a {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, + * will be reset upon layout to prevent initial prefetches (often large, since they're + * proportional to expected child count) from expanding cache permanently. + */ + int mPrefetchMaxCountObserved; + + /** + * If true, mPrefetchMaxCountObserved is only valid until next layout, and should be reset. + */ + boolean mPrefetchMaxObservedInInitialPrefetch; + + /** + * These measure specs might be the measure specs that were passed into RecyclerView's + * onMeasure method OR fake measure specs created by the RecyclerView. + * For example, when a layout is run, RecyclerView always sets these specs to be + * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass. + *

+ * Also, to be able to use the hint in unspecified measure specs, RecyclerView checks the + * API level and sets the size to 0 pre-M to avoid any issue that might be caused by + * corrupt values. Older platforms have no responsibility to provide a size if they set + * mode to unspecified. + */ + private int mWidthMode, mHeightMode; + private int mWidth, mHeight; + + + /** + * Interface for LayoutManagers to request items to be prefetched, based on position, with + * specified distance from viewport, which indicates priority. + * + * @see LayoutManager#collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + * @see LayoutManager#collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public interface LayoutPrefetchRegistry { + /** + * Requests an an item to be prefetched, based on position, with a specified distance, + * indicating priority. + * + * @param layoutPosition Position of the item to prefetch. + * @param pixelDistance Distance from the current viewport to the bounds of the item, + * must be non-negative. + */ + void addPosition(int layoutPosition, int pixelDistance); + } + + void setRecyclerView(RecyclerView recyclerView) { + if (recyclerView == null) { + mRecyclerView = null; + mChildHelper = null; + mWidth = 0; + mHeight = 0; + } else { + mRecyclerView = recyclerView; + mChildHelper = recyclerView.mChildHelper; + mWidth = recyclerView.getWidth(); + mHeight = recyclerView.getHeight(); + } + mWidthMode = MeasureSpec.EXACTLY; + mHeightMode = MeasureSpec.EXACTLY; + } + + void setMeasureSpecs(int wSpec, int hSpec) { + mWidth = MeasureSpec.getSize(wSpec); + mWidthMode = MeasureSpec.getMode(wSpec); + if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mWidth = 0; + } + + mHeight = MeasureSpec.getSize(hSpec); + mHeightMode = MeasureSpec.getMode(hSpec); + if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { + mHeight = 0; + } + } + + /** + * Called after a layout is calculated during a measure pass when using auto-measure. + *

+ * It simply traverses all children to calculate a bounding box then calls + * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method + * if they need to handle the bounding box differently. + *

+ * For example, GridLayoutManager override that method to ensure that even if a column is + * empty, the GridLayoutManager still measures wide enough to include it. + * + * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure + * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure + */ + void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) { + final int count = getChildCount(); + if (count == 0) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + return; + } + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(child, bounds); + if (bounds.left < minX) { + minX = bounds.left; + } + if (bounds.right > maxX) { + maxX = bounds.right; + } + if (bounds.top < minY) { + minY = bounds.top; + } + if (bounds.bottom > maxY) { + maxY = bounds.bottom; + } + } + mRecyclerView.mTempRect.set(minX, minY, maxX, maxY); + setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec); + } + + /** + * Sets the measured dimensions from the given bounding box of the children and the + * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is + * only called if a LayoutManager returns true from + * {@link #isAutoMeasureEnabled()} and it is called after the RecyclerView calls + * {@link LayoutManager#onLayoutChildren(Recycler, State)} in the execution of + * {@link RecyclerView#onMeasure(int, int)}. + *

+ * This method must call {@link #setMeasuredDimension(int, int)}. + *

+ * The default implementation adds the RecyclerView's padding to the given bounding box + * then caps the value to be within the given measurement specs. + * + * @param childrenBounds The bounding box of all children + * @param wSpec The widthMeasureSpec that was passed into the RecyclerView. + * @param hSpec The heightMeasureSpec that was passed into the RecyclerView. + * @see #isAutoMeasureEnabled() + * @see #setMeasuredDimension(int, int) + */ + public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { + int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight(); + int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom(); + int width = chooseSize(wSpec, usedWidth, getMinimumWidth()); + int height = chooseSize(hSpec, usedHeight, getMinimumHeight()); + setMeasuredDimension(width, height); + } + + /** + * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView + */ + public void requestLayout() { + if (mRecyclerView != null) { + mRecyclerView.requestLayout(); + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is not. + * + * @param message The message for the exception. Can be null. + * @see #assertNotInLayoutOrScroll(String) + */ + public void assertInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertInLayoutOrScroll(message); + } + } + + /** + * Chooses a size from the given specs and parameters that is closest to the desired size + * and also complies with the spec. + * + * @param spec The measureSpec + * @param desired The preferred measurement + * @param min The minimum value + * @return A size that fits to the given specs + */ + public static int chooseSize(int spec, int desired, int min) { + final int mode = View.MeasureSpec.getMode(spec); + final int size = View.MeasureSpec.getSize(spec); + switch (mode) { + case View.MeasureSpec.EXACTLY: + return size; + case View.MeasureSpec.AT_MOST: + return Math.min(size, Math.max(desired, min)); + case View.MeasureSpec.UNSPECIFIED: + default: + return Math.max(desired, min); + } + } + + /** + * Checks if RecyclerView is in the middle of a layout or scroll and throws an + * {@link IllegalStateException} if it is. + * + * @param message The message for the exception. Can be null. + * @see #assertInLayoutOrScroll(String) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void assertNotInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertNotInLayoutOrScroll(message); + } + } + + /** + * Defines whether the measuring pass of layout should use the AutoMeasure mechanism of + * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + * + * @param enabled True if layout measurement should be done by the + * RecyclerView, false if it should be done by this + * LayoutManager. + * @see #isAutoMeasureEnabled() + * @deprecated Implementors of LayoutManager should define whether or not it uses + * AutoMeasure by overriding {@link #isAutoMeasureEnabled()}. + */ + @Deprecated + public void setAutoMeasureEnabled(boolean enabled) { + mAutoMeasure = enabled; + } + + /** + * Returns whether the measuring pass of layout should use the AutoMeasure mechanism of + * {@link RecyclerView} or if it should be done by the LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + *

+ * This method returns false by default (it actually returns the value passed to the + * deprecated {@link #setAutoMeasureEnabled(boolean)}) and should be overridden to return + * true if a LayoutManager wants to be auto measured by the RecyclerView. + *

+ * If this method is overridden to return true, + * {@link LayoutManager#onMeasure(Recycler, State, int, int)} should not be overridden. + *

+ * AutoMeasure is a RecyclerView mechanism that handles the measuring pass of layout in a + * simple and contract satisfying way, including the wrapping of children laid out by + * LayoutManager. Simply put, it handles wrapping children by calling + * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a call to + * {@link RecyclerView#onMeasure(int, int)}, and then calculating desired dimensions based + * on children's dimensions and positions. It does this while supporting all existing + * animation capabilities of the RecyclerView. + *

+ * More specifically: + *

    + *
  1. When {@link RecyclerView#onMeasure(int, int)} is called, if the provided measure + * specs both have a mode of {@link View.MeasureSpec#EXACTLY}, RecyclerView will set its + * measured dimensions accordingly and return, allowing layout to continue as normal + * (Actually, RecyclerView will call + * {@link LayoutManager#onMeasure(Recycler, State, int, int)} for backwards compatibility + * reasons but it should not be overridden if AutoMeasure is being used).
  2. + *
  3. If one of the layout specs is not {@code EXACT}, the RecyclerView will start the + * layout process. It will first process all pending Adapter updates and + * then decide whether to run a predictive layout. If it decides to do so, it will first + * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to + * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still + * return the width and height of the RecyclerView as of the last layout calculation. + *

    + * After handling the predictive case, RecyclerView will call + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can + * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()}, + * {@link #getWidth()} and {@link #getWidthMode()}.

  4. + *
  5. After the layout calculation, RecyclerView sets the measured width & height by + * calculating the bounding box for the children (+ RecyclerView's padding). The + * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose + * different values. For instance, GridLayoutManager overrides this value to handle the case + * where if it is vertical and has 3 columns but only 2 items, it should still measure its + * width to fit 3 items, not 2.
  6. + *
  7. Any following calls to {@link RecyclerView#onMeasure(int, int)} will run + * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to + * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will + * take care of which views are actually added / removed / moved / changed for animations so + * that the LayoutManager should not worry about them and handle each + * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
  8. + *
  9. When measure is complete and RecyclerView's + * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks + * whether it already did layout calculations during the measure pass and if so, it re-uses + * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)} + * if the last measure spec was different from the final dimensions or adapter contents + * have changed between the measure call and the layout call.
  10. + *
  11. Finally, animations are calculated and run as usual.
  12. + *
+ * + * @return True if the measuring pass of layout should use the AutoMeasure + * mechanism of {@link RecyclerView} or False if it should be done by the + * LayoutManager's implementation of + * {@link LayoutManager#onMeasure(Recycler, State, int, int)}. + * @see #setMeasuredDimension(Rect, int, int) + * @see #onMeasure(Recycler, State, int, int) + */ + public boolean isAutoMeasureEnabled() { + return mAutoMeasure; + } + + /** + * Returns whether this LayoutManager supports "predictive item animations". + *

+ * "Predictive item animations" are automatically created animations that show + * where items came from, and where they are going to, as items are added, removed, + * or moved within a layout. + *

+ * A LayoutManager wishing to support predictive item animations must override this + * method to return true (the default implementation returns false) and must obey certain + * behavioral contracts outlined in {@link #onLayoutChildren(Recycler, State)}. + *

+ * Whether item animations actually occur in a RecyclerView is actually determined by both + * the return value from this method and the + * {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the + * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this + * method returns false, then only "simple item animations" will be enabled in the + * RecyclerView, in which views whose position are changing are simply faded in/out. If the + * RecyclerView has a non-null ItemAnimator and this method returns true, then predictive + * item animations will be enabled in the RecyclerView. + * + * @return true if this LayoutManager supports predictive item animations, false otherwise. + */ + public boolean supportsPredictiveItemAnimations() { + return false; + } + + /** + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. + * + *

If enabled, the LayoutManager will be queried for items to inflate/bind in between + * view system traversals on devices running API 21 or greater. Default value is true.

+ * + *

On platforms API level 21 and higher, the UI thread is idle between passing a frame + * to RenderThread and the starting up its next frame at the next VSync pulse. By + * prefetching out of window views in this time period, delays from inflation and view + * binding are much less likely to cause jank and stuttering during scrolls and flings.

+ * + *

While prefetch is enabled, it will have the side effect of expanding the effective + * size of the View cache to hold prefetched views.

+ * + * @param enabled True if items should be prefetched in between traversals. + * @see #isItemPrefetchEnabled() + */ + public final void setItemPrefetchEnabled(boolean enabled) { + if (enabled != mItemPrefetchEnabled) { + mItemPrefetchEnabled = enabled; + mPrefetchMaxCountObserved = 0; + if (mRecyclerView != null) { + mRecyclerView.mRecycler.updateViewCacheSize(); + } + } + } + + /** + * Sets whether the LayoutManager should be queried for views outside of + * its viewport while the UI thread is idle between frames. + * + * @return true if item prefetch is enabled, false otherwise + * @see #setItemPrefetchEnabled(boolean) + */ + public final boolean isItemPrefetchEnabled() { + return mItemPrefetchEnabled; + } + + /** + * Gather all positions from the LayoutManager to be prefetched, given specified momentum. + * + *

If item prefetch is enabled, this method is called in between traversals to gather + * which positions the LayoutManager will soon need, given upcoming movement in subsequent + * traversals.

+ * + *

The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.

+ * + * @param dx X movement component. + * @param dy Y movement component. + * @param state State of RecyclerView + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * @see #isItemPrefetchEnabled() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectAdjacentPrefetchPositions(int dx, int dy, State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + } + + /** + * Gather all positions from the LayoutManager to be prefetched in preperation for its + * RecyclerView to come on screen, due to the movement of another, containing RecyclerView. + * + *

This method is only called when a RecyclerView is nested in another RecyclerView.

+ * + *

If item prefetch is enabled for this LayoutManager, as well in another containing + * LayoutManager, this method is called in between draw traversals to gather + * which positions this LayoutManager will first need, once it appears on the screen.

+ * + *

For example, if this LayoutManager represents a horizontally scrolling list within a + * vertically scrolling LayoutManager, this method would be called when the horizontal list + * is about to come onscreen.

+ * + *

The LayoutManager should call {@link LayoutPrefetchRegistry#addPosition(int, int)} for + * each item to be prepared, and these positions will have their ViewHolders created and + * bound, if there is sufficient time available, in advance of being needed by a + * scroll or layout.

+ * + * @param adapterItemCount number of items in the associated adapter. + * @param layoutPrefetchRegistry PrefetchRegistry to add prefetch entries into. + * @see #isItemPrefetchEnabled() + * @see #collectAdjacentPrefetchPositions(int, int, State, LayoutPrefetchRegistry) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void collectInitialPrefetchPositions(int adapterItemCount, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + } + + void dispatchAttachedToWindow(RecyclerView view) { + mIsAttachedToWindow = true; + onAttachedToWindow(view); + } + + void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) { + mIsAttachedToWindow = false; + onDetachedFromWindow(view, recycler); + } + + /** + * Returns whether LayoutManager is currently attached to a RecyclerView which is attached + * to a window. + * + * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView + * is attached to window. + */ + public boolean isAttachedToWindow() { + return mIsAttachedToWindow; + } + + /** + * Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread. + *

+ * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * + * @param action The Runnable that will be executed. + * @see #removeCallbacks + */ + public void postOnAnimation(Runnable action) { + if (mRecyclerView != null) { + ViewCompat.postOnAnimation(mRecyclerView, action); + } + } + + /** + * Removes the specified Runnable from the message queue. + *

+ * Calling this method when LayoutManager is not attached to a RecyclerView has no effect. + * + * @param action The Runnable to remove from the message handling queue + * @return true if RecyclerView could ask the Handler to remove the Runnable, + * false otherwise. When the returned value is true, the Runnable + * may or may not have been actually removed from the message queue + * (for instance, if the Runnable was not in the queue already.) + * @see #postOnAnimation + */ + public boolean removeCallbacks(Runnable action) { + if (mRecyclerView != null) { + return mRecyclerView.removeCallbacks(action); + } + return false; + } + + /** + * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView + * is attached to a window. + *

+ * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. + *

+ * Subclass implementations should always call through to the superclass implementation. + * + * @param view The RecyclerView this LayoutManager is bound to + * @see #onDetachedFromWindow(RecyclerView, Recycler) + */ + @CallSuper + public void onAttachedToWindow(RecyclerView view) { + } + + /** + * @deprecated override {@link #onDetachedFromWindow(RecyclerView, Recycler)} + */ + @Deprecated + public void onDetachedFromWindow(RecyclerView view) { + + } + + /** + * Called when this LayoutManager is detached from its parent RecyclerView or when + * its parent RecyclerView is detached from its window. + *

+ * LayoutManager should clear all of its View references as another LayoutManager might be + * assigned to the RecyclerView. + *

+ * If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not + * call {@link #onLayoutChildren(Recycler, State)} if nothing has changed and a layout was + * not requested on the RecyclerView while it was detached. + *

+ * If your LayoutManager has View references that it cleans in on-detach, it should also + * call {@link RecyclerView#requestLayout()} to ensure that it is re-laid out when + * RecyclerView is re-attached. + *

+ * Subclass implementations should always call through to the superclass implementation. + * + * @param view The RecyclerView this LayoutManager is bound to + * @param recycler The recycler to use if you prefer to recycle your children instead of + * keeping them around. + * @see #onAttachedToWindow(RecyclerView) + */ + @CallSuper + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onDetachedFromWindow(RecyclerView view, Recycler recycler) { + onDetachedFromWindow(view); + } + + /** + * Check if the RecyclerView is configured to clip child views to its padding. + * + * @return true if this RecyclerView clips children to its padding, false otherwise + */ + public boolean getClipToPadding() { + return mRecyclerView != null && mRecyclerView.mClipToPadding; + } + + /** + * Lay out all relevant child views from the given adapter. + * + * The LayoutManager is in charge of the behavior of item animations. By default, + * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple + * item animations are enabled. This means that add/remove operations on the + * adapter will result in animations to add new or appearing items, removed or + * disappearing items, and moved items. If a LayoutManager returns false from + * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a + * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the + * RecyclerView will have enough information to run those animations in a simple + * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will + * simply fade views in and out, whether they are actually added/removed or whether + * they are moved on or off the screen due to other add/remove operations. + * + *

A LayoutManager wanting a better item animation experience, where items can be + * animated onto and off of the screen according to where the items exist when they + * are not on screen, then the LayoutManager should return true from + * {@link #supportsPredictiveItemAnimations()} and add additional logic to + * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations + * means that {@link #onLayoutChildren(Recycler, State)} will be called twice; + * once as a "pre" layout step to determine where items would have been prior to + * a real layout, and again to do the "real" layout. In the pre-layout phase, + * items will remember their pre-layout positions to allow them to be laid out + * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will + * be returned from the scrap to help determine correct placement of other items. + * These removed items should not be added to the child list, but should be used + * to help calculate correct positioning of other views, including views that + * were not previously onscreen (referred to as APPEARING views), but whose + * pre-layout offscreen position can be determined given the extra + * information about the pre-layout removed views.

+ * + *

The second layout pass is the real layout in which only non-removed views + * will be used. The only additional requirement during this pass is, if + * {@link #supportsPredictiveItemAnimations()} returns true, to note which + * views exist in the child list prior to layout and which are not there after + * layout (referred to as DISAPPEARING views), and to position/layout those views + * appropriately, without regard to the actual bounds of the RecyclerView. This allows + * the animation system to know the location to which to animate these disappearing + * views.

+ * + *

The default LayoutManager implementations for RecyclerView handle all of these + * requirements for animations already. Clients of RecyclerView can either use one + * of these layout managers directly or look at their implementations of + * onLayoutChildren() to see how they account for the APPEARING and + * DISAPPEARING views.

+ * + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutChildren(Recycler recycler, State state) { + Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) "); + } + + /** + * Called after a full layout calculation is finished. The layout calculation may include + * multiple {@link #onLayoutChildren(Recycler, State)} calls due to animations or + * layout measurement but it will include only one {@link #onLayoutCompleted(State)} call. + * This method will be called at the end of {@link View#layout(int, int, int, int)} call. + *

+ * This is a good place for the LayoutManager to do some cleanup like pending scroll + * position, saved state etc. + * + * @param state Transient state of RecyclerView + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onLayoutCompleted(State state) { + } + + /** + * Create a default LayoutParams object for a child of the RecyclerView. + * + *

LayoutManagers will often want to use a custom LayoutParams type + * to store extra information specific to the layout. Client code should subclass + * {@link RecyclerView.LayoutParams} for this purpose.

+ * + *

Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

+ * + * @return A new LayoutParams for a child view + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract LayoutParams generateDefaultLayoutParams(); + + /** + * Determines the validity of the supplied LayoutParams object. + * + *

This should check to make sure that the object is of the correct type + * and all values are within acceptable ranges. The default implementation + * returns true for non-null params.

+ * + * @param lp LayoutParams object to check + * @return true if this LayoutParams object is valid, false otherwise + */ + public boolean checkLayoutParams(LayoutParams lp) { + return lp != null; + } + + /** + * Create a LayoutParams object suitable for this LayoutManager, copying relevant + * values from the supplied LayoutParams object if possible. + * + *

Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

+ * + * @param lp Source LayoutParams object to copy values from + * @return a new LayoutParams object + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof LayoutParams) { + return new LayoutParams((LayoutParams) lp); + } else if (lp instanceof MarginLayoutParams) { + return new LayoutParams((MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + /** + * Create a LayoutParams object suitable for this LayoutManager from + * an inflated layout resource. + * + *

Important: if you use your own custom LayoutParams type + * you must also override + * {@link #checkLayoutParams(LayoutParams)}, + * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and + * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.

+ * + * @param c Context for obtaining styled attributes + * @param attrs AttributeSet describing the supplied arguments + * @return a new LayoutParams object + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + /** + * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. + * + * @param dx distance to scroll by in pixels. X increases as scroll position + * approaches the right. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dx was + * negative and scrolling proceeeded in that direction. + * Math.abs(result) may be less than dx if a boundary was reached. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollHorizontallyBy(int dx, Recycler recycler, State state) { + return 0; + } + + /** + * Scroll vertically by dy pixels in screen coordinates and return the distance traveled. + * The default implementation does nothing and returns 0. + * + * @param dy distance to scroll in pixels. Y increases as scroll position + * approaches the bottom. + * @param recycler Recycler to use for fetching potentially cached views for a + * position + * @param state Transient state of RecyclerView + * @return The actual distance scrolled. The return value will be negative if dy was + * negative and scrolling proceeeded in that direction. + * Math.abs(result) may be less than dy if a boundary was reached. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int scrollVerticallyBy(int dy, Recycler recycler, State state) { + return 0; + } + + /** + * Query if horizontal scrolling is currently supported. The default implementation + * returns false. + * + * @return True if this LayoutManager can scroll the current contents horizontally + */ + public boolean canScrollHorizontally() { + return false; + } + + /** + * Query if vertical scrolling is currently supported. The default implementation + * returns false. + * + * @return True if this LayoutManager can scroll the current contents vertically + */ + public boolean canScrollVertically() { + return false; + } + + /** + * Scroll to the specified adapter position. + * + * Actual position of the item on the screen depends on the LayoutManager implementation. + * + * @param position Scroll to this adapter position. + */ + public void scrollToPosition(int position) { + if (sVerboseLoggingEnabled) { + Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract"); + } + } + + /** + *

Smooth scroll to the specified adapter position.

+ *

To support smooth scrolling, override this method, create your {@link SmoothScroller} + * instance and call {@link #startSmoothScroll(SmoothScroller)}. + *

+ * + * @param recyclerView The RecyclerView to which this layout manager is attached + * @param state Current State of RecyclerView + * @param position Scroll to this adapter position. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void smoothScrollToPosition(RecyclerView recyclerView, State state, + int position) { + Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling"); + } + + /** + * Starts a smooth scroll using the provided {@link SmoothScroller}. + * + *

Each instance of SmoothScroller is intended to only be used once. Provide a new + * SmoothScroller instance each time this method is called. + * + *

Calling this method will cancel any previous smooth scroll request. + * + * @param smoothScroller Instance which defines how smooth scroll should be animated + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void startSmoothScroll(SmoothScroller smoothScroller) { + if (mSmoothScroller != null && smoothScroller != mSmoothScroller + && mSmoothScroller.isRunning()) { + mSmoothScroller.stop(); + } + mSmoothScroller = smoothScroller; + mSmoothScroller.start(mRecyclerView, this); + } + + /** + * @return true if RecyclerView is currently in the state of smooth scrolling. + */ + public boolean isSmoothScrolling() { + return mSmoothScroller != null && mSmoothScroller.isRunning(); + } + + /** + * Returns the resolved layout direction for this RecyclerView. + * + * @return {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout + * direction is RTL or returns + * {@link androidx.core.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction + * is not RTL. + */ + public int getLayoutDirection() { + return ViewCompat.getLayoutDirection(mRecyclerView); + } + + /** + * Ends all animations on the view created by the {@link ItemAnimator}. + * + * @param view The View for which the animations should be ended. + * @see RecyclerView.ItemAnimator#endAnimations() + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void endAnimation(View view) { + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view)); + } + } + + /** + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + *

+ * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. + * + * @param child View to add and then remove with animation. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addDisappearingView(View child) { + addDisappearingView(child, -1); + } + + /** + * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view + * to the layout that is known to be going away, either because it has been + * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the + * visible portion of the container but is being laid out in order to inform RecyclerView + * in how to animate the item out of view. + *

+ * Views added via this method are going to be invisible to LayoutManager after the + * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)} + * or won't be included in {@link #getChildCount()} method. + * + * @param child View to add and then remove with animation. + * @param index Index of the view. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addDisappearingView(View child, int index) { + addViewInt(child, index, true); + } + + /** + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. + * + * @param child View to add + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addView(View child) { + addView(child, -1); + } + + /** + * Add a view to the currently attached RecyclerView if needed. LayoutManagers should + * use this method to add views obtained from a {@link Recycler} using + * {@link Recycler#getViewForPosition(int)}. + * + * @param child View to add + * @param index Index to add child at + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void addView(View child, int index) { + addViewInt(child, index, false); + } + + private void addViewInt(View child, int index, boolean disappearing) { + final ViewHolder holder = getChildViewHolderInt(child); + if (disappearing || holder.isRemoved()) { + // these views will be hidden at the end of the layout pass. + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(holder); + } else { + // This may look like unnecessary but may happen if layout manager supports + // predictive layouts and adapter removed then re-added the same item. + // In this case, added version will be visible in the post layout (because add is + // deferred) but RV will still bind it to the same View. + // So if a View re-appears in post layout pass, remove it from disappearing list. + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(holder); + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (holder.wasReturnedFromScrap() || holder.isScrap()) { + if (holder.isScrap()) { + holder.unScrap(); + } else { + holder.clearReturnedFromScrapFlag(); + } + mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } + } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child + // ensure in correct position + int currentIndex = mChildHelper.indexOfChild(child); + if (index == -1) { + index = mChildHelper.getChildCount(); + } + if (currentIndex == -1) { + throw new IllegalStateException("Added View has RecyclerView as parent but" + + " view is not a real child. Unfiltered index:" + + mRecyclerView.indexOfChild(child) + mRecyclerView.exceptionLabel()); + } + if (currentIndex != index) { + mRecyclerView.mLayout.moveView(currentIndex, index); + } + } else { + mChildHelper.addView(child, index, false); + lp.mInsetsDirty = true; + if (mSmoothScroller != null && mSmoothScroller.isRunning()) { + mSmoothScroller.onChildAttachedToWindow(child); + } + } + if (lp.mPendingInvalidate) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "consuming pending invalidate on child " + lp.mViewHolder); + } + holder.itemView.invalidate(); + lp.mPendingInvalidate = false; + } + } + + /** + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. + * + * @param child View to remove + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void removeView(View child) { + mChildHelper.removeView(child); + } + + /** + * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should + * use this method to completely remove a child view that is no longer needed. + * LayoutManagers should strongly consider recycling removed views using + * {@link Recycler#recycleView(android.view.View)}. + * + * @param index Index of the child view to remove + */ + public void removeViewAt(int index) { + final View child = getChildAt(index); + if (child != null) { + mChildHelper.removeViewAt(index); + } + } + + /** + * Remove all views from the currently attached RecyclerView. This will not recycle + * any of the affected views; the LayoutManager is responsible for doing so if desired. + */ + public void removeAllViews() { + // Only remove non-animating views + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + mChildHelper.removeViewAt(i); + } + } + + /** + * Returns offset of the RecyclerView's text baseline from the its top boundary. + * + * @return The offset of the RecyclerView's text baseline from the its top boundary; -1 if + * there is no baseline. + */ + public int getBaseline() { + return -1; + } + + /** + * Returns the adapter position of the item represented by the given View. This does not + * contain any adapter changes that might have happened after the last layout. + * + * @param view The view to query + * @return The adapter position of the item which is rendered by this View. + */ + public int getPosition(@NonNull View view) { + return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); + } + + /** + * Returns the View type defined by the adapter. + * + * @param view The view to query + * @return The type of the view assigned by the adapter. + */ + public int getItemViewType(@NonNull View view) { + return getChildViewHolderInt(view).getItemViewType(); + } + + /** + * Traverses the ancestors of the given view and returns the item view that contains it + * and also a direct child of the LayoutManager. + *

+ * Note that this method may return null if the view is a child of the RecyclerView but + * not a child of the LayoutManager (e.g. running a disappear animation). + * + * @param view The view that is a descendant of the LayoutManager. + * @return The direct child of the LayoutManager which contains the given view or null if + * the provided view is not a descendant of this LayoutManager. + * @see RecyclerView#getChildViewHolder(View) + * @see RecyclerView#findContainingViewHolder(View) + */ + @Nullable + public View findContainingItemView(@NonNull View view) { + if (mRecyclerView == null) { + return null; + } + View found = mRecyclerView.findContainingItemView(view); + if (found == null) { + return null; + } + if (mChildHelper.isHidden(found)) { + return null; + } + return found; + } + + /** + * Finds the view which represents the given adapter position. + *

+ * This method traverses each child since it has no information about child order. + * Override this method to improve performance if your LayoutManager keeps data about + * child views. + *

+ * If a view is ignored via {@link #ignoreView(View)}, it is also ignored by this method. + * + * @param position Position of the item in adapter + * @return The child view that represents the given position or null if the position is not + * laid out + */ + @Nullable + public View findViewByPosition(int position) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + ViewHolder vh = getChildViewHolderInt(child); + if (vh == null) { + continue; + } + if (vh.getLayoutPosition() == position && !vh.shouldIgnore() + && (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) { + return child; + } + } + return null; + } + + /** + * Temporarily detach a child view. + * + *

LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.

+ * + *

If a LayoutManager uses this method to detach a view, it must + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.

+ * + * @param child Child to detach + */ + public void detachView(@NonNull View child) { + final int ind = mChildHelper.indexOfChild(child); + if (ind >= 0) { + detachViewInternal(ind, child); + } + } + + /** + * Temporarily detach a child view. + * + *

LayoutManagers may want to perform a lightweight detach operation to rearrange + * views currently attached to the RecyclerView. Generally LayoutManager implementations + * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} + * so that the detached view may be rebound and reused.

+ * + *

If a LayoutManager uses this method to detach a view, it must + * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach} + * or {@link #removeDetachedView(android.view.View) fully remove} the detached view + * before the LayoutManager entry point method called by RecyclerView returns.

+ * + * @param index Index of the child to detach + */ + public void detachViewAt(int index) { + detachViewInternal(index, getChildAt(index)); + } + + private void detachViewInternal(int index, @NonNull View view) { + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchStartTemporaryDetach(view); + } + mChildHelper.detachViewFromParent(index); + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + * @param lp LayoutParams for child + */ + public void attachView(@NonNull View child, int index, LayoutParams lp) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh.isRemoved()) { + mRecyclerView.mViewInfoStore.addToDisappearedInLayout(vh); + } else { + mRecyclerView.mViewInfoStore.removeFromDisappearedInLayout(vh); + } + mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved()); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + */ + public void attachView(@NonNull View child, int index) { + attachView(child, index, (LayoutParams) child.getLayoutParams()); + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + */ + public void attachView(@NonNull View child) { + attachView(child, -1); + } + + /** + * Finish removing a view that was previously temporarily + * {@link #detachView(android.view.View) detached}. + * + * @param child Detached child to remove + */ + public void removeDetachedView(@NonNull View child) { + mRecyclerView.removeDetachedView(child, false); + } + + /** + * Moves a View from one position to another. + * + * @param fromIndex The View's initial index + * @param toIndex The View's target index + */ + public void moveView(int fromIndex, int toIndex) { + View view = getChildAt(fromIndex); + if (view == null) { + throw new IllegalArgumentException("Cannot move a child from non-existing index:" + + fromIndex + mRecyclerView.toString()); + } + detachViewAt(fromIndex); + attachView(view, toIndex); + } + + /** + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + *

Scrapping a view allows it to be rebound and reused to show updated or + * different data.

+ * + * @param child Child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into + */ + public void detachAndScrapView(@NonNull View child, @NonNull Recycler recycler) { + int index = mChildHelper.indexOfChild(child); + scrapOrRecycleView(recycler, index, child); + } + + /** + * Detach a child view and add it to a {@link Recycler Recycler's} scrap heap. + * + *

Scrapping a view allows it to be rebound and reused to show updated or + * different data.

+ * + * @param index Index of child to detach and scrap + * @param recycler Recycler to deposit the new scrap view into + */ + public void detachAndScrapViewAt(int index, @NonNull Recycler recycler) { + final View child = getChildAt(index); + scrapOrRecycleView(recycler, index, child); + } + + /** + * Remove a child view and recycle it using the given Recycler. + * + * @param child Child to remove and recycle + * @param recycler Recycler to use to recycle child + */ + public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) { + removeView(child); + recycler.recycleView(child); + } + + /** + * Remove a child view and recycle it using the given Recycler. + * + * @param index Index of child to remove and recycle + * @param recycler Recycler to use to recycle child + */ + public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { + final View view = getChildAt(index); + removeViewAt(index); + recycler.recycleView(view); + } + + /** + * Return the current number of child views attached to the parent RecyclerView. + * This does not include child views that were temporarily detached and/or scrapped. + * + * @return Number of attached children + */ + public int getChildCount() { + return mChildHelper != null ? mChildHelper.getChildCount() : 0; + } + + /** + * Return the child view at the given index + * + * @param index Index of child to return + * @return Child view at index + */ + @Nullable + public View getChildAt(int index) { + return mChildHelper != null ? mChildHelper.getChildAt(index) : null; + } + + /** + * Return the width measurement spec mode that is currently relevant to the LayoutManager. + * + *

This value is set only if the LayoutManager opts into the AutoMeasure api via + * {@link #setAutoMeasureEnabled(boolean)}. + * + *

When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * + * @return Width measure spec mode + * @see View.MeasureSpec#getMode(int) + */ + public int getWidthMode() { + return mWidthMode; + } + + /** + * Return the height measurement spec mode that is currently relevant to the LayoutManager. + * + *

This value is set only if the LayoutManager opts into the AutoMeasure api via + * {@link #setAutoMeasureEnabled(boolean)}. + * + *

When RecyclerView is running a layout, this value is always set to + * {@link View.MeasureSpec#EXACTLY} even if it was measured with a different spec mode. + * + * @return Height measure spec mode + * @see View.MeasureSpec#getMode(int) + */ + public int getHeightMode() { + return mHeightMode; + } + + /** + * Returns the width that is currently relevant to the LayoutManager. + * + *

This value is usually equal to the laid out width of the {@link RecyclerView} but may + * reflect the current {@link android.view.View.MeasureSpec} width if the + * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of + * measuring. The LayoutManager must always use this method to retrieve the width relevant + * to it at any given time. + * + * @return Width in pixels + */ + @Px + public int getWidth() { + return mWidth; + } + + /** + * Returns the height that is currently relevant to the LayoutManager. + * + *

This value is usually equal to the laid out height of the {@link RecyclerView} but may + * reflect the current {@link android.view.View.MeasureSpec} height if the + * {@link LayoutManager} is using AutoMeasure and the RecyclerView is in the process of + * measuring. The LayoutManager must always use this method to retrieve the height relevant + * to it at any given time. + * + * @return Height in pixels + */ + @Px + public int getHeight() { + return mHeight; + } + + /** + * Return the left padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingLeft() { + return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0; + } + + /** + * Return the top padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingTop() { + return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0; + } + + /** + * Return the right padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingRight() { + return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0; + } + + /** + * Return the bottom padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingBottom() { + return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0; + } + + /** + * Return the start padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingStart() { + return mRecyclerView != null ? ViewCompat.getPaddingStart(mRecyclerView) : 0; + } + + /** + * Return the end padding of the parent RecyclerView + * + * @return Padding in pixels + */ + @Px + public int getPaddingEnd() { + return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0; + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has focus. + * + * @return True if the RecyclerView has focus, false otherwise. + * @see View#isFocused() + */ + public boolean isFocused() { + return mRecyclerView != null && mRecyclerView.isFocused(); + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus. + * + * @return true if the RecyclerView has or contains focus + * @see View#hasFocus() + */ + public boolean hasFocus() { + return mRecyclerView != null && mRecyclerView.hasFocus(); + } + + /** + * Returns the item View which has or contains focus. + * + * @return A direct child of RecyclerView which has focus or contains the focused child. + */ + @Nullable + public View getFocusedChild() { + if (mRecyclerView == null) { + return null; + } + final View focused = mRecyclerView.getFocusedChild(); + if (focused == null || mChildHelper.isHidden(focused)) { + return null; + } + return focused; + } + + /** + * Returns the number of items in the adapter bound to the parent RecyclerView. + *

+ * Note that this number is not necessarily equal to + * {@link State#getItemCount() State#getItemCount()}. In methods where {@link State} is + * available, you should use {@link State#getItemCount() State#getItemCount()} instead. + * For more details, check the documentation for + * {@link State#getItemCount() State#getItemCount()}. + * + * @return The number of items in the bound adapter + * @see State#getItemCount() + */ + public int getItemCount() { + final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null; + return a != null ? a.getItemCount() : 0; + } + + /** + * Offset all child views attached to the parent RecyclerView by dx pixels along + * the horizontal axis. + * + * @param dx Pixels to offset by + */ + public void offsetChildrenHorizontal(@Px int dx) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenHorizontal(dx); + } + } + + /** + * Offset all child views attached to the parent RecyclerView by dy pixels along + * the vertical axis. + * + * @param dy Pixels to offset by + */ + public void offsetChildrenVertical(@Px int dy) { + if (mRecyclerView != null) { + mRecyclerView.offsetChildrenVertical(dy); + } + } + + /** + * Flags a view so that it will not be scrapped or recycled. + *

+ * Scope of ignoring a child is strictly restricted to position tracking, scrapping and + * recyling. Methods like {@link #removeAndRecycleAllViews(Recycler)} will ignore the child + * whereas {@link #removeAllViews()} or {@link #offsetChildrenHorizontal(int)} will not + * ignore the child. + *

+ * Before this child can be recycled again, you have to call + * {@link #stopIgnoringView(View)}. + *

+ * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * + * @param view View to ignore. + * @see #stopIgnoringView(View) + */ + public void ignoreView(@NonNull View view) { + if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) { + // checking this because calling this method on a recycled or detached view may + // cause loss of state. + throw new IllegalArgumentException("View should be fully attached to be ignored" + + mRecyclerView.exceptionLabel()); + } + final ViewHolder vh = getChildViewHolderInt(view); + vh.addFlags(ViewHolder.FLAG_IGNORE); + mRecyclerView.mViewInfoStore.removeViewHolder(vh); + } + + /** + * View can be scrapped and recycled again. + *

+ * Note that calling this method removes all information in the view holder. + *

+ * You can call this method only if your LayoutManger is in onLayout or onScroll callback. + * + * @param view View to ignore. + */ + public void stopIgnoringView(@NonNull View view) { + final ViewHolder vh = getChildViewHolderInt(view); + vh.stopIgnoring(); + vh.resetInternal(); + vh.addFlags(ViewHolder.FLAG_INVALID); + } + + /** + * Temporarily detach and scrap all currently attached child views. Views will be scrapped + * into the given Recycler. The Recycler may prefer to reuse scrap views before + * other views that were previously recycled. + * + * @param recycler Recycler to scrap views into + */ + public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { + final int childCount = getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View v = getChildAt(i); + scrapOrRecycleView(recycler, i, v); + } + } + + private void scrapOrRecycleView(Recycler recycler, int index, View view) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + if (viewHolder.shouldIgnore()) { + if (sVerboseLoggingEnabled) { + Log.d(TAG, "ignoring view " + viewHolder); + } + return; + } + if (viewHolder.isInvalid() && !viewHolder.isRemoved() + && !mRecyclerView.mAdapter.hasStableIds()) { + removeViewAt(index); + recycler.recycleViewHolderInternal(viewHolder); + } else { + detachViewAt(index); + recycler.scrapView(view); + mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); + } + } + + /** + * Recycles the scrapped views. + *

+ * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is + * the expected behavior if scrapped views are used for animations. Otherwise, we need to + * call remove and invalidate RecyclerView to ensure UI update. + * + * @param recycler Recycler + */ + void removeAndRecycleScrapInt(Recycler recycler) { + final int scrapCount = recycler.getScrapCount(); + // Loop backward, recycler might be changed by removeDetachedView() + for (int i = scrapCount - 1; i >= 0; i--) { + final View scrap = recycler.getScrapViewAt(i); + final ViewHolder vh = getChildViewHolderInt(scrap); + if (vh.shouldIgnore()) { + continue; + } + // If the scrap view is animating, we need to cancel them first. If we cancel it + // here, ItemAnimator callback may recycle it which will cause double recycling. + // To avoid this, we mark it as not recyclable before calling the item animator. + // Since removeDetachedView calls a user API, a common mistake (ending animations on + // the view) may recycle it too, so we guard it before we call user APIs. + vh.setIsRecyclable(false); + if (vh.isTmpDetached()) { + mRecyclerView.removeDetachedView(scrap, false); + } + if (mRecyclerView.mItemAnimator != null) { + mRecyclerView.mItemAnimator.endAnimation(vh); + } + vh.setIsRecyclable(true); + recycler.quickRecycleScrapView(scrap); + } + recycler.clearScrap(); + if (scrapCount > 0) { + mRecyclerView.invalidate(); + } + } + + + /** + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView and any added item decorations into account. + * + *

If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

+ * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant + */ + public void measureChild(@NonNull View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } + } + + /** + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. + *

+ * Use this method if the View is already measured once in this layout pass. + */ + boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height); + } + + // we may consider making this public + + /** + * RecyclerView internally does its own View measurement caching which should help with + * WRAP_CONTENT. + *

+ * Use this method if the View is not yet measured and you need to decide whether to + * measure this View or not. + */ + boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) { + return child.isLayoutRequested() + || !mMeasurementCacheEnabled + || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width) + || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height); + } + + /** + * In addition to the View Framework's measurement cache, RecyclerView uses its own + * additional measurement cache for its children to avoid re-measuring them when not + * necessary. It is on by default but it can be turned off via + * {@link #setMeasurementCacheEnabled(boolean)}. + * + * @return True if measurement cache is enabled, false otherwise. + * @see #setMeasurementCacheEnabled(boolean) + */ + public boolean isMeasurementCacheEnabled() { + return mMeasurementCacheEnabled; + } + + /** + * Sets whether RecyclerView should use its own measurement cache for the children. This is + * a more aggressive cache than the framework uses. + * + * @param measurementCacheEnabled True to enable the measurement cache, false otherwise. + * @see #isMeasurementCacheEnabled() + */ + public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) { + mMeasurementCacheEnabled = measurementCacheEnabled; + } + + private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) { + final int specMode = MeasureSpec.getMode(spec); + final int specSize = MeasureSpec.getSize(spec); + if (dimension > 0 && childSize != dimension) { + return false; + } + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + return true; + case MeasureSpec.AT_MOST: + return specSize >= childSize; + case MeasureSpec.EXACTLY: + return specSize == childSize; + } + return false; + } + + /** + * Measure a child view using standard measurement policy, taking the padding + * of the parent RecyclerView, any added item decorations and the child margins + * into account. + * + *

If the RecyclerView can be scrolled in either dimension the caller may + * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.

+ * + * @param child Child view to measure + * @param widthUsed Width in pixels currently consumed by other views, if relevant + * @param heightUsed Height in pixels currently consumed by other views, if relevant + */ + public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + widthUsed += insets.left + insets.right; + heightUsed += insets.top + insets.bottom; + + final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(), + getPaddingLeft() + getPaddingRight() + + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(), + getPaddingTop() + getPaddingBottom() + + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, + canScrollVertically()); + if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) { + child.measure(widthSpec, heightSpec); + } + } + + /** + * Calculate a MeasureSpec value for measuring a child view in one dimension. + * + * @param parentSize Size of the parent view where the child will be placed + * @param padding Total space currently consumed by other elements of the parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * @return a MeasureSpec value for the child view + * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)} + */ + @Deprecated + public static int getChildMeasureSpec(int parentSize, int padding, int childDimension, + boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else { + // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap + // instead using UNSPECIFIED. + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + // TODO this should be my spec. + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + /** + * Calculate a MeasureSpec value for measuring a child view in one dimension. + * + * @param parentSize Size of the parent view where the child will be placed + * @param parentMode The measurement spec mode of the parent + * @param padding Total space currently consumed by other elements of parent + * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT. + * Generally obtained from the child view's LayoutParams + * @param canScroll true if the parent RecyclerView can scroll in this dimension + * @return a MeasureSpec value for the child view + */ + public static int getChildMeasureSpec(int parentSize, int parentMode, int padding, + int childDimension, boolean canScroll) { + int size = Math.max(0, parentSize - padding); + int resultSize = 0; + int resultMode = 0; + if (canScroll) { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + switch (parentMode) { + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + resultSize = size; + resultMode = parentMode; + break; + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + } + } else { + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.MATCH_PARENT) { + resultSize = size; + resultMode = parentMode; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) { + resultMode = MeasureSpec.AT_MOST; + } else { + resultMode = MeasureSpec.UNSPECIFIED; + } + + } + } + //noinspection WrongConstant + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + /** + * Returns the measured width of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. + * + * @param child Child view to query + * @return child's measured width plus ItemDecoration insets + * @see View#getMeasuredWidth() + */ + public int getDecoratedMeasuredWidth(@NonNull View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredWidth() + insets.left + insets.right; + } + + /** + * Returns the measured height of the given child, plus the additional size of + * any insets applied by {@link ItemDecoration ItemDecorations}. + * + * @param child Child view to query + * @return child's measured height plus ItemDecoration insets + * @see View#getMeasuredHeight() + */ + public int getDecoratedMeasuredHeight(@NonNull View child) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + return child.getMeasuredHeight() + insets.top + insets.bottom; + } + + /** + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations}. + * + *

LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:

+ *
    + *
  • {@link #layoutDecoratedWithMargins(View, int, int, int, int)}
  • + *
  • {@link #getDecoratedBoundsWithMargins(View, Rect)}
  • + *
  • {@link #measureChild(View, int, int)}
  • + *
  • {@link #measureChildWithMargins(View, int, int)}
  • + *
  • {@link #getDecoratedLeft(View)}
  • + *
  • {@link #getDecoratedTop(View)}
  • + *
  • {@link #getDecoratedRight(View)}
  • + *
  • {@link #getDecoratedBottom(View)}
  • + *
  • {@link #getDecoratedMeasuredWidth(View)}
  • + *
  • {@link #getDecoratedMeasuredHeight(View)}
  • + *
+ * + * @param child Child to lay out + * @param left Left edge, with item decoration insets included + * @param top Top edge, with item decoration insets included + * @param right Right edge, with item decoration insets included + * @param bottom Bottom edge, with item decoration insets included + * @see View#layout(int, int, int, int) + * @see #layoutDecoratedWithMargins(View, int, int, int, int) + */ + public void layoutDecorated(@NonNull View child, int left, int top, int right, int bottom) { + final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + child.layout(left + insets.left, top + insets.top, right - insets.right, + bottom - insets.bottom); + } + + /** + * Lay out the given child view within the RecyclerView using coordinates that + * include any current {@link ItemDecoration ItemDecorations} and margins. + * + *

LayoutManagers should prefer working in sizes and coordinates that include + * item decoration insets whenever possible. This allows the LayoutManager to effectively + * ignore decoration insets within measurement and layout code. See the following + * methods:

+ *
    + *
  • {@link #layoutDecorated(View, int, int, int, int)}
  • + *
  • {@link #measureChild(View, int, int)}
  • + *
  • {@link #measureChildWithMargins(View, int, int)}
  • + *
  • {@link #getDecoratedLeft(View)}
  • + *
  • {@link #getDecoratedTop(View)}
  • + *
  • {@link #getDecoratedRight(View)}
  • + *
  • {@link #getDecoratedBottom(View)}
  • + *
  • {@link #getDecoratedMeasuredWidth(View)}
  • + *
  • {@link #getDecoratedMeasuredHeight(View)}
  • + *
+ * + * @param child Child to lay out + * @param left Left edge, with item decoration insets and left margin included + * @param top Top edge, with item decoration insets and top margin included + * @param right Right edge, with item decoration insets and right margin included + * @param bottom Bottom edge, with item decoration insets and bottom margin included + * @see View#layout(int, int, int, int) + * @see #layoutDecorated(View, int, int, int, int) + */ + public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, + int bottom) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + final Rect insets = lp.mDecorInsets; + child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, + right - insets.right - lp.rightMargin, + bottom - insets.bottom - lp.bottomMargin); + } + + /** + * Calculates the bounding box of the View while taking into account its matrix changes + * (translation, scale etc) with respect to the RecyclerView. + *

+ * If {@code includeDecorInsets} is {@code true}, they are applied first before applying + * the View's matrix so that the decor offsets also go through the same transformation. + * + * @param child The ItemView whose bounding box should be calculated. + * @param includeDecorInsets True if the decor insets should be included in the bounding box + * @param out The rectangle into which the output will be written. + */ + public void getTransformedBoundingBox(@NonNull View child, boolean includeDecorInsets, + @NonNull Rect out) { + if (includeDecorInsets) { + Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets; + out.set(-insets.left, -insets.top, + child.getWidth() + insets.right, child.getHeight() + insets.bottom); + } else { + out.set(0, 0, child.getWidth(), child.getHeight()); + } + + if (mRecyclerView != null) { + final Matrix childMatrix = child.getMatrix(); + if (childMatrix != null && !childMatrix.isIdentity()) { + final RectF tempRectF = mRecyclerView.mTempRectF; + tempRectF.set(out); + childMatrix.mapRect(tempRectF); + out.set( + (int) Math.floor(tempRectF.left), + (int) Math.floor(tempRectF.top), + (int) Math.ceil(tempRectF.right), + (int) Math.ceil(tempRectF.bottom) + ); + } + } + out.offset(child.getLeft(), child.getTop()); + } + + /** + * Returns the bounds of the view including its decoration and margins. + * + * @param view The view element to check + * @param outBounds A rect that will receive the bounds of the element including its + * decoration and margins. + */ + public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) { + RecyclerView.getDecoratedBoundsWithMarginsInt(view, outBounds); + } + + /** + * Returns the left edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child left edge with offsets applied + * @see #getLeftDecorationWidth(View) + */ + public int getDecoratedLeft(@NonNull View child) { + return child.getLeft() - getLeftDecorationWidth(child); + } + + /** + * Returns the top edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child top edge with offsets applied + * @see #getTopDecorationHeight(View) + */ + public int getDecoratedTop(@NonNull View child) { + return child.getTop() - getTopDecorationHeight(child); + } + + /** + * Returns the right edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child right edge with offsets applied + * @see #getRightDecorationWidth(View) + */ + public int getDecoratedRight(@NonNull View child) { + return child.getRight() + getRightDecorationWidth(child); + } + + /** + * Returns the bottom edge of the given child view within its parent, offset by any applied + * {@link ItemDecoration ItemDecorations}. + * + * @param child Child to query + * @return Child bottom edge with offsets applied + * @see #getBottomDecorationHeight(View) + */ + public int getDecoratedBottom(@NonNull View child) { + return child.getBottom() + getBottomDecorationHeight(child); + } + + /** + * Calculates the item decor insets applied to the given child and updates the provided + * Rect instance with the inset values. + *

    + *
  • The Rect's left is set to the total width of left decorations.
  • + *
  • The Rect's top is set to the total height of top decorations.
  • + *
  • The Rect's right is set to the total width of right decorations.
  • + *
  • The Rect's bottom is set to total height of bottom decorations.
  • + *
+ *

+ * Note that item decorations are automatically calculated when one of the LayoutManager's + * measure child methods is called. If you need to measure the child with custom specs via + * {@link View#measure(int, int)}, you can use this method to get decorations. + * + * @param child The child view whose decorations should be calculated + * @param outRect The Rect to hold result values + */ + public void calculateItemDecorationsForChild(@NonNull View child, @NonNull Rect outRect) { + if (mRecyclerView == null) { + outRect.set(0, 0, 0, 0); + return; + } + Rect insets = mRecyclerView.getItemDecorInsetsForChild(child); + outRect.set(insets); + } + + /** + * Returns the total height of item decorations applied to child's top. + *

+ * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's top. + * @see #getDecoratedTop(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getTopDecorationHeight(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.top; + } + + /** + * Returns the total height of item decorations applied to child's bottom. + *

+ * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total height of item decorations applied to the child's bottom. + * @see #getDecoratedBottom(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getBottomDecorationHeight(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.bottom; + } + + /** + * Returns the total width of item decorations applied to child's left. + *

+ * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's left. + * @see #getDecoratedLeft(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getLeftDecorationWidth(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.left; + } + + /** + * Returns the total width of item decorations applied to child's right. + *

+ * Note that this value is not updated until the View is measured or + * {@link #calculateItemDecorationsForChild(View, Rect)} is called. + * + * @param child Child to query + * @return The total width of item decorations applied to the child's right. + * @see #getDecoratedRight(View) + * @see #calculateItemDecorationsForChild(View, Rect) + */ + public int getRightDecorationWidth(@NonNull View child) { + return ((LayoutParams) child.getLayoutParams()).mDecorInsets.right; + } + + /** + * Called when searching for a focusable view in the given direction has failed + * for the current content of the RecyclerView. + * + *

This is the LayoutManager's opportunity to populate views in the given direction + * to fulfill the request if it can. The LayoutManager should attach and return + * the view to be focused, if a focusable view in the given direction is found. + * Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns + * the next unfocusable view to become visible on the screen. This unfocusable view is + * typically the first view that's either partially or fully out of RV's padded bounded + * area in the given direction. The default implementation returns null.

+ * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @param recycler The recycler to use for obtaining views for currently offscreen items + * @param state Transient state of RecyclerView + * @return The chosen view to be focused if a focusable view is found, otherwise an + * unfocusable view to become visible onto the screen, else null. + */ + @Nullable + public View onFocusSearchFailed(@NonNull View focused, int direction, + @NonNull Recycler recycler, @NonNull State state) { + return null; + } + + /** + * This method gives a LayoutManager an opportunity to intercept the initial focus search + * before the default behavior of {@link FocusFinder} is used. If this method returns + * null FocusFinder will attempt to find a focusable child view. If it fails + * then {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} + * will be called to give the LayoutManager an opportunity to add new views for items + * that did not have attached views representing them. The LayoutManager should not add + * or remove views from this method. + * + * @param focused The currently focused view + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @return A descendant view to focus or null to fall back to default behavior. + * The default implementation returns null. + */ + @Nullable + public View onInterceptFocusSearch(@NonNull View focused, int direction) { + return null; + } + + /** + * Returns the scroll amount that brings the given rect in child's coordinate system within + * the padded area of RecyclerView. + * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @return The array containing the scroll amount in x and y directions that brings the + * given rect into RV's padded area. + */ + private int[] getChildRectangleOnScreenScrollAmount(View child, Rect rect) { + int[] out = new int[2]; + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final int childLeft = child.getLeft() + rect.left - child.getScrollX(); + final int childTop = child.getTop() + rect.top - child.getScrollY(); + final int childRight = childLeft + rect.width(); + final int childBottom = childTop + rect.height(); + + final int offScreenLeft = Math.min(0, childLeft - parentLeft); + final int offScreenTop = Math.min(0, childTop - parentTop); + final int offScreenRight = Math.max(0, childRight - parentRight); + final int offScreenBottom = Math.max(0, childBottom - parentBottom); + + // Favor the "start" layout direction over the end when bringing one side or the other + // of a large rect into view. If we decide to bring in end because start is already + // visible, limit the scroll such that start won't go out of bounds. + final int dx; + if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { + dx = offScreenRight != 0 ? offScreenRight + : Math.max(offScreenLeft, childRight - parentRight); + } else { + dx = offScreenLeft != 0 ? offScreenLeft + : Math.min(childLeft - parentLeft, offScreenRight); + } + + // Favor bringing the top into view over the bottom. If top is already visible and + // we should scroll to make bottom visible, make sure top does not go out of bounds. + final int dy = offScreenTop != 0 ? offScreenTop + : Math.min(childTop - parentTop, offScreenBottom); + out[0] = dx; + out[1] = dy; + return out; + } + + /** + * Called when a child of the RecyclerView wants a particular rectangle to be positioned + * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View, + * android.graphics.Rect, boolean)} for more details. + * + *

The base implementation will attempt to perform a standard programmatic scroll + * to bring the given rect into view, within the padded area of the RecyclerView.

+ * + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, + @NonNull View child, @NonNull Rect rect, boolean immediate) { + return requestChildRectangleOnScreen(parent, child, rect, immediate, false); + } + + /** + * Requests that the given child of the RecyclerView be positioned onto the screen. This + * method can be called for both unfocusable and focusable child views. For unfocusable + * child views, focusedChildVisible is typically true in which case, layout manager + * makes the child view visible only if the currently focused child stays in-bounds of RV. + * + * @param parent The parent RecyclerView. + * @param child The direct child making the request. + * @param rect The rectangle in the child's coordinates the child + * wishes to be on the screen. + * @param immediate True to forbid animated or delayed scrolling, + * false otherwise + * @param focusedChildVisible Whether the currently focused view must stay visible. + * @return Whether the group scrolled to handle the operation + */ + public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, + @NonNull View child, @NonNull Rect rect, boolean immediate, + boolean focusedChildVisible) { + int[] scrollAmount = getChildRectangleOnScreenScrollAmount(child, rect + ); + int dx = scrollAmount[0]; + int dy = scrollAmount[1]; + if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) { + if (dx != 0 || dy != 0) { + if (immediate) { + parent.scrollBy(dx, dy); + } else { + parent.smoothScrollBy(dx, dy); + } + return true; + } + } + return false; + } + + /** + * Returns whether the given child view is partially or fully visible within the padded + * bounded area of RecyclerView, depending on the input parameters. + * A view is partially visible if it has non-zero overlap with RV's padded bounded area. + * If acceptEndPointInclusion flag is set to true, it's also considered partially + * visible if it's located outside RV's bounds and it's hitting either RV's start or end + * bounds. + * + * @param child The child view to be examined. + * @param completelyVisible If true, the method returns true if and only if the + * child is + * completely visible. If false, the method returns true + * if and + * only if the child is only partially visible (that is it + * will + * return false if the child is either completely visible + * or out + * of RV's bounds). + * @param acceptEndPointInclusion If the view's endpoint intersection with RV's start of end + * bounds is enough to consider it partially visible, + * false otherwise. + * @return True if the given child is partially or fully visible, false otherwise. + */ + public boolean isViewPartiallyVisible(@NonNull View child, boolean completelyVisible, + boolean acceptEndPointInclusion) { + int boundsFlag = (ViewBoundsCheck.FLAG_CVS_GT_PVS | ViewBoundsCheck.FLAG_CVS_EQ_PVS + | ViewBoundsCheck.FLAG_CVE_LT_PVE | ViewBoundsCheck.FLAG_CVE_EQ_PVE); + boolean isViewFullyVisible = mHorizontalBoundCheck.isViewWithinBoundFlags(child, + boundsFlag) + && mVerticalBoundCheck.isViewWithinBoundFlags(child, boundsFlag); + if (completelyVisible) { + return isViewFullyVisible; + } else { + return !isViewFullyVisible; + } + } + + /** + * Returns whether the currently focused child stays within RV's bounds with the given + * amount of scrolling. + * + * @param parent The parent RecyclerView. + * @param dx The scrolling in x-axis direction to be performed. + * @param dy The scrolling in y-axis direction to be performed. + * @return {@code false} if the focused child is not at least partially visible after + * scrolling or no focused child exists, {@code true} otherwise. + */ + private boolean isFocusedChildVisibleAfterScrolling(RecyclerView parent, int dx, int dy) { + final View focusedChild = parent.getFocusedChild(); + if (focusedChild == null) { + return false; + } + final int parentLeft = getPaddingLeft(); + final int parentTop = getPaddingTop(); + final int parentRight = getWidth() - getPaddingRight(); + final int parentBottom = getHeight() - getPaddingBottom(); + final Rect bounds = mRecyclerView.mTempRect; + getDecoratedBoundsWithMargins(focusedChild, bounds); + + if (bounds.left - dx >= parentRight || bounds.right - dx <= parentLeft + || bounds.top - dy >= parentBottom || bounds.bottom - dy <= parentTop) { + return false; + } + return true; + } + + /** + * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} + */ + @Deprecated + public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull View child, + @Nullable View focused) { + // eat the request if we are in the middle of a scroll or layout + return isSmoothScrolling() || parent.isComputingLayout(); + } + + /** + * Called when a descendant view of the RecyclerView requests focus. + * + *

A LayoutManager wishing to keep focused views aligned in a specific + * portion of the view may implement that behavior in an override of this method.

+ * + *

If the LayoutManager executes different behavior that should override the default + * behavior of scrolling the focused child on screen instead of running alongside it, + * this method should return true.

+ * + * @param parent The RecyclerView hosting this LayoutManager + * @param state Current state of RecyclerView + * @param child Direct child of the RecyclerView containing the newly focused view + * @param focused The newly focused view. This may be the same view as child or it may be + * null + * @return true if the default scroll behavior should be suppressed + */ + public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state, + @NonNull View child, @Nullable View focused) { + return onRequestChildFocus(parent, child, focused); + } + + /** + * Called if the RecyclerView this LayoutManager is bound to has a different adapter set via + * {@link RecyclerView#setAdapter(Adapter)} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)}. The LayoutManager may use this + * opportunity to clear caches and configure state such that it can relayout appropriately + * with the new data and potentially new view types. + * + *

The default implementation removes all currently attached views.

+ * + * @param oldAdapter The previous adapter instance. Will be null if there was previously no + * adapter. + * @param newAdapter The new adapter instance. Might be null if + * {@link RecyclerView#setAdapter(RecyclerView.Adapter)} is called with + * {@code null}. + */ + public void onAdapterChanged(@Nullable Adapter oldAdapter, @Nullable Adapter newAdapter) { + } + + /** + * Called to populate focusable views within the RecyclerView. + * + *

The LayoutManager implementation should return true if the default + * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be + * suppressed.

+ * + *

The default implementation returns false to trigger RecyclerView + * to fall back to the default ViewGroup behavior.

+ * + * @param recyclerView The RecyclerView hosting this LayoutManager + * @param views List of output views. This method should add valid focusable views + * to this list. + * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * @param focusableMode The type of focusables to be added. + * @return true to suppress the default behavior, false to add default focusables after + * this method returns. + * @see #FOCUSABLES_ALL + * @see #FOCUSABLES_TOUCH_MODE + */ + public boolean onAddFocusables(@NonNull RecyclerView recyclerView, + @NonNull ArrayList views, int direction, int focusableMode) { + return false; + } + + /** + * Called in response to a call to {@link Adapter#notifyDataSetChanged()} or + * {@link RecyclerView#swapAdapter(Adapter, boolean)} ()} and signals that the the entire + * data set has changed. + */ + public void onItemsChanged(@NonNull RecyclerView recyclerView) { + } + + /** + * Called when items have been added to the adapter. The LayoutManager may choose to + * requestLayout if the inserted items would require refreshing the currently visible set + * of child views. (e.g. currently empty space would be filled by appended items, etc.) + */ + public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { + } + + /** + * Called when items have been removed from the adapter. + */ + public void onItemsRemoved(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { + } + + /** + * Called when items have been changed in the adapter. + * To receive payload, override {@link #onItemsUpdated(RecyclerView, int, int, Object)} + * instead, then this callback will not be invoked. + */ + public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount) { + } + + /** + * Called when items have been changed in the adapter and with optional payload. + * Default implementation calls {@link #onItemsUpdated(RecyclerView, int, int)}. + */ + public void onItemsUpdated(@NonNull RecyclerView recyclerView, int positionStart, + int itemCount, @Nullable Object payload) { + onItemsUpdated(recyclerView, positionStart, itemCount); + } + + /** + * Called when an item is moved withing the adapter. + *

+ * Note that, an item may also change position in response to another ADD/REMOVE/MOVE + * operation. This callback is only called if and only if {@link Adapter#notifyItemMoved} + * is called. + */ + public void onItemsMoved(@NonNull RecyclerView recyclerView, int from, int to, + int itemCount) { + + } + + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current state of RecyclerView + * @return The horizontal extent of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollExtent() + */ + public int computeHorizontalScrollExtent(@NonNull State state) { + return 0; + } + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current State of RecyclerView where you can find total item count + * @return The horizontal offset of the scrollbar's thumb + * @see RecyclerView#computeHorizontalScrollOffset() + */ + public int computeHorizontalScrollOffset(@NonNull State state) { + return 0; + } + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeHorizontalScrollRange()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current State of RecyclerView where you can find total item count + * @return The total horizontal range represented by the vertical scrollbar + * @see RecyclerView#computeHorizontalScrollRange() + */ + public int computeHorizontalScrollRange(@NonNull State state) { + return 0; + } + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeVerticalScrollExtent()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current state of RecyclerView + * @return The vertical extent of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollExtent() + */ + public int computeVerticalScrollExtent(@NonNull State state) { + return 0; + } + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeVerticalScrollOffset()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current State of RecyclerView where you can find total item count + * @return The vertical offset of the scrollbar's thumb + * @see RecyclerView#computeVerticalScrollOffset() + */ + public int computeVerticalScrollOffset(@NonNull State state) { + return 0; + } + + /** + *

Override this method if you want to support scroll bars.

+ * + *

Read {@link RecyclerView#computeVerticalScrollRange()} for details.

+ * + *

Default implementation returns 0.

+ * + * @param state Current State of RecyclerView where you can find total item count + * @return The total vertical range represented by the vertical scrollbar + * @see RecyclerView#computeVerticalScrollRange() + */ + public int computeVerticalScrollRange(@NonNull State state) { + return 0; + } + + /** + * Measure the attached RecyclerView. Implementations must call + * {@link #setMeasuredDimension(int, int)} before returning. + *

+ * It is strongly advised to use the AutoMeasure mechanism by overriding + * {@link #isAutoMeasureEnabled()} to return true as AutoMeasure handles all the standard + * measure cases including when the RecyclerView's layout_width or layout_height have been + * set to wrap_content. If {@link #isAutoMeasureEnabled()} is overridden to return true, + * this method should not be overridden. + *

+ * The default implementation will handle EXACTLY measurements and respect + * the minimum width and height properties of the host RecyclerView if measured + * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView + * will consume all available space. + * + * @param recycler Recycler + * @param state Transient state of RecyclerView + * @param widthSpec Width {@link android.view.View.MeasureSpec} + * @param heightSpec Height {@link android.view.View.MeasureSpec} + * @see #isAutoMeasureEnabled() + * @see #setMeasuredDimension(int, int) + */ + public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, + int heightSpec) { + mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); + } + + /** + * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the + * host RecyclerView. + * + * @param widthSize Measured width + * @param heightSize Measured height + */ + public void setMeasuredDimension(int widthSize, int heightSize) { + mRecyclerView.setMeasuredDimension(widthSize, heightSize); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumWidth()} + */ + @Px + public int getMinimumWidth() { + return ViewCompat.getMinimumWidth(mRecyclerView); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumHeight()} + */ + @Px + public int getMinimumHeight() { + return ViewCompat.getMinimumHeight(mRecyclerView); + } + + /** + *

Called when the LayoutManager should save its state. This is a good time to save your + * scroll position, configuration and anything else that may be required to restore the same + * layout state if the LayoutManager is recreated.

+ *

RecyclerView does NOT verify if the LayoutManager has changed between state save and + * restore. This will let you share information between your LayoutManagers but it is also + * your responsibility to make sure they use the same parcelable class.

+ * + * @return Necessary information for LayoutManager to be able to restore its state + */ + @Nullable + public Parcelable onSaveInstanceState() { + return null; + } + + /** + * Called when the RecyclerView is ready to restore the state based on a previous + * RecyclerView. + * + * Notice that this might happen after an actual layout, based on how Adapter prefers to + * restore State. See {@link Adapter#getStateRestorationPolicy()} for more information. + * + * @param state The parcelable that was returned by the previous LayoutManager's + * {@link #onSaveInstanceState()} method. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onRestoreInstanceState(Parcelable state) { + + } + + void stopSmoothScroller() { + if (mSmoothScroller != null) { + mSmoothScroller.stop(); + } + } + + void onSmoothScrollerStopped(SmoothScroller smoothScroller) { + if (mSmoothScroller == smoothScroller) { + mSmoothScroller = null; + } + } + + /** + * RecyclerView calls this method to notify LayoutManager that scroll state has changed. + * + * @param state The new scroll state for RecyclerView + */ + public void onScrollStateChanged(int state) { + } + + /** + * Removes all views and recycles them using the given recycler. + *

+ * If you want to clean cached views as well, you should call {@link Recycler#clear()} too. + *

+ * If a View is marked as "ignored", it is not removed nor recycled. + * + * @param recycler Recycler to use to recycle children + * @see #removeAndRecycleView(View, Recycler) + * @see #removeAndRecycleViewAt(int, Recycler) + * @see #ignoreView(View) + */ + public void removeAndRecycleAllViews(@NonNull Recycler recycler) { + for (int i = getChildCount() - 1; i >= 0; i--) { + final View view = getChildAt(i); + if (!getChildViewHolderInt(view).shouldIgnore()) { + removeAndRecycleViewAt(i, recycler); + } + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfoCompat info) { + onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info); + } + + /** + * Called by the AccessibilityDelegate when the information about the current layout should + * be populated. + *

+ * Default implementation adds a {@link + * androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat}. + *

+ * You should override + * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and + * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for + * more accurate accessibility information. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param info The info that should be filled by the LayoutManager + * @see View#onInitializeAccessibilityNodeInfo( + *android.view.accessibility.AccessibilityNodeInfo) + * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) + * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) + */ + public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler, + @NonNull State state, @NonNull AccessibilityNodeInfoCompat info) { + if (mRecyclerView.canScrollVertically(-1) || mRecyclerView.canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); + } + if (mRecyclerView.canScrollVertically(1) || mRecyclerView.canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + info.setScrollable(true); + } + final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo = + AccessibilityNodeInfoCompat.CollectionInfoCompat + .obtain(getRowCountForAccessibility(recycler, state), + getColumnCountForAccessibility(recycler, state), + isLayoutHierarchical(recycler, state), + getSelectionModeForAccessibility(recycler, state)); + info.setCollectionInfo(collectionInfo); + } + + // called by accessibility delegate + public void onInitializeAccessibilityEvent(@NonNull AccessibilityEvent event) { + onInitializeAccessibilityEvent(mRecyclerView.mRecycler, mRecyclerView.mState, event); + } + + /** + * Called by the accessibility delegate to initialize an accessibility event. + *

+ * Default implementation adds item count and scroll information to the event. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param event The event instance to initialize + * @see View#onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(@NonNull Recycler recycler, @NonNull State state, + @NonNull AccessibilityEvent event) { + if (mRecyclerView == null || event == null) { + return; + } + event.setScrollable(mRecyclerView.canScrollVertically(1) + || mRecyclerView.canScrollVertically(-1) + || mRecyclerView.canScrollHorizontally(-1) + || mRecyclerView.canScrollHorizontally(1)); + + if (mRecyclerView.mAdapter != null) { + event.setItemCount(mRecyclerView.mAdapter.getItemCount()); + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfoCompat info) { + final ViewHolder vh = getChildViewHolderInt(host); + // avoid trying to create accessibility node info for removed children + if (vh != null && !vh.isRemoved() && !mChildHelper.isHidden(vh.itemView)) { + onInitializeAccessibilityNodeInfoForItem(mRecyclerView.mRecycler, + mRecyclerView.mState, host, info); + } + } + + /** + * Called by the AccessibilityDelegate when the accessibility information for a specific + * item should be populated. + *

+ * Default implementation adds basic positioning information about the item. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param host The child for which accessibility node info should be populated + * @param info The info to fill out about the item + * @see android.widget.AbsListView#onInitializeAccessibilityNodeInfoForItem(View, int, + * android.view.accessibility.AccessibilityNodeInfo) + */ + public void onInitializeAccessibilityNodeInfoForItem(@NonNull Recycler recycler, + @NonNull State state, @NonNull View host, + @NonNull AccessibilityNodeInfoCompat info) { + } + + /** + * A LayoutManager can call this method to force RecyclerView to run simple animations in + * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data + * change). + *

+ * Note that, calling this method will not guarantee that RecyclerView will run animations + * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will + * not run any animations but will still clear this flag after the layout is complete. + */ + public void requestSimpleAnimationsInNextLayout() { + mRequestedSimpleAnimations = true; + } + + /** + * Returns the selection mode for accessibility. Should be + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}, + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_SINGLE} or + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_MULTIPLE}. + *

+ * Default implementation returns + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return Selection mode for accessibility. Default implementation returns + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}. + */ + public int getSelectionModeForAccessibility(@NonNull Recycler recycler, + @NonNull State state) { + return AccessibilityNodeInfoCompat.CollectionInfoCompat.SELECTION_MODE_NONE; + } + + /** + * Returns the number of rows for accessibility. + *

+ * Default implementation returns the number of items in the adapter if LayoutManager + * supports vertical scrolling or 1 if LayoutManager does not support vertical + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getRowCountForAccessibility(@NonNull Recycler recycler, @NonNull State state) { + return -1; + } + + /** + * Returns the number of columns for accessibility. + *

+ * Default implementation returns the number of items in the adapter if LayoutManager + * supports horizontal scrolling or 1 if LayoutManager does not support horizontal + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getColumnCountForAccessibility(@NonNull Recycler recycler, + @NonNull State state) { + return -1; + } + + /** + * Returns whether layout is hierarchical or not to be used for accessibility. + *

+ * Default implementation returns false. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return True if layout is hierarchical. + */ + public boolean isLayoutHierarchical(@NonNull Recycler recycler, @NonNull State state) { + return false; + } + + // called by accessibility delegate + boolean performAccessibilityAction(int action, @Nullable Bundle args) { + return performAccessibilityAction(mRecyclerView.mRecycler, mRecyclerView.mState, + action, args); + } + + /** + * Called by AccessibilityDelegate when an action is requested from the RecyclerView. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param action The action to perform + * @param args Optional action arguments + * @see View#performAccessibilityAction(int, android.os.Bundle) + */ + public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state, + int action, @Nullable Bundle args) { + if (mRecyclerView == null) { + return false; + } + int vScroll = 0, hScroll = 0; + int height = getHeight(); + int width = getWidth(); + Rect rect = new Rect(); + // Gets the visible rect on the screen except for the rotation or scale cases which + // might affect the result. + if (mRecyclerView.getMatrix().isIdentity() && mRecyclerView.getGlobalVisibleRect( + rect)) { + height = rect.height(); + width = rect.width(); + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: + if (mRecyclerView.canScrollVertically(-1)) { + vScroll = -(height - getPaddingTop() - getPaddingBottom()); + } + if (mRecyclerView.canScrollHorizontally(-1)) { + hScroll = -(width - getPaddingLeft() - getPaddingRight()); + } + break; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: + if (mRecyclerView.canScrollVertically(1)) { + vScroll = height - getPaddingTop() - getPaddingBottom(); + } + if (mRecyclerView.canScrollHorizontally(1)) { + hScroll = width - getPaddingLeft() - getPaddingRight(); + } + break; + } + if (vScroll == 0 && hScroll == 0) { + return false; + } + mRecyclerView.smoothScrollBy(hScroll, vScroll, null, UNDEFINED_DURATION, true); + return true; + } + + // called by accessibility delegate + boolean performAccessibilityActionForItem(@NonNull View view, int action, + @Nullable Bundle args) { + return performAccessibilityActionForItem(mRecyclerView.mRecycler, mRecyclerView.mState, + view, action, args); + } + + /** + * Called by AccessibilityDelegate when an accessibility action is requested on one of the + * children of LayoutManager. + *

+ * Default implementation does not do anything. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param view The child view on which the action is performed + * @param action The action to perform + * @param args Optional action arguments + * @return true if action is handled + * @see View#performAccessibilityAction(int, android.os.Bundle) + */ + public boolean performAccessibilityActionForItem(@NonNull Recycler recycler, + @NonNull State state, @NonNull View view, int action, @Nullable Bundle args) { + return false; + } + + /** + * Parse the xml attributes to get the most common properties used by layout managers. + * + * {@link android.R.attr#orientation} + * {@link androidx.recyclerview.R.attr#spanCount} + * {@link androidx.recyclerview.R.attr#reverseLayout} + * {@link androidx.recyclerview.R.attr#stackFromEnd} + * + * @return an object containing the properties as specified in the attrs. + */ + public static Properties getProperties(@NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + Properties properties = new Properties(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyleAttr, defStyleRes); + properties.orientation = a.getInt(R.styleable.RecyclerView_android_orientation, + DEFAULT_ORIENTATION); + properties.spanCount = a.getInt(R.styleable.RecyclerView_spanCount, 1); + properties.reverseLayout = a.getBoolean(R.styleable.RecyclerView_reverseLayout, false); + properties.stackFromEnd = a.getBoolean(R.styleable.RecyclerView_stackFromEnd, false); + a.recycle(); + return properties; + } + + void setExactMeasureSpecsFrom(RecyclerView recyclerView) { + setMeasureSpecs( + MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY) + ); + } + + /** + * Internal API to allow LayoutManagers to be measured twice. + *

+ * This is not public because LayoutManagers should be able to handle their layouts in one + * pass but it is very convenient to make existing LayoutManagers support wrapping content + * when both orientations are undefined. + *

+ * This API will be removed after default LayoutManagers properly implement wrap content in + * non-scroll orientation. + */ + boolean shouldMeasureTwice() { + return false; + } + + boolean hasFlexibleChildInBothOrientations() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final ViewGroup.LayoutParams lp = child.getLayoutParams(); + if (lp.width < 0 && lp.height < 0) { + return true; + } + } + return false; + } + + /** + * Some general properties that a LayoutManager may want to use. + */ + public static class Properties { + /** {@link android.R.attr#orientation} */ + public int orientation; + /** {@link androidx.recyclerview.R.attr#spanCount} */ + public int spanCount; + /** {@link androidx.recyclerview.R.attr#reverseLayout} */ + public boolean reverseLayout; + /** {@link androidx.recyclerview.R.attr#stackFromEnd} */ + public boolean stackFromEnd; + } + } + + /** + * An ItemDecoration allows the application to add a special drawing and layout offset + * to specific item views from the adapter's data set. This can be useful for drawing dividers + * between items, highlights, visual grouping boundaries and more. + * + *

All ItemDecorations are drawn in the order they were added, before the item + * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()} + * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView, + * RecyclerView.State)}.

+ */ + public abstract static class ItemDecoration { + /** + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn before the item views are drawn, + * and will thus appear underneath the views. + * + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView + */ + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull State state) { + onDraw(c, parent); + } + + /** + * @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent) { + } + + /** + * Draw any appropriate decorations into the Canvas supplied to the RecyclerView. + * Any content drawn by this method will be drawn after the item views are drawn + * and will thus appear over the views. + * + * @param c Canvas to draw into + * @param parent RecyclerView this ItemDecoration is drawing into + * @param state The current state of RecyclerView. + */ + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, + @NonNull State state) { + onDrawOver(c, parent); + } + + /** + * @deprecated Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent) { + } + + + /** + * @deprecated Use {@link #getItemOffsets(Rect, View, RecyclerView, State)} + */ + @Deprecated + public void getItemOffsets(@NonNull Rect outRect, int itemPosition, + @NonNull RecyclerView parent) { + outRect.set(0, 0, 0, 0); + } + + /** + * Retrieve any offsets for the given item. Each field of outRect specifies + * the number of pixels that the item view should be inset by, similar to padding or margin. + * The default implementation sets the bounds of outRect to 0 and returns. + * + *

+ * If this ItemDecoration does not affect the positioning of item views, it should set + * all four fields of outRect (left, top, right, bottom) to zero + * before returning. + * + *

+ * If you need to access Adapter for additional data, you can call + * {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the + * View. + * + * @param outRect Rect to receive the output. + * @param view The child view to decorate + * @param parent RecyclerView this ItemDecoration is decorating + * @param state The current state of RecyclerView. + */ + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, + @NonNull RecyclerView parent, @NonNull State state) { + getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(), + parent); + } + } + + /** + * An OnItemTouchListener allows the application to intercept touch events in progress at the + * view hierarchy level of the RecyclerView before those touch events are considered for + * RecyclerView's own scrolling behavior. + * + *

This can be useful for applications that wish to implement various forms of gestural + * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept + * a touch interaction already in progress even if the RecyclerView is already handling that + * gesture stream itself for the purposes of scrolling.

+ * + * @see SimpleOnItemTouchListener + */ + public interface OnItemTouchListener { + /** + * Silently observe and/or take over touch events sent to the RecyclerView + * before they are handled by either the RecyclerView itself or its child views. + * + *

The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run + * in the order in which each listener was added, before any other touch processing + * by the RecyclerView itself or child views occurs.

+ * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false + * to continue with the current behavior and continue observing future events in + * the gesture. + */ + boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); + + /** + * Process a touch event as part of a gesture that was claimed by returning true from + * a previous call to {@link #onInterceptTouchEvent}. + * + * @param e MotionEvent describing the touch event. All coordinates are in + * the RecyclerView's coordinate system. + */ + void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e); + + /** + * Called when a child of RecyclerView does not want RecyclerView and its ancestors to + * intercept touch events with + * {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}. + * + * @param disallowIntercept True if the child does not want the parent to + * intercept touch events. + * @see ViewParent#requestDisallowInterceptTouchEvent(boolean) + */ + void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept); + } + + /** + * An implementation of {@link RecyclerView.OnItemTouchListener} that has empty method bodies + * and default return values. + *

+ * You may prefer to extend this class if you don't need to override all methods. Another + * benefit of using this class is future compatibility. As the interface may change, we'll + * always provide a default implementation on this class so that your code won't break when + * you update to a new version of the support library. + */ + public static class SimpleOnItemTouchListener implements RecyclerView.OnItemTouchListener { + @Override + public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + return false; + } + + @Override + public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) { + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } + } + + + /** + * An OnScrollListener can be added to a RecyclerView to receive messages when a scrolling event + * has occurred on that RecyclerView. + *

+ * + * @see RecyclerView#addOnScrollListener(OnScrollListener) + * @see RecyclerView#clearOnChildAttachStateChangeListeners() + */ + public abstract static class OnScrollListener { + /** + * Callback method to be invoked when RecyclerView's scroll state changes. + * + * @param recyclerView The RecyclerView whose scroll state has changed. + * @param newState The updated scroll state. One of {@link #SCROLL_STATE_IDLE}, + * {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}. + */ + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + } + + /** + * Callback method to be invoked when the RecyclerView has been scrolled. This will be + * called after the scroll has completed. + *

+ * This callback will also be called if visible item range changes after a layout + * calculation. In that case, dx and dy will be 0. + * + * @param recyclerView The RecyclerView which scrolled. + * @param dx The amount of horizontal scroll. + * @param dy The amount of vertical scroll. + */ + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + } + } + + /** + * A RecyclerListener can be set on a RecyclerView to receive messages whenever + * a view is recycled. + * + * @see RecyclerView#setRecyclerListener(RecyclerListener) + */ + public interface RecyclerListener { + + /** + * This method is called whenever the view in the ViewHolder is recycled. + * + * RecyclerView calls this method right before clearing ViewHolder's internal data and + * sending it to RecycledViewPool. This way, if ViewHolder was holding valid information + * before being recycled, you can call {@link ViewHolder#getBindingAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder containing the view that was recycled + */ + void onViewRecycled(@NonNull ViewHolder holder); + } + + /** + * A Listener interface that can be attached to a RecylcerView to get notified + * whenever a ViewHolder is attached to or detached from RecyclerView. + */ + public interface OnChildAttachStateChangeListener { + + /** + * Called when a view is attached to the RecyclerView. + * + * @param view The View which is attached to the RecyclerView + */ + void onChildViewAttachedToWindow(@NonNull View view); + + /** + * Called when a view is detached from RecyclerView. + * + * @param view The View which is being detached from the RecyclerView + */ + void onChildViewDetachedFromWindow(@NonNull View view); + } + + /** + * A ViewHolder describes an item view and metadata about its place within the RecyclerView. + * + *

{@link Adapter} implementations should subclass ViewHolder and add fields for caching + * potentially expensive {@link View#findViewById(int)} results.

+ * + *

While {@link LayoutParams} belong to the {@link LayoutManager}, + * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use + * their own custom ViewHolder implementations to store data that makes binding view contents + * easier. Implementations should assume that individual item views will hold strong references + * to ViewHolder objects and that RecyclerView instances may hold + * strong references to extra off-screen item views for caching purposes

+ */ + public abstract static class ViewHolder { + @NonNull + public final View itemView; + WeakReference mNestedRecyclerView; + int mPosition = NO_POSITION; + int mOldPosition = NO_POSITION; + long mItemId = NO_ID; + int mItemViewType = INVALID_TYPE; + int mPreLayoutPosition = NO_POSITION; + + // The item that this holder is shadowing during an item change event/animation + ViewHolder mShadowedHolder = null; + // The item that is shadowing this holder during an item change event/animation + ViewHolder mShadowingHolder = null; + + /** + * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType + * are all valid. + */ + static final int FLAG_BOUND = 1 << 0; + + /** + * The data this ViewHolder's view reflects is stale and needs to be rebound + * by the adapter. mPosition and mItemId are consistent. + */ + static final int FLAG_UPDATE = 1 << 1; + + /** + * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId + * are not to be trusted and may no longer match the item view type. + * This ViewHolder must be fully rebound to different data. + */ + static final int FLAG_INVALID = 1 << 2; + + /** + * This ViewHolder points at data that represents an item previously removed from the + * data set. Its view may still be used for things like outgoing animations. + */ + static final int FLAG_REMOVED = 1 << 3; + + /** + * This ViewHolder should not be recycled. This flag is set via setIsRecyclable() + * and is intended to keep views around during animations. + */ + static final int FLAG_NOT_RECYCLABLE = 1 << 4; + + /** + * This ViewHolder is returned from scrap which means we are expecting an addView call + * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until + * the end of the layout pass and then recycled by RecyclerView if it is not added back to + * the RecyclerView. + */ + static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5; + + /** + * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove + * it unless LayoutManager is replaced. + * It is still fully visible to the LayoutManager. + */ + static final int FLAG_IGNORE = 1 << 7; + + /** + * When the View is detached form the parent, we set this flag so that we can take correct + * action when we need to remove it or add it back. + */ + static final int FLAG_TMP_DETACHED = 1 << 8; + + /** + * Set when we can no longer determine the adapter position of this ViewHolder until it is + * rebound to a new position. It is different than FLAG_INVALID because FLAG_INVALID is + * set even when the type does not match. Also, FLAG_ADAPTER_POSITION_UNKNOWN is set as soon + * as adapter notification arrives vs FLAG_INVALID is set lazily before layout is + * re-calculated. + */ + static final int FLAG_ADAPTER_POSITION_UNKNOWN = 1 << 9; + + /** + * Set when a addChangePayload(null) is called + */ + static final int FLAG_ADAPTER_FULLUPDATE = 1 << 10; + + /** + * Used by ItemAnimator when a ViewHolder's position changes + */ + static final int FLAG_MOVED = 1 << 11; + + /** + * Used by ItemAnimator when a ViewHolder appears in pre-layout + */ + static final int FLAG_APPEARED_IN_PRE_LAYOUT = 1 << 12; + + static final int PENDING_ACCESSIBILITY_STATE_NOT_SET = -1; + + /** + * Used when a ViewHolder starts the layout pass as a hidden ViewHolder but is re-used from + * hidden list (as if it was scrap) without being recycled in between. + * + * When a ViewHolder is hidden, there are 2 paths it can be re-used: + * a) Animation ends, view is recycled and used from the recycle pool. + * b) LayoutManager asks for the View for that position while the ViewHolder is hidden. + * + * This flag is used to represent "case b" where the ViewHolder is reused without being + * recycled (thus "bounced" from the hidden list). This state requires special handling + * because the ViewHolder must be added to pre layout maps for animations as if it was + * already there. + */ + static final int FLAG_BOUNCED_FROM_HIDDEN_LIST = 1 << 13; + + int mFlags; + + private static final List FULLUPDATE_PAYLOADS = Collections.emptyList(); + + List mPayloads = null; + List mUnmodifiedPayloads = null; + + private int mIsRecyclableCount = 0; + + // If non-null, view is currently considered scrap and may be reused for other data by the + // scrap container. + Recycler mScrapContainer = null; + // Keeps whether this ViewHolder lives in Change scrap or Attached scrap + boolean mInChangeScrap = false; + + // Saves isImportantForAccessibility value for the view item while it's in hidden state and + // marked as unimportant for accessibility. + private int mWasImportantForAccessibilityBeforeHidden = + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + // set if we defer the accessibility state change of the view holder + @VisibleForTesting + int mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + + /** + * Is set when VH is bound from the adapter and cleaned right before it is sent to + * {@link RecycledViewPool}. + */ + RecyclerView mOwnerRecyclerView; + + // The last adapter that bound this ViewHolder. It is cleaned before VH is recycled. + Adapter mBindingAdapter; + + public ViewHolder(@NonNull View itemView) { + if (itemView == null) { + throw new IllegalArgumentException("itemView may not be null"); + } + this.itemView = itemView; + } + + void flagRemovedAndOffsetPosition(int mNewPosition, int offset, boolean applyToPreLayout) { + addFlags(ViewHolder.FLAG_REMOVED); + offsetPosition(offset, applyToPreLayout); + mPosition = mNewPosition; + } + + void offsetPosition(int offset, boolean applyToPreLayout) { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; + } + if (mPreLayoutPosition == NO_POSITION) { + mPreLayoutPosition = mPosition; + } + if (applyToPreLayout) { + mPreLayoutPosition += offset; + } + mPosition += offset; + if (itemView.getLayoutParams() != null) { + ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true; + } + } + + void clearOldPosition() { + mOldPosition = NO_POSITION; + mPreLayoutPosition = NO_POSITION; + } + + void saveOldPosition() { + if (mOldPosition == NO_POSITION) { + mOldPosition = mPosition; + } + } + + boolean shouldIgnore() { + return (mFlags & FLAG_IGNORE) != 0; + } + + /** + * @see #getLayoutPosition() + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() + * @deprecated This method is deprecated because its meaning is ambiguous due to the async + * handling of adapter updates. You should use {@link #getLayoutPosition()}, + * {@link #getBindingAdapterPosition()} or {@link #getAbsoluteAdapterPosition()} + * depending on your use case. + */ + @Deprecated + public final int getPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } + + /** + * Returns the position of the ViewHolder in terms of the latest layout pass. + *

+ * This position is mostly used by RecyclerView components to be consistent while + * RecyclerView lazily processes adapter updates. + *

+ * For performance and animation reasons, RecyclerView batches all adapter updates until the + * next layout pass. This may cause mismatches between the Adapter position of the item and + * the position it had in the latest layout calculations. + *

+ * LayoutManagers should always call this method while doing calculations based on item + * positions. All methods in {@link RecyclerView.LayoutManager}, {@link RecyclerView.State}, + * {@link RecyclerView.Recycler} that receive a position expect it to be the layout position + * of the item. + *

+ * If LayoutManager needs to call an external method that requires the adapter position of + * the item, it can use {@link #getAbsoluteAdapterPosition()} or + * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. + * + * @return Returns the adapter position of the ViewHolder in the latest layout pass. + * @see #getBindingAdapterPosition() + * @see #getAbsoluteAdapterPosition() + */ + public final int getLayoutPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } + + + /** + * @return {@link #getBindingAdapterPosition()} + * @deprecated This method is confusing when adapters nest other adapters. + * If you are calling this in the context of an Adapter, you probably want to call + * {@link #getBindingAdapterPosition()} or if you want the position as {@link RecyclerView} + * sees it, you should call {@link #getAbsoluteAdapterPosition()}. + */ + @Deprecated + public final int getAdapterPosition() { + return getBindingAdapterPosition(); + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link Adapter} that bound it. + *

+ * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + *

+ * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + *

+ * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + *

+ * If the {@link Adapter} that bound this {@link ViewHolder} is inside another + * {@link Adapter} (e.g. {@link ConcatAdapter}), this position might be different than + * {@link #getAbsoluteAdapterPosition()}. If you would like to know the position that + * {@link RecyclerView} considers (e.g. for saved state), you should use + * {@link #getAbsoluteAdapterPosition()}. + * + * @return The adapter position of the item if it still exists in the adapter. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + * @see #getAbsoluteAdapterPosition() + * @see #getLayoutPosition() + */ + public final int getBindingAdapterPosition() { + if (mBindingAdapter == null) { + return NO_POSITION; + } + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + @SuppressWarnings("unchecked") + Adapter rvAdapter = mOwnerRecyclerView.getAdapter(); + if (rvAdapter == null) { + return NO_POSITION; + } + int globalPosition = mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + if (globalPosition == NO_POSITION) { + return NO_POSITION; + } + return rvAdapter.findRelativeAdapterPositionIn(mBindingAdapter, this, globalPosition); + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder with respect to + * the {@link RecyclerView}'s {@link Adapter}. If the {@link Adapter} that bound this + * {@link ViewHolder} is inside another adapter (e.g. {@link ConcatAdapter}), this + * position might be different and will include + * the offsets caused by other adapters in the {@link ConcatAdapter}. + *

+ * Note that this might be different than the {@link #getLayoutPosition()} if there are + * pending adapter updates but a new layout pass has not happened yet. + *

+ * RecyclerView does not handle any adapter updates until the next layout traversal. This + * may create temporary inconsistencies between what user sees on the screen and what + * adapter contents have. This inconsistency is not important since it will be less than + * 16ms but it might be a problem if you want to use ViewHolder position to access the + * adapter. Sometimes, you may need to get the exact adapter position to do + * some actions in response to user events. In that case, you should use this method which + * will calculate the Adapter position of the ViewHolder. + *

+ * Note that if you've called {@link RecyclerView.Adapter#notifyDataSetChanged()}, until the + * next layout pass, the return value of this method will be {@link #NO_POSITION}. + *

+ * Note that if you are querying the position as {@link RecyclerView} sees, you should use + * {@link #getAbsoluteAdapterPosition()} (e.g. you want to use it to save scroll + * state). If you are querying the position to access the {@link Adapter} contents, + * you should use {@link #getBindingAdapterPosition()}. + * + * @return The adapter position of the item from {@link RecyclerView}'s perspective if it + * still exists in the adapter and bound to a valid item. + * {@link RecyclerView#NO_POSITION} if item has been removed from the adapter, + * {@link RecyclerView.Adapter#notifyDataSetChanged()} has been called after the last + * layout pass or the ViewHolder has already been recycled. + * @see #getBindingAdapterPosition() + * @see #getLayoutPosition() + */ + public final int getAbsoluteAdapterPosition() { + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + return mOwnerRecyclerView.getAdapterPositionInRecyclerView(this); + } + + /** + * Returns the {@link Adapter} that last bound this {@link ViewHolder}. + * Might return {@code null} if this {@link ViewHolder} is not bound to any adapter. + * + * @return The {@link Adapter} that last bound this {@link ViewHolder} or {@code null} if + * this {@link ViewHolder} is not bound by any adapter (e.g. recycled). + */ + @Nullable + public final Adapter getBindingAdapter() { + return mBindingAdapter; + } + + /** + * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders + * to perform animations. + *

+ * If a ViewHolder was laid out in the previous onLayout call, old position will keep its + * adapter index in the previous layout. + * + * @return The previous adapter index of the Item represented by this ViewHolder or + * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is + * complete). + */ + public final int getOldPosition() { + return mOldPosition; + } + + /** + * Returns The itemId represented by this ViewHolder. + * + * @return The item's id if adapter has stable ids, {@link RecyclerView#NO_ID} + * otherwise + */ + public final long getItemId() { + return mItemId; + } + + /** + * @return The view type of this ViewHolder. + */ + public final int getItemViewType() { + return mItemViewType; + } + + boolean isScrap() { + return mScrapContainer != null; + } + + void unScrap() { + mScrapContainer.unscrapView(this); + } + + boolean wasReturnedFromScrap() { + return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0; + } + + void clearReturnedFromScrapFlag() { + mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP; + } + + void clearTmpDetachFlag() { + mFlags = mFlags & ~FLAG_TMP_DETACHED; + } + + void stopIgnoring() { + mFlags = mFlags & ~FLAG_IGNORE; + } + + void setScrapContainer(Recycler recycler, boolean isChangeScrap) { + mScrapContainer = recycler; + mInChangeScrap = isChangeScrap; + } + + boolean isInvalid() { + return (mFlags & FLAG_INVALID) != 0; + } + + boolean needsUpdate() { + return (mFlags & FLAG_UPDATE) != 0; + } + + boolean isBound() { + return (mFlags & FLAG_BOUND) != 0; + } + + boolean isRemoved() { + return (mFlags & FLAG_REMOVED) != 0; + } + + boolean hasAnyOfTheFlags(int flags) { + return (mFlags & flags) != 0; + } + + boolean isTmpDetached() { + return (mFlags & FLAG_TMP_DETACHED) != 0; + } + + boolean isAttachedToTransitionOverlay() { + return itemView.getParent() != null && itemView.getParent() != mOwnerRecyclerView; + } + + boolean isAdapterPositionUnknown() { + return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid(); + } + + void setFlags(int flags, int mask) { + mFlags = (mFlags & ~mask) | (flags & mask); + } + + void addFlags(int flags) { + mFlags |= flags; + } + + void addChangePayload(Object payload) { + if (payload == null) { + addFlags(FLAG_ADAPTER_FULLUPDATE); + } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + createPayloadsIfNeeded(); + mPayloads.add(payload); + } + } + + private void createPayloadsIfNeeded() { + if (mPayloads == null) { + mPayloads = new ArrayList(); + mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads); + } + } + + void clearPayload() { + if (mPayloads != null) { + mPayloads.clear(); + } + mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE; + } + + List getUnmodifiedPayloads() { + if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + if (mPayloads == null || mPayloads.size() == 0) { + // Initial state, no update being called. + return FULLUPDATE_PAYLOADS; + } + // there are none-null payloads + return mUnmodifiedPayloads; + } else { + // a full update has been called. + return FULLUPDATE_PAYLOADS; + } + } + + void resetInternal() { + if (sDebugAssertionsEnabled && isTmpDetached()) { + throw new IllegalStateException("Attempting to reset temp-detached ViewHolder: " + + this + ". ViewHolders should be fully detached before resetting."); + } + + mFlags = 0; + mPosition = NO_POSITION; + mOldPosition = NO_POSITION; + mItemId = NO_ID; + mPreLayoutPosition = NO_POSITION; + mIsRecyclableCount = 0; + mShadowedHolder = null; + mShadowingHolder = null; + clearPayload(); + mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + clearNestedRecyclerViewIfNotNested(this); + } + + /** + * Called when the child view enters the hidden state + */ + void onEnteredHiddenState(RecyclerView parent) { + // While the view item is in hidden state, make it invisible for the accessibility. + if (mPendingAccessibilityState != PENDING_ACCESSIBILITY_STATE_NOT_SET) { + mWasImportantForAccessibilityBeforeHidden = mPendingAccessibilityState; + } else { + mWasImportantForAccessibilityBeforeHidden = + ViewCompat.getImportantForAccessibility(itemView); + } + parent.setChildImportantForAccessibilityInternal(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + + /** + * Called when the child view leaves the hidden state + */ + void onLeftHiddenState(RecyclerView parent) { + parent.setChildImportantForAccessibilityInternal(this, + mWasImportantForAccessibilityBeforeHidden); + mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + } + + @Override + public String toString() { + String className = + getClass().isAnonymousClass() ? "ViewHolder" : getClass().getSimpleName(); + final StringBuilder sb = new StringBuilder(className + "{" + + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId + + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); + if (isScrap()) { + sb.append(" scrap ") + .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); + } + if (isInvalid()) sb.append(" invalid"); + if (!isBound()) sb.append(" unbound"); + if (needsUpdate()) sb.append(" update"); + if (isRemoved()) sb.append(" removed"); + if (shouldIgnore()) sb.append(" ignored"); + if (isTmpDetached()) sb.append(" tmpDetached"); + if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")"); + if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); + + if (itemView.getParent() == null) sb.append(" no parent"); + sb.append("}"); + return sb.toString(); + } + + /** + * Informs the recycler whether this item can be recycled. Views which are not + * recyclable will not be reused for other items until setIsRecyclable() is + * later set to true. Calls to setIsRecyclable() should always be paired (one + * call to setIsRecyclabe(false) should always be matched with a later call to + * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally + * reference-counted. + * + * @param recyclable Whether this item is available to be recycled. Default value + * is true. + * @see #isRecyclable() + */ + public final void setIsRecyclable(boolean recyclable) { + mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1; + if (mIsRecyclableCount < 0) { + mIsRecyclableCount = 0; + if (sDebugAssertionsEnabled) { + throw new RuntimeException("isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } + Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } else if (!recyclable && mIsRecyclableCount == 1) { + mFlags |= FLAG_NOT_RECYCLABLE; + } else if (recyclable && mIsRecyclableCount == 0) { + mFlags &= ~FLAG_NOT_RECYCLABLE; + } + if (sVerboseLoggingEnabled) { + Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this); + } + } + + /** + * @return true if this item is available to be recycled, false otherwise. + * @see #setIsRecyclable(boolean) + */ + public final boolean isRecyclable() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 + && !ViewCompat.hasTransientState(itemView); + } + + /** + * Returns whether we have animations referring to this view holder or not. + * This is similar to isRecyclable flag but does not check transient state. + */ + boolean shouldBeKeptAsChild() { + return (mFlags & FLAG_NOT_RECYCLABLE) != 0; + } + + /** + * @return True if ViewHolder is not referenced by RecyclerView animations but has + * transient state which will prevent it from being recycled. + */ + boolean doesTransientStatePreventRecycling() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && ViewCompat.hasTransientState(itemView); + } + + boolean isUpdated() { + return (mFlags & FLAG_UPDATE) != 0; + } + } + + /** + * This method is here so that we can control the important for a11y changes and test it. + */ + @VisibleForTesting + boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, + int importantForAccessibility) { + if (isComputingLayout()) { + viewHolder.mPendingAccessibilityState = importantForAccessibility; + mPendingAccessibilityImportanceChange.add(viewHolder); + return false; + } + ViewCompat.setImportantForAccessibility(viewHolder.itemView, importantForAccessibility); + return true; + } + + void dispatchPendingImportantForAccessibilityChanges() { + for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) { + ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i); + if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) { + continue; + } + int state = viewHolder.mPendingAccessibilityState; + if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) { + //noinspection WrongConstant + ViewCompat.setImportantForAccessibility(viewHolder.itemView, state); + viewHolder.mPendingAccessibilityState = + ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET; + } + } + mPendingAccessibilityImportanceChange.clear(); + } + + int getAdapterPositionInRecyclerView(ViewHolder viewHolder) { + if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) + || !viewHolder.isBound()) { + return RecyclerView.NO_POSITION; + } + return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); + } + + @VisibleForTesting + void initFastScroller(StateListDrawable verticalThumbDrawable, + Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, + Drawable horizontalTrackDrawable) { + if (verticalThumbDrawable == null || verticalTrackDrawable == null + || horizontalThumbDrawable == null || horizontalTrackDrawable == null) { + throw new IllegalArgumentException( + "Trying to set fast scroller without both required drawables." + + exceptionLabel()); + } + + Resources resources = getContext().getResources(); + new FastScroller(this, verticalThumbDrawable, verticalTrackDrawable, + horizontalThumbDrawable, horizontalTrackDrawable, + resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness), + resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range), + resources.getDimensionPixelOffset(R.dimen.fastscroll_margin)); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + getScrollingChildHelper().setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return getScrollingChildHelper().isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return getScrollingChildHelper().startNestedScroll(axes); + } + + @Override + public boolean startNestedScroll(int axes, int type) { + return getScrollingChildHelper().startNestedScroll(axes, type); + } + + @Override + public void stopNestedScroll() { + getScrollingChildHelper().stopNestedScroll(); + } + + @Override + public void stopNestedScroll(int type) { + getScrollingChildHelper().stopNestedScroll(type); + } + + @Override + public boolean hasNestedScrollingParent() { + return getScrollingChildHelper().hasNestedScrollingParent(); + } + + @Override + public boolean hasNestedScrollingParent(int type) { + return getScrollingChildHelper().hasNestedScrollingParent(type); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type) { + return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow, type); + } + + @Override + public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type, @NonNull int[] consumed) { + getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, + int type) { + return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, + type); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); + } + + /** + * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of + * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged + * to create their own subclass of this LayoutParams class + * to store any additional required per-child view metadata about the layout. + */ + public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { + ViewHolder mViewHolder; + final Rect mDecorInsets = new Rect(); + boolean mInsetsDirty = true; + // Flag is set to true if the view is bound while it is detached from RV. + // In this case, we need to manually call invalidate after view is added to guarantee that + // invalidation is populated through the View hierarchy + boolean mPendingInvalidate = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super((ViewGroup.LayoutParams) source); + } + + /** + * Returns true if the view this LayoutParams is attached to needs to have its content + * updated from the corresponding adapter. + * + * @return true if the view should have its content updated + */ + public boolean viewNeedsUpdate() { + return mViewHolder.needsUpdate(); + } + + /** + * Returns true if the view this LayoutParams is attached to is now representing + * potentially invalid data. A LayoutManager should scrap/recycle it. + * + * @return true if the view is invalid + */ + public boolean isViewInvalid() { + return mViewHolder.isInvalid(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been removed from the data set. A LayoutManager may choose to + * treat it differently in order to animate its outgoing or disappearing state. + * + * @return true if the item the view corresponds to was removed from the data set + */ + public boolean isItemRemoved() { + return mViewHolder.isRemoved(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been changed in the data set. A LayoutManager may choose to + * treat it differently in order to animate its changing state. + * + * @return true if the item the view corresponds to was changed in the data set + */ + public boolean isItemChanged() { + return mViewHolder.isUpdated(); + } + + /** + * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} + */ + @Deprecated + public int getViewPosition() { + return mViewHolder.getPosition(); + } + + /** + * Returns the adapter position that the view this LayoutParams is attached to corresponds + * to as of latest layout calculation. + * + * @return the adapter position this view as of latest layout pass + */ + public int getViewLayoutPosition() { + return mViewHolder.getLayoutPosition(); + } + + /** + * @deprecated This method is confusing when nested adapters are used. + * If you are calling from the context of an {@link Adapter}, + * use {@link #getBindingAdapterPosition()}. If you need the position that + * {@link RecyclerView} sees, use {@link #getAbsoluteAdapterPosition()}. + */ + @Deprecated + public int getViewAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); + } + + /** + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to in the {@link RecyclerView}. If the {@link RecyclerView} has an + * {@link Adapter} that merges other adapters, this position will be with respect to the + * adapter that is assigned to the {@link RecyclerView}. + * + * @return the up-to-date adapter position this view with respect to the RecyclerView. It + * may return {@link RecyclerView#NO_POSITION} if item represented by this View has been + * removed or + * its up-to-date position cannot be calculated. + */ + public int getAbsoluteAdapterPosition() { + return mViewHolder.getAbsoluteAdapterPosition(); + } + + /** + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to with respect to the {@link Adapter} that bound this View. + * + * @return the up-to-date adapter position this view relative to the {@link Adapter} that + * bound this View. It may return {@link RecyclerView#NO_POSITION} if item represented by + * this View has been removed or its up-to-date position cannot be calculated. + */ + public int getBindingAdapterPosition() { + return mViewHolder.getBindingAdapterPosition(); + } + } + + /** + * Observer base class for watching changes to an {@link Adapter}. + * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}. + */ + public abstract static class AdapterDataObserver { + public void onChanged() { + // Do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) { + // fallback to onItemRangeChanged(positionStart, itemCount) if app + // does not override this method. + onItemRangeChanged(positionStart, itemCount); + } + + public void onItemRangeInserted(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeRemoved(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + // do nothing + } + + /** + * Called when the {@link Adapter.StateRestorationPolicy} of the {@link Adapter} changed. + * When this method is called, the Adapter might be ready to restore its state if it has + * not already been restored. + * + * @see Adapter#getStateRestorationPolicy() + * @see Adapter#setStateRestorationPolicy(Adapter.StateRestorationPolicy) + */ + public void onStateRestorationPolicyChanged() { + // do nothing + } + } + + /** + * Base class for smooth scrolling. Handles basic tracking of the target view position and + * provides methods to trigger a programmatic scroll. + * + *

An instance of SmoothScroller is only intended to be used once. You should create a new + * instance for each call to {@link LayoutManager#startSmoothScroll(SmoothScroller)}. + * + * @see LinearSmoothScroller + */ + public abstract static class SmoothScroller { + + private int mTargetPosition = RecyclerView.NO_POSITION; + + private RecyclerView mRecyclerView; + + private LayoutManager mLayoutManager; + + private boolean mPendingInitialRun; + + private boolean mRunning; + + private View mTargetView; + + private final Action mRecyclingAction; + + private boolean mStarted; + + public SmoothScroller() { + mRecyclingAction = new Action(0, 0); + } + + /** + * Starts a smooth scroll for the given target position. + *

In each animation step, {@link RecyclerView} will check + * for the target view and call either + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until + * SmoothScroller is stopped.

+ * + *

Note that if RecyclerView finds the target view, it will automatically stop the + * SmoothScroller. This does not mean that scroll will stop, it only means it will + * stop calling SmoothScroller in each animation step.

+ */ + void start(RecyclerView recyclerView, LayoutManager layoutManager) { + + // Stop any previous ViewFlinger animations now because we are about to start a new one. + recyclerView.mViewFlinger.stop(); + + if (mStarted) { + Log.w(TAG, "An instance of " + this.getClass().getSimpleName() + " was started " + + "more than once. Each instance of" + this.getClass().getSimpleName() + " " + + "is intended to only be used once. You should create a new instance for " + + "each use."); + } + + mRecyclerView = recyclerView; + mLayoutManager = layoutManager; + if (mTargetPosition == RecyclerView.NO_POSITION) { + throw new IllegalArgumentException("Invalid target position"); + } + mRecyclerView.mState.mTargetPosition = mTargetPosition; + mRunning = true; + mPendingInitialRun = true; + mTargetView = findViewByPosition(getTargetPosition()); + onStart(); + mRecyclerView.mViewFlinger.postOnAnimation(); + + mStarted = true; + } + + public void setTargetPosition(int targetPosition) { + mTargetPosition = targetPosition; + } + + /** + * Compute the scroll vector for a given target position. + *

+ * This method can return null if the layout manager cannot calculate a scroll vector + * for the given position (e.g. it has no current scroll position). + * + * @param targetPosition the position to which the scroller is scrolling + * @return the scroll vector for a given target position + */ + @Nullable + public PointF computeScrollVectorForPosition(int targetPosition) { + LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof ScrollVectorProvider) { + return ((ScrollVectorProvider) layoutManager) + .computeScrollVectorForPosition(targetPosition); + } + Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" + + " does not implement " + ScrollVectorProvider.class.getCanonicalName()); + return null; + } + + /** + * @return The LayoutManager to which this SmoothScroller is attached. Will return + * null after the SmoothScroller is stopped. + */ + @Nullable + public LayoutManager getLayoutManager() { + return mLayoutManager; + } + + /** + * Stops running the SmoothScroller in each animation callback. Note that this does not + * cancel any existing {@link Action} updated by + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}. + */ + protected final void stop() { + if (!mRunning) { + return; + } + mRunning = false; + onStop(); + mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION; + mTargetView = null; + mTargetPosition = RecyclerView.NO_POSITION; + mPendingInitialRun = false; + // trigger a cleanup + mLayoutManager.onSmoothScrollerStopped(this); + // clear references to avoid any potential leak by a custom smooth scroller + mLayoutManager = null; + mRecyclerView = null; + } + + /** + * Returns true if SmoothScroller has been started but has not received the first + * animation + * callback yet. + * + * @return True if this SmoothScroller is waiting to start + */ + public boolean isPendingInitialRun() { + return mPendingInitialRun; + } + + + /** + * @return True if SmoothScroller is currently active + */ + public boolean isRunning() { + return mRunning; + } + + /** + * Returns the adapter position of the target item + * + * @return Adapter position of the target item or + * {@link RecyclerView#NO_POSITION} if no target view is set. + */ + public int getTargetPosition() { + return mTargetPosition; + } + + void onAnimation(int dx, int dy) { + final RecyclerView recyclerView = mRecyclerView; + if (mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) { + stop(); + } + + // The following if block exists to have the LayoutManager scroll 1 pixel in the correct + // direction in order to cause the LayoutManager to draw two pages worth of views so + // that the target view may be found before scrolling any further. This is done to + // prevent an initial scroll distance from scrolling past the view, which causes a + // jittery looking animation. + if (mPendingInitialRun && mTargetView == null && mLayoutManager != null) { + PointF pointF = computeScrollVectorForPosition(mTargetPosition); + if (pointF != null && (pointF.x != 0 || pointF.y != 0)) { + recyclerView.scrollStep( + (int) Math.signum(pointF.x), + (int) Math.signum(pointF.y), + null); + } + } + + mPendingInitialRun = false; + + if (mTargetView != null) { + // verify target position + if (getChildPosition(mTargetView) == mTargetPosition) { + onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(recyclerView); + stop(); + } else { + Log.e(TAG, "Passed over target position while smooth scrolling."); + mTargetView = null; + } + } + if (mRunning) { + onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction); + boolean hadJumpTarget = mRecyclingAction.hasJumpTarget(); + mRecyclingAction.runIfNecessary(recyclerView); + if (hadJumpTarget) { + // It is not stopped so needs to be restarted + if (mRunning) { + mPendingInitialRun = true; + recyclerView.mViewFlinger.postOnAnimation(); + } + } + } + } + + /** + * @see RecyclerView#getChildLayoutPosition(android.view.View) + */ + public int getChildPosition(View view) { + return mRecyclerView.getChildLayoutPosition(view); + } + + /** + * @see RecyclerView.LayoutManager#getChildCount() + */ + public int getChildCount() { + return mRecyclerView.mLayout.getChildCount(); + } + + /** + * @see RecyclerView.LayoutManager#findViewByPosition(int) + */ + public View findViewByPosition(int position) { + return mRecyclerView.mLayout.findViewByPosition(position); + } + + /** + * @see RecyclerView#scrollToPosition(int) + * @deprecated Use {@link Action#jumpTo(int)}. + */ + @Deprecated + public void instantScrollToPosition(int position) { + mRecyclerView.scrollToPosition(position); + } + + protected void onChildAttachedToWindow(View child) { + if (getChildPosition(child) == getTargetPosition()) { + mTargetView = child; + if (sVerboseLoggingEnabled) { + Log.d(TAG, "smooth scroll target view has been attached"); + } + } + } + + /** + * Normalizes the vector. + * + * @param scrollVector The vector that points to the target scroll position + */ + protected void normalize(@NonNull PointF scrollVector) { + final float magnitude = (float) Math.sqrt(scrollVector.x * scrollVector.x + + scrollVector.y * scrollVector.y); + scrollVector.x /= magnitude; + scrollVector.y /= magnitude; + } + + /** + * Called when smooth scroll is started. This might be a good time to do setup. + */ + protected abstract void onStart(); + + /** + * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. + * + * @see #stop() + */ + protected abstract void onStop(); + + /** + *

RecyclerView will call this method each time it scrolls until it can find the target + * position in the layout.

+ *

SmoothScroller should check dx, dy and if scroll should be changed, update the + * provided {@link Action} to define the next scroll.

+ * + * @param dx Last scroll amount horizontally + * @param dy Last scroll amount vertically + * @param state Transient state of RecyclerView + * @param action If you want to trigger a new smooth scroll and cancel the previous one, + * update this object. + */ + protected abstract void onSeekTargetStep(@Px int dx, @Px int dy, @NonNull State state, + @NonNull Action action); + + /** + * Called when the target position is laid out. This is the last callback SmoothScroller + * will receive and it should update the provided {@link Action} to define the scroll + * details towards the target view. + * + * @param targetView The view element which render the target position. + * @param state Transient state of RecyclerView + * @param action Action instance that you should update to define final scroll action + * towards the targetView + */ + protected abstract void onTargetFound(@NonNull View targetView, @NonNull State state, + @NonNull Action action); + + /** + * Holds information about a smooth scroll request by a {@link SmoothScroller}. + */ + public static class Action { + + public static final int UNDEFINED_DURATION = RecyclerView.UNDEFINED_DURATION; + + private int mDx; + + private int mDy; + + private int mDuration; + + private int mJumpToPosition = NO_POSITION; + + private Interpolator mInterpolator; + + private boolean mChanged = false; + + // we track this variable to inform custom implementer if they are updating the action + // in every animation callback + private int mConsecutiveUpdates = 0; + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + */ + public Action(@Px int dx, @Px int dy) { + this(dx, dy, UNDEFINED_DURATION, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + */ + public Action(@Px int dx, @Px int dy, int duration) { + this(dx, dy, duration, null); + } + + /** + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step + */ + public Action(@Px int dx, @Px int dy, int duration, + @Nullable Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + } + + /** + * Instead of specifying pixels to scroll, use the target position to jump using + * {@link RecyclerView#scrollToPosition(int)}. + *

+ * You may prefer using this method if scroll target is really far away and you prefer + * to jump to a location and smooth scroll afterwards. + *

+ * Note that calling this method takes priority over other update methods such as + * {@link #update(int, int, int, Interpolator)}, {@link #setX(float)}, + * {@link #setY(float)} and #{@link #setInterpolator(Interpolator)}. If you call + * {@link #jumpTo(int)}, the other changes will not be considered for this animation + * frame. + * + * @param targetPosition The target item position to scroll to using instant scrolling. + */ + public void jumpTo(int targetPosition) { + mJumpToPosition = targetPosition; + } + + boolean hasJumpTarget() { + return mJumpToPosition >= 0; + } + + void runIfNecessary(RecyclerView recyclerView) { + if (mJumpToPosition >= 0) { + final int position = mJumpToPosition; + mJumpToPosition = NO_POSITION; + recyclerView.jumpToPositionForSmoothScroller(position); + mChanged = false; + return; + } + if (mChanged) { + validate(); + recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator); + mConsecutiveUpdates++; + if (mConsecutiveUpdates > 10) { + // A new action is being set in every animation step. This looks like a bad + // implementation. Inform developer. + Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure" + + " you are not changing it unless necessary"); + } + mChanged = false; + } else { + mConsecutiveUpdates = 0; + } + } + + private void validate() { + if (mInterpolator != null && mDuration < 1) { + throw new IllegalStateException("If you provide an interpolator, you must" + + " set a positive duration"); + } else if (mDuration < 1) { + throw new IllegalStateException("Scroll duration must be a positive number"); + } + } + + @Px + public int getDx() { + return mDx; + } + + public void setDx(@Px int dx) { + mChanged = true; + mDx = dx; + } + + @Px + public int getDy() { + return mDy; + } + + public void setDy(@Px int dy) { + mChanged = true; + mDy = dy; + } + + public int getDuration() { + return mDuration; + } + + public void setDuration(int duration) { + mChanged = true; + mDuration = duration; + } + + @Nullable + public Interpolator getInterpolator() { + return mInterpolator; + } + + /** + * Sets the interpolator to calculate scroll steps + * + * @param interpolator The interpolator to use. If you specify an interpolator, you must + * also set the duration. + * @see #setDuration(int) + */ + public void setInterpolator(@Nullable Interpolator interpolator) { + mChanged = true; + mInterpolator = interpolator; + } + + /** + * Updates the action with given parameters. + * + * @param dx Pixels to scroll horizontally + * @param dy Pixels to scroll vertically + * @param duration Duration of the animation in milliseconds + * @param interpolator Interpolator to be used when calculating scroll position in each + * animation step + */ + public void update(@Px int dx, @Px int dy, int duration, + @Nullable Interpolator interpolator) { + mDx = dx; + mDy = dy; + mDuration = duration; + mInterpolator = interpolator; + mChanged = true; + } + } + + /** + * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager} + * to provide a hint to a {@link SmoothScroller} about the location of the target position. + */ + public interface ScrollVectorProvider { + /** + * Should calculate the vector that points to the direction where the target position + * can be found. + *

+ * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards + * the target position. + *

+ * The magnitude of the vector is not important. It is always normalized before being + * used by the {@link LinearSmoothScroller}. + *

+ * LayoutManager should not check whether the position exists in the adapter or not. + * + * @param targetPosition the target position to which the returned vector should point + * @return the scroll vector for a given position. + */ + @Nullable + PointF computeScrollVectorForPosition(int targetPosition); + } + } + + static class AdapterDataObservable extends Observable { + public boolean hasObservers() { + return !mObservers.isEmpty(); + } + + public void notifyChanged() { + // since onChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onChanged(); + } + } + + public void notifyStateRestorationPolicyChanged() { + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onStateRestorationPolicyChanged(); + } + } + + public void notifyItemRangeChanged(int positionStart, int itemCount) { + notifyItemRangeChanged(positionStart, itemCount, null); + } + + public void notifyItemRangeChanged(int positionStart, int itemCount, + @Nullable Object payload) { + // since onItemRangeChanged() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload); + } + } + + public void notifyItemRangeInserted(int positionStart, int itemCount) { + // since onItemRangeInserted() is implemented by the app, it could do anything, + // including removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeInserted(positionStart, itemCount); + } + } + + public void notifyItemRangeRemoved(int positionStart, int itemCount) { + // since onItemRangeRemoved() is implemented by the app, it could do anything, including + // removing itself from {@link mObservers} - and that could cause problems if + // an iterator is used on the ArrayList {@link mObservers}. + // to avoid such problems, just march thru the list in the reverse order. + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeRemoved(positionStart, itemCount); + } + } + + public void notifyItemMoved(int fromPosition, int toPosition) { + for (int i = mObservers.size() - 1; i >= 0; i--) { + mObservers.get(i).onItemRangeMoved(fromPosition, toPosition, 1); + } + } + } + + /** + * This is public so that the CREATOR can be accessed on cold launch. + * + * @hide + */ + @RestrictTo(LIBRARY) + public static class SavedState extends AbsSavedState { + + Parcelable mLayoutState; + + /** + * called by CREATOR + */ + @SuppressWarnings("deprecation") + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + mLayoutState = in.readParcelable( + loader != null ? loader : LayoutManager.class.getClassLoader()); + } + + /** + * Called by onSaveInstanceState + */ + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(mLayoutState, 0); + } + + void copyFrom(SavedState other) { + mLayoutState = other.mLayoutState; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + *

Contains useful information about the current RecyclerView state like target scroll + * position or view focus. State object can also keep arbitrary data, identified by resource + * ids.

+ *

Often times, RecyclerView components will need to pass information between each other. + * To provide a well defined data bus between components, RecyclerView passes the same State + * object to component callbacks and these components can use it to exchange data.

+ *

If you implement custom components, you can use State's put/get/remove methods to pass + * data between your components without needing to manage their lifecycles.

+ */ + public static class State { + static final int STEP_START = 1; + static final int STEP_LAYOUT = 1 << 1; + static final int STEP_ANIMATIONS = 1 << 2; + + void assertLayoutStep(int accepted) { + if ((accepted & mLayoutStep) == 0) { + throw new IllegalStateException("Layout state should be one of " + + Integer.toBinaryString(accepted) + " but it is " + + Integer.toBinaryString(mLayoutStep)); + } + } + + + /** Owned by SmoothScroller */ + int mTargetPosition = RecyclerView.NO_POSITION; + + private SparseArray mData; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are carried from one layout pass to the next + //////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Number of items adapter had in the previous layout. + */ + int mPreviousLayoutItemCount = 0; + + /** + * Number of items that were NOT laid out but has been deleted from the adapter after the + * previous layout. + */ + int mDeletedInvisibleItemCountSincePreviousLayout = 0; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below must be updated or cleared before they are used (generally before a pass) + //////////////////////////////////////////////////////////////////////////////////////////// + + @IntDef(flag = true, value = { + STEP_START, STEP_LAYOUT, STEP_ANIMATIONS + }) + @Retention(RetentionPolicy.SOURCE) + @interface LayoutState { + } + + @LayoutState + int mLayoutStep = STEP_START; + + /** + * Number of items adapter has. + */ + int mItemCount = 0; + + boolean mStructureChanged = false; + + /** + * True if the associated {@link RecyclerView} is in the pre-layout step where it is having + * its {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. + */ + boolean mInPreLayout = false; + + boolean mTrackOldChangeHolders = false; + + boolean mIsMeasuring = false; + + //////////////////////////////////////////////////////////////////////////////////////////// + // Fields below are always reset outside of the pass (or passes) that use them + //////////////////////////////////////////////////////////////////////////////////////////// + + boolean mRunSimpleAnimations = false; + + boolean mRunPredictiveAnimations = false; + + /** + * This data is saved before a layout calculation happens. After the layout is finished, + * if the previously focused view has been replaced with another view for the same item, we + * move the focus to the new item automatically. + */ + int mFocusedItemPosition; + long mFocusedItemId; + // when a sub child has focus, record its id and see if we can directly request focus on + // that one instead + int mFocusedSubChildId; + + int mRemainingScrollHorizontal; + int mRemainingScrollVertical; + + //////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Prepare for a prefetch occurring on the RecyclerView in between traversals, potentially + * prior to any layout passes. + * + *

Don't touch any state stored between layout passes, only reset per-layout state, so + * that Recycler#getViewForPosition() can function safely.

+ */ + void prepareForNestedPrefetch(Adapter adapter) { + mLayoutStep = STEP_START; + mItemCount = adapter.getItemCount(); + mInPreLayout = false; + mTrackOldChangeHolders = false; + mIsMeasuring = false; + } + + /** + * Returns true if the RecyclerView is currently measuring the layout. This value is + * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView + * has non-exact measurement specs. + *

+ * Note that if the LayoutManager supports predictive animations and it is calculating the + * pre-layout step, this value will be {@code false} even if the RecyclerView is in + * {@code onMeasure} call. This is because pre-layout means the previous state of the + * RecyclerView and measurements made for that state cannot change the RecyclerView's size. + * LayoutManager is always guaranteed to receive another call to + * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens. + * + * @return True if the RecyclerView is currently calculating its bounds, false otherwise. + */ + public boolean isMeasuring() { + return mIsMeasuring; + } + + /** + * Returns true if the {@link RecyclerView} is in the pre-layout step where it is having its + * {@link LayoutManager} layout items where they will be at the beginning of a set of + * predictive item animations. + */ + public boolean isPreLayout() { + return mInPreLayout; + } + + /** + * Returns whether RecyclerView will run predictive animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating predictive animations to be run at the end + * of the layout pass. + */ + public boolean willRunPredictiveAnimations() { + return mRunPredictiveAnimations; + } + + /** + * Returns whether RecyclerView will run simple animations in this layout pass + * or not. + * + * @return true if RecyclerView is calculating simple animations to be run at the end of + * the layout pass. + */ + public boolean willRunSimpleAnimations() { + return mRunSimpleAnimations; + } + + /** + * Removes the mapping from the specified id, if there was any. + * + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + */ + public void remove(int resourceId) { + if (mData == null) { + return; + } + mData.remove(resourceId); + } + + /** + * Gets the Object mapped from the specified id, or null + * if no such data exists. + * + * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* + * to + * preserve cross functionality and avoid conflicts. + */ + @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"}) + public T get(int resourceId) { + if (mData == null) { + return null; + } + return (T) mData.get(resourceId); + } + + /** + * Adds a mapping from the specified id to the specified value, replacing the previous + * mapping from the specified key if there was one. + * + * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to + * preserve cross functionality and avoid conflicts. + * @param data The data you want to associate with the resourceId. + */ + public void put(int resourceId, Object data) { + if (mData == null) { + mData = new SparseArray(); + } + mData.put(resourceId, data); + } + + /** + * If scroll is triggered to make a certain item visible, this value will return the + * adapter index of that item. + * + * @return Adapter index of the target item or + * {@link RecyclerView#NO_POSITION} if there is no target + * position. + */ + public int getTargetScrollPosition() { + return mTargetPosition; + } + + /** + * Returns if current scroll has a target position. + * + * @return true if scroll is being triggered to make a certain position visible + * @see #getTargetScrollPosition() + */ + public boolean hasTargetScrollPosition() { + return mTargetPosition != RecyclerView.NO_POSITION; + } + + /** + * @return true if the structure of the data set has changed since the last call to + * onLayoutChildren, false otherwise + */ + public boolean didStructureChange() { + return mStructureChanged; + } + + /** + * Returns the total number of items that can be laid out. Note that this number is not + * necessarily equal to the number of items in the adapter, so you should always use this + * number for your position calculations and never access the adapter directly. + *

+ * RecyclerView listens for Adapter's notify events and calculates the effects of adapter + * data changes on existing Views. These calculations are used to decide which animations + * should be run. + *

+ * To support predictive animations, RecyclerView may rewrite or reorder Adapter changes to + * present the correct state to LayoutManager in pre-layout pass. + *

+ * For example, a newly added item is not included in pre-layout item count because + * pre-layout reflects the contents of the adapter before the item is added. Behind the + * scenes, RecyclerView offsets {@link Recycler#getViewForPosition(int)} calls such that + * LayoutManager does not know about the new item's existence in pre-layout. The item will + * be available in second layout pass and will be included in the item count. Similar + * adjustments are made for moved and removed items as well. + *

+ * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method. + * + * @return The number of items currently available + * @see LayoutManager#getItemCount() + */ + public int getItemCount() { + return mInPreLayout + ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) + : mItemCount; + } + + /** + * Returns remaining horizontal scroll distance of an ongoing scroll animation(fling/ + * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is + * other than {@link #SCROLL_STATE_SETTLING}. + * + * @return Remaining horizontal scroll distance + */ + public int getRemainingScrollHorizontal() { + return mRemainingScrollHorizontal; + } + + /** + * Returns remaining vertical scroll distance of an ongoing scroll animation(fling/ + * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is + * other than {@link #SCROLL_STATE_SETTLING}. + * + * @return Remaining vertical scroll distance + */ + public int getRemainingScrollVertical() { + return mRemainingScrollVertical; + } + + @Override + public String toString() { + return "State{" + + "mTargetPosition=" + mTargetPosition + + ", mData=" + mData + + ", mItemCount=" + mItemCount + + ", mIsMeasuring=" + mIsMeasuring + + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + + ", mDeletedInvisibleItemCountSincePreviousLayout=" + + mDeletedInvisibleItemCountSincePreviousLayout + + ", mStructureChanged=" + mStructureChanged + + ", mInPreLayout=" + mInPreLayout + + ", mRunSimpleAnimations=" + mRunSimpleAnimations + + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations + + '}'; + } + } + + /** + * This class defines the behavior of fling if the developer wishes to handle it. + *

+ * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior. + * + * @see #setOnFlingListener(OnFlingListener) + */ + public abstract static class OnFlingListener { + + /** + * Override this to handle a fling given the velocities in both x and y directions. + * Note that this method will only be called if the associated {@link LayoutManager} + * supports scrolling and the fling is not handled by nested scrolls first. + * + * @param velocityX the fling velocity on the X axis + * @param velocityY the fling velocity on the Y axis + * @return true if the fling was handled, false otherwise. + */ + public abstract boolean onFling(int velocityX, int velocityY); + } + + /** + * Internal listener that manages items after animations finish. This is how items are + * retained (not recycled) during animations, but allowed to be recycled afterwards. + * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished() + * method on the animator's listener when it is done animating any item. + */ + private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener { + + ItemAnimatorRestoreListener() { + } + + @Override + public void onAnimationFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh + item.mShadowedHolder = null; + } + // always null this because an OldViewHolder can never become NewViewHolder w/o being + // recycled. + item.mShadowingHolder = null; + if (!item.shouldBeKeptAsChild()) { + if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { + removeDetachedView(item.itemView, false); + } + } + } + } + + /** + * This class defines the animations that take place on items as changes are made + * to the adapter. + * + * Subclasses of ItemAnimator can be used to implement custom animations for actions on + * ViewHolder items. The RecyclerView will manage retaining these items while they + * are being animated, but implementors must call {@link #dispatchAnimationFinished(ViewHolder)} + * when a ViewHolder's animation is finished. In other words, there must be a matching + * {@link #dispatchAnimationFinished(ViewHolder)} call for each + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) animatePersistence()}, + * and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()} call. + * + *

By default, RecyclerView uses {@link DefaultItemAnimator}.

+ * + * @see #setItemAnimator(ItemAnimator) + */ + @SuppressWarnings("UnusedParameters") + public abstract static class ItemAnimator { + + /** + * The Item represented by this ViewHolder is updated. + *

+ * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_CHANGED = ViewHolder.FLAG_UPDATE; + + /** + * The Item represented by this ViewHolder is removed from the adapter. + *

+ * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_REMOVED = ViewHolder.FLAG_REMOVED; + + /** + * Adapter {@link Adapter#notifyDataSetChanged()} has been called and the content + * represented by this ViewHolder is invalid. + *

+ * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_INVALIDATED = ViewHolder.FLAG_INVALID; + + /** + * The position of the Item represented by this ViewHolder has been changed. This flag is + * not bound to {@link Adapter#notifyItemMoved(int, int)}. It might be set in response to + * any adapter change that may have a side effect on this item. (e.g. The item before this + * one has been removed from the Adapter). + *

+ * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_MOVED = ViewHolder.FLAG_MOVED; + + /** + * This ViewHolder was not laid out but has been added to the layout in pre-layout state + * by the {@link LayoutManager}. This means that the item was already in the Adapter but + * invisible and it may become visible in the post layout phase. LayoutManagers may prefer + * to add new items in pre-layout to specify their virtual location when they are invisible + * (e.g. to specify the item should animate in from below the visible area). + *

+ * + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + */ + public static final int FLAG_APPEARED_IN_PRE_LAYOUT = + ViewHolder.FLAG_APPEARED_IN_PRE_LAYOUT; + + /** + * The set of flags that might be passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + */ + @IntDef(flag = true, value = { + FLAG_CHANGED, FLAG_REMOVED, FLAG_MOVED, FLAG_INVALIDATED, + FLAG_APPEARED_IN_PRE_LAYOUT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AdapterChanges { + } + + private ItemAnimatorListener mListener = null; + private ArrayList mFinishedListeners = + new ArrayList(); + + private long mAddDuration = 120; + private long mRemoveDuration = 120; + private long mMoveDuration = 250; + private long mChangeDuration = 250; + + /** + * Gets the current duration for which all move animations will run. + * + * @return The current move duration + */ + public long getMoveDuration() { + return mMoveDuration; + } + + /** + * Sets the duration for which all move animations will run. + * + * @param moveDuration The move duration + */ + public void setMoveDuration(long moveDuration) { + mMoveDuration = moveDuration; + } + + /** + * Gets the current duration for which all add animations will run. + * + * @return The current add duration + */ + public long getAddDuration() { + return mAddDuration; + } + + /** + * Sets the duration for which all add animations will run. + * + * @param addDuration The add duration + */ + public void setAddDuration(long addDuration) { + mAddDuration = addDuration; + } + + /** + * Gets the current duration for which all remove animations will run. + * + * @return The current remove duration + */ + public long getRemoveDuration() { + return mRemoveDuration; + } + + /** + * Sets the duration for which all remove animations will run. + * + * @param removeDuration The remove duration + */ + public void setRemoveDuration(long removeDuration) { + mRemoveDuration = removeDuration; + } + + /** + * Gets the current duration for which all change animations will run. + * + * @return The current change duration + */ + public long getChangeDuration() { + return mChangeDuration; + } + + /** + * Sets the duration for which all change animations will run. + * + * @param changeDuration The change duration + */ + public void setChangeDuration(long changeDuration) { + mChangeDuration = changeDuration; + } + + /** + * Internal only: + * Sets the listener that must be called when the animator is finished + * animating the item (or immediately if no animation happens). This is set + * internally and is not intended to be set by external code. + * + * @param listener The listener that must be called. + */ + void setListener(ItemAnimatorListener listener) { + mListener = listener; + } + + /** + * Called by the RecyclerView before the layout begins. Item animator should record + * necessary information about the View before it is potentially rebound, moved or removed. + *

+ * The data returned from this method will be passed to the related animate** + * methods. + *

+ * Note that this method may be called after pre-layout phase if LayoutManager adds new + * Views to the layout in pre-layout pass. + *

+ * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View and the adapter change flags. + * + * @param state The current State of RecyclerView which includes some useful data + * about the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * @param changeFlags Additional information about what changes happened in the Adapter + * about the Item represented by this ViewHolder. For instance, if + * item is deleted from the adapter, {@link #FLAG_REMOVED} will be set. + * @param payloads The payload list that was previously passed to + * {@link Adapter#notifyItemChanged(int, Object)} or + * {@link Adapter#notifyItemRangeChanged(int, int, Object)}. + * @return An ItemHolderInfo instance that preserves necessary information about the + * ViewHolder. This object will be passed back to related animate** methods + * after layout is complete. + * @see #recordPostLayoutInformation(State, ViewHolder) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public @NonNull + ItemHolderInfo recordPreLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder, @AdapterChanges int changeFlags, + @NonNull List payloads) { + return obtainHolderInfo().setFrom(viewHolder); + } + + /** + * Called by the RecyclerView after the layout is complete. Item animator should record + * necessary information about the View's final state. + *

+ * The data returned from this method will be passed to the related animate** + * methods. + *

+ * The default implementation returns an {@link ItemHolderInfo} which holds the bounds of + * the View. + * + * @param state The current State of RecyclerView which includes some useful data about + * the layout that will be calculated. + * @param viewHolder The ViewHolder whose information should be recorded. + * @return An ItemHolderInfo that preserves necessary information about the ViewHolder. + * This object will be passed back to related animate** methods when + * RecyclerView decides how items should be animated. + * @see #recordPreLayoutInformation(State, ViewHolder, int, List) + * @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public @NonNull + ItemHolderInfo recordPostLayoutInformation(@NonNull State state, + @NonNull ViewHolder viewHolder) { + return obtainHolderInfo().setFrom(viewHolder); + } + + /** + * Called by the RecyclerView when a ViewHolder has disappeared from the layout. + *

+ * This means that the View was a child of the LayoutManager when layout started but has + * been removed by the LayoutManager. It might have been removed from the adapter or simply + * become invisible due to other factors. You can distinguish these two cases by checking + * the change flags that were passed to + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + *

+ * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange} instead of {@code animateDisappearance} if and only if the ItemAnimator + * returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + *

+ * If LayoutManager supports predictive animations, it might provide a target disappear + * location for the View by laying it out in that location. When that happens, + * RecyclerView will call {@link #recordPostLayoutInformation(State, ViewHolder)} and the + * response of that call will be passed to this method as the postLayoutInfo. + *

+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from + * {@link #recordPostLayoutInformation(State, ViewHolder)}. Might be + * null if the LayoutManager did not layout the item. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateDisappearance(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when a ViewHolder is added to the layout. + *

+ * In detail, this means that the ViewHolder was not a child when the layout started + * but has been added by the LayoutManager. It might be newly added to the adapter or + * simply become visible due to other factors. + *

+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * Might be null if Item was just added to the adapter or + * LayoutManager does not support predictive animations or it could + * not predict that this ViewHolder will become visible. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateAppearance(@NonNull ViewHolder viewHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when a ViewHolder is present in both before and after the + * layout and RecyclerView has not received a {@link Adapter#notifyItemChanged(int)} call + * for it or a {@link Adapter#notifyDataSetChanged()} call. + *

+ * This ViewHolder still represents the same data that it was representing when the layout + * started but its position / size may be changed by the LayoutManager. + *

+ * If the Item's layout position didn't change, RecyclerView still calls this method because + * it does not track this information (or does not necessarily know that an animation is + * not required). Your ItemAnimator should handle this case and if there is nothing to + * animate, it should call {@link #dispatchAnimationFinished(ViewHolder)} and return + * false. + *

+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} when the animation + * is complete (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it + * decides not to animate the view). + * + * @param viewHolder The ViewHolder which should be animated + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animatePersistence(@NonNull ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + /** + * Called by the RecyclerView when an adapter item is present both before and after the + * layout and RecyclerView has received a {@link Adapter#notifyItemChanged(int)} call + * for it. This method may also be called when + * {@link Adapter#notifyDataSetChanged()} is called and adapter has stable ids so that + * RecyclerView could still rebind views to the same ViewHolders. If viewType changes when + * {@link Adapter#notifyDataSetChanged()} is called, this method will not be called, + * instead, {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)} will be + * called for the new ViewHolder and the old one will be recycled. + *

+ * If this method is called due to a {@link Adapter#notifyDataSetChanged()} call, there is + * a good possibility that item contents didn't really change but it is rebound from the + * adapter. {@link DefaultItemAnimator} will skip animating the View if its location on the + * screen didn't change and your animator should handle this case as well and avoid creating + * unnecessary animations. + *

+ * When an item is updated, ItemAnimator has a chance to ask RecyclerView to keep the + * previous presentation of the item as-is and supply a new ViewHolder for the updated + * presentation (see: {@link #canReuseUpdatedViewHolder(ViewHolder, List)}. + * This is useful if you don't know the contents of the Item and would like + * to cross-fade the old and the new one ({@link DefaultItemAnimator} uses this technique). + *

+ * When you are writing a custom item animator for your layout, it might be more performant + * and elegant to re-use the same ViewHolder and animate the content changes manually. + *

+ * When {@link Adapter#notifyItemChanged(int)} is called, the Item's view type may change. + * If the Item's view type has changed or ItemAnimator returned false for + * this ViewHolder when {@link #canReuseUpdatedViewHolder(ViewHolder, List)} was called, the + * oldHolder and newHolder will be different ViewHolder instances + * which represent the same Item. In that case, only the new ViewHolder is visible + * to the LayoutManager but RecyclerView keeps old ViewHolder attached for animations. + *

+ * ItemAnimator must call {@link #dispatchAnimationFinished(ViewHolder)} for each distinct + * ViewHolder when their animation is complete + * (or instantly call {@link #dispatchAnimationFinished(ViewHolder)} if it decides not to + * animate the view). + *

+ * If oldHolder and newHolder are the same instance, you should call + * {@link #dispatchAnimationFinished(ViewHolder)} only once. + *

+ * Note that when a ViewHolder both changes and disappears in the same layout pass, the + * animation callback method which will be called by the RecyclerView depends on the + * ItemAnimator's decision whether to re-use the same ViewHolder or not, and also the + * LayoutManager's decision whether to layout the changed version of a disappearing + * ViewHolder or not. RecyclerView will call + * {@code animateChange} instead of + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance} if and only if the ItemAnimator returns {@code false} from + * {@link #canReuseUpdatedViewHolder(ViewHolder) canReuseUpdatedViewHolder} and the + * LayoutManager lays out a new disappearing view that holds the updated information. + * Built-in LayoutManagers try to avoid laying out updated versions of disappearing views. + * + * @param oldHolder The ViewHolder before the layout is started, might be the same + * instance with newHolder. + * @param newHolder The ViewHolder after the layout is finished, might be the same + * instance with oldHolder. + * @param preLayoutInfo The information that was returned from + * {@link #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @param postLayoutInfo The information that was returned from {@link + * #recordPreLayoutInformation(State, ViewHolder, int, List)}. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateChange(@NonNull ViewHolder oldHolder, + @NonNull ViewHolder newHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo); + + @AdapterChanges + static int buildAdapterChangeFlagsForAnimations(ViewHolder viewHolder) { + int flags = viewHolder.mFlags & (FLAG_INVALIDATED | FLAG_REMOVED | FLAG_CHANGED); + if (viewHolder.isInvalid()) { + return FLAG_INVALIDATED; + } + if ((flags & FLAG_INVALIDATED) == 0) { + final int oldPos = viewHolder.getOldPosition(); + final int pos = viewHolder.getAbsoluteAdapterPosition(); + if (oldPos != NO_POSITION && pos != NO_POSITION && oldPos != pos) { + flags |= FLAG_MOVED; + } + } + return flags; + } + + /** + * Called when there are pending animations waiting to be started. This state + * is governed by the return values from + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()} + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, and + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, which inform the RecyclerView that the ItemAnimator wants to be + * called later to start the associated animations. runPendingAnimations() will be scheduled + * to be run on the next frame. + */ + public abstract void runPendingAnimations(); + + /** + * Method called when an animation on a view should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on the item + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + * + * @param item The item for which an animation should be stopped. + */ + public abstract void endAnimation(@NonNull ViewHolder item); + + /** + * Method called when all item animations should be ended immediately. + * This could happen when other events, like scrolling, occur, so that + * animating views can be quickly put into their proper end locations. + * Implementations should ensure that any animations running on any items + * are canceled and affected properties are set to their end values. + * Also, {@link #dispatchAnimationFinished(ViewHolder)} should be called for each finished + * animation since the animations are effectively done when this method is called. + */ + public abstract void endAnimations(); + + /** + * Method which returns whether there are any item animations currently running. + * This method can be used to determine whether to delay other actions until + * animations end. + * + * @return true if there are any item animations currently running, false otherwise. + */ + public abstract boolean isRunning(); + + /** + * Method to be called by subclasses when an animation is finished. + *

+ * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there + * should + * be a matching {@link #dispatchAnimationFinished(ViewHolder)} call by the subclass. + *

+ * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the oldHolder + * and newHolder (if they are not the same instance). + * + * @param viewHolder The ViewHolder whose animation is finished. + * @see #onAnimationFinished(ViewHolder) + */ + public final void dispatchAnimationFinished(@NonNull ViewHolder viewHolder) { + onAnimationFinished(viewHolder); + if (mListener != null) { + mListener.onAnimationFinished(viewHolder); + } + } + + /** + * Called after {@link #dispatchAnimationFinished(ViewHolder)} is called by the + * ItemAnimator. + * + * @param viewHolder The ViewHolder whose animation is finished. There might still be other + * animations running on this ViewHolder. + * @see #dispatchAnimationFinished(ViewHolder) + */ + public void onAnimationFinished(@NonNull ViewHolder viewHolder) { + } + + /** + * Method to be called by subclasses when an animation is started. + *

+ * For each call RecyclerView makes to + * {@link #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateAppearance()}, + * {@link #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animatePersistence()}, or + * {@link #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateDisappearance()}, there should be a matching + * {@link #dispatchAnimationStarted(ViewHolder)} call by the subclass. + *

+ * For {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo) + * animateChange()}, subclass should call this method for both the oldHolder + * and newHolder (if they are not the same instance). + *

+ * If your ItemAnimator decides not to animate a ViewHolder, it should call + * {@link #dispatchAnimationFinished(ViewHolder)} without calling + * {@link #dispatchAnimationStarted(ViewHolder)}. + * + * @param viewHolder The ViewHolder whose animation is starting. + * @see #onAnimationStarted(ViewHolder) + */ + public final void dispatchAnimationStarted(@NonNull ViewHolder viewHolder) { + onAnimationStarted(viewHolder); + } + + /** + * Called when a new animation is started on the given ViewHolder. + * + * @param viewHolder The ViewHolder which started animating. Note that the ViewHolder + * might already be animating and this might be another animation. + * @see #dispatchAnimationStarted(ViewHolder) + */ + public void onAnimationStarted(@NonNull ViewHolder viewHolder) { + + } + + /** + * Like {@link #isRunning()}, this method returns whether there are any item + * animations currently running. Additionally, the listener passed in will be called + * when there are no item animations running, either immediately (before the method + * returns) if no animations are currently running, or when the currently running + * animations are {@link #dispatchAnimationsFinished() finished}. + * + *

Note that the listener is transient - it is either called immediately and not + * stored at all, or stored only until it is called when running animations + * are finished sometime later.

+ * + * @param listener A listener to be called immediately if no animations are running + * or later when currently-running animations have finished. A null + * listener is + * equivalent to calling {@link #isRunning()}. + * @return true if there are any item animations currently running, false otherwise. + */ + public final boolean isRunning(@Nullable ItemAnimatorFinishedListener listener) { + boolean running = isRunning(); + if (listener != null) { + if (!running) { + listener.onAnimationsFinished(); + } else { + mFinishedListeners.add(listener); + } + } + return running; + } + + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + *

+ * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + *

+ * If your application is using change payloads, you can override + * {@link #canReuseUpdatedViewHolder(ViewHolder, List)} to decide based on payloads. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation returns true. + * @see #canReuseUpdatedViewHolder(ViewHolder, List) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) { + return true; + } + + /** + * When an item is changed, ItemAnimator can decide whether it wants to re-use + * the same ViewHolder for animations or RecyclerView should create a copy of the + * item and ItemAnimator will use both to run the animation (e.g. cross-fade). + *

+ * Note that this method will only be called if the {@link ViewHolder} still has the same + * type ({@link Adapter#getItemViewType(int)}). Otherwise, ItemAnimator will always receive + * both {@link ViewHolder}s in the + * {@link #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)} method. + * + * @param viewHolder The ViewHolder which represents the changed item's old content. + * @param payloads A non-null list of merged payloads that were sent with change + * notifications. Can be empty if the adapter is invalidated via + * {@link RecyclerView.Adapter#notifyDataSetChanged()}. The same list of + * payloads will be passed into + * {@link RecyclerView.Adapter#onBindViewHolder(ViewHolder, int, List)} + * method if this method returns true. + * @return True if RecyclerView should just rebind to the same ViewHolder or false if + * RecyclerView should create a new ViewHolder and pass this ViewHolder to the + * ItemAnimator to animate. Default implementation calls + * {@link #canReuseUpdatedViewHolder(ViewHolder)}. + * @see #canReuseUpdatedViewHolder(ViewHolder) + */ + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List payloads) { + return canReuseUpdatedViewHolder(viewHolder); + } + + /** + * This method should be called by ItemAnimator implementations to notify + * any listeners that all pending and active item animations are finished. + */ + public final void dispatchAnimationsFinished() { + final int count = mFinishedListeners.size(); + for (int i = 0; i < count; ++i) { + mFinishedListeners.get(i).onAnimationsFinished(); + } + mFinishedListeners.clear(); + } + + /** + * Returns a new {@link ItemHolderInfo} which will be used to store information about the + * ViewHolder. This information will later be passed into animate** methods. + *

+ * You can override this method if you want to extend {@link ItemHolderInfo} and provide + * your own instances. + * + * @return A new {@link ItemHolderInfo}. + */ + @NonNull + public ItemHolderInfo obtainHolderInfo() { + return new ItemHolderInfo(); + } + + /** + * The interface to be implemented by listeners to animation events from this + * ItemAnimator. This is used internally and is not intended for developers to + * create directly. + */ + interface ItemAnimatorListener { + void onAnimationFinished(@NonNull ViewHolder item); + } + + /** + * This interface is used to inform listeners when all pending or running animations + * in an ItemAnimator are finished. This can be used, for example, to delay an action + * in a data set until currently-running animations are complete. + * + * @see #isRunning(ItemAnimatorFinishedListener) + */ + public interface ItemAnimatorFinishedListener { + /** + * Notifies when all pending or running animations in an ItemAnimator are finished. + */ + void onAnimationsFinished(); + } + + /** + * A simple data structure that holds information about an item's bounds. + * This information is used in calculating item animations. Default implementation of + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)} and + * {@link #recordPostLayoutInformation(RecyclerView.State, ViewHolder)} returns this data + * structure. You can extend this class if you would like to keep more information about + * the Views. + *

+ * If you want to provide your own implementation but still use `super` methods to record + * basic information, you can override {@link #obtainHolderInfo()} to provide your own + * instances. + */ + public static class ItemHolderInfo { + + /** + * The left edge of the View (excluding decorations) + */ + public int left; + + /** + * The top edge of the View (excluding decorations) + */ + public int top; + + /** + * The right edge of the View (excluding decorations) + */ + public int right; + + /** + * The bottom edge of the View (excluding decorations) + */ + public int bottom; + + /** + * The change flags that were passed to + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, List)}. + */ + @AdapterChanges + public int changeFlags; + + public ItemHolderInfo() { + } + + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder. Clears all {@link #changeFlags}. + * + * @param holder The ViewHolder whose bounds should be copied. + * @return This {@link ItemHolderInfo} + */ + @NonNull + public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder) { + return setFrom(holder, 0); + } + + /** + * Sets the {@link #left}, {@link #top}, {@link #right} and {@link #bottom} values from + * the given ViewHolder and sets the {@link #changeFlags} to the given flags parameter. + * + * @param holder The ViewHolder whose bounds should be copied. + * @param flags The adapter change flags that were passed into + * {@link #recordPreLayoutInformation(RecyclerView.State, ViewHolder, int, + * List)}. + * @return This {@link ItemHolderInfo} + */ + @NonNull + public ItemHolderInfo setFrom(@NonNull RecyclerView.ViewHolder holder, + @AdapterChanges int flags) { + final View view = holder.itemView; + this.left = view.getLeft(); + this.top = view.getTop(); + this.right = view.getRight(); + this.bottom = view.getBottom(); + return this; + } + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mChildDrawingOrderCallback == null) { + return super.getChildDrawingOrder(childCount, i); + } else { + return mChildDrawingOrderCallback.onGetChildDrawingOrder(childCount, i); + } + } + + /** + * A callback interface that can be used to alter the drawing order of RecyclerView children. + *

+ * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case + * that applies to that method also applies to this callback. For example, changing the drawing + * order of two views will not have any effect if their elevation values are different since + * elevation overrides the result of this callback. + */ + public interface ChildDrawingOrderCallback { + /** + * Returns the index of the child to draw for this iteration. Override this + * if you want to change the drawing order of children. By default, it + * returns i. + * + * @param i The current iteration. + * @return The index of the child to draw this iteration. + * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) + */ + int onGetChildDrawingOrder(int childCount, int i); + } + + private NestedScrollingChildHelper getScrollingChildHelper() { + if (mScrollingChildHelper == null) { + mScrollingChildHelper = new NestedScrollingChildHelper(this); + } + return mScrollingChildHelper; + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java b/app/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java new file mode 100644 index 0000000000..fd9d86c345 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/RecyclerViewAccessibilityDelegate.java @@ -0,0 +1,272 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityNodeProviderCompat; + +import java.util.Map; +import java.util.WeakHashMap; + +/** + * The AccessibilityDelegate used by RecyclerView. + *

+ * This class handles basic accessibility actions and delegates them to LayoutManager. + */ +public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat { + final RecyclerView mRecyclerView; + private final ItemDelegate mItemDelegate; + + public RecyclerViewAccessibilityDelegate(@NonNull RecyclerView recyclerView) { + mRecyclerView = recyclerView; + AccessibilityDelegateCompat itemDelegate = getItemDelegate(); + if (itemDelegate != null && itemDelegate instanceof ItemDelegate) { + mItemDelegate = (ItemDelegate) itemDelegate; + } else { + mItemDelegate = new ItemDelegate(this); + } + } + + boolean shouldIgnore() { + return mRecyclerView.hasPendingAdapterUpdates(); + } + + @Override + public boolean performAccessibilityAction( + @SuppressLint("InvalidNullabilityOverride") @NonNull View host, + int action, + @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args + ) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args); + } + + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo( + @SuppressLint("InvalidNullabilityOverride") @NonNull View host, + @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityNodeInfoCompat info + ) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info); + } + } + + @Override + public void onInitializeAccessibilityEvent( + @SuppressLint("InvalidNullabilityOverride") @NonNull View host, + @SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityEvent event + ) { + super.onInitializeAccessibilityEvent(host, event); + if (host instanceof RecyclerView && !shouldIgnore()) { + RecyclerView rv = (RecyclerView) host; + if (rv.getLayoutManager() != null) { + rv.getLayoutManager().onInitializeAccessibilityEvent(event); + } + } + } + + /** + * Gets the AccessibilityDelegate for an individual item in the RecyclerView. + * A basic item delegate is provided by default, but you can override this + * method to provide a custom per-item delegate. + * For now, returning an {@code AccessibilityDelegateCompat} as opposed to an + * {@code ItemDelegate} will prevent use of the {@code ViewCompat} accessibility API on + * item views. + */ + @NonNull + public AccessibilityDelegateCompat getItemDelegate() { + return mItemDelegate; + } + + /** + * The default implementation of accessibility delegate for the individual items of the + * RecyclerView. + *

+ * If you are overriding {@code RecyclerViewAccessibilityDelegate#getItemDelegate()} but still + * want to keep some default behavior, you can create an instance of this class and delegate to + * the parent as necessary. + */ + public static class ItemDelegate extends AccessibilityDelegateCompat { + final RecyclerViewAccessibilityDelegate mRecyclerViewDelegate; + private Map mOriginalItemDelegates = new WeakHashMap<>(); + + /** + * Creates an item delegate for the given {@code RecyclerViewAccessibilityDelegate}. + * + * @param recyclerViewDelegate The parent RecyclerView's accessibility delegate. + */ + public ItemDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate) { + mRecyclerViewDelegate = recyclerViewDelegate; + } + + /** + * Saves a reference to the original delegate of the itemView so that it's behavior can be + * combined with the ItemDelegate's behavior. + */ + void saveOriginalDelegate(View itemView) { + AccessibilityDelegateCompat delegate = ViewCompat.getAccessibilityDelegate(itemView); + if (delegate != null && delegate != this) { + mOriginalItemDelegates.put(itemView, delegate); + } + } + + /** + * @return The delegate associated with itemView before the view was bound. + */ + AccessibilityDelegateCompat getAndRemoveOriginalDelegateForItem(View itemView) { + return mOriginalItemDelegates.remove(itemView); + } + + @Override + public void onInitializeAccessibilityNodeInfo( + @SuppressLint("InvalidNullabilityOverride") @NonNull View host, + @SuppressLint("InvalidNullabilityOverride") @NonNull + AccessibilityNodeInfoCompat info + ) { + if (!mRecyclerViewDelegate.shouldIgnore() + && mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) { + mRecyclerViewDelegate.mRecyclerView.getLayoutManager() + .onInitializeAccessibilityNodeInfoForItem(host, info); + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + originalDelegate.onInitializeAccessibilityNodeInfo(host, info); + } else { + super.onInitializeAccessibilityNodeInfo(host, info); + } + } else { + super.onInitializeAccessibilityNodeInfo(host, info); + } + } + + @Override + public boolean performAccessibilityAction( + @SuppressLint("InvalidNullabilityOverride") @NonNull View host, + int action, + @SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args + ) { + if (!mRecyclerViewDelegate.shouldIgnore() + && mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + if (originalDelegate.performAccessibilityAction(host, action, args)) { + return true; + } + } else if (super.performAccessibilityAction(host, action, args)) { + return true; + } + return mRecyclerViewDelegate.mRecyclerView.getLayoutManager() + .performAccessibilityActionForItem(host, action, args); + } else { + return super.performAccessibilityAction(host, action, args); + } + } + + @Override + public void sendAccessibilityEvent(@NonNull View host, int eventType) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + originalDelegate.sendAccessibilityEvent(host, eventType); + } else { + super.sendAccessibilityEvent(host, eventType); + } + } + + @Override + public void sendAccessibilityEventUnchecked(@NonNull View host, + @NonNull AccessibilityEvent event) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + originalDelegate.sendAccessibilityEventUnchecked(host, event); + } else { + super.sendAccessibilityEventUnchecked(host, event); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(@NonNull View host, + @NonNull AccessibilityEvent event) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + return originalDelegate.dispatchPopulateAccessibilityEvent(host, event); + } else { + return super.dispatchPopulateAccessibilityEvent(host, event); + } + } + + @Override + public void onPopulateAccessibilityEvent(@NonNull View host, + @NonNull AccessibilityEvent event) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + originalDelegate.onPopulateAccessibilityEvent(host, event); + } else { + super.onPopulateAccessibilityEvent(host, event); + } + } + + @Override + public void onInitializeAccessibilityEvent(@NonNull View host, + @NonNull AccessibilityEvent event) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + originalDelegate.onInitializeAccessibilityEvent(host, event); + } else { + super.onInitializeAccessibilityEvent(host, event); + } + } + + @Override + public boolean onRequestSendAccessibilityEvent(@NonNull ViewGroup host, + @NonNull View child, @NonNull AccessibilityEvent event) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + return originalDelegate.onRequestSendAccessibilityEvent(host, child, event); + } else { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + } + + @Override + @Nullable + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(@NonNull View host) { + AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host); + if (originalDelegate != null) { + return originalDelegate.getAccessibilityNodeProvider(host); + } else { + return super.getAccessibilityNodeProvider(host); + } + } + } +} + diff --git a/app/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java b/app/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java new file mode 100644 index 0000000000..8cc24aeb1e --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ScrollbarHelper.java @@ -0,0 +1,101 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.view.View; + +/** + * A helper class to do scroll offset calculations. + */ +class ScrollbarHelper { + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled, boolean reverseLayout) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + final int minPosition = Math.min(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int maxPosition = Math.max(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int itemsBefore = reverseLayout + ? Math.max(0, state.getItemCount() - maxPosition - 1) + : Math.max(0, minPosition); + if (!smoothScrollbarEnabled) { + return itemsBefore; + } + final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild)); + final int itemRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + 1; + final float avgSizePerRow = (float) laidOutArea / itemRange; + + return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding() + - orientation.getDecoratedStart(startChild))); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; + } + final int extend = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + return Math.min(orientation.getTotalSpace(), extend); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return state.getItemCount(); + } + // smooth scrollbar enabled. try to estimate better. + final int laidOutArea = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + final int laidOutRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + + 1; + // estimate a size for full list. + return (int) ((float) laidOutArea / laidOutRange * state.getItemCount()); + } + + private ScrollbarHelper() { + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java b/app/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java new file mode 100644 index 0000000000..4dc33c6315 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/SimpleItemAnimator.java @@ -0,0 +1,479 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * A wrapper class for ItemAnimator that records View bounds and decides whether it should run + * move, change, add or remove animations. This class also replicates the original ItemAnimator + * API. + *

+ * It uses {@link RecyclerView.ItemAnimator.ItemHolderInfo} to track the bounds information of + * the Views. If you would like to extend this class, you can override {@link #obtainHolderInfo()} + * method to provide your own info class that extends + * {@link RecyclerView.ItemAnimator.ItemHolderInfo}. + */ +public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator { + + private static final boolean DEBUG = false; + + private static final String TAG = "SimpleItemAnimator"; + + boolean mSupportsChangeAnimations = true; + + /** + * Returns whether this ItemAnimator supports animations of change events. + * + * @return true if change animations are supported, false otherwise + */ + @SuppressWarnings("unused") + public boolean getSupportsChangeAnimations() { + return mSupportsChangeAnimations; + } + + /** + * Sets whether this ItemAnimator supports animations of item change events. + * If you set this property to false, actions on the data set which change the + * contents of items will not be animated. What those animations do is left + * up to the discretion of the ItemAnimator subclass, in its + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} implementation. + * The value of this property is true by default. + * + * @param supportsChangeAnimations true if change animations are supported by + * this ItemAnimator, false otherwise. If the property is false, + * the ItemAnimator + * will not receive a call to + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, + * int)} when changes occur. + * @see RecyclerView.Adapter#notifyItemChanged(int) + * @see RecyclerView.Adapter#notifyItemRangeChanged(int, int) + */ + public void setSupportsChangeAnimations(boolean supportsChangeAnimations) { + mSupportsChangeAnimations = supportsChangeAnimations; + } + + /** + * {@inheritDoc} + * + * @return True if change animations are not supported or the ViewHolder is invalid, + * false otherwise. + * + * @see #setSupportsChangeAnimations(boolean) + */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { + return !mSupportsChangeAnimations || viewHolder.isInvalid(); + } + + @Override + public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) { + int oldLeft = preLayoutInfo.left; + int oldTop = preLayoutInfo.top; + View disappearingItemView = viewHolder.itemView; + int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left; + int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top; + if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) { + disappearingItemView.layout(newLeft, newTop, + newLeft + disappearingItemView.getWidth(), + newTop + disappearingItemView.getHeight()); + if (DEBUG) { + Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView); + } + return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop); + } else { + if (DEBUG) { + Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView); + } + return animateRemove(viewHolder); + } + } + + @Override + public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, + @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left + || preLayoutInfo.top != postLayoutInfo.top)) { + // slide items in if before/after locations differ + if (DEBUG) { + Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder); + } + return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top, + postLayoutInfo.left, postLayoutInfo.top); + } else { + if (DEBUG) { + Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder); + } + return animateAdd(viewHolder); + } + } + + @Override + public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder, + @NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) { + if (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top) { + if (DEBUG) { + Log.d(TAG, "PERSISTENT: " + viewHolder + + " with view " + viewHolder.itemView); + } + return animateMove(viewHolder, + preLayoutInfo.left, preLayoutInfo.top, postLayoutInfo.left, postLayoutInfo.top); + } + dispatchMoveFinished(viewHolder); + return false; + } + + @Override + public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder, + @NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo, + @NonNull ItemHolderInfo postLayoutInfo) { + if (DEBUG) { + Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView); + } + final int fromLeft = preLayoutInfo.left; + final int fromTop = preLayoutInfo.top; + final int toLeft, toTop; + if (newHolder.shouldIgnore()) { + toLeft = preLayoutInfo.left; + toTop = preLayoutInfo.top; + } else { + toLeft = postLayoutInfo.left; + toTop = postLayoutInfo.top; + } + return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop); + } + + /** + * Called when an item is removed from the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchRemoveFinished(RecyclerView.ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()}, + * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + *

This method may also be called for disappearing items which continue to exist in the + * RecyclerView, but for which the system does not have enough information to animate + * them out of view. In that case, the default animation for removing items is run + * on those items as well.

+ * + * @param holder The item that is being removed. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract boolean animateRemove(RecyclerView.ViewHolder holder); + + /** + * Called when an item is added to the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchAddFinished(RecyclerView.ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()}, + * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + *

This method may also be called for appearing items which were already in the + * RecyclerView, but for which the system does not have enough information to animate + * them into view. In that case, the default animation for adding items is run + * on those items as well.

+ * + * @param holder The item that is being added. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract boolean animateAdd(RecyclerView.ViewHolder holder); + + /** + * Called when an item is moved in the RecyclerView. Implementors can choose + * whether and how to animate that change, but must always call + * {@link #dispatchMoveFinished(RecyclerView.ViewHolder)} when done, either + * immediately (if no animation will occur) or after the animation actually finishes. + * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()}, + * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * @param holder The item that is being moved. + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, + int toX, int toY); + + /** + * Called when an item is changed in the RecyclerView, as indicated by a call to + * {@link RecyclerView.Adapter#notifyItemChanged(int)} or + * {@link RecyclerView.Adapter#notifyItemRangeChanged(int, int)}. + *

+ * Implementers can choose whether and how to animate changes, but must always call + * {@link #dispatchChangeFinished(RecyclerView.ViewHolder, boolean)} for each non-null + * distinct ViewHolder, + * either immediately (if no animation will occur) or after the animation actually finishes. + * If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call + * {@link #dispatchChangeFinished(RecyclerView.ViewHolder, boolean)} once and only once. In + * that case, the + * second parameter of {@code dispatchChangeFinished} is ignored. + *

+ * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()}, + * {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * @param oldHolder The original item that changed. + * @param newHolder The new item that was created with the changed content. Might be null + * @param fromLeft Left of the old view holder + * @param fromTop Top of the old view holder + * @param toLeft Left of the new view holder + * @param toTop Top of the new view holder + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract boolean animateChange(RecyclerView.ViewHolder oldHolder, + RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop); + + /** + * Method to be called by subclasses when a remove animation is done. + * + * @param item The item which has been removed + * @see RecyclerView.ItemAnimator#animateDisappearance(RecyclerView.ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchRemoveFinished(RecyclerView.ViewHolder item) { + onRemoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a move animation is done. + * + * @param item The item which has been moved + * @see RecyclerView.ItemAnimator#animateDisappearance(RecyclerView.ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animatePersistence(RecyclerView.ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animateAppearance(RecyclerView.ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchMoveFinished(RecyclerView.ViewHolder item) { + onMoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when an add animation is done. + * + * @param item The item which has been added + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchAddFinished(RecyclerView.ViewHolder item) { + onAddFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a change animation is done. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + * @see #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int) + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { + onChangeFinished(item, oldItem); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a remove animation is being started. + * + * @param item The item being removed + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchRemoveStarting(RecyclerView.ViewHolder item) { + onRemoveStarting(item); + } + + /** + * Method to be called by subclasses when a move animation is being started. + * + * @param item The item being moved + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchMoveStarting(RecyclerView.ViewHolder item) { + onMoveStarting(item); + } + + /** + * Method to be called by subclasses when an add animation is being started. + * + * @param item The item being added + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchAddStarting(RecyclerView.ViewHolder item) { + onAddStarting(item); + } + + /** + * Method to be called by subclasses when a change animation is being started. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public final void dispatchChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) { + onChangeStarting(item, oldItem); + } + + /** + * Called when a remove animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + @SuppressWarnings("UnusedParameters") + public void onRemoveStarting(RecyclerView.ViewHolder item) { + } + + /** + * Called when a remove animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onRemoveFinished(RecyclerView.ViewHolder item) { + } + + /** + * Called when an add animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onAddStarting(RecyclerView.ViewHolder item) { + } + + /** + * Called when an add animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onAddFinished(RecyclerView.ViewHolder item) { + } + + /** + * Called when a move animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onMoveStarting(RecyclerView.ViewHolder item) { + } + + /** + * Called when a move animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onMoveFinished(RecyclerView.ViewHolder item) { + } + + /** + * Called when a change animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + @SuppressWarnings("UnusedParameters") + public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) { + } + + /** + * Called when a change animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { + } +} + diff --git a/app/src/main/java/androidx/recyclerview/widget/SnapHelper.java b/app/src/main/java/androidx/recyclerview/widget/SnapHelper.java new file mode 100644 index 0000000000..5ba267b385 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/SnapHelper.java @@ -0,0 +1,308 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.animation.DecelerateInterpolator; +import android.widget.Scroller; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Class intended to support snapping for a {@link RecyclerView}. + *

+ * SnapHelper tries to handle fling as well but for this to work properly, the + * {@link RecyclerView.LayoutManager} must implement the {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface or + * you should override {@link #onFling(int, int)} and handle fling manually. + */ +public abstract class SnapHelper extends RecyclerView.OnFlingListener { + + static final float MILLISECONDS_PER_INCH = 100f; + + RecyclerView mRecyclerView; + private Scroller mGravityScroller; + + // Handles the snap on scroll case. + private final RecyclerView.OnScrollListener mScrollListener = + new RecyclerView.OnScrollListener() { + boolean mScrolled = false; + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) { + mScrolled = false; + snapToTargetExistingView(); + } + } + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (dx != 0 || dy != 0) { + mScrolled = true; + } + } + }; + + @Override + public boolean onFling(int velocityX, int velocityY) { + RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (layoutManager == null) { + return false; + } + RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); + if (adapter == null) { + return false; + } + int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); + return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) + && snapFromFling(layoutManager, velocityX, velocityY); + } + + /** + * Attaches the {@link SnapHelper} to the provided RecyclerView, by calling + * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}. + * You can call this method with {@code null} to detach it from the current RecyclerView. + * + * @param recyclerView The RecyclerView instance to which you want to add this helper or + * {@code null} if you want to remove SnapHelper from the current + * RecyclerView. + * + * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener} + * attached to the provided {@link RecyclerView}. + * + */ + public void attachToRecyclerView(@Nullable RecyclerView recyclerView) + throws IllegalStateException { + if (mRecyclerView == recyclerView) { + return; // nothing to do + } + if (mRecyclerView != null) { + destroyCallbacks(); + } + mRecyclerView = recyclerView; + if (mRecyclerView != null) { + setupCallbacks(); + mGravityScroller = new Scroller(mRecyclerView.getContext(), + new DecelerateInterpolator()); + snapToTargetExistingView(); + } + } + + /** + * Called when an instance of a {@link RecyclerView} is attached. + */ + private void setupCallbacks() throws IllegalStateException { + if (mRecyclerView.getOnFlingListener() != null) { + throw new IllegalStateException("An instance of OnFlingListener already set."); + } + mRecyclerView.addOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(this); + } + + /** + * Called when the instance of a {@link RecyclerView} is detached. + */ + private void destroyCallbacks() { + mRecyclerView.removeOnScrollListener(mScrollListener); + mRecyclerView.setOnFlingListener(null); + } + + /** + * Calculated the estimated scroll distance in each direction given velocities on both axes. + * + * @param velocityX Fling velocity on the horizontal axis. + * @param velocityY Fling velocity on the vertical axis. + * + * @return array holding the calculated distances in x and y directions + * respectively. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public int[] calculateScrollDistance(int velocityX, int velocityY) { + int[] outDist = new int[2]; + mGravityScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + outDist[0] = mGravityScroller.getFinalX(); + outDist[1] = mGravityScroller.getFinalY(); + return outDist; + } + + /** + * Helper method to facilitate for snapping triggered by a fling. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * @param velocityX Fling velocity on the horizontal axis. + * @param velocityY Fling velocity on the vertical axis. + * + * @return true if it is handled, false otherwise. + */ + private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX, + int velocityY) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return false; + } + + RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager); + if (smoothScroller == null) { + return false; + } + + int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY); + if (targetPosition == RecyclerView.NO_POSITION) { + return false; + } + + smoothScroller.setTargetPosition(targetPosition); + layoutManager.startSmoothScroll(smoothScroller); + return true; + } + + /** + * Snaps to a target view which currently exists in the attached {@link RecyclerView}. This + * method is used to snap the view when the {@link RecyclerView} is first attached; when + * snapping was triggered by a scroll and when the fling is at its final stages. + */ + void snapToTargetExistingView() { + if (mRecyclerView == null) { + return; + } + RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager(); + if (layoutManager == null) { + return; + } + View snapView = findSnapView(layoutManager); + if (snapView == null) { + return; + } + int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView); + if (snapDistance[0] != 0 || snapDistance[1] != 0) { + mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]); + } + } + + /** + * Creates a scroller to be used in the snapping implementation. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * + * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling. + */ + @Nullable + protected RecyclerView.SmoothScroller createScroller( + @NonNull RecyclerView.LayoutManager layoutManager) { + return createSnapScroller(layoutManager); + } + + /** + * Creates a scroller to be used in the snapping implementation. + * + * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView}. + * + * @return a {@link LinearSmoothScroller} which will handle the scrolling. + * @deprecated use {@link #createScroller(RecyclerView.LayoutManager)} instead. + */ + @Nullable + @Deprecated + protected LinearSmoothScroller createSnapScroller( + @NonNull RecyclerView.LayoutManager layoutManager) { + if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) { + return null; + } + return new LinearSmoothScroller(mRecyclerView.getContext()) { + @Override + protected void onTargetFound(View targetView, RecyclerView.State state, Action action) { + if (mRecyclerView == null) { + // The associated RecyclerView has been removed so there is no action to take. + return; + } + int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(), + targetView); + final int dx = snapDistances[0]; + final int dy = snapDistances[1]; + final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy))); + if (time > 0) { + action.update(dx, dy, time, mDecelerateInterpolator); + } + } + + @Override + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + }; + } + + /** + * Override this method to snap to a particular point within the target view or the container + * view on any axis. + *

+ * This method is called when the {@link SnapHelper} has intercepted a fling and it needs + * to know the exact distance required to scroll by in order to snap to the target view. + * + * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView} + * @param targetView the target view that is chosen as the view to snap + * + * @return the output coordinates the put the result into. out[0] is the distance + * on horizontal axis and out[1] is the distance on vertical axis. + */ + @SuppressWarnings("WeakerAccess") + @Nullable + public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, + @NonNull View targetView); + + /** + * Override this method to provide a particular target view for snapping. + *

+ * This method is called when the {@link SnapHelper} is ready to start snapping and requires + * a target view to snap to. It will be explicitly called when the scroll state becomes idle + * after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap + * after a fling and requires a reference view from the current set of child views. + *

+ * If this method returns {@code null}, SnapHelper will not snap to any view. + * + * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView} + * + * @return the target view to which to snap on fling or end of scroll + */ + @SuppressWarnings("WeakerAccess") + @Nullable + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract View findSnapView(RecyclerView.LayoutManager layoutManager); + + /** + * Override to provide a particular adapter target position for snapping. + * + * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached + * {@link RecyclerView} + * @param velocityX fling velocity on the horizontal axis + * @param velocityY fling velocity on the vertical axis + * + * @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION} + * if no snapping should happen + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public abstract int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, + int velocityX, int velocityY); +} diff --git a/app/src/main/java/androidx/recyclerview/widget/SortedList.java b/app/src/main/java/androidx/recyclerview/widget/SortedList.java new file mode 100644 index 0000000000..3ae96b81fc --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/SortedList.java @@ -0,0 +1,1015 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; + +/** + * A Sorted list implementation that can keep items in order and also notify for changes in the + * list + * such that it can be bound to a {@link RecyclerView.Adapter + * RecyclerView.Adapter}. + *

+ * It keeps items ordered using the {@link Callback#compare(Object, Object)} method and uses + * binary search to retrieve items. If the sorting criteria of your items may change, make sure you + * call appropriate methods while editing them to avoid data inconsistencies. + *

+ * You can control the order of items and change notifications via the {@link Callback} parameter. + */ +@SuppressWarnings("unchecked") +public class SortedList { + + /** + * Used by {@link #indexOf(Object)} when the item cannot be found in the list. + */ + public static final int INVALID_POSITION = -1; + + private static final int MIN_CAPACITY = 10; + private static final int CAPACITY_GROWTH = MIN_CAPACITY; + private static final int INSERTION = 1; + private static final int DELETION = 1 << 1; + private static final int LOOKUP = 1 << 2; + T[] mData; + + /** + * A reference to the previous set of data that is kept during a mutation operation (addAll or + * replaceAll). + */ + private T[] mOldData; + + /** + * The current index into mOldData that has not yet been processed during a mutation operation + * (addAll or replaceAll). + */ + private int mOldDataStart; + private int mOldDataSize; + + /** + * The current index into the new data that has not yet been processed during a mutation + * operation (addAll or replaceAll). + */ + private int mNewDataStart; + + /** + * The callback instance that controls the behavior of the SortedList and get notified when + * changes happen. + */ + private Callback mCallback; + + private BatchedCallback mBatchedCallback; + + private int mSize; + private final Class mTClass; + + /** + * Creates a new SortedList of type T. + * + * @param klass The class of the contents of the SortedList. + * @param callback The callback that controls the behavior of SortedList. + */ + public SortedList(@NonNull Class klass, @NonNull Callback callback) { + this(klass, callback, MIN_CAPACITY); + } + + /** + * Creates a new SortedList of type T. + * + * @param klass The class of the contents of the SortedList. + * @param callback The callback that controls the behavior of SortedList. + * @param initialCapacity The initial capacity to hold items. + */ + public SortedList(@NonNull Class klass, @NonNull Callback callback, int initialCapacity) { + mTClass = klass; + mData = (T[]) Array.newInstance(klass, initialCapacity); + mCallback = callback; + mSize = 0; + } + + /** + * The number of items in the list. + * + * @return The number of items in the list. + */ + public int size() { + return mSize; + } + + /** + * Adds the given item to the list. If this is a new item, SortedList calls + * {@link Callback#onInserted(int, int)}. + *

+ * If the item already exists in the list and its sorting criteria is not changed, it is + * replaced with the existing Item. SortedList uses + * {@link Callback#areItemsTheSame(Object, Object)} to check if two items are the same item + * and uses {@link Callback#areContentsTheSame(Object, Object)} to decide whether it should + * call {@link Callback#onChanged(int, int)} or not. In both cases, it always removes the + * reference to the old item and puts the new item into the backing array even if + * {@link Callback#areContentsTheSame(Object, Object)} returns false. + *

+ * If the sorting criteria of the item is changed, SortedList won't be able to find + * its duplicate in the list which will result in having a duplicate of the Item in the list. + * If you need to update sorting criteria of an item that already exists in the list, + * use {@link #updateItemAt(int, Object)}. You can find the index of the item using + * {@link #indexOf(Object)} before you update the object. + * + * @param item The item to be added into the list. + * + * @return The index of the newly added item. + * @see Callback#compare(Object, Object) + * @see Callback#areItemsTheSame(Object, Object) + * @see Callback#areContentsTheSame(Object, Object)} + */ + public int add(T item) { + throwIfInMutationOperation(); + return add(item, true); + } + + /** + * Adds the given items to the list. Equivalent to calling {@link SortedList#add} in a loop, + * except the callback events may be in a different order/granularity since addAll can batch + * them for better performance. + *

+ * If allowed, will reference the input array during, and possibly after, the operation to avoid + * extra memory allocation, in which case you should not continue to reference or modify the + * array yourself. + *

+ * @param items Array of items to be added into the list. + * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the + * input array. + * @see SortedList#addAll(T[] items) + */ + public void addAll(@NonNull T[] items, boolean mayModifyInput) { + throwIfInMutationOperation(); + if (items.length == 0) { + return; + } + + if (mayModifyInput) { + addAllInternal(items); + } else { + addAllInternal(copyArray(items)); + } + } + + /** + * Adds the given items to the list. Does not modify or retain the input. + * + * @see SortedList#addAll(T[] items, boolean mayModifyInput) + * + * @param items Array of items to be added into the list. + */ + public void addAll(@NonNull T... items) { + addAll(items, false); + } + + /** + * Adds the given items to the list. Does not modify or retain the input. + * + * @see SortedList#addAll(T[] items, boolean mayModifyInput) + * + * @param items Collection of items to be added into the list. + */ + public void addAll(@NonNull Collection items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.size()); + addAll(items.toArray(copy), true); + } + + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. + *

+ * If allowed, will reference the input array during, and possibly after, the operation to avoid + * extra memory allocation, in which case you should not continue to reference or modify the + * array yourself. + *

+ * Note: this method does not detect moves or dispatch + * {@link ListUpdateCallback#onMoved(int, int)} events. It instead treats moves as a remove + * followed by an add and therefore dispatches {@link ListUpdateCallback#onRemoved(int, int)} + * and {@link ListUpdateCallback#onRemoved(int, int)} events. See {@link DiffUtil} if you want + * your implementation to dispatch move events. + *

+ * @param items Array of items to replace current items. + * @param mayModifyInput If true, SortedList is allowed to modify and permanently reference the + * input array. + * @see #replaceAll(T[]) + */ + public void replaceAll(@NonNull T[] items, boolean mayModifyInput) { + throwIfInMutationOperation(); + + if (mayModifyInput) { + replaceAllInternal(items); + } else { + replaceAllInternal(copyArray(items)); + } + } + + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. Does not modify or retain the input. + * + * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. + */ + public void replaceAll(@NonNull T... items) { + replaceAll(items, false); + } + + /** + * Replaces the current items with the new items, dispatching {@link ListUpdateCallback} events + * for each change detected as appropriate. Does not modify or retain the input. + * + * @see #replaceAll(T[], boolean) + * + * @param items Array of items to replace current items. + */ + public void replaceAll(@NonNull Collection items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.size()); + replaceAll(items.toArray(copy), true); + } + + private void addAllInternal(T[] newItems) { + if (newItems.length < 1) { + return; + } + + final int newSize = sortAndDedup(newItems); + + if (mSize == 0) { + mData = newItems; + mSize = newSize; + mCallback.onInserted(0, newSize); + } else { + merge(newItems, newSize); + } + } + + private void replaceAllInternal(@NonNull T[] newData) { + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + if (forceBatchedUpdates) { + beginBatchedUpdates(); + } + + mOldDataStart = 0; + mOldDataSize = mSize; + mOldData = mData; + + mNewDataStart = 0; + int newSize = sortAndDedup(newData); + mData = (T[]) Array.newInstance(mTClass, newSize); + + while (mNewDataStart < newSize || mOldDataStart < mOldDataSize) { + if (mOldDataStart >= mOldDataSize) { + int insertIndex = mNewDataStart; + int itemCount = newSize - mNewDataStart; + System.arraycopy(newData, insertIndex, mData, insertIndex, itemCount); + mNewDataStart += itemCount; + mSize += itemCount; + mCallback.onInserted(insertIndex, itemCount); + break; + } + if (mNewDataStart >= newSize) { + int itemCount = mOldDataSize - mOldDataStart; + mSize -= itemCount; + mCallback.onRemoved(mNewDataStart, itemCount); + break; + } + + T oldItem = mOldData[mOldDataStart]; + T newItem = newData[mNewDataStart]; + + int result = mCallback.compare(oldItem, newItem); + if (result < 0) { + replaceAllRemove(); + } else if (result > 0) { + replaceAllInsert(newItem); + } else { + if (!mCallback.areItemsTheSame(oldItem, newItem)) { + // The items aren't the same even though they were supposed to occupy the same + // place, so both notify to remove and add an item in the current location. + replaceAllRemove(); + replaceAllInsert(newItem); + } else { + mData[mNewDataStart] = newItem; + mOldDataStart++; + mNewDataStart++; + if (!mCallback.areContentsTheSame(oldItem, newItem)) { + // The item is the same but the contents have changed, so notify that an + // onChanged event has occurred. + mCallback.onChanged(mNewDataStart - 1, 1, + mCallback.getChangePayload(oldItem, newItem)); + } + } + } + } + + mOldData = null; + + if (forceBatchedUpdates) { + endBatchedUpdates(); + } + } + + private void replaceAllInsert(T newItem) { + mData[mNewDataStart] = newItem; + mNewDataStart++; + mSize++; + mCallback.onInserted(mNewDataStart - 1, 1); + } + + private void replaceAllRemove() { + mSize--; + mOldDataStart++; + mCallback.onRemoved(mNewDataStart, 1); + } + + /** + * Sorts and removes duplicate items, leaving only the last item from each group of "same" + * items. Move the remaining items to the beginning of the array. + * + * @return Number of deduplicated items at the beginning of the array. + */ + private int sortAndDedup(@NonNull T[] items) { + if (items.length == 0) { + return 0; + } + + // Arrays.sort is stable. + Arrays.sort(items, mCallback); + + // Keep track of the range of equal items at the end of the output. + // Start with the range containing just the first item. + int rangeStart = 0; + int rangeEnd = 1; + + for (int i = 1; i < items.length; ++i) { + T currentItem = items[i]; + + int compare = mCallback.compare(items[rangeStart], currentItem); + + if (compare == 0) { + // The range of equal items continues, update it. + final int sameItemPos = findSameItem(currentItem, items, rangeStart, rangeEnd); + if (sameItemPos != INVALID_POSITION) { + // Replace the duplicate item. + items[sameItemPos] = currentItem; + } else { + // Expand the range. + if (rangeEnd != i) { // Avoid redundant copy. + items[rangeEnd] = currentItem; + } + rangeEnd++; + } + } else { + // The range has ended. Reset it to contain just the current item. + if (rangeEnd != i) { // Avoid redundant copy. + items[rangeEnd] = currentItem; + } + rangeStart = rangeEnd++; + } + } + return rangeEnd; + } + + + private int findSameItem(T item, T[] items, int from, int to) { + for (int pos = from; pos < to; pos++) { + if (mCallback.areItemsTheSame(items[pos], item)) { + return pos; + } + } + return INVALID_POSITION; + } + + /** + * This method assumes that newItems are sorted and deduplicated. + */ + private void merge(T[] newData, int newDataSize) { + final boolean forceBatchedUpdates = !(mCallback instanceof BatchedCallback); + if (forceBatchedUpdates) { + beginBatchedUpdates(); + } + + mOldData = mData; + mOldDataStart = 0; + mOldDataSize = mSize; + + final int mergedCapacity = mSize + newDataSize + CAPACITY_GROWTH; + mData = (T[]) Array.newInstance(mTClass, mergedCapacity); + mNewDataStart = 0; + + int newDataStart = 0; + while (mOldDataStart < mOldDataSize || newDataStart < newDataSize) { + if (mOldDataStart == mOldDataSize) { + // No more old items, copy the remaining new items. + int itemCount = newDataSize - newDataStart; + System.arraycopy(newData, newDataStart, mData, mNewDataStart, itemCount); + mNewDataStart += itemCount; + mSize += itemCount; + mCallback.onInserted(mNewDataStart - itemCount, itemCount); + break; + } + + if (newDataStart == newDataSize) { + // No more new items, copy the remaining old items. + int itemCount = mOldDataSize - mOldDataStart; + System.arraycopy(mOldData, mOldDataStart, mData, mNewDataStart, itemCount); + mNewDataStart += itemCount; + break; + } + + T oldItem = mOldData[mOldDataStart]; + T newItem = newData[newDataStart]; + int compare = mCallback.compare(oldItem, newItem); + if (compare > 0) { + // New item is lower, output it. + mData[mNewDataStart++] = newItem; + mSize++; + newDataStart++; + mCallback.onInserted(mNewDataStart - 1, 1); + } else if (compare == 0 && mCallback.areItemsTheSame(oldItem, newItem)) { + // Items are the same. Output the new item, but consume both. + mData[mNewDataStart++] = newItem; + newDataStart++; + mOldDataStart++; + if (!mCallback.areContentsTheSame(oldItem, newItem)) { + mCallback.onChanged(mNewDataStart - 1, 1, + mCallback.getChangePayload(oldItem, newItem)); + } + } else { + // Old item is lower than or equal to (but not the same as the new). Output it. + // New item with the same sort order will be inserted later. + mData[mNewDataStart++] = oldItem; + mOldDataStart++; + } + } + + mOldData = null; + + if (forceBatchedUpdates) { + endBatchedUpdates(); + } + } + + /** + * Throws an exception if called while we are in the middle of a mutation operation (addAll or + * replaceAll). + */ + private void throwIfInMutationOperation() { + if (mOldData != null) { + throw new IllegalStateException("Data cannot be mutated in the middle of a batch " + + "update operation such as addAll or replaceAll."); + } + } + + /** + * Batches adapter updates that happen after calling this method and before calling + * {@link #endBatchedUpdates()}. For example, if you add multiple items in a loop + * and they are placed into consecutive indices, SortedList calls + * {@link Callback#onInserted(int, int)} only once with the proper item count. If an event + * cannot be merged with the previous event, the previous event is dispatched + * to the callback instantly. + *

+ * After running your data updates, you must call {@link #endBatchedUpdates()} + * which will dispatch any deferred data change event to the current callback. + *

+ * A sample implementation may look like this: + *

+     *     mSortedList.beginBatchedUpdates();
+     *     try {
+     *         mSortedList.add(item1)
+     *         mSortedList.add(item2)
+     *         mSortedList.remove(item3)
+     *         ...
+     *     } finally {
+     *         mSortedList.endBatchedUpdates();
+     *     }
+     * 
+ *

+ * Instead of using this method to batch calls, you can use a Callback that extends + * {@link BatchedCallback}. In that case, you must make sure that you are manually calling + * {@link BatchedCallback#dispatchLastEvent()} right after you complete your data changes. + * Failing to do so may create data inconsistencies with the Callback. + *

+ * If the current Callback is an instance of {@link BatchedCallback}, calling this method + * has no effect. + */ + public void beginBatchedUpdates() { + throwIfInMutationOperation(); + if (mCallback instanceof BatchedCallback) { + return; + } + if (mBatchedCallback == null) { + mBatchedCallback = new BatchedCallback(mCallback); + } + mCallback = mBatchedCallback; + } + + /** + * Ends the update transaction and dispatches any remaining event to the callback. + */ + public void endBatchedUpdates() { + throwIfInMutationOperation(); + if (mCallback instanceof BatchedCallback) { + ((BatchedCallback) mCallback).dispatchLastEvent(); + } + if (mCallback == mBatchedCallback) { + mCallback = mBatchedCallback.mWrappedCallback; + } + } + + private int add(T item, boolean notify) { + int index = findIndexOf(item, mData, 0, mSize, INSERTION); + if (index == INVALID_POSITION) { + index = 0; + } else if (index < mSize) { + T existing = mData[index]; + if (mCallback.areItemsTheSame(existing, item)) { + if (mCallback.areContentsTheSame(existing, item)) { + //no change but still replace the item + mData[index] = item; + return index; + } else { + mData[index] = item; + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); + return index; + } + } + } + addToData(index, item); + if (notify) { + mCallback.onInserted(index, 1); + } + return index; + } + + /** + * Removes the provided item from the list and calls {@link Callback#onRemoved(int, int)}. + * + * @param item The item to be removed from the list. + * + * @return True if item is removed, false if item cannot be found in the list. + */ + public boolean remove(T item) { + throwIfInMutationOperation(); + return remove(item, true); + } + + /** + * Removes the item at the given index and calls {@link Callback#onRemoved(int, int)}. + * + * @param index The index of the item to be removed. + * + * @return The removed item. + */ + public T removeItemAt(int index) { + throwIfInMutationOperation(); + T item = get(index); + removeItemAtIndex(index, true); + return item; + } + + private boolean remove(T item, boolean notify) { + int index = findIndexOf(item, mData, 0, mSize, DELETION); + if (index == INVALID_POSITION) { + return false; + } + removeItemAtIndex(index, notify); + return true; + } + + private void removeItemAtIndex(int index, boolean notify) { + System.arraycopy(mData, index + 1, mData, index, mSize - index - 1); + mSize--; + mData[mSize] = null; + if (notify) { + mCallback.onRemoved(index, 1); + } + } + + /** + * Updates the item at the given index and calls {@link Callback#onChanged(int, int)} and/or + * {@link Callback#onMoved(int, int)} if necessary. + *

+ * You can use this method if you need to change an existing Item such that its position in the + * list may change. + *

+ * If the new object is a different object (get(index) != item) and + * {@link Callback#areContentsTheSame(Object, Object)} returns true, SortedList + * avoids calling {@link Callback#onChanged(int, int)} otherwise it calls + * {@link Callback#onChanged(int, int)}. + *

+ * If the new position of the item is different than the provided index, + * SortedList + * calls {@link Callback#onMoved(int, int)}. + * + * @param index The index of the item to replace + * @param item The item to replace the item at the given Index. + * @see #add(Object) + */ + public void updateItemAt(int index, T item) { + throwIfInMutationOperation(); + final T existing = get(index); + // assume changed if the same object is given back + boolean contentsChanged = existing == item || !mCallback.areContentsTheSame(existing, item); + if (existing != item) { + // different items, we can use comparison and may avoid lookup + final int cmp = mCallback.compare(existing, item); + if (cmp == 0) { + mData[index] = item; + if (contentsChanged) { + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); + } + return; + } + } + if (contentsChanged) { + mCallback.onChanged(index, 1, mCallback.getChangePayload(existing, item)); + } + // TODO this done in 1 pass to avoid shifting twice. + removeItemAtIndex(index, false); + int newIndex = add(item, false); + if (index != newIndex) { + mCallback.onMoved(index, newIndex); + } + } + + /** + * This method can be used to recalculate the position of the item at the given index, without + * triggering an {@link Callback#onChanged(int, int)} callback. + *

+ * If you are editing objects in the list such that their position in the list may change but + * you don't want to trigger an onChange animation, you can use this method to re-position it. + * If the item changes position, SortedList will call {@link Callback#onMoved(int, int)} + * without + * calling {@link Callback#onChanged(int, int)}. + *

+ * A sample usage may look like: + * + *

+     *     final int position = mSortedList.indexOf(item);
+     *     item.incrementPriority(); // assume items are sorted by priority
+     *     mSortedList.recalculatePositionOfItemAt(position);
+     * 
+ * In the example above, because the sorting criteria of the item has been changed, + * mSortedList.indexOf(item) will not be able to find the item. This is why the code above + * first + * gets the position before editing the item, edits it and informs the SortedList that item + * should be repositioned. + * + * @param index The current index of the Item whose position should be re-calculated. + * @see #updateItemAt(int, Object) + * @see #add(Object) + */ + public void recalculatePositionOfItemAt(int index) { + throwIfInMutationOperation(); + // TODO can be improved + final T item = get(index); + removeItemAtIndex(index, false); + int newIndex = add(item, false); + if (index != newIndex) { + mCallback.onMoved(index, newIndex); + } + } + + /** + * Returns the item at the given index. + * + * @param index The index of the item to retrieve. + * + * @return The item at the given index. + * @throws java.lang.IndexOutOfBoundsException if provided index is negative or larger than the + * size of the list. + */ + public T get(int index) throws IndexOutOfBoundsException { + if (index >= mSize || index < 0) { + throw new IndexOutOfBoundsException("Asked to get item at " + index + " but size is " + + mSize); + } + if (mOldData != null) { + // The call is made from a callback during addAll execution. The data is split + // between mData and mOldData. + if (index >= mNewDataStart) { + return mOldData[index - mNewDataStart + mOldDataStart]; + } + } + return mData[index]; + } + + /** + * Returns the position of the provided item. + * + * @param item The item to query for position. + * + * @return The position of the provided item or {@link #INVALID_POSITION} if item is not in the + * list. + */ + public int indexOf(T item) { + if (mOldData != null) { + int index = findIndexOf(item, mData, 0, mNewDataStart, LOOKUP); + if (index != INVALID_POSITION) { + return index; + } + index = findIndexOf(item, mOldData, mOldDataStart, mOldDataSize, LOOKUP); + if (index != INVALID_POSITION) { + return index - mOldDataStart + mNewDataStart; + } + return INVALID_POSITION; + } + return findIndexOf(item, mData, 0, mSize, LOOKUP); + } + + private int findIndexOf(T item, T[] mData, int left, int right, int reason) { + while (left < right) { + final int middle = (left + right) / 2; + T myItem = mData[middle]; + final int cmp = mCallback.compare(myItem, item); + if (cmp < 0) { + left = middle + 1; + } else if (cmp == 0) { + if (mCallback.areItemsTheSame(myItem, item)) { + return middle; + } else { + int exact = linearEqualitySearch(item, middle, left, right); + if (reason == INSERTION) { + return exact == INVALID_POSITION ? middle : exact; + } else { + return exact; + } + } + } else { + right = middle; + } + } + return reason == INSERTION ? left : INVALID_POSITION; + } + + private int linearEqualitySearch(T item, int middle, int left, int right) { + // go left + for (int next = middle - 1; next >= left; next--) { + T nextItem = mData[next]; + int cmp = mCallback.compare(nextItem, item); + if (cmp != 0) { + break; + } + if (mCallback.areItemsTheSame(nextItem, item)) { + return next; + } + } + for (int next = middle + 1; next < right; next++) { + T nextItem = mData[next]; + int cmp = mCallback.compare(nextItem, item); + if (cmp != 0) { + break; + } + if (mCallback.areItemsTheSame(nextItem, item)) { + return next; + } + } + return INVALID_POSITION; + } + + private void addToData(int index, T item) { + if (index > mSize) { + throw new IndexOutOfBoundsException( + "cannot add item to " + index + " because size is " + mSize); + } + if (mSize == mData.length) { + // we are at the limit enlarge + T[] newData = (T[]) Array.newInstance(mTClass, mData.length + CAPACITY_GROWTH); + System.arraycopy(mData, 0, newData, 0, index); + newData[index] = item; + System.arraycopy(mData, index, newData, index + 1, mSize - index); + mData = newData; + } else { + // just shift, we fit + System.arraycopy(mData, index, mData, index + 1, mSize - index); + mData[index] = item; + } + mSize++; + } + + private T[] copyArray(T[] items) { + T[] copy = (T[]) Array.newInstance(mTClass, items.length); + System.arraycopy(items, 0, copy, 0, items.length); + return copy; + } + + /** + * Removes all items from the SortedList. + */ + public void clear() { + throwIfInMutationOperation(); + if (mSize == 0) { + return; + } + final int prevSize = mSize; + Arrays.fill(mData, 0, prevSize, null); + mSize = 0; + mCallback.onRemoved(0, prevSize); + } + + /** + * The class that controls the behavior of the {@link SortedList}. + *

+ * It defines how items should be sorted and how duplicates should be handled. + *

+ * SortedList calls the callback methods on this class to notify changes about the underlying + * data. + */ + public static abstract class Callback implements Comparator, ListUpdateCallback { + + /** + * Similar to {@link java.util.Comparator#compare(Object, Object)}, should compare two and + * return how they should be ordered. + * + * @param o1 The first object to compare. + * @param o2 The second object to compare. + * + * @return a negative integer, zero, or a positive integer as the + * first argument is less than, equal to, or greater than the + * second. + */ + @Override + abstract public int compare(T2 o1, T2 o2); + + /** + * Called by the SortedList when the item at the given position is updated. + * + * @param position The position of the item which has been updated. + * @param count The number of items which has changed. + */ + abstract public void onChanged(int position, int count); + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChanged(int position, int count, Object payload) { + onChanged(position, count); + } + + /** + * Called by the SortedList when it wants to check whether two items have the same data + * or not. SortedList uses this information to decide whether it should call + * {@link #onChanged(int, int)} or not. + *

+ * SortedList uses this method to check equality instead of {@link Object#equals(Object)} + * so + * that you can change its behavior depending on your UI. + *

+ * For example, if you are using SortedList with a + * {@link RecyclerView.Adapter RecyclerView.Adapter}, you should + * return whether the items' visual representations are the same or not. + * + * @param oldItem The previous representation of the object. + * @param newItem The new object that replaces the previous one. + * + * @return True if the contents of the items are the same or false if they are different. + */ + abstract public boolean areContentsTheSame(T2 oldItem, T2 newItem); + + /** + * Called by the SortedList to decide whether two objects represent the same Item or not. + *

+ * For example, if your items have unique ids, this method should check their equality. + * + * @param item1 The first item to check. + * @param item2 The second item to check. + * + * @return True if the two items represent the same object or false if they are different. + */ + abstract public boolean areItemsTheSame(T2 item1, T2 item2); + + /** + * When {@link #areItemsTheSame(T2, T2)} returns {@code true} for two items and + * {@link #areContentsTheSame(T2, T2)} returns false for them, {@link Callback} calls this + * method to get a payload about the change. + *

+ * For example, if you are using {@link Callback} with + * {@link RecyclerView}, you can return the particular field that + * changed in the item and your + * {@link RecyclerView.ItemAnimator ItemAnimator} can use that + * information to run the correct animation. + *

+ * Default implementation returns {@code null}. + * + * @param item1 The first item to check. + * @param item2 The second item to check. + * @return A payload object that represents the changes between the two items. + */ + @Nullable + public Object getChangePayload(T2 item1, T2 item2) { + return null; + } + } + + /** + * A callback implementation that can batch notify events dispatched by the SortedList. + *

+ * This class can be useful if you want to do multiple operations on a SortedList but don't + * want to dispatch each event one by one, which may result in a performance issue. + *

+ * For example, if you are going to add multiple items to a SortedList, BatchedCallback call + * convert individual onInserted(index, 1) calls into one + * onInserted(index, N) if items are added into consecutive indices. This change + * can help RecyclerView resolve changes much more easily. + *

+ * If consecutive changes in the SortedList are not suitable for batching, BatchingCallback + * dispatches them as soon as such case is detected. After your edits on the SortedList is + * complete, you must always call {@link BatchedCallback#dispatchLastEvent()} to flush + * all changes to the Callback. + */ + public static class BatchedCallback extends Callback { + + final Callback mWrappedCallback; + private final BatchingListUpdateCallback mBatchingListUpdateCallback; + /** + * Creates a new BatchedCallback that wraps the provided Callback. + * + * @param wrappedCallback The Callback which should received the data change callbacks. + * Other method calls (e.g. {@link #compare(Object, Object)} from + * the SortedList are directly forwarded to this Callback. + */ + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public BatchedCallback(Callback wrappedCallback) { + mWrappedCallback = wrappedCallback; + mBatchingListUpdateCallback = new BatchingListUpdateCallback(mWrappedCallback); + } + + @Override + public int compare(T2 o1, T2 o2) { + return mWrappedCallback.compare(o1, o2); + } + + @Override + public void onInserted(int position, int count) { + mBatchingListUpdateCallback.onInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + mBatchingListUpdateCallback.onRemoved(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + mBatchingListUpdateCallback.onMoved(fromPosition, toPosition); + } + + @Override + public void onChanged(int position, int count) { + mBatchingListUpdateCallback.onChanged(position, count, null); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChanged(int position, int count, Object payload) { + mBatchingListUpdateCallback.onChanged(position, count, payload); + } + + @Override + public boolean areContentsTheSame(T2 oldItem, T2 newItem) { + return mWrappedCallback.areContentsTheSame(oldItem, newItem); + } + + @Override + public boolean areItemsTheSame(T2 item1, T2 item2) { + return mWrappedCallback.areItemsTheSame(item1, item2); + } + + @Nullable + @Override + public Object getChangePayload(T2 item1, T2 item2) { + return mWrappedCallback.getChangePayload(item1, item2); + } + + /** + * This method dispatches any pending event notifications to the wrapped Callback. + * You must always call this method after you are done with editing the SortedList. + */ + public void dispatchLastEvent() { + mBatchingListUpdateCallback.dispatchLastEvent(); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java b/app/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java new file mode 100644 index 0000000000..639a26a690 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/SortedListAdapterCallback.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; + +/** + * A {@link SortedList.Callback} implementation that can bind a {@link SortedList} to a + * {@link RecyclerView.Adapter}. + */ +public abstract class SortedListAdapterCallback extends SortedList.Callback { + + final RecyclerView.Adapter mAdapter; + + /** + * Creates a {@link SortedList.Callback} that will forward data change events to the provided + * Adapter. + * + * @param adapter The Adapter instance which should receive events from the SortedList. + */ + public SortedListAdapterCallback( + // b/240775049: Cannot annotate properly + @SuppressLint({"UnknownNullness", "MissingNullability"}) + RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + @Override + public void onInserted(int position, int count) { + mAdapter.notifyItemRangeInserted(position, count); + } + + @Override + public void onRemoved(int position, int count) { + mAdapter.notifyItemRangeRemoved(position, count); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + mAdapter.notifyItemMoved(fromPosition, toPosition); + } + + @Override + public void onChanged(int position, int count) { + mAdapter.notifyItemRangeChanged(position, count); + } + + @Override + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + public void onChanged(int position, int count, Object payload) { + mAdapter.notifyItemRangeChanged(position, count, payload); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/StableIdStorage.java b/app/src/main/java/androidx/recyclerview/widget/StableIdStorage.java new file mode 100644 index 0000000000..8c64384402 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/StableIdStorage.java @@ -0,0 +1,106 @@ +/* + * Copyright 2020 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 androidx.recyclerview.widget; + +import androidx.annotation.NonNull; +import androidx.collection.LongSparseArray; + +/** + * Used by {@link ConcatAdapter} to isolate item ids between nested adapters, if necessary. + */ +interface StableIdStorage { + @NonNull + StableIdLookup createStableIdLookup(); + + /** + * Interface that provides {@link NestedAdapterWrapper}s a way to map their local stable ids + * into global stable ids, based on the configuration of the {@link ConcatAdapter}. + */ + interface StableIdLookup { + long localToGlobal(long localId); + } + + /** + * Returns {@link RecyclerView#NO_ID} for all positions. In other words, stable ids are not + * supported. + */ + class NoStableIdStorage implements StableIdStorage { + private final StableIdLookup mNoIdLookup = new StableIdLookup() { + @Override + public long localToGlobal(long localId) { + return RecyclerView.NO_ID; + } + }; + + @NonNull + @Override + public StableIdLookup createStableIdLookup() { + return mNoIdLookup; + } + } + + /** + * A pass-through implementation that reports the stable id in sub adapters as is. + */ + class SharedPoolStableIdStorage implements StableIdStorage { + private final StableIdLookup mSameIdLookup = new StableIdLookup() { + @Override + public long localToGlobal(long localId) { + return localId; + } + }; + + @NonNull + @Override + public StableIdLookup createStableIdLookup() { + return mSameIdLookup; + } + } + + /** + * An isolating implementation that ensures the stable ids among adapters do not conflict with + * each-other. It keeps a mapping for each adapter from its local stable ids to a global domain + * and always replaces the local id w/ a globally available ID to be consistent. + */ + class IsolatedStableIdStorage implements StableIdStorage { + long mNextStableId = 0; + + long obtainId() { + return mNextStableId++; + } + + @NonNull + @Override + public StableIdLookup createStableIdLookup() { + return new WrapperStableIdLookup(); + } + + class WrapperStableIdLookup implements StableIdLookup { + private final LongSparseArray mLocalToGlobalLookup = new LongSparseArray<>(); + + @Override + public long localToGlobal(long localId) { + Long globalId = mLocalToGlobalLookup.get(localId); + if (globalId == null) { + globalId = obtainId(); + mLocalToGlobalLookup.put(localId, globalId); + } + return globalId; + } + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/app/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java new file mode 100644 index 0000000000..0387e9605e --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java @@ -0,0 +1,3282 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.core.view.ViewCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +/** + * A LayoutManager that lays out children in a staggered grid formation. + * It supports horizontal & vertical layout as well as an ability to layout children in reverse. + *

+ * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps, + * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can + * control this behavior via {@link #setGapStrategy(int)}. + */ +public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements + RecyclerView.SmoothScroller.ScrollVectorProvider { + + private static final String TAG = "StaggeredGridLManager"; + + static final boolean DEBUG = false; + + public static final int HORIZONTAL = RecyclerView.HORIZONTAL; + + public static final int VERTICAL = RecyclerView.VERTICAL; + + /** + * Does not do anything to hide gaps. + */ + public static final int GAP_HANDLING_NONE = 0; + + /** + * @deprecated No longer supported. + */ + @SuppressWarnings("unused") + @Deprecated + public static final int GAP_HANDLING_LAZY = 1; + + /** + * When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will + * check if there are gaps in the because of full span items. If it finds, it will re-layout + * and move items to correct positions with animations. + *

+ * For example, if LayoutManager ends up with the following layout due to adapter changes: + *

+     * AAA
+     * _BC
+     * DDD
+     * 
+ *

+ * It will animate to the following state: + *

+     * AAA
+     * BC_
+     * DDD
+     * 
+ */ + public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2; + + static final int INVALID_OFFSET = Integer.MIN_VALUE; + /** + * While trying to find next view to focus, LayoutManager will not try to scroll more + * than this factor times the total space of the list. If layout is vertical, total space is the + * height minus padding, if layout is horizontal, total space is the width minus padding. + */ + private static final float MAX_SCROLL_FACTOR = 1 / 3f; + + /** + * Number of spans + */ + private int mSpanCount = -1; + + Span[] mSpans; + + /** + * Primary orientation is the layout's orientation, secondary orientation is the orientation + * for spans. Having both makes code much cleaner for calculations. + */ + @NonNull + OrientationHelper mPrimaryOrientation; + @NonNull + OrientationHelper mSecondaryOrientation; + + private int mOrientation; + + /** + * The width or height per span, depending on the orientation. + */ + private int mSizePerSpan; + + @NonNull + private final LayoutState mLayoutState; + + boolean mReverseLayout = false; + + /** + * Aggregated reverse layout value that takes RTL into account. + */ + boolean mShouldReverseLayout = false; + + /** + * Temporary variable used during fill method to check which spans needs to be filled. + */ + private BitSet mRemainingSpans; + + /** + * When LayoutManager needs to scroll to a position, it sets this variable and requests a + * layout which will check this variable and re-layout accordingly. + */ + int mPendingScrollPosition = RecyclerView.NO_POSITION; + + /** + * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is + * called. + */ + int mPendingScrollPositionOffset = INVALID_OFFSET; + + /** + * Keeps the mapping between the adapter positions and spans. This is necessary to provide + * a consistent experience when user scrolls the list. + */ + LazySpanLookup mLazySpanLookup = new LazySpanLookup(); + + /** + * how we handle gaps in UI. + */ + private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; + + /** + * Saved state needs this information to properly layout on restore. + */ + private boolean mLastLayoutFromEnd; + + /** + * Saved state and onLayout needs this information to re-layout properly + */ + private boolean mLastLayoutRTL; + + /** + * SavedState is not handled until a layout happens. This is where we keep it until next + * layout. + */ + private SavedState mPendingSavedState; + + /** + * Re-used measurement specs. updated by onLayout. + */ + private int mFullSizeSpec; + + /** + * Re-used rectangle to get child decor offsets. + */ + private final Rect mTmpRect = new Rect(); + + /** + * Re-used anchor info. + */ + private final AnchorInfo mAnchorInfo = new AnchorInfo(); + + /** + * If a full span item is invalid / or created in reverse direction; it may create gaps in + * the UI. While laying out, if such case is detected, we set this flag. + *

+ * After scrolling stops, we check this flag and if it is set, re-layout. + */ + private boolean mLaidOutInvalidFullSpan = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + /** + * Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and + * sorting distances to views being prefetched. + */ + private int[] mPrefetchDistances; + + private final Runnable mCheckForGapsRunnable = new Runnable() { + @Override + public void run() { + checkForGaps(); + } + }; + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to single column and vertical. + */ + @SuppressWarnings("unused") + public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); + setOrientation(properties.orientation); + setSpanCount(properties.spanCount); + setReverseLayout(properties.reverseLayout); + mLayoutState = new LayoutState(); + createOrientationHelpers(); + } + + /** + * Creates a StaggeredGridLayoutManager with given parameters. + * + * @param spanCount If orientation is vertical, spanCount is number of columns. If + * orientation is horizontal, spanCount is number of rows. + * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} + */ + public StaggeredGridLayoutManager(int spanCount, int orientation) { + mOrientation = orientation; + setSpanCount(spanCount); + mLayoutState = new LayoutState(); + createOrientationHelpers(); + } + + @Override + public boolean isAutoMeasureEnabled() { + return mGapStrategy != GAP_HANDLING_NONE; + } + + private void createOrientationHelpers() { + mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation); + mSecondaryOrientation = OrientationHelper + .createOrientationHelper(this, 1 - mOrientation); + } + + /** + * Checks for gaps in the UI that may be caused by adapter changes. + *

+ * When a full span item is laid out in reverse direction, it sets a flag which we check when + * scroll is stopped (or re-layout happens) and re-layout after first valid item. + */ + boolean checkForGaps() { + if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE || !isAttachedToWindow()) { + return false; + } + final int minPos, maxPos; + if (mShouldReverseLayout) { + minPos = getLastChildPosition(); + maxPos = getFirstChildPosition(); + } else { + minPos = getFirstChildPosition(); + maxPos = getLastChildPosition(); + } + if (minPos == 0) { + View gapView = hasGapsToFix(); + if (gapView != null) { + mLazySpanLookup.clear(); + requestSimpleAnimationsInNextLayout(); + requestLayout(); + return true; + } + } + if (!mLaidOutInvalidFullSpan) { + return false; + } + int invalidGapDir = mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; + final LazySpanLookup.FullSpanItem invalidFsi = mLazySpanLookup + .getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir, true); + if (invalidFsi == null) { + mLaidOutInvalidFullSpan = false; + mLazySpanLookup.forceInvalidateAfter(maxPos + 1); + return false; + } + final LazySpanLookup.FullSpanItem validFsi = mLazySpanLookup + .getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition, + invalidGapDir * -1, true); + if (validFsi == null) { + mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition); + } else { + mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1); + } + requestSimpleAnimationsInNextLayout(); + requestLayout(); + return true; + } + + @Override + public void onScrollStateChanged(int state) { + if (state == RecyclerView.SCROLL_STATE_IDLE) { + checkForGaps(); + } + } + + @Override + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + + removeCallbacks(mCheckForGapsRunnable); + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].clear(); + } + // SGLM will require fresh layout call to recover state after detach + view.requestLayout(); + } + + /** + * Checks for gaps if we've reached to the top of the list. + *

+ * Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field. + */ + View hasGapsToFix() { + int startChildIndex = 0; + int endChildIndex = getChildCount() - 1; + BitSet mSpansToCheck = new BitSet(mSpanCount); + mSpansToCheck.set(0, mSpanCount, true); + + final int firstChildIndex, childLimit; + final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1; + + if (mShouldReverseLayout) { + firstChildIndex = endChildIndex; + childLimit = startChildIndex - 1; + } else { + firstChildIndex = startChildIndex; + childLimit = endChildIndex + 1; + } + final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1; + for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) { + View child = getChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (mSpansToCheck.get(lp.mSpan.mIndex)) { + if (checkSpanForGap(lp.mSpan)) { + return child; + } + mSpansToCheck.clear(lp.mSpan.mIndex); + } + if (lp.mFullSpan) { + continue; // quick reject + } + + if (i + nextChildDiff != childLimit) { + View nextChild = getChildAt(i + nextChildDiff); + boolean compareSpans = false; + if (mShouldReverseLayout) { + // ensure child's end is below nextChild's end + int myEnd = mPrimaryOrientation.getDecoratedEnd(child); + int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild); + if (myEnd < nextEnd) { + return child; //i should have a better position + } else if (myEnd == nextEnd) { + compareSpans = true; + } + } else { + int myStart = mPrimaryOrientation.getDecoratedStart(child); + int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild); + if (myStart > nextStart) { + return child; //i should have a better position + } else if (myStart == nextStart) { + compareSpans = true; + } + } + if (compareSpans) { + // equal, check span indices. + LayoutParams nextLp = (LayoutParams) nextChild.getLayoutParams(); + if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) { + return child; + } + } + } + } + // everything looks good + return null; + } + + private boolean checkSpanForGap(Span span) { + if (mShouldReverseLayout) { + if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) { + // if it is full span, it is OK + final View endView = span.mViews.get(span.mViews.size() - 1); + final LayoutParams lp = span.getLayoutParams(endView); + return !lp.mFullSpan; + } + } else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) { + // if it is full span, it is OK + final View startView = span.mViews.get(0); + final LayoutParams lp = span.getLayoutParams(startView); + return !lp.mFullSpan; + } + return false; + } + + /** + * Sets the number of spans for the layout. This will invalidate all of the span assignments + * for Views. + *

+ * Calling this method will automatically result in a new layout request unless the spanCount + * parameter is equal to current span count. + * + * @param spanCount Number of spans to layout + */ + public void setSpanCount(int spanCount) { + assertNotInLayoutOrScroll(null); + if (spanCount != mSpanCount) { + invalidateSpanAssignments(); + mSpanCount = spanCount; + mRemainingSpans = new BitSet(mSpanCount); + mSpans = new Span[mSpanCount]; + for (int i = 0; i < mSpanCount; i++) { + mSpans[i] = new Span(i); + } + requestLayout(); + } + } + + /** + * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep + * scroll position if this method is called after views are laid out. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation."); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + OrientationHelper tmp = mPrimaryOrientation; + mPrimaryOrientation = mSecondaryOrientation; + mSecondaryOrientation = tmp; + requestLayout(); + } + + /** + * Sets whether LayoutManager should start laying out items from the end of the UI. The order + * items are traversed is not affected by this call. + *

+ * For vertical layout, if it is set to true, first item will be at the bottom of + * the list. + *

+ * For horizontal layouts, it depends on the layout direction. + * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if + * {@link RecyclerView}} is RTL, it will layout from LTR. + * + * @param reverseLayout Whether layout should be in reverse or not + */ + public void setReverseLayout(boolean reverseLayout) { + assertNotInLayoutOrScroll(null); + if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) { + mPendingSavedState.mReverseLayout = reverseLayout; + } + mReverseLayout = reverseLayout; + requestLayout(); + } + + /** + * Returns the current gap handling strategy for StaggeredGridLayoutManager. + *

+ * Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps, + * StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and + * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details. + *

+ * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}. + * + * @return Current gap handling strategy. + * @see #setGapStrategy(int) + * @see #GAP_HANDLING_NONE + * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS + */ + public int getGapStrategy() { + return mGapStrategy; + } + + /** + * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter + * is different than the current strategy, calling this method will trigger a layout request. + * + * @param gapStrategy The new gap handling strategy. Should be + * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link + * #GAP_HANDLING_NONE}. + * @see #getGapStrategy() + */ + public void setGapStrategy(int gapStrategy) { + assertNotInLayoutOrScroll(null); + if (gapStrategy == mGapStrategy) { + return; + } + if (gapStrategy != GAP_HANDLING_NONE + && gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { + throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE " + + "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS"); + } + mGapStrategy = gapStrategy; + requestLayout(); + } + + @Override + public void assertNotInLayoutOrScroll(String message) { + if (mPendingSavedState == null) { + super.assertNotInLayoutOrScroll(message); + } + } + + /** + * Returns the number of spans laid out by StaggeredGridLayoutManager. + * + * @return Number of spans in the layout + */ + public int getSpanCount() { + return mSpanCount; + } + + /** + * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. + *

+ * If you need to cancel current assignments, you can call this method which will clear all + * assignments and request a new layout. + */ + public void invalidateSpanAssignments() { + mLazySpanLookup.clear(); + requestLayout(); + } + + /** + * Calculates the views' layout order. (e.g. from end to start or start to end) + * RTL layout support is applied automatically. So if layout is RTL and + * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. + */ + private void resolveShouldLayoutReverse() { + // A == B is the same result, but we rather keep it readable + if (mOrientation == VERTICAL || !isLayoutRTL()) { + mShouldReverseLayout = mReverseLayout; + } else { + mShouldReverseLayout = !mReverseLayout; + } + } + + boolean isLayoutRTL() { + return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL; + } + + /** + * Returns whether views are laid out in reverse order or not. + *

+ * Not that this value is not affected by RecyclerView's layout direction. + * + * @return True if layout is reversed, false otherwise + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + + @Override + public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) { + // we don't like it to wrap content in our non-scroll direction. + final int width, height; + final int horizontalPadding = getPaddingLeft() + getPaddingRight(); + final int verticalPadding = getPaddingTop() + getPaddingBottom(); + if (mOrientation == VERTICAL) { + final int usedHeight = childrenBounds.height() + verticalPadding; + height = chooseSize(hSpec, usedHeight, getMinimumHeight()); + width = chooseSize(wSpec, mSizePerSpan * mSpanCount + horizontalPadding, + getMinimumWidth()); + } else { + final int usedWidth = childrenBounds.width() + horizontalPadding; + width = chooseSize(wSpec, usedWidth, getMinimumWidth()); + height = chooseSize(hSpec, mSizePerSpan * mSpanCount + verticalPadding, + getMinimumHeight()); + } + setMeasuredDimension(width, height); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + onLayoutChildren(recycler, state, true); + } + + @Override + public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter, + @Nullable RecyclerView.Adapter newAdapter) { + // RV will remove all views so we should clear all spans and assignments of views into spans + mLazySpanLookup.clear(); + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].clear(); + } + } + + private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean shouldCheckForGaps) { + final AnchorInfo anchorInfo = mAnchorInfo; + if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { + if (state.getItemCount() == 0) { + removeAndRecycleAllViews(recycler); + anchorInfo.reset(); + return; + } + } + + boolean recalculateAnchor = !anchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION + || mPendingSavedState != null; + if (recalculateAnchor) { + anchorInfo.reset(); + if (mPendingSavedState != null) { + applyPendingSavedState(anchorInfo); + } else { + resolveShouldLayoutReverse(); + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + } + updateAnchorInfoForLayout(state, anchorInfo); + anchorInfo.mValid = true; + } + if (mPendingSavedState == null && mPendingScrollPosition == RecyclerView.NO_POSITION) { + if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd + || isLayoutRTL() != mLastLayoutRTL) { + mLazySpanLookup.clear(); + anchorInfo.mInvalidateOffsets = true; + } + } + + if (getChildCount() > 0 && (mPendingSavedState == null + || mPendingSavedState.mSpanOffsetsSize < 1)) { + if (anchorInfo.mInvalidateOffsets) { + for (int i = 0; i < mSpanCount; i++) { + // Scroll to position is set, clear. + mSpans[i].clear(); + if (anchorInfo.mOffset != INVALID_OFFSET) { + mSpans[i].setLine(anchorInfo.mOffset); + } + } + } else { + if (recalculateAnchor || mAnchorInfo.mSpanReferenceLines == null) { + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, + anchorInfo.mOffset); + } + mAnchorInfo.saveSpanReferenceLines(mSpans); + } else { + for (int i = 0; i < mSpanCount; i++) { + final Span span = mSpans[i]; + span.clear(); + span.setLine(mAnchorInfo.mSpanReferenceLines[i]); + } + } + } + } + detachAndScrapAttachedViews(recycler); + mLayoutState.mRecycle = false; + mLaidOutInvalidFullSpan = false; + updateMeasureSpecs(mSecondaryOrientation.getTotalSpace()); + updateLayoutState(anchorInfo.mPosition, state); + if (anchorInfo.mLayoutFromEnd) { + // Layout start. + setLayoutStateDirection(LayoutState.LAYOUT_START); + fill(recycler, mLayoutState, state); + // Layout end. + setLayoutStateDirection(LayoutState.LAYOUT_END); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); + } else { + // Layout end. + setLayoutStateDirection(LayoutState.LAYOUT_END); + fill(recycler, mLayoutState, state); + // Layout start. + setLayoutStateDirection(LayoutState.LAYOUT_START); + mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); + } + + repositionToWrapContentIfNecessary(); + + if (getChildCount() > 0) { + if (mShouldReverseLayout) { + fixEndGap(recycler, state, true); + fixStartGap(recycler, state, false); + } else { + fixStartGap(recycler, state, true); + fixEndGap(recycler, state, false); + } + } + boolean hasGaps = false; + if (shouldCheckForGaps && !state.isPreLayout()) { + final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE + && getChildCount() > 0 + && (mLaidOutInvalidFullSpan || hasGapsToFix() != null); + if (needToCheckForGaps) { + removeCallbacks(mCheckForGapsRunnable); + if (checkForGaps()) { + hasGaps = true; + } + } + } + if (state.isPreLayout()) { + mAnchorInfo.reset(); + } + mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd; + mLastLayoutRTL = isLayoutRTL(); + if (hasGaps) { + mAnchorInfo.reset(); + onLayoutChildren(recycler, state, false); + } + } + + @Override + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingScrollPosition = RecyclerView.NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + mPendingSavedState = null; // we don't need this anymore + mAnchorInfo.reset(); + } + + private void repositionToWrapContentIfNecessary() { + if (mSecondaryOrientation.getMode() == View.MeasureSpec.EXACTLY) { + return; // nothing to do + } + float maxSize = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + float size = mSecondaryOrientation.getDecoratedMeasurement(child); + if (size < maxSize) { + continue; + } + LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + if (layoutParams.isFullSpan()) { + size = 1f * size / mSpanCount; + } + maxSize = Math.max(maxSize, size); + } + int before = mSizePerSpan; + int desired = Math.round(maxSize * mSpanCount); + if (mSecondaryOrientation.getMode() == View.MeasureSpec.AT_MOST) { + desired = Math.min(desired, mSecondaryOrientation.getTotalSpace()); + } + updateMeasureSpecs(desired); + if (mSizePerSpan == before) { + return; // nothing has changed + } + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.mFullSpan) { + continue; + } + if (isLayoutRTL() && mOrientation == VERTICAL) { + int newOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * mSizePerSpan; + int prevOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * before; + child.offsetLeftAndRight(newOffset - prevOffset); + } else { + int newOffset = lp.mSpan.mIndex * mSizePerSpan; + int prevOffset = lp.mSpan.mIndex * before; + if (mOrientation == VERTICAL) { + child.offsetLeftAndRight(newOffset - prevOffset); + } else { + child.offsetTopAndBottom(newOffset - prevOffset); + } + } + } + } + + private void applyPendingSavedState(AnchorInfo anchorInfo) { + if (DEBUG) { + Log.d(TAG, "found saved state: " + mPendingSavedState); + } + if (mPendingSavedState.mSpanOffsetsSize > 0) { + if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) { + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].clear(); + int line = mPendingSavedState.mSpanOffsets[i]; + if (line != Span.INVALID_LINE) { + if (mPendingSavedState.mAnchorLayoutFromEnd) { + line += mPrimaryOrientation.getEndAfterPadding(); + } else { + line += mPrimaryOrientation.getStartAfterPadding(); + } + } + mSpans[i].setLine(line); + } + } else { + mPendingSavedState.invalidateSpanInfo(); + mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; + } + } + mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL; + setReverseLayout(mPendingSavedState.mReverseLayout); + resolveShouldLayoutReverse(); + + if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + } else { + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + } + if (mPendingSavedState.mSpanLookupSize > 1) { + mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; + mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems; + } + } + + void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) { + if (updateAnchorFromPendingData(state, anchorInfo)) { + return; + } + if (updateAnchorFromChildren(state, anchorInfo)) { + return; + } + if (DEBUG) { + Log.d(TAG, "Deciding anchor info from fresh state"); + } + anchorInfo.assignCoordinateFromPadding(); + anchorInfo.mPosition = 0; + } + + private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) { + // We don't recycle views out of adapter order. This way, we can rely on the first or + // last child as the anchor position. + // Layout direction may change but we should select the child depending on the latest + // layout direction. Otherwise, we'll choose the wrong child. + anchorInfo.mPosition = mLastLayoutFromEnd + ? findLastReferenceChildPosition(state.getItemCount()) + : findFirstReferenceChildPosition(state.getItemCount()); + anchorInfo.mOffset = INVALID_OFFSET; + return true; + } + + boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { + // Validate scroll position if exists. + if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) { + return false; + } + // Validate it. + if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { + mPendingScrollPosition = RecyclerView.NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + return false; + } + + if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION + || mPendingSavedState.mSpanOffsetsSize < 1) { + // If item is visible, make it fully visible. + final View child = findViewByPosition(mPendingScrollPosition); + if (child != null) { + // Use regular anchor position, offset according to pending offset and target + // child + anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition() + : getFirstChildPosition(); + if (mPendingScrollPositionOffset != INVALID_OFFSET) { + if (anchorInfo.mLayoutFromEnd) { + final int target = mPrimaryOrientation.getEndAfterPadding() + - mPendingScrollPositionOffset; + anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child); + } else { + final int target = mPrimaryOrientation.getStartAfterPadding() + + mPendingScrollPositionOffset; + anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child); + } + return true; + } + + // no offset provided. Decide according to the child location + final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); + if (childSize > mPrimaryOrientation.getTotalSpace()) { + // Item does not fit. Fix depending on layout direction. + anchorInfo.mOffset = anchorInfo.mLayoutFromEnd + ? mPrimaryOrientation.getEndAfterPadding() + : mPrimaryOrientation.getStartAfterPadding(); + return true; + } + + final int startGap = mPrimaryOrientation.getDecoratedStart(child) + - mPrimaryOrientation.getStartAfterPadding(); + if (startGap < 0) { + anchorInfo.mOffset = -startGap; + return true; + } + final int endGap = mPrimaryOrientation.getEndAfterPadding() + - mPrimaryOrientation.getDecoratedEnd(child); + if (endGap < 0) { + anchorInfo.mOffset = endGap; + return true; + } + // child already visible. just layout as usual + anchorInfo.mOffset = INVALID_OFFSET; + } else { + // Child is not visible. Set anchor coordinate depending on in which direction + // child will be visible. + anchorInfo.mPosition = mPendingScrollPosition; + if (mPendingScrollPositionOffset == INVALID_OFFSET) { + final int position = calculateScrollDirectionForPosition( + anchorInfo.mPosition); + anchorInfo.mLayoutFromEnd = position == LayoutState.LAYOUT_END; + anchorInfo.assignCoordinateFromPadding(); + } else { + anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset); + } + anchorInfo.mInvalidateOffsets = true; + } + } else { + anchorInfo.mOffset = INVALID_OFFSET; + anchorInfo.mPosition = mPendingScrollPosition; + } + return true; + } + + void updateMeasureSpecs(int totalSpace) { + mSizePerSpan = totalSpace / mSpanCount; + //noinspection ResourceType + mFullSizeSpec = View.MeasureSpec.makeMeasureSpec( + totalSpace, mSecondaryOrientation.getMode()); + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null; + } + + /** + * Returns the adapter position of the first visible view for each span. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those + * views are ignored in this method. + * + * @param into An array to put the results into. If you don't provide any, LayoutManager will + * create a new one. + * @return The adapter position of the first visible item in each span. If a span does not have + * any items, {@link RecyclerView#NO_POSITION} is returned for that span. + * @see #findFirstCompletelyVisibleItemPositions(int[]) + * @see #findLastVisibleItemPositions(int[]) + */ + public int[] findFirstVisibleItemPositions(int[] into) { + if (into == null) { + into = new int[mSpanCount]; + } else if (into.length < mSpanCount) { + throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" + + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); + } + for (int i = 0; i < mSpanCount; i++) { + into[i] = mSpans[i].findFirstVisibleItemPosition(); + } + return into; + } + + /** + * Returns the adapter position of the first completely visible view for each span. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those + * views are ignored in this method. + * + * @param into An array to put the results into. If you don't provide any, LayoutManager will + * create a new one. + * @return The adapter position of the first fully visible item in each span. If a span does + * not have any items, {@link RecyclerView#NO_POSITION} is returned for that span. + * @see #findFirstVisibleItemPositions(int[]) + * @see #findLastCompletelyVisibleItemPositions(int[]) + */ + public int[] findFirstCompletelyVisibleItemPositions(int[] into) { + if (into == null) { + into = new int[mSpanCount]; + } else if (into.length < mSpanCount) { + throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" + + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); + } + for (int i = 0; i < mSpanCount; i++) { + into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition(); + } + return into; + } + + /** + * Returns the adapter position of the last visible view for each span. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those + * views are ignored in this method. + * + * @param into An array to put the results into. If you don't provide any, LayoutManager will + * create a new one. + * @return The adapter position of the last visible item in each span. If a span does not have + * any items, {@link RecyclerView#NO_POSITION} is returned for that span. + * @see #findLastCompletelyVisibleItemPositions(int[]) + * @see #findFirstVisibleItemPositions(int[]) + */ + public int[] findLastVisibleItemPositions(int[] into) { + if (into == null) { + into = new int[mSpanCount]; + } else if (into.length < mSpanCount) { + throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" + + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); + } + for (int i = 0; i < mSpanCount; i++) { + into[i] = mSpans[i].findLastVisibleItemPosition(); + } + return into; + } + + /** + * Returns the adapter position of the last completely visible view for each span. + *

+ * Note that, this value is not affected by layout orientation or item order traversal. + * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, + * not in the layout. + *

+ * If RecyclerView has item decorators, they will be considered in calculations as well. + *

+ * StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those + * views are ignored in this method. + * + * @param into An array to put the results into. If you don't provide any, LayoutManager will + * create a new one. + * @return The adapter position of the last fully visible item in each span. If a span does not + * have any items, {@link RecyclerView#NO_POSITION} is returned for that span. + * @see #findFirstCompletelyVisibleItemPositions(int[]) + * @see #findLastVisibleItemPositions(int[]) + */ + public int[] findLastCompletelyVisibleItemPositions(int[] into) { + if (into == null) { + into = new int[mSpanCount]; + } else if (into.length < mSpanCount) { + throw new IllegalArgumentException("Provided int[]'s size must be more than or equal" + + " to span count. Expected:" + mSpanCount + ", array size:" + into.length); + } + for (int i = 0; i < mSpanCount; i++) { + into[i] = mSpans[i].findLastCompletelyVisibleItemPosition(); + } + return into; + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + private int computeScrollOffset(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation, + findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), + findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), + this, mSmoothScrollbarEnabled, mShouldReverseLayout); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + private int computeScrollExtent(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + return ScrollbarHelper.computeScrollExtent(state, mPrimaryOrientation, + findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), + findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), + this, mSmoothScrollbarEnabled); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + private int computeScrollRange(RecyclerView.State state) { + if (getChildCount() == 0) { + return 0; + } + return ScrollbarHelper.computeScrollRange(state, mPrimaryOrientation, + findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled), + findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled), + this, mSmoothScrollbarEnabled); + } + + @Override + public int computeVerticalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp, + boolean alreadyMeasured) { + if (lp.mFullSpan) { + if (mOrientation == VERTICAL) { + measureChildWithDecorationsAndMargin(child, mFullSizeSpec, + getChildMeasureSpec( + getHeight(), + getHeightMode(), + getPaddingTop() + getPaddingBottom(), + lp.height, + true), + alreadyMeasured); + } else { + measureChildWithDecorationsAndMargin( + child, + getChildMeasureSpec( + getWidth(), + getWidthMode(), + getPaddingLeft() + getPaddingRight(), + lp.width, + true), + mFullSizeSpec, + alreadyMeasured); + } + } else { + if (mOrientation == VERTICAL) { + // Padding for width measure spec is 0 because left and right padding were already + // factored into mSizePerSpan. + measureChildWithDecorationsAndMargin( + child, + getChildMeasureSpec( + mSizePerSpan, + getWidthMode(), + 0, + lp.width, + false), + getChildMeasureSpec( + getHeight(), + getHeightMode(), + getPaddingTop() + getPaddingBottom(), + lp.height, + true), + alreadyMeasured); + } else { + // Padding for height measure spec is 0 because top and bottom padding were already + // factored into mSizePerSpan. + measureChildWithDecorationsAndMargin( + child, + getChildMeasureSpec( + getWidth(), + getWidthMode(), + getPaddingLeft() + getPaddingRight(), + lp.width, + true), + getChildMeasureSpec( + mSizePerSpan, + getHeightMode(), + 0, + lp.height, + false), + alreadyMeasured); + } + } + } + + private void measureChildWithDecorationsAndMargin(View child, int widthSpec, + int heightSpec, boolean alreadyMeasured) { + calculateItemDecorationsForChild(child, mTmpRect); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left, + lp.rightMargin + mTmpRect.right); + heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top, + lp.bottomMargin + mTmpRect.bottom); + final boolean measure = alreadyMeasured + ? shouldReMeasureChild(child, widthSpec, heightSpec, lp) + : shouldMeasureChild(child, widthSpec, heightSpec, lp); + if (measure) { + child.measure(widthSpec, heightSpec); + } + + } + + private int updateSpecWithExtra(int spec, int startInset, int endInset) { + if (startInset == 0 && endInset == 0) { + return spec; + } + final int mode = View.MeasureSpec.getMode(spec); + if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) { + return View.MeasureSpec.makeMeasureSpec( + Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode); + } + return spec; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; + if (mPendingScrollPosition != RecyclerView.NO_POSITION) { + mPendingSavedState.invalidateAnchorPositionInfo(); + mPendingSavedState.invalidateSpanInfo(); + } + requestLayout(); + } else if (DEBUG) { + Log.d(TAG, "invalid saved state class"); + } + } + + @Override + public Parcelable onSaveInstanceState() { + if (mPendingSavedState != null) { + return new SavedState(mPendingSavedState); + } + SavedState state = new SavedState(); + state.mReverseLayout = mReverseLayout; + state.mAnchorLayoutFromEnd = mLastLayoutFromEnd; + state.mLastLayoutRTL = mLastLayoutRTL; + + if (mLazySpanLookup != null && mLazySpanLookup.mData != null) { + state.mSpanLookup = mLazySpanLookup.mData; + state.mSpanLookupSize = state.mSpanLookup.length; + state.mFullSpanItems = mLazySpanLookup.mFullSpanItems; + } else { + state.mSpanLookupSize = 0; + } + + if (getChildCount() > 0) { + state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition() + : getFirstChildPosition(); + state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt(); + state.mSpanOffsetsSize = mSpanCount; + state.mSpanOffsets = new int[mSpanCount]; + for (int i = 0; i < mSpanCount; i++) { + int line; + if (mLastLayoutFromEnd) { + line = mSpans[i].getEndLine(Span.INVALID_LINE); + if (line != Span.INVALID_LINE) { + line -= mPrimaryOrientation.getEndAfterPadding(); + } + } else { + line = mSpans[i].getStartLine(Span.INVALID_LINE); + if (line != Span.INVALID_LINE) { + line -= mPrimaryOrientation.getStartAfterPadding(); + } + } + state.mSpanOffsets[i] = line; + } + } else { + state.mAnchorPosition = RecyclerView.NO_POSITION; + state.mVisibleAnchorPosition = RecyclerView.NO_POSITION; + state.mSpanOffsetsSize = 0; + } + if (DEBUG) { + Log.d(TAG, "saved state:\n" + state); + } + return state; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (getChildCount() > 0) { + final View start = findFirstVisibleItemClosestToStart(false); + final View end = findFirstVisibleItemClosestToEnd(false); + if (start == null || end == null) { + return; + } + final int startPos = getPosition(start); + final int endPos = getPosition(end); + if (startPos < endPos) { + event.setFromIndex(startPos); + event.setToIndex(endPos); + } else { + event.setFromIndex(endPos); + event.setToIndex(startPos); + } + } + } + + /** + * Finds the first fully visible child to be used as an anchor child if span count changes when + * state is restored. If no children is fully visible, returns a partially visible child instead + * of returning null. + */ + int findFirstVisibleItemPositionInt() { + final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) : + findFirstVisibleItemClosestToStart(true); + return first == null ? RecyclerView.NO_POSITION : getPosition(first); + } + + /** + * This is for internal use. Not necessarily the child closest to start but the first child + * we find that matches the criteria. + * This method does not do any sorting based on child's start coordinate, instead, it uses + * children order. + */ + View findFirstVisibleItemClosestToStart(boolean fullyVisible) { + final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); + final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); + final int limit = getChildCount(); + View partiallyVisible = null; + for (int i = 0; i < limit; i++) { + final View child = getChildAt(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + if (childEnd <= boundsStart || childStart >= boundsEnd) { + continue; // not visible at all + } + if (childStart >= boundsStart || !fullyVisible) { + // when checking for start, it is enough even if part of the child's top is visible + // as long as fully visible is not requested. + return child; + } + if (partiallyVisible == null) { + partiallyVisible = child; + } + } + return partiallyVisible; + } + + /** + * This is for internal use. Not necessarily the child closest to bottom but the first child + * we find that matches the criteria. + * This method does not do any sorting based on child's end coordinate, instead, it uses + * children order. + */ + View findFirstVisibleItemClosestToEnd(boolean fullyVisible) { + final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); + final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); + View partiallyVisible = null; + for (int i = getChildCount() - 1; i >= 0; i--) { + final View child = getChildAt(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + if (childEnd <= boundsStart || childStart >= boundsEnd) { + continue; // not visible at all + } + if (childEnd <= boundsEnd || !fullyVisible) { + // when checking for end, it is enough even if part of the child's bottom is visible + // as long as fully visible is not requested. + return child; + } + if (partiallyVisible == null) { + partiallyVisible = child; + } + } + return partiallyVisible; + } + + private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean canOffsetChildren) { + final int maxEndLine = getMaxEnd(Integer.MIN_VALUE); + if (maxEndLine == Integer.MIN_VALUE) { + return; + } + int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine; + int fixOffset; + if (gap > 0) { + fixOffset = -scrollBy(-gap, recycler, state); + } else { + return; // nothing to fix + } + gap -= fixOffset; + if (canOffsetChildren && gap > 0) { + mPrimaryOrientation.offsetChildren(gap); + } + } + + private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state, + boolean canOffsetChildren) { + final int minStartLine = getMinStart(Integer.MAX_VALUE); + if (minStartLine == Integer.MAX_VALUE) { + return; + } + int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding(); + int fixOffset; + if (gap > 0) { + fixOffset = scrollBy(gap, recycler, state); + } else { + return; // nothing to fix + } + gap -= fixOffset; + if (canOffsetChildren && gap > 0) { + mPrimaryOrientation.offsetChildren(-gap); + } + } + + private void updateLayoutState(int anchorPosition, RecyclerView.State state) { + mLayoutState.mAvailable = 0; + mLayoutState.mCurrentPosition = anchorPosition; + int startExtra = 0; + int endExtra = 0; + if (isSmoothScrolling()) { + final int targetPos = state.getTargetScrollPosition(); + if (targetPos != RecyclerView.NO_POSITION) { + if (mShouldReverseLayout == targetPos < anchorPosition) { + endExtra = mPrimaryOrientation.getTotalSpace(); + } else { + startExtra = mPrimaryOrientation.getTotalSpace(); + } + } + } + + // Line of the furthest row. + final boolean clipToPadding = getClipToPadding(); + if (clipToPadding) { + mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra; + mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra; + } else { + mLayoutState.mEndLine = mPrimaryOrientation.getEnd() + endExtra; + mLayoutState.mStartLine = -startExtra; + } + mLayoutState.mStopInFocusable = false; + mLayoutState.mRecycle = true; + mLayoutState.mInfinite = mPrimaryOrientation.getMode() == View.MeasureSpec.UNSPECIFIED + && mPrimaryOrientation.getEnd() == 0; + } + + private void setLayoutStateDirection(int direction) { + mLayoutState.mLayoutDirection = direction; + mLayoutState.mItemDirection = (mShouldReverseLayout == (direction == LayoutState.LAYOUT_START)) + ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; + } + + @Override + public void offsetChildrenHorizontal(int dx) { + super.offsetChildrenHorizontal(dx); + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].onOffset(dx); + } + } + + @Override + public void offsetChildrenVertical(int dy) { + super.offsetChildrenVertical(dy); + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].onOffset(dy); + } + } + + @Override + public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) { + handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE); + } + + @Override + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { + handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD); + } + + @Override + public void onItemsChanged(RecyclerView recyclerView) { + mLazySpanLookup.clear(); + requestLayout(); + } + + @Override + public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) { + handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE); + } + + @Override + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + Object payload) { + handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE); + } + + /** + * Checks whether it should invalidate span assignments in response to an adapter change. + */ + private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) { + int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition(); + final int affectedRangeEnd; // exclusive + final int affectedRangeStart; // inclusive + + if (cmd == AdapterHelper.UpdateOp.MOVE) { + if (positionStart < itemCountOrToPosition) { + affectedRangeEnd = itemCountOrToPosition + 1; + affectedRangeStart = positionStart; + } else { + affectedRangeEnd = positionStart + 1; + affectedRangeStart = itemCountOrToPosition; + } + } else { + affectedRangeStart = positionStart; + affectedRangeEnd = positionStart + itemCountOrToPosition; + } + + mLazySpanLookup.invalidateAfter(affectedRangeStart); + switch (cmd) { + case AdapterHelper.UpdateOp.ADD: + mLazySpanLookup.offsetForAddition(positionStart, itemCountOrToPosition); + break; + case AdapterHelper.UpdateOp.REMOVE: + mLazySpanLookup.offsetForRemoval(positionStart, itemCountOrToPosition); + break; + case AdapterHelper.UpdateOp.MOVE: + // TODO optimize + mLazySpanLookup.offsetForRemoval(positionStart, 1); + mLazySpanLookup.offsetForAddition(itemCountOrToPosition, 1); + break; + } + + if (affectedRangeEnd <= minPosition) { + return; + } + + int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition(); + if (affectedRangeStart <= maxPosition) { + requestLayout(); + } + } + + private int fill(RecyclerView.Recycler recycler, LayoutState layoutState, + RecyclerView.State state) { + mRemainingSpans.set(0, mSpanCount, true); + // The target position we are trying to reach. + final int targetLine; + + // Line of the furthest row. + if (mLayoutState.mInfinite) { + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + targetLine = Integer.MAX_VALUE; + } else { // LAYOUT_START + targetLine = Integer.MIN_VALUE; + } + } else { + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + targetLine = layoutState.mEndLine + layoutState.mAvailable; + } else { // LAYOUT_START + targetLine = layoutState.mStartLine - layoutState.mAvailable; + } + } + + updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine); + if (DEBUG) { + Log.d(TAG, "FILLING targetLine: " + targetLine + "," + + "remaining spans:" + mRemainingSpans + ", state: " + layoutState); + } + + // the default coordinate to add new view. + final int defaultNewViewLine = mShouldReverseLayout + ? mPrimaryOrientation.getEndAfterPadding() + : mPrimaryOrientation.getStartAfterPadding(); + boolean added = false; + while (layoutState.hasMore(state) + && (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) { + View view = layoutState.next(recycler); + LayoutParams lp = ((LayoutParams) view.getLayoutParams()); + final int position = lp.getViewLayoutPosition(); + final int spanIndex = mLazySpanLookup.getSpan(position); + Span currentSpan; + final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID; + if (assignSpan) { + currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState); + mLazySpanLookup.setSpan(position, currentSpan); + if (DEBUG) { + Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position); + } + } else { + if (DEBUG) { + Log.d(TAG, "using " + spanIndex + " for pos " + position); + } + currentSpan = mSpans[spanIndex]; + } + // assign span before measuring so that item decorators can get updated span index + lp.mSpan = currentSpan; + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + addView(view); + } else { + addView(view, 0); + } + measureChildWithDecorationsAndMargin(view, lp, false); + + final int start; + final int end; + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine) + : currentSpan.getEndLine(defaultNewViewLine); + end = start + mPrimaryOrientation.getDecoratedMeasurement(view); + if (assignSpan && lp.mFullSpan) { + LazySpanLookup.FullSpanItem fullSpanItem; + fullSpanItem = createFullSpanItemFromEnd(start); + fullSpanItem.mGapDir = LayoutState.LAYOUT_START; + fullSpanItem.mPosition = position; + mLazySpanLookup.addFullSpanItem(fullSpanItem); + } + } else { + end = lp.mFullSpan ? getMinStart(defaultNewViewLine) + : currentSpan.getStartLine(defaultNewViewLine); + start = end - mPrimaryOrientation.getDecoratedMeasurement(view); + if (assignSpan && lp.mFullSpan) { + LazySpanLookup.FullSpanItem fullSpanItem; + fullSpanItem = createFullSpanItemFromStart(end); + fullSpanItem.mGapDir = LayoutState.LAYOUT_END; + fullSpanItem.mPosition = position; + mLazySpanLookup.addFullSpanItem(fullSpanItem); + } + } + + // check if this item may create gaps in the future + if (lp.mFullSpan && layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_HEAD) { + if (assignSpan) { + mLaidOutInvalidFullSpan = true; + } else { + final boolean hasInvalidGap; + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + hasInvalidGap = !areAllEndsEqual(); + } else { // layoutState.mLayoutDirection == LAYOUT_START + hasInvalidGap = !areAllStartsEqual(); + } + if (hasInvalidGap) { + final LazySpanLookup.FullSpanItem fullSpanItem = mLazySpanLookup + .getFullSpanItem(position); + if (fullSpanItem != null) { + fullSpanItem.mHasUnwantedGapAfter = true; + } + mLaidOutInvalidFullSpan = true; + } + } + } + attachViewToSpans(view, lp, layoutState); + final int otherStart; + final int otherEnd; + if (isLayoutRTL() && mOrientation == VERTICAL) { + otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() : + mSecondaryOrientation.getEndAfterPadding() + - (mSpanCount - 1 - currentSpan.mIndex) * mSizePerSpan; + otherStart = otherEnd - mSecondaryOrientation.getDecoratedMeasurement(view); + } else { + otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding() + : currentSpan.mIndex * mSizePerSpan + + mSecondaryOrientation.getStartAfterPadding(); + otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view); + } + + if (mOrientation == VERTICAL) { + layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end); + } else { + layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd); + } + + if (lp.mFullSpan) { + updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine); + } else { + updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine); + } + recycle(recycler, mLayoutState); + if (mLayoutState.mStopInFocusable && view.hasFocusable()) { + if (lp.mFullSpan) { + mRemainingSpans.clear(); + } else { + mRemainingSpans.set(currentSpan.mIndex, false); + } + } + added = true; + } + if (!added) { + recycle(recycler, mLayoutState); + } + final int diff; + if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding()); + diff = mPrimaryOrientation.getStartAfterPadding() - minStart; + } else { + final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding()); + diff = maxEnd - mPrimaryOrientation.getEndAfterPadding(); + } + return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0; + } + + private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) { + LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); + fsi.mGapPerSpan = new int[mSpanCount]; + for (int i = 0; i < mSpanCount; i++) { + fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop); + } + return fsi; + } + + private LazySpanLookup.FullSpanItem createFullSpanItemFromStart(int newItemBottom) { + LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem(); + fsi.mGapPerSpan = new int[mSpanCount]; + for (int i = 0; i < mSpanCount; i++) { + fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom; + } + return fsi; + } + + private void attachViewToSpans(View view, LayoutParams lp, LayoutState layoutState) { + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + if (lp.mFullSpan) { + appendViewToAllSpans(view); + } else { + lp.mSpan.appendToSpan(view); + } + } else { + if (lp.mFullSpan) { + prependViewToAllSpans(view); + } else { + lp.mSpan.prependToSpan(view); + } + } + } + + private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (!layoutState.mRecycle || layoutState.mInfinite) { + return; + } + if (layoutState.mAvailable == 0) { + // easy, recycle line is still valid + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + recycleFromEnd(recycler, layoutState.mEndLine); + } else { + recycleFromStart(recycler, layoutState.mStartLine); + } + } else { + // scrolling case, recycle line can be shifted by how much space we could cover + // by adding new views + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + // calculate recycle line + int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine); + final int line; + if (scrolled < 0) { + line = layoutState.mEndLine; + } else { + line = layoutState.mEndLine - Math.min(scrolled, layoutState.mAvailable); + } + recycleFromEnd(recycler, line); + } else { + // calculate recycle line + int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine; + final int line; + if (scrolled < 0) { + line = layoutState.mStartLine; + } else { + line = layoutState.mStartLine + Math.min(scrolled, layoutState.mAvailable); + } + recycleFromStart(recycler, line); + } + } + + } + + private void appendViewToAllSpans(View view) { + // traverse in reverse so that we end up assigning full span items to 0 + for (int i = mSpanCount - 1; i >= 0; i--) { + mSpans[i].appendToSpan(view); + } + } + + private void prependViewToAllSpans(View view) { + // traverse in reverse so that we end up assigning full span items to 0 + for (int i = mSpanCount - 1; i >= 0; i--) { + mSpans[i].prependToSpan(view); + } + } + + private void updateAllRemainingSpans(int layoutDir, int targetLine) { + for (int i = 0; i < mSpanCount; i++) { + if (mSpans[i].mViews.isEmpty()) { + continue; + } + updateRemainingSpans(mSpans[i], layoutDir, targetLine); + } + } + + private void updateRemainingSpans(Span span, int layoutDir, int targetLine) { + final int deletedSize = span.getDeletedSize(); + if (layoutDir == LayoutState.LAYOUT_START) { + final int line = span.getStartLine(); + if (line + deletedSize <= targetLine) { + mRemainingSpans.set(span.mIndex, false); + } + } else { + final int line = span.getEndLine(); + if (line - deletedSize >= targetLine) { + mRemainingSpans.set(span.mIndex, false); + } + } + } + + private int getMaxStart(int def) { + int maxStart = mSpans[0].getStartLine(def); + for (int i = 1; i < mSpanCount; i++) { + final int spanStart = mSpans[i].getStartLine(def); + if (spanStart > maxStart) { + maxStart = spanStart; + } + } + return maxStart; + } + + private int getMinStart(int def) { + int minStart = mSpans[0].getStartLine(def); + for (int i = 1; i < mSpanCount; i++) { + final int spanStart = mSpans[i].getStartLine(def); + if (spanStart < minStart) { + minStart = spanStart; + } + } + return minStart; + } + + boolean areAllEndsEqual() { + int end = mSpans[0].getEndLine(Span.INVALID_LINE); + for (int i = 1; i < mSpanCount; i++) { + if (mSpans[i].getEndLine(Span.INVALID_LINE) != end) { + return false; + } + } + return true; + } + + boolean areAllStartsEqual() { + int start = mSpans[0].getStartLine(Span.INVALID_LINE); + for (int i = 1; i < mSpanCount; i++) { + if (mSpans[i].getStartLine(Span.INVALID_LINE) != start) { + return false; + } + } + return true; + } + + private int getMaxEnd(int def) { + int maxEnd = mSpans[0].getEndLine(def); + for (int i = 1; i < mSpanCount; i++) { + final int spanEnd = mSpans[i].getEndLine(def); + if (spanEnd > maxEnd) { + maxEnd = spanEnd; + } + } + return maxEnd; + } + + private int getMinEnd(int def) { + int minEnd = mSpans[0].getEndLine(def); + for (int i = 1; i < mSpanCount; i++) { + final int spanEnd = mSpans[i].getEndLine(def); + if (spanEnd < minEnd) { + minEnd = spanEnd; + } + } + return minEnd; + } + + private void recycleFromStart(RecyclerView.Recycler recycler, int line) { + while (getChildCount() > 0) { + View child = getChildAt(0); + if (mPrimaryOrientation.getDecoratedEnd(child) <= line + && mPrimaryOrientation.getTransformedEndWithDecoration(child) <= line) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + // Don't recycle the last View in a span not to lose span's start/end lines + if (lp.mFullSpan) { + for (int j = 0; j < mSpanCount; j++) { + if (mSpans[j].mViews.size() == 1) { + return; + } + } + for (int j = 0; j < mSpanCount; j++) { + mSpans[j].popStart(); + } + } else { + if (lp.mSpan.mViews.size() == 1) { + return; + } + lp.mSpan.popStart(); + } + removeAndRecycleView(child, recycler); + } else { + return; // done + } + } + } + + private void recycleFromEnd(RecyclerView.Recycler recycler, int line) { + final int childCount = getChildCount(); + int i; + for (i = childCount - 1; i >= 0; i--) { + View child = getChildAt(i); + if (mPrimaryOrientation.getDecoratedStart(child) >= line + && mPrimaryOrientation.getTransformedStartWithDecoration(child) >= line) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + // Don't recycle the last View in a span not to lose span's start/end lines + if (lp.mFullSpan) { + for (int j = 0; j < mSpanCount; j++) { + if (mSpans[j].mViews.size() == 1) { + return; + } + } + for (int j = 0; j < mSpanCount; j++) { + mSpans[j].popEnd(); + } + } else { + if (lp.mSpan.mViews.size() == 1) { + return; + } + lp.mSpan.popEnd(); + } + removeAndRecycleView(child, recycler); + } else { + return; // done + } + } + } + + /** + * @return True if last span is the first one we want to fill + */ + private boolean preferLastSpan(int layoutDir) { + if (mOrientation == HORIZONTAL) { + return (layoutDir == LayoutState.LAYOUT_START) != mShouldReverseLayout; + } + return ((layoutDir == LayoutState.LAYOUT_START) == mShouldReverseLayout) == isLayoutRTL(); + } + + /** + * Finds the span for the next view. + */ + private Span getNextSpan(LayoutState layoutState) { + final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection); + final int startIndex, endIndex, diff; + if (preferLastSpan) { + startIndex = mSpanCount - 1; + endIndex = -1; + diff = -1; + } else { + startIndex = 0; + endIndex = mSpanCount; + diff = 1; + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) { + Span min = null; + int minLine = Integer.MAX_VALUE; + final int defaultLine = mPrimaryOrientation.getStartAfterPadding(); + for (int i = startIndex; i != endIndex; i += diff) { + final Span other = mSpans[i]; + int otherLine = other.getEndLine(defaultLine); + if (otherLine < minLine) { + min = other; + minLine = otherLine; + } + } + return min; + } else { + Span max = null; + int maxLine = Integer.MIN_VALUE; + final int defaultLine = mPrimaryOrientation.getEndAfterPadding(); + for (int i = startIndex; i != endIndex; i += diff) { + final Span other = mSpans[i]; + int otherLine = other.getStartLine(defaultLine); + if (otherLine > maxLine) { + max = other; + maxLine = otherLine; + } + } + return max; + } + } + + @Override + public boolean canScrollVertically() { + return mOrientation == VERTICAL; + } + + @Override + public boolean canScrollHorizontally() { + return mOrientation == HORIZONTAL; + } + + @Override + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { + return scrollBy(dx, recycler, state); + } + + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { + return scrollBy(dy, recycler, state); + } + + private int calculateScrollDirectionForPosition(int position) { + if (getChildCount() == 0) { + return mShouldReverseLayout ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + } + final int firstChildPos = getFirstChildPosition(); + return position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; + } + + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + final int direction = calculateScrollDirectionForPosition(targetPosition); + PointF outVector = new PointF(); + if (direction == 0) { + return null; + } + if (mOrientation == HORIZONTAL) { + outVector.x = direction; + outVector.y = 0; + } else { + outVector.x = 0; + outVector.y = direction; + } + return outVector; + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { + LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()); + scroller.setTargetPosition(position); + startSmoothScroll(scroller); + } + + @Override + public void scrollToPosition(int position) { + if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) { + mPendingSavedState.invalidateAnchorPositionInfo(); + } + mPendingScrollPosition = position; + mPendingScrollPositionOffset = INVALID_OFFSET; + requestLayout(); + } + + /** + * Scroll to the specified adapter position with the given offset from layout start. + *

+ * Note that scroll position change will not be reflected until the next layout call. + *

+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. + * + * @param position Index (starting at 0) of the reference item. + * @param offset The distance (in pixels) between the start edge of the item view and + * start edge of the RecyclerView. + * @see #setReverseLayout(boolean) + * @see #scrollToPosition(int) + */ + public void scrollToPositionWithOffset(int position, int offset) { + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchorPositionInfo(); + } + mPendingScrollPosition = position; + mPendingScrollPositionOffset = offset; + requestLayout(); + } + + /** @hide */ + @Override + @RestrictTo(LIBRARY) + public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, + LayoutPrefetchRegistry layoutPrefetchRegistry) { + /* This method uses the simplifying assumption that the next N items (where N = span count) + * will be assigned, one-to-one, to spans, where ordering is based on which span extends + * least beyond the viewport. + * + * While this simplified model will be incorrect in some cases, it's difficult to know + * item heights, or whether individual items will be full span prior to construction. + * + * While this greedy estimation approach may underestimate the distance to prefetch items, + * it's very unlikely to overestimate them, so distances can be conservatively used to know + * the soonest (in terms of scroll distance) a prefetched view may come on screen. + */ + int delta = (mOrientation == HORIZONTAL) ? dx : dy; + if (getChildCount() == 0 || delta == 0) { + // can't support this scroll, so don't bother prefetching + return; + } + prepareLayoutStateForDelta(delta, state); + + // build sorted list of distances to end of each span (though we don't care which is which) + if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) { + mPrefetchDistances = new int[mSpanCount]; + } + + int itemPrefetchCount = 0; + for (int i = 0; i < mSpanCount; i++) { + // compute number of pixels past the edge of the viewport that the current span extends + int distance = mLayoutState.mItemDirection == LayoutState.LAYOUT_START + ? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine) + : mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine; + if (distance >= 0) { + // span extends to the edge, so prefetch next item + mPrefetchDistances[itemPrefetchCount] = distance; + itemPrefetchCount++; + } + } + Arrays.sort(mPrefetchDistances, 0, itemPrefetchCount); + + // then assign them in order to the next N views (where N = span count) + for (int i = 0; i < itemPrefetchCount && mLayoutState.hasMore(state); i++) { + layoutPrefetchRegistry.addPosition(mLayoutState.mCurrentPosition, + mPrefetchDistances[i]); + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + } + } + + void prepareLayoutStateForDelta(int delta, RecyclerView.State state) { + final int referenceChildPosition; + final int layoutDir; + if (delta > 0) { // layout towards end + layoutDir = LayoutState.LAYOUT_END; + referenceChildPosition = getLastChildPosition(); + } else { + layoutDir = LayoutState.LAYOUT_START; + referenceChildPosition = getFirstChildPosition(); + } + mLayoutState.mRecycle = true; + updateLayoutState(referenceChildPosition, state); + setLayoutStateDirection(layoutDir); + mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; + mLayoutState.mAvailable = Math.abs(delta); + } + + int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dt == 0) { + return 0; + } + + prepareLayoutStateForDelta(dt, state); + int consumed = fill(recycler, mLayoutState, state); + final int available = mLayoutState.mAvailable; + final int totalScroll; + if (available < consumed) { + totalScroll = dt; + } else if (dt < 0) { + totalScroll = -consumed; + } else { // dt > 0 + totalScroll = consumed; + } + if (DEBUG) { + Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); + } + + mPrimaryOrientation.offsetChildren(-totalScroll); + // always reset this if we scroll for a proper save instance state + mLastLayoutFromEnd = mShouldReverseLayout; + mLayoutState.mAvailable = 0; + recycle(recycler, mLayoutState); + return totalScroll; + } + + int getLastChildPosition() { + final int childCount = getChildCount(); + return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); + } + + int getFirstChildPosition() { + final int childCount = getChildCount(); + return childCount == 0 ? 0 : getPosition(getChildAt(0)); + } + + /** + * Finds the first View that can be used as an anchor View. + * + * @return Position of the View or 0 if it cannot find any such View. + */ + private int findFirstReferenceChildPosition(int itemCount) { + final int limit = getChildCount(); + for (int i = 0; i < limit; i++) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + return position; + } + } + return 0; + } + + /** + * Finds the last View that can be used as an anchor View. + * + * @return Position of the View or 0 if it cannot find any such View. + */ + private int findLastReferenceChildPosition(int itemCount) { + for (int i = getChildCount() - 1; i >= 0; i--) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + return position; + } + } + return 0; + } + + @SuppressWarnings("deprecation") + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + if (mOrientation == HORIZONTAL) { + return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT); + } else { + return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof ViewGroup.MarginLayoutParams) { + return new LayoutParams((ViewGroup.MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + public int getOrientation() { + return mOrientation; + } + + @Nullable + @Override + public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (getChildCount() == 0) { + return null; + } + + final View directChild = findContainingItemView(focused); + if (directChild == null) { + return null; + } + + resolveShouldLayoutReverse(); + final int layoutDir = convertFocusDirectionToLayoutDirection(direction); + if (layoutDir == LayoutState.INVALID_LAYOUT) { + return null; + } + LayoutParams prevFocusLayoutParams = (LayoutParams) directChild.getLayoutParams(); + boolean prevFocusFullSpan = prevFocusLayoutParams.mFullSpan; + final Span prevFocusSpan = prevFocusLayoutParams.mSpan; + final int referenceChildPosition; + if (layoutDir == LayoutState.LAYOUT_END) { // layout towards end + referenceChildPosition = getLastChildPosition(); + } else { + referenceChildPosition = getFirstChildPosition(); + } + updateLayoutState(referenceChildPosition, state); + setLayoutStateDirection(layoutDir); + + mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; + mLayoutState.mAvailable = (int) (MAX_SCROLL_FACTOR * mPrimaryOrientation.getTotalSpace()); + mLayoutState.mStopInFocusable = true; + mLayoutState.mRecycle = false; + fill(recycler, mLayoutState, state); + mLastLayoutFromEnd = mShouldReverseLayout; + if (!prevFocusFullSpan) { + View view = prevFocusSpan.getFocusableViewAfter(referenceChildPosition, layoutDir); + if (view != null && view != directChild) { + return view; + } + } + + // either could not find from the desired span or prev view is full span. + // traverse all spans + if (preferLastSpan(layoutDir)) { + for (int i = mSpanCount - 1; i >= 0; i--) { + View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir); + if (view != null && view != directChild) { + return view; + } + } + } else { + for (int i = 0; i < mSpanCount; i++) { + View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir); + if (view != null && view != directChild) { + return view; + } + } + } + + // Could not find any focusable views from any of the existing spans. Now start the search + // to find the best unfocusable candidate to become visible on the screen next. The search + // is done in the same fashion: first, check the views in the desired span and if no + // candidate is found, traverse the views in all the remaining spans. + boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START); + View unfocusableCandidate = null; + if (!prevFocusFullSpan) { + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? prevFocusSpan.findFirstPartiallyVisibleItemPosition() : + prevFocusSpan.findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + + if (preferLastSpan(layoutDir)) { + for (int i = mSpanCount - 1; i >= 0; i--) { + if (i == prevFocusSpan.mIndex) { + continue; + } + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? mSpans[i].findFirstPartiallyVisibleItemPosition() : + mSpans[i].findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + } else { + for (int i = 0; i < mSpanCount; i++) { + unfocusableCandidate = findViewByPosition(shouldSearchFromStart + ? mSpans[i].findFirstPartiallyVisibleItemPosition() : + mSpans[i].findLastPartiallyVisibleItemPosition()); + if (unfocusableCandidate != null && unfocusableCandidate != directChild) { + return unfocusableCandidate; + } + } + } + return null; + } + + /** + * Converts a focusDirection to orientation. + * + * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction + * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. + */ + private int convertFocusDirectionToLayoutDirection(int focusDirection) { + switch (focusDirection) { + case View.FOCUS_BACKWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_START; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_END; + } else { + return LayoutState.LAYOUT_START; + } + case View.FOCUS_FORWARD: + if (mOrientation == VERTICAL) { + return LayoutState.LAYOUT_END; + } else if (isLayoutRTL()) { + return LayoutState.LAYOUT_START; + } else { + return LayoutState.LAYOUT_END; + } + case View.FOCUS_UP: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_DOWN: + return mOrientation == VERTICAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_LEFT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START + : LayoutState.INVALID_LAYOUT; + case View.FOCUS_RIGHT: + return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END + : LayoutState.INVALID_LAYOUT; + default: + if (DEBUG) { + Log.d(TAG, "Unknown focus request:" + focusDirection); + } + return LayoutState.INVALID_LAYOUT; + } + + } + + /** + * LayoutParams used by StaggeredGridLayoutManager. + *

+ * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the + * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is + * expected to fill all of the space given to it. + */ + public static class LayoutParams extends RecyclerView.LayoutParams { + + /** + * Span Id for Views that are not laid out yet. + */ + public static final int INVALID_SPAN_ID = -1; + + // Package scope to be able to access from tests. + Span mSpan; + + boolean mFullSpan; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + /** + * When set to true, the item will layout using all span area. That means, if orientation + * is vertical, the view will have full width; if orientation is horizontal, the view will + * have full height. + * + * @param fullSpan True if this item should traverse all spans. + * @see #isFullSpan() + */ + public void setFullSpan(boolean fullSpan) { + mFullSpan = fullSpan; + } + + /** + * Returns whether this View occupies all available spans or just one. + * + * @return True if the View occupies all spans or false otherwise. + * @see #setFullSpan(boolean) + */ + public boolean isFullSpan() { + return mFullSpan; + } + + /** + * Returns the Span index to which this View is assigned. + * + * @return The Span index of the View. If View is not yet assigned to any span, returns + * {@link #INVALID_SPAN_ID}. + */ + public final int getSpanIndex() { + if (mSpan == null) { + return INVALID_SPAN_ID; + } + return mSpan.mIndex; + } + } + + // Package scoped to access from tests. + class Span { + + static final int INVALID_LINE = Integer.MIN_VALUE; + ArrayList mViews = new ArrayList<>(); + int mCachedStart = INVALID_LINE; + int mCachedEnd = INVALID_LINE; + int mDeletedSize = 0; + final int mIndex; + + Span(int index) { + mIndex = index; + } + + int getStartLine(int def) { + if (mCachedStart != INVALID_LINE) { + return mCachedStart; + } + if (mViews.size() == 0) { + return def; + } + calculateCachedStart(); + return mCachedStart; + } + + void calculateCachedStart() { + final View startView = mViews.get(0); + final LayoutParams lp = getLayoutParams(startView); + mCachedStart = mPrimaryOrientation.getDecoratedStart(startView); + if (lp.mFullSpan) { + LazySpanLookup.FullSpanItem fsi = mLazySpanLookup + .getFullSpanItem(lp.getViewLayoutPosition()); + if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) { + mCachedStart -= fsi.getGapForSpan(mIndex); + } + } + } + + // Use this one when default value does not make sense and not having a value means a bug. + int getStartLine() { + if (mCachedStart != INVALID_LINE) { + return mCachedStart; + } + calculateCachedStart(); + return mCachedStart; + } + + int getEndLine(int def) { + if (mCachedEnd != INVALID_LINE) { + return mCachedEnd; + } + final int size = mViews.size(); + if (size == 0) { + return def; + } + calculateCachedEnd(); + return mCachedEnd; + } + + void calculateCachedEnd() { + final View endView = mViews.get(mViews.size() - 1); + final LayoutParams lp = getLayoutParams(endView); + mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView); + if (lp.mFullSpan) { + LazySpanLookup.FullSpanItem fsi = mLazySpanLookup + .getFullSpanItem(lp.getViewLayoutPosition()); + if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_END) { + mCachedEnd += fsi.getGapForSpan(mIndex); + } + } + } + + // Use this one when default value does not make sense and not having a value means a bug. + int getEndLine() { + if (mCachedEnd != INVALID_LINE) { + return mCachedEnd; + } + calculateCachedEnd(); + return mCachedEnd; + } + + void prependToSpan(View view) { + LayoutParams lp = getLayoutParams(view); + lp.mSpan = this; + mViews.add(0, view); + mCachedStart = INVALID_LINE; + if (mViews.size() == 1) { + mCachedEnd = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); + } + } + + void appendToSpan(View view) { + LayoutParams lp = getLayoutParams(view); + lp.mSpan = this; + mViews.add(view); + mCachedEnd = INVALID_LINE; + if (mViews.size() == 1) { + mCachedStart = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); + } + } + + // Useful method to preserve positions on a re-layout. + void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { + int reference; + if (reverseLayout) { + reference = getEndLine(INVALID_LINE); + } else { + reference = getStartLine(INVALID_LINE); + } + clear(); + if (reference == INVALID_LINE) { + return; + } + if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding()) + || (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) { + return; + } + if (offset != INVALID_OFFSET) { + reference += offset; + } + mCachedStart = mCachedEnd = reference; + } + + void clear() { + mViews.clear(); + invalidateCache(); + mDeletedSize = 0; + } + + void invalidateCache() { + mCachedStart = INVALID_LINE; + mCachedEnd = INVALID_LINE; + } + + void setLine(int line) { + mCachedEnd = mCachedStart = line; + } + + void popEnd() { + final int size = mViews.size(); + View end = mViews.remove(size - 1); + final LayoutParams lp = getLayoutParams(end); + lp.mSpan = null; + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); + } + if (size == 1) { + mCachedStart = INVALID_LINE; + } + mCachedEnd = INVALID_LINE; + } + + void popStart() { + View start = mViews.remove(0); + final LayoutParams lp = getLayoutParams(start); + lp.mSpan = null; + if (mViews.size() == 0) { + mCachedEnd = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); + } + mCachedStart = INVALID_LINE; + } + + public int getDeletedSize() { + return mDeletedSize; + } + + LayoutParams getLayoutParams(View view) { + return (LayoutParams) view.getLayoutParams(); + } + + void onOffset(int dt) { + if (mCachedStart != INVALID_LINE) { + mCachedStart += dt; + } + if (mCachedEnd != INVALID_LINE) { + mCachedEnd += dt; + } + } + + public int findFirstVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(mViews.size() - 1, -1, false) + : findOneVisibleChild(0, mViews.size(), false); + } + + public int findFirstPartiallyVisibleItemPosition() { + return mReverseLayout + ? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true) + : findOnePartiallyVisibleChild(0, mViews.size(), true); + } + + public int findFirstCompletelyVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(mViews.size() - 1, -1, true) + : findOneVisibleChild(0, mViews.size(), true); + } + + public int findLastVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(0, mViews.size(), false) + : findOneVisibleChild(mViews.size() - 1, -1, false); + } + + public int findLastPartiallyVisibleItemPosition() { + return mReverseLayout + ? findOnePartiallyVisibleChild(0, mViews.size(), true) + : findOnePartiallyVisibleChild(mViews.size() - 1, -1, true); + } + + public int findLastCompletelyVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(0, mViews.size(), true) + : findOneVisibleChild(mViews.size() - 1, -1, true); + } + + /** + * Returns the first view within this span that is partially or fully visible. Partially + * visible refers to a view that overlaps but is not fully contained within RV's padded + * bounded area. This view returned can be defined to have an area of overlap strictly + * greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint + * inclusion is enough to consider it partially visible. The latter case can then refer to + * an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded + * area. This is used e.g. inside + * {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for + * calculating the next unfocusable child to become visible on the screen. + * @param fromIndex The child position index to start the search from. + * @param toIndex The child position index to end the search at. + * @param completelyVisible True if we have to only consider completely visible views, + * false otherwise. + * @param acceptCompletelyVisible True if we can consider both partially or fully visible + * views, false, if only a partially visible child should be + * returned. + * @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded + * bounded area is enough to consider it partially visible, + * false otherwise + * @return The adapter position of the first view that's either partially or fully visible. + * {@link RecyclerView#NO_POSITION} if no such view is found. + */ + int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex, + boolean completelyVisible, + boolean acceptCompletelyVisible, + boolean acceptEndPointInclusion) { + final int start = mPrimaryOrientation.getStartAfterPadding(); + final int end = mPrimaryOrientation.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = mViews.get(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end) + : (childStart < end); + boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start) + : (childEnd > start); + if (childStartInclusion && childEndInclusion) { + if (completelyVisible && acceptCompletelyVisible) { + // the child has to be completely visible to be returned. + if (childStart >= start && childEnd <= end) { + return getPosition(child); + } + } else if (acceptCompletelyVisible) { + // can return either a partially or completely visible child. + return getPosition(child); + } else if (childStart < start || childEnd > end) { + // should return a partially visible child if exists and a completely + // visible child is not acceptable in this case. + return getPosition(child); + } + } + } + return RecyclerView.NO_POSITION; + } + + int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { + return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible, + true, false); + } + + int findOnePartiallyVisibleChild(int fromIndex, int toIndex, + boolean acceptEndPointInclusion) { + return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false, + acceptEndPointInclusion); + } + + /** + * Depending on the layout direction, returns the View that is after the given position. + */ + public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) { + View candidate = null; + if (layoutDir == LayoutState.LAYOUT_START) { + final int limit = mViews.size(); + for (int i = 0; i < limit; i++) { + final View view = mViews.get(i); + if ((mReverseLayout && getPosition(view) <= referenceChildPosition) + || (!mReverseLayout && getPosition(view) >= referenceChildPosition)) { + break; + } + if (view.hasFocusable()) { + candidate = view; + } else { + break; + } + } + } else { + for (int i = mViews.size() - 1; i >= 0; i--) { + final View view = mViews.get(i); + if ((mReverseLayout && getPosition(view) >= referenceChildPosition) + || (!mReverseLayout && getPosition(view) <= referenceChildPosition)) { + break; + } + if (view.hasFocusable()) { + candidate = view; + } else { + break; + } + } + } + return candidate; + } + } + + /** + * An array of mappings from adapter position to span. + * This only grows when a write happens and it grows up to the size of the adapter. + */ + static class LazySpanLookup { + + private static final int MIN_SIZE = 10; + int[] mData; + List mFullSpanItems; + + + /** + * Invalidates everything after this position, including full span information + */ + int forceInvalidateAfter(int position) { + if (mFullSpanItems != null) { + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + mFullSpanItems.remove(i); + } + } + } + return invalidateAfter(position); + } + + /** + * returns end position for invalidation. + */ + int invalidateAfter(int position) { + if (mData == null) { + return RecyclerView.NO_POSITION; + } + if (position >= mData.length) { + return RecyclerView.NO_POSITION; + } + int endPosition = invalidateFullSpansAfter(position); + if (endPosition == RecyclerView.NO_POSITION) { + Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); + return mData.length; + } else { + // Just invalidate items in between `position` and the next full span item, or the + // end of the tracked spans in mData if it's not been lengthened yet. + final int invalidateToIndex = Math.min(endPosition + 1, mData.length); + Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID); + return invalidateToIndex; + } + } + + int getSpan(int position) { + if (mData == null || position >= mData.length) { + return LayoutParams.INVALID_SPAN_ID; + } else { + return mData[position]; + } + } + + void setSpan(int position, Span span) { + ensureSize(position); + mData[position] = span.mIndex; + } + + int sizeForPosition(int position) { + int len = mData.length; + while (len <= position) { + len *= 2; + } + return len; + } + + void ensureSize(int position) { + if (mData == null) { + mData = new int[Math.max(position, MIN_SIZE) + 1]; + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } else if (position >= mData.length) { + int[] old = mData; + mData = new int[sizeForPosition(position)]; + System.arraycopy(old, 0, mData, 0, old.length); + Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); + } + } + + void clear() { + if (mData != null) { + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } + mFullSpanItems = null; + } + + void offsetForRemoval(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart + itemCount, mData, positionStart, + mData.length - positionStart - itemCount); + Arrays.fill(mData, mData.length - itemCount, mData.length, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForRemoval(positionStart, itemCount); + } + + private void offsetFullSpansForRemoval(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + final int end = positionStart + itemCount; + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + if (fsi.mPosition < end) { + mFullSpanItems.remove(i); + } else { + fsi.mPosition -= itemCount; + } + } + } + + void offsetForAddition(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart, mData, positionStart + itemCount, + mData.length - positionStart - itemCount); + Arrays.fill(mData, positionStart, positionStart + itemCount, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForAddition(positionStart, itemCount); + } + + private void offsetFullSpansForAddition(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + fsi.mPosition += itemCount; + } + } + + /** + * Returns when invalidation should end. e.g. hitting a full span position. + * Returned position SHOULD BE invalidated. + */ + private int invalidateFullSpansAfter(int position) { + if (mFullSpanItems == null) { + return RecyclerView.NO_POSITION; + } + final FullSpanItem item = getFullSpanItem(position); + // if there is an fsi at this position, get rid of it. + if (item != null) { + mFullSpanItems.remove(item); + } + int nextFsiIndex = -1; + final int count = mFullSpanItems.size(); + for (int i = 0; i < count; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + nextFsiIndex = i; + break; + } + } + if (nextFsiIndex != -1) { + FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); + mFullSpanItems.remove(nextFsiIndex); + return fsi.mPosition; + } + return RecyclerView.NO_POSITION; + } + + public void addFullSpanItem(FullSpanItem fullSpanItem) { + if (mFullSpanItems == null) { + mFullSpanItems = new ArrayList<>(); + } + final int size = mFullSpanItems.size(); + for (int i = 0; i < size; i++) { + FullSpanItem other = mFullSpanItems.get(i); + if (other.mPosition == fullSpanItem.mPosition) { + if (DEBUG) { + throw new IllegalStateException("two fsis for same position"); + } else { + mFullSpanItems.remove(i); + } + } + if (other.mPosition >= fullSpanItem.mPosition) { + mFullSpanItems.add(i, fullSpanItem); + return; + } + } + // if it is not added to a position. + mFullSpanItems.add(fullSpanItem); + } + + public FullSpanItem getFullSpanItem(int position) { + if (mFullSpanItems == null) { + return null; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + final FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition == position) { + return fsi; + } + } + return null; + } + + /** + * @param minPos inclusive + * @param maxPos exclusive + * @param gapDir if not 0, returns FSIs on in that direction + * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be + * returned even if its gap direction does not match. + */ + public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir, + boolean hasUnwantedGapAfter) { + if (mFullSpanItems == null) { + return null; + } + final int limit = mFullSpanItems.size(); + for (int i = 0; i < limit; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= maxPos) { + return null; + } + if (fsi.mPosition >= minPos + && (gapDir == 0 || fsi.mGapDir == gapDir + || (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) { + return fsi; + } + } + return null; + } + + /** + * We keep information about full span items because they may create gaps in the UI. + */ + @SuppressLint("BanParcelableUsage") + static class FullSpanItem implements Parcelable { + + int mPosition; + int mGapDir; + int[] mGapPerSpan; + // A full span may be laid out in primary direction but may have gaps due to + // invalidation of views after it. This is recorded during a reverse scroll and if + // view is still on the screen after scroll stops, we have to recalculate layout + boolean mHasUnwantedGapAfter; + + FullSpanItem(Parcel in) { + mPosition = in.readInt(); + mGapDir = in.readInt(); + mHasUnwantedGapAfter = in.readInt() == 1; + int spanCount = in.readInt(); + if (spanCount > 0) { + mGapPerSpan = new int[spanCount]; + in.readIntArray(mGapPerSpan); + } + } + + FullSpanItem() { + } + + int getGapForSpan(int spanIndex) { + return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex]; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPosition); + dest.writeInt(mGapDir); + dest.writeInt(mHasUnwantedGapAfter ? 1 : 0); + if (mGapPerSpan != null && mGapPerSpan.length > 0) { + dest.writeInt(mGapPerSpan.length); + dest.writeIntArray(mGapPerSpan); + } else { + dest.writeInt(0); + } + } + + @Override + public String toString() { + return "FullSpanItem{" + + "mPosition=" + mPosition + + ", mGapDir=" + mGapDir + + ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter + + ", mGapPerSpan=" + Arrays.toString(mGapPerSpan) + + '}'; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public FullSpanItem createFromParcel(Parcel in) { + return new FullSpanItem(in); + } + + @Override + public FullSpanItem[] newArray(int size) { + return new FullSpanItem[size]; + } + }; + } + } + + /** + * @hide + */ + @RestrictTo(LIBRARY) + @SuppressLint("BanParcelableUsage") + public static class SavedState implements Parcelable { + + int mAnchorPosition; + int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated + int mSpanOffsetsSize; + int[] mSpanOffsets; + int mSpanLookupSize; + int[] mSpanLookup; + List mFullSpanItems; + boolean mReverseLayout; + boolean mAnchorLayoutFromEnd; + boolean mLastLayoutRTL; + + public SavedState() { + } + + SavedState(Parcel in) { + mAnchorPosition = in.readInt(); + mVisibleAnchorPosition = in.readInt(); + mSpanOffsetsSize = in.readInt(); + if (mSpanOffsetsSize > 0) { + mSpanOffsets = new int[mSpanOffsetsSize]; + in.readIntArray(mSpanOffsets); + } + + mSpanLookupSize = in.readInt(); + if (mSpanLookupSize > 0) { + mSpanLookup = new int[mSpanLookupSize]; + in.readIntArray(mSpanLookup); + } + mReverseLayout = in.readInt() == 1; + mAnchorLayoutFromEnd = in.readInt() == 1; + mLastLayoutRTL = in.readInt() == 1; + @SuppressWarnings("unchecked") + List fullSpanItems = + in.readArrayList(LazySpanLookup.FullSpanItem.class.getClassLoader()); + mFullSpanItems = fullSpanItems; + } + + public SavedState(SavedState other) { + mSpanOffsetsSize = other.mSpanOffsetsSize; + mAnchorPosition = other.mAnchorPosition; + mVisibleAnchorPosition = other.mVisibleAnchorPosition; + mSpanOffsets = other.mSpanOffsets; + mSpanLookupSize = other.mSpanLookupSize; + mSpanLookup = other.mSpanLookup; + mReverseLayout = other.mReverseLayout; + mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; + mLastLayoutRTL = other.mLastLayoutRTL; + mFullSpanItems = other.mFullSpanItems; + } + + void invalidateSpanInfo() { + mSpanOffsets = null; + mSpanOffsetsSize = 0; + mSpanLookupSize = 0; + mSpanLookup = null; + mFullSpanItems = null; + } + + void invalidateAnchorPositionInfo() { + mSpanOffsets = null; + mSpanOffsetsSize = 0; + mAnchorPosition = RecyclerView.NO_POSITION; + mVisibleAnchorPosition = RecyclerView.NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorPosition); + dest.writeInt(mVisibleAnchorPosition); + dest.writeInt(mSpanOffsetsSize); + if (mSpanOffsetsSize > 0) { + dest.writeIntArray(mSpanOffsets); + } + dest.writeInt(mSpanLookupSize); + if (mSpanLookupSize > 0) { + dest.writeIntArray(mSpanLookup); + } + dest.writeInt(mReverseLayout ? 1 : 0); + dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); + dest.writeInt(mLastLayoutRTL ? 1 : 0); + dest.writeList(mFullSpanItems); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Data class to hold the information about an anchor position which is used in onLayout call. + */ + class AnchorInfo { + + int mPosition; + int mOffset; + boolean mLayoutFromEnd; + boolean mInvalidateOffsets; + boolean mValid; + // this is where we save span reference lines in case we need to re-use them for multi-pass + // measure steps + int[] mSpanReferenceLines; + + AnchorInfo() { + reset(); + } + + void reset() { + mPosition = RecyclerView.NO_POSITION; + mOffset = INVALID_OFFSET; + mLayoutFromEnd = false; + mInvalidateOffsets = false; + mValid = false; + if (mSpanReferenceLines != null) { + Arrays.fill(mSpanReferenceLines, -1); + } + } + + void saveSpanReferenceLines(Span[] spans) { + int spanCount = spans.length; + if (mSpanReferenceLines == null || mSpanReferenceLines.length < spanCount) { + mSpanReferenceLines = new int[mSpans.length]; + } + for (int i = 0; i < spanCount; i++) { + // does not matter start or end since this is only recorded when span is reset + mSpanReferenceLines[i] = spans[i].getStartLine(Span.INVALID_LINE); + } + } + + void assignCoordinateFromPadding() { + mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() + : mPrimaryOrientation.getStartAfterPadding(); + } + + void assignCoordinateFromPadding(int addedDistance) { + if (mLayoutFromEnd) { + mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance; + } else { + mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance; + } + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ThreadUtil.java b/app/src/main/java/androidx/recyclerview/widget/ThreadUtil.java new file mode 100644 index 0000000000..541f388749 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ThreadUtil.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.annotation.SuppressLint; + +interface ThreadUtil { + + interface MainThreadCallback { + + void updateItemCount(int generation, int itemCount); + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void addTile(int generation, TileList.Tile tile); + + void removeTile(int generation, int position); + } + + interface BackgroundCallback { + + void refresh(int generation); + + void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd, + int scrollHint); + + void loadTile(int position, int scrollHint); + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + void recycleTile(TileList.Tile tile); + } + + MainThreadCallback getMainThreadProxy(MainThreadCallback callback); + + BackgroundCallback getBackgroundProxy(BackgroundCallback callback); +} diff --git a/app/src/main/java/androidx/recyclerview/widget/TileList.java b/app/src/main/java/androidx/recyclerview/widget/TileList.java new file mode 100644 index 0000000000..284b1b7092 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/TileList.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.util.SparseArray; + +import androidx.annotation.NonNull; + +import java.lang.reflect.Array; + +/** + * A sparse collection of tiles sorted for efficient access. + */ +class TileList { + + final int mTileSize; + + // Keyed by start position. + private final SparseArray> mTiles = new SparseArray>(10); + + Tile mLastAccessedTile; + + public TileList(int tileSize) { + mTileSize = tileSize; + } + + public T getItemAt(int pos) { + if (mLastAccessedTile == null || !mLastAccessedTile.containsPosition(pos)) { + final int startPosition = pos - (pos % mTileSize); + final int index = mTiles.indexOfKey(startPosition); + if (index < 0) { + return null; + } + mLastAccessedTile = mTiles.valueAt(index); + } + return mLastAccessedTile.getByPosition(pos); + } + + public int size() { + return mTiles.size(); + } + + public void clear() { + mTiles.clear(); + } + + /** + * Returns the {@link Tile} at the provided {@param index}, or {@code null} if the index + * provided is out of bounds. + */ + public Tile getAtIndex(int index) { + if (index < 0 || index >= mTiles.size()) { + return null; + } + return mTiles.valueAt(index); + } + + public Tile addOrReplace(Tile newTile) { + final int index = mTiles.indexOfKey(newTile.mStartPosition); + if (index < 0) { + mTiles.put(newTile.mStartPosition, newTile); + return null; + } + Tile oldTile = mTiles.valueAt(index); + mTiles.setValueAt(index, newTile); + if (mLastAccessedTile == oldTile) { + mLastAccessedTile = newTile; + } + return oldTile; + } + + public Tile removeAtPos(int startPosition) { + Tile tile = mTiles.get(startPosition); + if (mLastAccessedTile == tile) { + mLastAccessedTile = null; + } + mTiles.delete(startPosition); + return tile; + } + + public static class Tile { + public final T[] mItems; + public int mStartPosition; + public int mItemCount; + Tile mNext; // Used only for pooling recycled tiles. + + Tile(@NonNull Class klass, int size) { + @SuppressWarnings("unchecked") + T[] items = (T[]) Array.newInstance(klass, size); + mItems = items; + } + + boolean containsPosition(int pos) { + return mStartPosition <= pos && pos < mStartPosition + mItemCount; + } + + T getByPosition(int pos) { + return mItems[pos - mStartPosition]; + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java b/app/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java new file mode 100644 index 0000000000..8ea8f75333 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ViewBoundsCheck.java @@ -0,0 +1,269 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import android.view.View; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A utility class used to check the boundaries of a given view within its parent view based on + * a set of boundary flags. + */ +class ViewBoundsCheck { + + static final int GT = 1 << 0; + static final int EQ = 1 << 1; + static final int LT = 1 << 2; + + + static final int CVS_PVS_POS = 0; + /** + * The child view's start should be strictly greater than parent view's start. + */ + static final int FLAG_CVS_GT_PVS = GT << CVS_PVS_POS; + + /** + * The child view's start can be equal to its parent view's start. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVS_EQ_PVS = EQ << CVS_PVS_POS; + + /** + * The child view's start should be strictly less than parent view's start. + */ + static final int FLAG_CVS_LT_PVS = LT << CVS_PVS_POS; + + + static final int CVS_PVE_POS = 4; + /** + * The child view's start should be strictly greater than parent view's end. + */ + static final int FLAG_CVS_GT_PVE = GT << CVS_PVE_POS; + + /** + * The child view's start can be equal to its parent view's end. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVS_EQ_PVE = EQ << CVS_PVE_POS; + + /** + * The child view's start should be strictly less than parent view's end. + */ + static final int FLAG_CVS_LT_PVE = LT << CVS_PVE_POS; + + + static final int CVE_PVS_POS = 8; + /** + * The child view's end should be strictly greater than parent view's start. + */ + static final int FLAG_CVE_GT_PVS = GT << CVE_PVS_POS; + + /** + * The child view's end can be equal to its parent view's start. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVE_EQ_PVS = EQ << CVE_PVS_POS; + + /** + * The child view's end should be strictly less than parent view's start. + */ + static final int FLAG_CVE_LT_PVS = LT << CVE_PVS_POS; + + + static final int CVE_PVE_POS = 12; + /** + * The child view's end should be strictly greater than parent view's end. + */ + static final int FLAG_CVE_GT_PVE = GT << CVE_PVE_POS; + + /** + * The child view's end can be equal to its parent view's end. This flag follows with GT + * or LT indicating greater (less) than or equal relation. + */ + static final int FLAG_CVE_EQ_PVE = EQ << CVE_PVE_POS; + + /** + * The child view's end should be strictly less than parent view's end. + */ + static final int FLAG_CVE_LT_PVE = LT << CVE_PVE_POS; + + static final int MASK = GT | EQ | LT; + + final Callback mCallback; + BoundFlags mBoundFlags; + /** + * The set of flags that can be passed for checking the view boundary conditions. + * CVS in the flag name indicates the child view, and PV indicates the parent view.\ + * The following S, E indicate a view's start and end points, respectively. + * GT and LT indicate a strictly greater and less than relationship. + * Greater than or equal (or less than or equal) can be specified by setting both GT and EQ (or + * LT and EQ) flags. + * For instance, setting both {@link #FLAG_CVS_GT_PVS} and {@link #FLAG_CVS_EQ_PVS} indicate the + * child view's start should be greater than or equal to its parent start. + */ + @IntDef(flag = true, value = { + FLAG_CVS_GT_PVS, FLAG_CVS_EQ_PVS, FLAG_CVS_LT_PVS, + FLAG_CVS_GT_PVE, FLAG_CVS_EQ_PVE, FLAG_CVS_LT_PVE, + FLAG_CVE_GT_PVS, FLAG_CVE_EQ_PVS, FLAG_CVE_LT_PVS, + FLAG_CVE_GT_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ViewBounds {} + + ViewBoundsCheck(Callback callback) { + mCallback = callback; + mBoundFlags = new BoundFlags(); + } + + static class BoundFlags { + int mBoundFlags = 0; + int mRvStart, mRvEnd, mChildStart, mChildEnd; + + void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) { + mRvStart = rvStart; + mRvEnd = rvEnd; + mChildStart = childStart; + mChildEnd = childEnd; + } + + void addFlags(@ViewBounds int flags) { + mBoundFlags |= flags; + } + + void resetFlags() { + mBoundFlags = 0; + } + + int compare(int x, int y) { + if (x > y) { + return GT; + } + if (x == y) { + return EQ; + } + return LT; + } + + boolean boundsMatch() { + if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) { + if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) { + if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) { + if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) { + return false; + } + } + + if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) { + if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) { + return false; + } + } + return true; + } + }; + + /** + * Returns the first view starting from fromIndex to toIndex in views whose bounds lie within + * its parent bounds based on the provided preferredBoundFlags. If no match is found based on + * the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose + * bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such + * view is found based on either of these two flags, null is returned. + * @param fromIndex The view position index to start the search from. + * @param toIndex The view position index to end the search at. + * @param preferredBoundFlags The flags indicating the preferred match. Once a match is found + * based on this flag, that view is returned instantly. + * @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match + * is found. If so, and if acceptableBoundFlags is non-zero, the + * last matching acceptable view is returned. Otherwise, null is + * returned. + * @return The first view that satisfies acceptableBoundFlags or the last view satisfying + * acceptableBoundFlags boundary conditions. + */ + View findOneViewWithinBoundFlags(int fromIndex, int toIndex, + @ViewBounds int preferredBoundFlags, + @ViewBounds int acceptableBoundFlags) { + final int start = mCallback.getParentStart(); + final int end = mCallback.getParentEnd(); + final int next = toIndex > fromIndex ? 1 : -1; + View acceptableMatch = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = mCallback.getChildAt(i); + final int childStart = mCallback.getChildStart(child); + final int childEnd = mCallback.getChildEnd(child); + mBoundFlags.setBounds(start, end, childStart, childEnd); + if (preferredBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(preferredBoundFlags); + if (mBoundFlags.boundsMatch()) { + // found a perfect match + return child; + } + } + if (acceptableBoundFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(acceptableBoundFlags); + if (mBoundFlags.boundsMatch()) { + acceptableMatch = child; + } + } + } + return acceptableMatch; + } + + /** + * Returns whether the specified view lies within the boundary condition of its parent view. + * @param child The child view to be checked. + * @param boundsFlags The flag against which the child view and parent view are matched. + * @return True if the view meets the boundsFlag, false otherwise. + */ + boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) { + mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(), + mCallback.getChildStart(child), mCallback.getChildEnd(child)); + if (boundsFlags != 0) { + mBoundFlags.resetFlags(); + mBoundFlags.addFlags(boundsFlags); + return mBoundFlags.boundsMatch(); + } + return false; + } + + /** + * Callback provided by the user of this class in order to retrieve information about child and + * parent boundaries. + */ + interface Callback { + View getChildAt(int index); + int getParentStart(); + int getParentEnd(); + int getChildStart(View view); + int getChildEnd(View view); + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java b/app/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java new file mode 100644 index 0000000000..312edad353 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ViewInfoStore.java @@ -0,0 +1,329 @@ +/* + * Copyright 2018 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 androidx.recyclerview.widget; + +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_POST; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE; +import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.collection.LongSparseArray; +import androidx.collection.SimpleArrayMap; +import androidx.core.util.Pools; + +/** + * This class abstracts all tracking for Views to run animations. + */ +class ViewInfoStore { + + private static final boolean DEBUG = false; + + /** + * View data records for pre-layout + */ + @VisibleForTesting + final SimpleArrayMap mLayoutHolderMap = + new SimpleArrayMap<>(); + + @VisibleForTesting + final LongSparseArray mOldChangedHolders = new LongSparseArray<>(); + + /** + * Clears the state and all existing tracking data + */ + void clear() { + mLayoutHolderMap.clear(); + mOldChangedHolders.clear(); + } + + /** + * Adds the item information to the prelayout tracking + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.preInfo = info; + record.flags |= FLAG_PRE; + } + + boolean isDisappearing(RecyclerView.ViewHolder holder) { + final InfoRecord record = mLayoutHolderMap.get(holder); + return record != null && ((record.flags & FLAG_DISAPPEARED) != 0); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + RecyclerView.ItemAnimator.ItemHolderInfo popFromPreLayout(RecyclerView.ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_PRE); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + RecyclerView.ItemAnimator.ItemHolderInfo popFromPostLayout(RecyclerView.ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_POST); + } + + private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView.ViewHolder vh, int flag) { + int index = mLayoutHolderMap.indexOfKey(vh); + if (index < 0) { + return null; + } + final InfoRecord record = mLayoutHolderMap.valueAt(index); + if (record != null && (record.flags & flag) != 0) { + record.flags &= ~flag; + final RecyclerView.ItemAnimator.ItemHolderInfo info; + if (flag == FLAG_PRE) { + info = record.preInfo; + } else if (flag == FLAG_POST) { + info = record.postInfo; + } else { + throw new IllegalArgumentException("Must provide flag PRE or POST"); + } + // if not pre-post flag is left, clear. + if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) { + mLayoutHolderMap.removeAt(index); + InfoRecord.recycle(record); + } + return info; + } + return null; + } + + /** + * Adds the given ViewHolder to the oldChangeHolders list + * @param key The key to identify the ViewHolder. + * @param holder The ViewHolder to store + */ + void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) { + mOldChangedHolders.put(key, holder); + } + + /** + * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the + * LayoutManager during a pre-layout pass. We distinguish them from other views that were + * already in the pre-layout so that ItemAnimator can choose to run a different animation for + * them. + * + * @param holder The ViewHolder to store + * @param info The information to save + */ + void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_APPEAR; + record.preInfo = info; + } + + /** + * Checks whether the given ViewHolder is in preLayout list + * @param viewHolder The ViewHolder to query + * + * @return True if the ViewHolder is present in preLayout, false otherwise + */ + boolean isInPreLayout(RecyclerView.ViewHolder viewHolder) { + final InfoRecord record = mLayoutHolderMap.get(viewHolder); + return record != null && (record.flags & FLAG_PRE) != 0; + } + + /** + * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns + * null. + * @param key The key to be used to find the ViewHolder. + * + * @return A ViewHolder if exists or null if it does not exist. + */ + RecyclerView.ViewHolder getFromOldChangeHolders(long key) { + return mOldChangedHolders.get(key); + } + + /** + * Adds the item information to the post layout list + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.postInfo = info; + record.flags |= FLAG_POST; + } + + /** + * A ViewHolder might be added by the LayoutManager just to animate its disappearance. + * This list holds such items so that we can animate / recycle these ViewHolders properly. + * + * @param holder The ViewHolder which disappeared during a layout. + */ + void addToDisappearedInLayout(RecyclerView.ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_DISAPPEARED; + } + + /** + * Removes a ViewHolder from disappearing list. + * @param holder The ViewHolder to be removed from the disappearing list. + */ + void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + return; + } + record.flags &= ~FLAG_DISAPPEARED; + } + + void process(ProcessCallback callback) { + for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { + final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); + final InfoRecord record = mLayoutHolderMap.removeAt(index); + if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { + // Appeared then disappeared. Not useful for animations. + callback.unused(viewHolder); + } else if ((record.flags & FLAG_DISAPPEARED) != 0) { + // Set as "disappeared" by the LayoutManager (addDisappearingView) + if (record.preInfo == null) { + // similar to appear disappear but happened between different layout passes. + // this can happen when the layout manager is using auto-measure + callback.unused(viewHolder); + } else { + callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); + } + } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { + // Appeared in the layout but not in the adapter (e.g. entered the viewport) + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { + // Persistent in both passes. Animate persistence + callback.processPersistent(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE) != 0) { + // Was in pre-layout, never been added to post layout + callback.processDisappeared(viewHolder, record.preInfo, null); + } else if ((record.flags & FLAG_POST) != 0) { + // Was not in pre-layout, been added to post layout + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_APPEAR) != 0) { + // Scrap view. RecyclerView will handle removing/recycling this. + } else if (DEBUG) { + throw new IllegalStateException("record without any reasonable flag combination:/"); + } + InfoRecord.recycle(record); + } + } + + /** + * Removes the ViewHolder from all list + * @param holder The ViewHolder which we should stop tracking + */ + void removeViewHolder(RecyclerView.ViewHolder holder) { + for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) { + if (holder == mOldChangedHolders.valueAt(i)) { + mOldChangedHolders.removeAt(i); + break; + } + } + final InfoRecord info = mLayoutHolderMap.remove(holder); + if (info != null) { + InfoRecord.recycle(info); + } + } + + void onDetach() { + InfoRecord.drainCache(); + } + + public void onViewDetached(RecyclerView.ViewHolder viewHolder) { + removeFromDisappearedInLayout(viewHolder); + } + + interface ProcessCallback { + void processDisappeared(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, + @Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo); + void processAppeared(RecyclerView.ViewHolder viewHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preInfo, + RecyclerView.ItemAnimator.ItemHolderInfo postInfo); + void processPersistent(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo, + @NonNull RecyclerView.ItemAnimator.ItemHolderInfo postInfo); + void unused(RecyclerView.ViewHolder holder); + } + + static class InfoRecord { + // disappearing list + static final int FLAG_DISAPPEARED = 1; + // appear in pre layout list + static final int FLAG_APPEAR = 1 << 1; + // pre layout, this is necessary to distinguish null item info + static final int FLAG_PRE = 1 << 2; + // post layout, this is necessary to distinguish null item info + static final int FLAG_POST = 1 << 3; + static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; + static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; + static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; + int flags; + @Nullable + RecyclerView.ItemAnimator.ItemHolderInfo preInfo; + @Nullable + RecyclerView.ItemAnimator.ItemHolderInfo postInfo; + static Pools.Pool sPool = new Pools.SimplePool<>(20); + + private InfoRecord() { + } + + static InfoRecord obtain() { + InfoRecord record = sPool.acquire(); + return record == null ? new InfoRecord() : record; + } + + static void recycle(InfoRecord record) { + record.flags = 0; + record.preInfo = null; + record.postInfo = null; + sPool.release(record); + } + + static void drainCache() { + //noinspection StatementWithEmptyBody + while (sPool.acquire() != null); + } + } +} diff --git a/app/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java b/app/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java new file mode 100644 index 0000000000..adf9d01d77 --- /dev/null +++ b/app/src/main/java/androidx/recyclerview/widget/ViewTypeStorage.java @@ -0,0 +1,197 @@ +/* + * Copyright 2020 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 androidx.recyclerview.widget; + +import android.util.SparseArray; +import android.util.SparseIntArray; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used by {@link ConcatAdapter} to isolate view types between nested adapters, if necessary. + */ +interface ViewTypeStorage { + @NonNull + NestedAdapterWrapper getWrapperForGlobalType(int globalViewType); + + @NonNull + ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper + ); + + /** + * Api given to {@link NestedAdapterWrapper}s. + */ + interface ViewTypeLookup { + int localToGlobal(int localType); + + int globalToLocal(int globalType); + + void dispose(); + } + + class SharedIdRangeViewTypeStorage implements ViewTypeStorage { + // we keep a list of nested wrappers here even though we only need 1 to create because + // they might be removed. + SparseArray> mGlobalTypeToWrapper = new SparseArray<>(); + + @NonNull + @Override + public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) { + List nestedAdapterWrappers = mGlobalTypeToWrapper.get( + globalViewType); + if (nestedAdapterWrappers == null || nestedAdapterWrappers.isEmpty()) { + throw new IllegalArgumentException("Cannot find the wrapper for global view" + + " type " + globalViewType); + } + // just return the first one since they are shared + return nestedAdapterWrappers.get(0); + } + + @NonNull + @Override + public ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper) { + return new WrapperViewTypeLookup(wrapper); + } + + void removeWrapper(@NonNull NestedAdapterWrapper wrapper) { + for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) { + List wrappers = mGlobalTypeToWrapper.valueAt(i); + if (wrappers.remove(wrapper)) { + if (wrappers.isEmpty()) { + mGlobalTypeToWrapper.removeAt(i); + } + } + } + } + + class WrapperViewTypeLookup implements ViewTypeLookup { + final NestedAdapterWrapper mWrapper; + + WrapperViewTypeLookup(NestedAdapterWrapper wrapper) { + mWrapper = wrapper; + } + + @Override + public int localToGlobal(int localType) { + // register it first + List wrappers = mGlobalTypeToWrapper.get( + localType); + if (wrappers == null) { + wrappers = new ArrayList<>(); + mGlobalTypeToWrapper.put(localType, wrappers); + } + if (!wrappers.contains(mWrapper)) { + wrappers.add(mWrapper); + } + return localType; + } + + @Override + public int globalToLocal(int globalType) { + return globalType; + } + + @Override + public void dispose() { + removeWrapper(mWrapper); + } + } + } + + class IsolatedViewTypeStorage implements ViewTypeStorage { + SparseArray mGlobalTypeToWrapper = new SparseArray<>(); + + int mNextViewType = 0; + + int obtainViewType(NestedAdapterWrapper wrapper) { + int nextId = mNextViewType++; + mGlobalTypeToWrapper.put(nextId, wrapper); + return nextId; + } + + @NonNull + @Override + public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) { + NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get( + globalViewType); + if (wrapper == null) { + throw new IllegalArgumentException("Cannot find the wrapper for global" + + " view type " + globalViewType); + } + return wrapper; + } + + @Override + @NonNull + public ViewTypeLookup createViewTypeWrapper( + @NonNull NestedAdapterWrapper wrapper) { + return new WrapperViewTypeLookup(wrapper); + } + + void removeWrapper(@NonNull NestedAdapterWrapper wrapper) { + for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) { + NestedAdapterWrapper existingWrapper = mGlobalTypeToWrapper.valueAt(i); + if (existingWrapper == wrapper) { + mGlobalTypeToWrapper.removeAt(i); + } + } + } + + class WrapperViewTypeLookup implements ViewTypeLookup { + private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1); + private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1); + final NestedAdapterWrapper mWrapper; + + WrapperViewTypeLookup(NestedAdapterWrapper wrapper) { + mWrapper = wrapper; + } + + @Override + public int localToGlobal(int localType) { + int index = mLocalToGlobalMapping.indexOfKey(localType); + if (index > -1) { + return mLocalToGlobalMapping.valueAt(index); + } + // get a new key. + int globalType = obtainViewType(mWrapper); + mLocalToGlobalMapping.put(localType, globalType); + mGlobalToLocalMapping.put(globalType, localType); + return globalType; + } + + @Override + public int globalToLocal(int globalType) { + int index = mGlobalToLocalMapping.indexOfKey(globalType); + if (index < 0) { + throw new IllegalStateException("requested global type " + globalType + " does" + + " not belong to the adapter:" + mWrapper.adapter); + } + return mGlobalToLocalMapping.valueAt(index); + } + + @Override + public void dispose() { + removeWrapper(mWrapper); + } + } + } +}