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 List
s 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 extends ViewHolder>... 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 extends ViewHolder>... 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 extends Adapter extends ViewHolder>> 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 extends Adapter extends ViewHolder>> adapters) {
+ mController = new ConcatAdapterController(this, config);
+ for (Adapter extends ViewHolder> 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 extends ViewHolder> 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 extends ViewHolder> 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 extends ViewHolder> 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 extends Adapter extends ViewHolder>> 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 extends ViewHolder> 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 extends ViewHolder> 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 extends ViewHolder> 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:
+ *
+ * An in bounds child.
+ * An out of bounds child.
+ * 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 extends LayoutManager> layoutManagerClass =
+ Class.forName(className, false, classLoader)
+ .asSubclass(LayoutManager.class);
+ Constructor extends LayoutManager> 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 extends ViewHolder> 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:
+ *
+ * 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).
+ * 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()}.
+ * 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.
+ * 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.
+ * 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.
+ * Finally, animations are calculated and run as usual.
+ *
+ *
+ * @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 extends ViewHolder> 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 extends ViewHolder> 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 extends ViewHolder> 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