From 7149a63961c5fe6706160bc717a3b6cbb083ca54 Mon Sep 17 00:00:00 2001 From: Aurimas Liutikas Date: Wed, 18 Jan 2017 17:36:10 -0800 Subject: [PATCH] Port RecyclerView to com.android.internal.widget.RecyclerView Change-Id: I5d5912fee6f736bfc93d28ad53fc35e22be953c3 --- .../internal/widget/AdapterHelper.java | 775 + .../android/internal/widget/ChildHelper.java | 538 + .../internal/widget/DefaultItemAnimator.java | 668 + .../android/internal/widget/GapWorker.java | 379 + .../internal/widget/LinearLayoutManager.java | 2398 +++ .../internal/widget/LinearSmoothScroller.java | 361 + .../internal/widget/NestedScrollingChild.java | 226 + .../android/internal/widget/OpReorderer.java | 239 + .../internal/widget/OrientationHelper.java | 439 + .../android/internal/widget/RecyclerView.java | 12255 ++++++++++++++++ .../RecyclerViewAccessibilityDelegate.java | 107 + .../internal/widget/ScrollbarHelper.java | 99 + .../internal/widget/ScrollingView.java | 134 + .../internal/widget/SimpleItemAnimator.java | 457 + .../internal/widget/ViewInfoStore.java | 330 + .../widget/helper/ItemTouchHelper.java | 2391 +++ .../widget/helper/ItemTouchUIUtil.java | 65 + .../widget/helper/ItemTouchUIUtilImpl.java | 84 + core/res/res/values/attrs.xml | 12 +- core/res/res/values/dimens.xml | 6 + core/res/res/values/ids.xml | 3 + core/res/res/values/symbols.xml | 6 + 22 files changed, 21971 insertions(+), 1 deletion(-) create mode 100644 core/java/com/android/internal/widget/AdapterHelper.java create mode 100644 core/java/com/android/internal/widget/ChildHelper.java create mode 100644 core/java/com/android/internal/widget/DefaultItemAnimator.java create mode 100644 core/java/com/android/internal/widget/GapWorker.java create mode 100644 core/java/com/android/internal/widget/LinearLayoutManager.java create mode 100644 core/java/com/android/internal/widget/LinearSmoothScroller.java create mode 100644 core/java/com/android/internal/widget/NestedScrollingChild.java create mode 100644 core/java/com/android/internal/widget/OpReorderer.java create mode 100644 core/java/com/android/internal/widget/OrientationHelper.java create mode 100644 core/java/com/android/internal/widget/RecyclerView.java create mode 100644 core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java create mode 100644 core/java/com/android/internal/widget/ScrollbarHelper.java create mode 100644 core/java/com/android/internal/widget/ScrollingView.java create mode 100644 core/java/com/android/internal/widget/SimpleItemAnimator.java create mode 100644 core/java/com/android/internal/widget/ViewInfoStore.java create mode 100644 core/java/com/android/internal/widget/helper/ItemTouchHelper.java create mode 100644 core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java create mode 100644 core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java diff --git a/core/java/com/android/internal/widget/AdapterHelper.java b/core/java/com/android/internal/widget/AdapterHelper.java new file mode 100644 index 0000000000000..f47d43075d38f --- /dev/null +++ b/core/java/com/android/internal/widget/AdapterHelper.java @@ -0,0 +1,775 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.util.Log; +import android.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. + */ +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 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 == null || getClass() != o.getClass()) { + return false; + } + + UpdateOp op = (UpdateOp) o; + + if (cmd != op.cmd) { + return false; + } + if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) { + // reverse of this is also true + if (itemCount == op.positionStart && positionStart == op.itemCount) { + return true; + } + } + if (itemCount != op.itemCount) { + return false; + } + if (positionStart != op.positionStart) { + return false; + } + 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/core/java/com/android/internal/widget/ChildHelper.java b/core/java/com/android/internal/widget/ChildHelper.java new file mode 100644 index 0000000000000..e9136d09fb595 --- /dev/null +++ b/core/java/com/android/internal/widget/ChildHelper.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2017 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 com.android.internal.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"; + + final Callback mCallback; + + final Bucket mBucket; + + final List mHiddenViews; + + 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) { + 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); + } + } + + /** + * 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) { + final int offset = getOffset(index); + final View view = mCallback.getChildAt(offset); + if (view == null) { + return; + } + if (mBucket.remove(offset)) { + unhideViewInternal(view); + } + mCallback.removeViewAt(offset); + if (DEBUG) { + Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this); + } + } + + /** + * 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) { + 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; + } + + /** + * 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/core/java/com/android/internal/widget/DefaultItemAnimator.java b/core/java/com/android/internal/widget/DefaultItemAnimator.java new file mode 100644 index 0000000000000..92345af9d1186 --- /dev/null +++ b/core/java/com/android/internal/widget/DefaultItemAnimator.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.view.View; +import android.view.ViewPropertyAnimator; + +import com.android.internal.widget.RecyclerView.ViewHolder; + +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 ViewHolder holder; + public int fromX, fromY, toX, toY; + + MoveInfo(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 ViewHolder oldHolder, newHolder; + public int fromX, fromY, toX, toY; + private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) { + this.oldHolder = oldHolder; + this.newHolder = newHolder; + } + + ChangeInfo(ViewHolder oldHolder, 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 + 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 (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; + view.postOnAnimationDelayed(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) { + ViewHolder holder = changes.get(0).oldHolder; + holder.itemView.postOnAnimationDelayed(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 (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; + view.postOnAnimationDelayed(adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + resetAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final 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 + public boolean animateAdd(final ViewHolder holder) { + resetAnimation(holder); + holder.itemView.setAlpha(0); + mPendingAdditions.add(holder); + return true; + } + + void animateAddImpl(final 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 + public boolean animateMove(final ViewHolder holder, int fromX, int fromY, + int toX, int toY) { + final View view = holder.itemView; + fromX += holder.itemView.getTranslationX(); + fromY += 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 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 + public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder, + int fromX, int fromY, int toX, int toY) { + 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, fromX, fromY, toX, toY); + } + final float prevTranslationX = oldHolder.itemView.getTranslationX(); + final float prevTranslationY = oldHolder.itemView.getTranslationY(); + final float prevAlpha = oldHolder.itemView.getAlpha(); + resetAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - 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, fromX, fromY, toX, toY)); + return true; + } + + void animateChangeImpl(final ChangeInfo changeInfo) { + final ViewHolder holder = changeInfo.oldHolder; + final View view = holder == null ? null : holder.itemView; + final 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, 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, 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 + public void endAnimation(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(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--) { + ViewHolder item = mPendingRemovals.get(i); + dispatchRemoveFinished(item); + mPendingRemovals.remove(i); + } + count = mPendingAdditions.size(); + for (int i = count - 1; i >= 0; i--) { + 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); + 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--) { + 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: + *

+ */ + @Override + public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, + @NonNull List payloads) { + return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); + } +} diff --git a/core/java/com/android/internal/widget/GapWorker.java b/core/java/com/android/internal/widget/GapWorker.java new file mode 100644 index 0000000000000..5972396b53a36 --- /dev/null +++ b/core/java/com/android/internal/widget/GapWorker.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.annotation.Nullable; +import android.os.Trace; +import android.view.View; + +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. + */ + 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 (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); + } + } + } + + public void add(RecyclerView recyclerView) { + if (RecyclerView.DEBUG && 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.DEBUG && !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.DEBUG && !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); + 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); + 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 = recycler.tryGetViewHolderForPositionByDeadline( + position, false, deadlineNs); + + if (holder != null) { + if (holder.isBound()) { + // 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); + } + } + 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 { + Trace.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 { + Trace.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) { + 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 { + Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG); + + if (mRecyclerViews.isEmpty()) { + // abort - no work to do + return; + } + + // Query last 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. + long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos( + mRecyclerViews.get(0).getDrawingTime()); + if (lastFrameVsyncNs == 0) { + // abort - couldn't get last vsync for estimating next + return; + } + + // TODO: consider rebasing deadline if frame was already dropped due to long UI work. + // Next frame will still wait for VSYNC, so we can still use the gap if it exists. + long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs; + + prefetch(nextFrameNs); + + // TODO: consider rescheduling self, if there's more work to do + } finally { + mPostTimeNs = 0; + Trace.endSection(); + } + } +} diff --git a/core/java/com/android/internal/widget/LinearLayoutManager.java b/core/java/com/android/internal/widget/LinearLayoutManager.java new file mode 100644 index 0000000000000..d82c746649e2f --- /dev/null +++ b/core/java/com/android/internal/widget/LinearLayoutManager.java @@ -0,0 +1,2398 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import static com.android.internal.widget.RecyclerView.NO_POSITION; + +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 com.android.internal.widget.RecyclerView.LayoutParams; +import com.android.internal.widget.helper.ItemTouchHelper; + +import java.util.List; + +/** + * A {@link com.android.internal.widget.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 = OrientationHelper.HORIZONTAL; + + public static final int VERTICAL = OrientationHelper.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} + */ + int mOrientation; + + /** + * 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. + * Based on {@link #mOrientation}, an implementation is lazily created in + * {@link #ensureLayoutState} method. + */ + 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 = 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 mInitialItemPrefetchCount = 2; + + /** + * Creates a vertical LinearLayoutManager + * + * @param context Current context, will be used to access resources. + */ + public LinearLayoutManager(Context context) { + this(context, VERTICAL, 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(Context context, int orientation, boolean reverseLayout) { + setOrientation(orientation); + setReverseLayout(reverseLayout); + setAutoMeasureEnabled(true); + } + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". Defaults to vertical orientation. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + */ + 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); + setAutoMeasureEnabled(true); + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateDefaultLayoutParams() { + return new 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 + public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { + super.onDetachedFromWindow(view, recycler); + if (mRecycleChildrenOnDetach) { + removeAndRecycleAllViews(recycler); + recycler.clear(); + } + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (getChildCount() > 0) { + event.setFromIndex(findFirstVisibleItemPosition()); + event.setToIndex(findLastVisibleItemPosition()); + } + } + + @Override + 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 + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof SavedState) { + mPendingSavedState = (SavedState) state; + 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) + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation of the layout. {@link com.android.internal.widget.LinearLayoutManager} + * will do its best to keep scroll position. + * + * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} + */ + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL && orientation != VERTICAL) { + throw new IllegalArgumentException("invalid orientation:" + orientation); + } + assertNotInLayoutOrScroll(null); + if (orientation == mOrientation) { + return; + } + mOrientation = orientation; + mOrientationHelper = null; + 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 com.android.internal.widget.RecyclerView} is LTR, than it will + * layout from RTL, if {@link com.android.internal.widget.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 + 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 com.android.internal.widget.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). + */ + protected int getExtraLayoutSpace(RecyclerView.State state) { + if (state.hasTargetScrollPosition()) { + return mOrientationHelper.getTotalSpace(); + } else { + return 0; + } + } + + @Override + public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, + int position) { + LinearSmoothScroller linearSmoothScroller = + new LinearSmoothScroller(recyclerView.getContext()); + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + @Override + 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 + 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 != 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(); + + if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION + || mPendingSavedState != null) { + mAnchorInfo.reset(); + mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; + // calculate anchor position and coordinate + updateAnchorInfoForLayout(recycler, state, mAnchorInfo); + mAnchorInfo.mValid = true; + } + 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. + int extraForStart; + int extraForEnd; + final int extra = getExtraLayoutSpace(state); + // If the previous scroll delta was less than zero, the extra space should be laid out + // at the start. Otherwise, it should be at the end. + if (mLayoutState.mLastScrollDelta >= 0) { + extraForEnd = extra; + extraForStart = 0; + } else { + extraForStart = extra; + extraForEnd = 0; + } + extraForStart += mOrientationHelper.getStartAfterPadding(); + extraForEnd += mOrientationHelper.getEndPadding(); + if (state.isPreLayout() && mPendingScrollPosition != 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(); + if (mAnchorInfo.mLayoutFromEnd) { + // fill towards start + updateLayoutStateToFillStart(mAnchorInfo); + mLayoutState.mExtra = 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.mExtra = 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.mExtra = extraForStart; + fill(recycler, mLayoutState, state, false); + startOffset = mLayoutState.mOffset; + } + } else { + // fill towards end + updateLayoutStateToFillEnd(mAnchorInfo); + mLayoutState.mExtra = 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.mExtra = 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.mExtra = 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 + public void onLayoutCompleted(RecyclerView.State state) { + super.onLayoutCompleted(state); + mPendingSavedState = null; // we don't need this anymore + mPendingScrollPosition = 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.mExtra = scrapExtraStart; + mLayoutState.mAvailable = 0; + mLayoutState.assignPositionFromScrapList(); + fill(recycler, mLayoutState, state, false); + } + + if (scrapExtraEnd > 0) { + View anchor = getChildClosestToEnd(); + updateLayoutStateToFillEnd(getPosition(anchor), endOffset); + mLayoutState.mExtra = 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); + return true; + } + if (mLastStackFromEnd != mStackFromEnd) { + return false; + } + View referenceChild = anchorInfo.mLayoutFromEnd + ? findReferenceChildClosestToEnd(recycler, state) + : findReferenceChildClosestToStart(recycler, state); + if (referenceChild != null) { + anchorInfo.assignFromView(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 boolean notVisible = + mOrientationHelper.getDecoratedStart(referenceChild) >= mOrientationHelper + .getEndAfterPadding() + || mOrientationHelper.getDecoratedEnd(referenceChild) + < mOrientationHelper.getStartAfterPadding(); + if (notVisible) { + anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd + ? mOrientationHelper.getEndAfterPadding() + : mOrientationHelper.getStartAfterPadding(); + } + } + 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 == NO_POSITION) { + return false; + } + // validate scroll position + if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { + mPendingScrollPosition = 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() == View.LAYOUT_DIRECTION_RTL; + } + + void ensureLayoutState() { + if (mLayoutState == null) { + mLayoutState = createLayoutState(); + } + if (mOrientationHelper == null) { + mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); + } + } + + /** + * 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 View#getLayoutDirection()} 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 + public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == VERTICAL) { + return 0; + } + return scrollBy(dx, recycler, state); + } + + /** + * {@inheritDoc} + */ + @Override + public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, + RecyclerView.State state) { + if (mOrientation == HORIZONTAL) { + return 0; + } + return scrollBy(dy, recycler, state); + } + + @Override + public int computeHorizontalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + public int computeVerticalScrollOffset(RecyclerView.State state) { + return computeScrollOffset(state); + } + + @Override + public int computeHorizontalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + public int computeVerticalScrollExtent(RecyclerView.State state) { + return computeScrollExtent(state); + } + + @Override + public int computeHorizontalScrollRange(RecyclerView.State state) { + return computeScrollRange(state); + } + + @Override + 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.mExtra = getExtraLayoutSpace(state); + mLayoutState.mLayoutDirection = layoutDirection; + int scrollingOffset; + if (layoutDirection == LayoutState.LAYOUT_END) { + mLayoutState.mExtra += 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.mExtra += 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, layoutState.mScrollingOffset); + } + } + + @Override + 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 == 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 < mInitialItemPrefetchCount; 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 #getInitialItemPrefetchCount() + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + */ + public void setInitialPrefetchItemCount(int itemCount) { + mInitialItemPrefetchCount = 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. + * + * @see #isItemPrefetchEnabled() + * @see #setInitialPrefetchItemCount(int) + * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) + * + * @return number of items to prefetch. + */ + public int getInitialItemPrefetchCount() { + return mInitialItemPrefetchCount; + } + + @Override + 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; + } + + final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDy = Math.abs(delta); + updateLayoutState(layoutDirection, absDy, true, state); + collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry); + } + + int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { + if (getChildCount() == 0 || dy == 0) { + return 0; + } + mLayoutState.mRecycle = true; + ensureLayoutState(); + final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; + final int absDy = Math.abs(dy); + updateLayoutState(layoutDirection, absDy, 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 = absDy > consumed ? layoutDirection * consumed : dy; + mOrientationHelper.offsetChildren(-scrolled); + if (DEBUG) { + Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); + } + mLayoutState.mLastScrollDelta = scrolled; + return scrolled; + } + + @Override + 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 com.android.internal.widget.RecyclerView} + * @param dt 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. + */ + private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { + if (dt < 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 = dt; + 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 com.android.internal.widget.RecyclerView} + * @param dt 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. + */ + private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) { + final int childCount = getChildCount(); + if (dt < 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() - dt; + 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(com.android.internal.widget.RecyclerView.Recycler, int) + * @see #recycleViewsFromEnd(com.android.internal.widget.RecyclerView.Recycler, int) + * @see com.android.internal.widget.LinearLayoutManager.LayoutState#mLayoutDirection + */ + private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (!layoutState.mRecycle || layoutState.mInfinite) { + return; + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); + } else { + recycleViewsFromStart(recycler, layoutState.mScrollingOffset); + } + } + + /** + * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly + * independent from the rest of the {@link com.android.internal.widget.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.mExtra; + LayoutChunkResult layoutChunkResult = mLayoutChunkResult; + while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { + layoutChunkResult.resetInternal(); + layoutChunk(recycler, state, layoutState, layoutChunkResult); + 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 || mLayoutState.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; + } + LayoutParams params = (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.isFocusable(); + } + + @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. + */ + private 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. + */ + private View findFirstVisibleChildClosestToEnd(boolean completelyVisible, + boolean acceptPartiallyVisible) { + if (mShouldReverseLayout) { + return findOneVisibleChild(0, getChildCount(), completelyVisible, + acceptPartiallyVisible); + } else { + return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, + acceptPartiallyVisible); + } + } + + + /** + * Among the children that are suitable to be considered as an anchor child, returns the one + * closest to the end of the layout. + *

+ * 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 that are within the visible bounds. + * @return A View that can be used an an anchor View. + */ + private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) : + findLastReferenceChild(recycler, state); + } + + /** + * Among the children that are suitable to be considered as an anchor child, returns the one + * closest to the start of the layout. + *

+ * 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 that are within the visible bounds. + * + * @return A View that can be used an an anchor View. + */ + private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler, + RecyclerView.State state) { + return mShouldReverseLayout ? findLastReferenceChild(recycler, state) : + findFirstReferenceChild(recycler, state); + } + + private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { + return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount()); + } + + private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { + return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount()); + } + + // overridden by GridLayoutManager + View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, + int start, int end, int itemCount) { + ensureLayoutState(); + View invalidMatch = null; + View outOfBoundsMatch = null; + final int boundsStart = mOrientationHelper.getStartAfterPadding(); + final int boundsEnd = mOrientationHelper.getEndAfterPadding(); + final int diff = end > start ? 1 : -1; + for (int i = start; i != end; i += diff) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + if (((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; + } + + /** + * 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 ? 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 ? 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 ? 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 ? NO_POSITION : getPosition(child); + } + + View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, + boolean acceptPartiallyVisible) { + ensureLayoutState(); + final int start = mOrientationHelper.getStartAfterPadding(); + final int end = mOrientationHelper.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; + View partiallyVisible = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = getChildAt(i); + final int childStart = mOrientationHelper.getDecoratedStart(child); + final int childEnd = mOrientationHelper.getDecoratedEnd(child); + if (childStart < end && childEnd > start) { + if (completelyVisible) { + if (childStart >= start && childEnd <= end) { + return child; + } else if (acceptPartiallyVisible && partiallyVisible == null) { + partiallyVisible = child; + } + } else { + return child; + } + } + } + return partiallyVisible; + } + + @Override + public View onFocusSearchFailed(View focused, int focusDirection, + RecyclerView.Recycler recycler, RecyclerView.State state) { + resolveShouldLayoutReverse(); + if (getChildCount() == 0) { + return null; + } + + final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); + if (layoutDir == LayoutState.INVALID_LAYOUT) { + return null; + } + ensureLayoutState(); + final View referenceChild; + if (layoutDir == LayoutState.LAYOUT_START) { + referenceChild = findReferenceChildClosestToStart(recycler, state); + } else { + referenceChild = findReferenceChildClosestToEnd(recycler, state); + } + if (referenceChild == null) { + if (DEBUG) { + Log.d(TAG, + "Cannot find a child with a valid position to be used for focus search."); + } + 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); + final View nextFocus; + if (layoutDir == LayoutState.LAYOUT_START) { + nextFocus = getChildClosestToStart(); + } else { + nextFocus = getChildClosestToEnd(); + } + if (nextFocus == referenceChild || !nextFocus.isFocusable()) { + return null; + } + return nextFocus; + } + + /** + * 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; + } + + /** + * @hide This method should be called by ItemTouchHelper only. + */ + @Override + public void prepareForDrop(View view, 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 #mExtra} is not considered to avoid recycling visible children. + */ + int mExtra = 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 LayoutParams lp = (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 = NO_POSITION; + } else { + mCurrentPosition = ((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 LayoutParams lp = (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 + */ + 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; + } + + public SavedState(SavedState other) { + mAnchorPosition = other.mAnchorPosition; + mAnchorOffset = other.mAnchorOffset; + mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; + } + + boolean hasValidAnchor() { + return mAnchorPosition >= 0; + } + + void invalidateAnchor() { + mAnchorPosition = 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 + */ + class AnchorInfo { + int mPosition; + int mCoordinate; + boolean mLayoutFromEnd; + boolean mValid; + + AnchorInfo() { + reset(); + } + + void reset() { + mPosition = 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) { + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0 + && lp.getViewLayoutPosition() < state.getItemCount(); + } + + public void assignFromViewAndKeepVisibleRect(View child) { + final int spaceChange = mOrientationHelper.getTotalSpaceChange(); + if (spaceChange >= 0) { + assignFromView(child); + return; + } + mPosition = getPosition(child); + 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) { + if (mLayoutFromEnd) { + mCoordinate = mOrientationHelper.getDecoratedEnd(child) + + mOrientationHelper.getTotalSpaceChange(); + } else { + mCoordinate = mOrientationHelper.getDecoratedStart(child); + } + + mPosition = getPosition(child); + } + } + + 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/core/java/com/android/internal/widget/LinearSmoothScroller.java b/core/java/com/android/internal/widget/LinearSmoothScroller.java new file mode 100644 index 0000000000000..d024f21a4cd04 --- /dev/null +++ b/core/java/com/android/internal/widget/LinearSmoothScroller.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.PointF; +import android.util.DisplayMetrics; +import android.util.Log; +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 String TAG = "LinearSmoothScroller"; + + 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(); + + protected PointF mTargetVector; + + private final float MILLISECONDS_PER_PX; + + // 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; + + public LinearSmoothScroller(Context context) { + MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics()); + } + + /** + * {@inheritDoc} + */ + @Override + protected void onStart() { + + } + + /** + * {@inheritDoc} + */ + @Override + 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 + protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) { + 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. + * + * @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. + */ + protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) { + return MILLISECONDS_PER_INCH / displayMetrics.densityDpi; + } + + /** + *

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) * MILLISECONDS_PER_PX); + } + + /** + * 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) + */ + 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. + */ + 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. + */ + 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); + } + + /** + * Compute the scroll vector for a given target position. + *

+ * This method can return null if the layout manager cannot calculate a scroll vector + * for the given position (e.g. it has no current scroll position). + * + * @param targetPosition the position to which the scroller is scrolling + * + * @return the scroll vector for a given target position + */ + @Nullable + public PointF computeScrollVectorForPosition(int targetPosition) { + RecyclerView.LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof ScrollVectorProvider) { + return ((ScrollVectorProvider) layoutManager) + .computeScrollVectorForPosition(targetPosition); + } + Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" + + " does not implement " + ScrollVectorProvider.class.getCanonicalName()); + return null; + } +} diff --git a/core/java/com/android/internal/widget/NestedScrollingChild.java b/core/java/com/android/internal/widget/NestedScrollingChild.java new file mode 100644 index 0000000000000..20285b5ff44dc --- /dev/null +++ b/core/java/com/android/internal/widget/NestedScrollingChild.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +/** + * This interface should be implemented by {@link android.view.View View} subclasses that wish + * to support dispatching nested scrolling operations to a cooperating parent + * {@link android.view.ViewGroup ViewGroup}. + * + *

Classes implementing this interface should create a final instance of a + * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the + * NestedScrollingChildHelper methods of the same signature.

+ * + *

Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

+ */ +public interface NestedScrollingChild { + /** + * Enable or disable nested scrolling for this view. + * + *

If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.

+ * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled() + */ + void setNestedScrollingEnabled(boolean enabled); + + /** + * Returns true if nested scrolling is enabled for this view. + * + *

If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.

+ * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(boolean) + */ + boolean isNestedScrollingEnabled(); + + /** + * Begin a nestable scroll operation along the given axes. + * + *

A view starting a nested scroll promises to abide by the following contract:

+ * + *

The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll()} to indicate the end of the nested scroll.

+ * + *

If startNestedScroll returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.

+ * + *

At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.

+ * + *

After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + *

+ * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll() + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + boolean startNestedScroll(int axes); + + /** + * Stop a nested scroll in progress. + * + *

Calling this method when a nested scroll is not currently in progress is harmless.

+ * + * @see #startNestedScroll(int) + */ + void stopNestedScroll(); + + /** + * Returns true if this view has a nested scrolling parent. + * + *

The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.

+ * + * @return whether this view has a nested scrolling parent + */ + boolean hasNestedScrollingParent(); + + /** + * Dispatch one step of a nested scroll in progress. + * + *

Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.

+ * + *

Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.

+ * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + */ + boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + *

Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * dispatchNestedPreScroll offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.

+ * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); + + /** + * Dispatch a fling to a nested scrolling parent. + * + *

This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

+ * + *

If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

+ * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + *

Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. dispatchNestedPreFling + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns true, a nested + * parent view consumed the fling and this view should not scroll as a result.

+ * + *

For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:

+ * + *
    + *
  • If a custom view is paged and needs to settle to a fixed page-point, do not + * call dispatchNestedPreFling; consume the fling and settle to a valid + * position regardless.
  • + *
  • If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.
  • + *
+ * + *

Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.

+ * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + boolean dispatchNestedPreFling(float velocityX, float velocityY); +} diff --git a/core/java/com/android/internal/widget/OpReorderer.java b/core/java/com/android/internal/widget/OpReorderer.java new file mode 100644 index 0000000000000..babb0875b598a --- /dev/null +++ b/core/java/com/android/internal/widget/OpReorderer.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import static com.android.internal.widget.AdapterHelper.UpdateOp.ADD; +import static com.android.internal.widget.AdapterHelper.UpdateOp.MOVE; +import static com.android.internal.widget.AdapterHelper.UpdateOp.REMOVE; +import static com.android.internal.widget.AdapterHelper.UpdateOp.UPDATE; + +import com.android.internal.widget.AdapterHelper.UpdateOp; + +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 UpdateOp moveOp = list.get(badMove); + final UpdateOp nextOp = list.get(next); + switch (nextOp.cmd) { + case REMOVE: + swapMoveRemove(list, badMove, moveOp, next, nextOp); + break; + case ADD: + swapMoveAdd(list, badMove, moveOp, next, nextOp); + break; + case UPDATE: + swapMoveUpdate(list, badMove, moveOp, next, nextOp); + break; + } + } + + void swapMoveRemove(List list, int movePos, UpdateOp moveOp, + int removePos, UpdateOp removeOp) { + 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 = 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(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, UpdateOp moveOp, int add, + 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, UpdateOp moveOp, int update, + UpdateOp updateOp) { + UpdateOp extraUp1 = null; + 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(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(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 UpdateOp op1 = list.get(i); + if (op1.cmd == MOVE) { + if (foundNonMove) { + return i; + } + } else { + foundNonMove = true; + } + } + return -1; + } + + interface Callback { + + UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload); + + void recycleUpdateOp(UpdateOp op); + } +} diff --git a/core/java/com/android/internal/widget/OrientationHelper.java b/core/java/com/android/internal/widget/OrientationHelper.java new file mode 100644 index 0000000000000..1b02c886f56f8 --- /dev/null +++ b/core/java/com/android/internal/widget/OrientationHelper.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.graphics.Rect; +import android.view.View; +import android.widget.LinearLayout; + +/** + * 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 = LinearLayout.HORIZONTAL; + + public static final int VERTICAL = LinearLayout.VERTICAL; + + private int mLastTotalSpace = INVALID_SIZE; + + final Rect mTmpRect = new Rect(); + + private OrientationHelper(RecyclerView.LayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + /** + * 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, 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/core/java/com/android/internal/widget/RecyclerView.java b/core/java/com/android/internal/widget/RecyclerView.java new file mode 100644 index 0000000000000..0cf3164de98e3 --- /dev/null +++ b/core/java/com/android/internal/widget/RecyclerView.java @@ -0,0 +1,12255 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.annotation.CallSuper; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +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.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.os.Trace; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.AbsSavedState; +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.accessibility.AccessibilityNodeInfo; +import android.view.animation.Interpolator; +import android.widget.EdgeEffect; +import android.widget.OverScroller; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.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.List; + +/** + * 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#getAdapterPosition()}, + * {@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#getAdapterPosition()}. 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. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_layoutManager + */ +public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild { + + static final String TAG = "RecyclerView"; + + static final boolean DEBUG = false; + + private static final int[] NESTED_SCROLLING_ATTRS = { android.R.attr.nestedScrollingEnabled }; + + private static final int[] CLIP_TO_PADDING_ATTR = {android.R.attr.clipToPadding}; + + /** + * 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. + */ + private 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; + + static final boolean DISPATCH_TEMP_DETACH = false; + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + 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; + + 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}; + + private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); + + final Recycler mRecycler = new Recycler(); + + private 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 (mLayoutFrozen) { + mLayoutRequestEaten = 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; + RecyclerListener mRecyclerListener; + final ArrayList mItemDecorations = new ArrayList<>(); + private final ArrayList mOnItemTouchListeners = + new ArrayList<>(); + private OnItemTouchListener mActiveOnItemTouchListener; + boolean mIsAttached; + boolean mHasFixedSize; + @VisibleForTesting boolean mFirstLayoutComplete; + + // Counting lock to control whether we should ignore requestLayout calls from children or not. + private int mEatRequestLayout = 0; + + boolean mLayoutRequestEaten; + boolean mLayoutFrozen; + 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; + + /** + * Set to true when an adapter data set changed notification is received. + * 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 #setDataSetChangedAfterLayout() + */ + boolean mDataSetHasChangedAfterLayout = 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; + + 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 generic motion events. + private float mScrollFactor = 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 final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private final int[] mNestedOffsets = 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; + } + }; + + /** + * 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(Context context) { + this(context, null); + } + + public RecyclerView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0); + mClipToPadding = a.getBoolean(0, true); + a.recycle(); + } else { + mClipToPadding = true; + } + setScrollContainer(true); + setFocusableInTouchMode(true); + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); + + mItemAnimator.setListener(mItemAnimatorListener); + initAdapterManager(); + initChildrenHelper(); + // If not explicitly specified this view is important for accessibility. + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + mAccessibilityManager = (AccessibilityManager) getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); + // Create the layoutManager if specified. + + boolean nestedScrollingEnabled = true; + + if (attrs != null) { + int defStyleRes = 0; + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, + defStyle, defStyleRes); + String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager); + int descendantFocusability = a.getInt( + R.styleable.RecyclerView_descendantFocusability, -1); + if (descendantFocusability == -1) { + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + } + a.recycle(); + createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes); + + if (Build.VERSION.SDK_INT >= 21) { + a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, + defStyle, defStyleRes); + nestedScrollingEnabled = a.getBoolean(0, true); + a.recycle(); + } + } else { + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + } + + // Re-set whether nested scrolling is enabled so that it is set on all API levels + setNestedScrollingEnabled(nestedScrollingEnabled); + } + + /** + * Returns the accessibility delegate compatibility implementation used by the RecyclerView. + * @return An instance of AccessibilityDelegateCompat used by RecyclerView + */ + 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( + RecyclerViewAccessibilityDelegate accessibilityDelegate) { + mAccessibilityDelegate = accessibilityDelegate; + setAccessibilityDelegate(mAccessibilityDelegate); + } + + /** + * 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.length() != 0) { // Can't use isEmpty since it was added in API 9. + className = getFullClassName(context, className); + try { + ClassLoader classLoader; + if (isInEditMode()) { + // Stupid layoutlib cannot handle simple class loaders. + classLoader = this.getClass().getClassLoader(); + } else { + classLoader = context.getClassLoader(); + } + Class layoutManagerClass = + classLoader.loadClass(className).asSubclass(LayoutManager.class); + Constructor constructor; + Object[] constructorArgs = null; + try { + constructor = layoutManagerClass + .getConstructor(LAYOUT_MANAGER_CONSTRUCTOR_SIGNATURE); + constructorArgs = new Object[]{context, attrs, defStyleAttr, defStyleRes}; + } catch (NoSuchMethodException e) { + try { + constructor = layoutManagerClass.getConstructor(); + } catch (NoSuchMethodException e1) { + e1.initCause(e); + throw new IllegalStateException(attrs.getPositionDescription() + + ": Error creating LayoutManager " + className, e1); + } + } + constructor.setAccessible(true); + setLayoutManager(constructor.newInstance(constructorArgs)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Unable to find LayoutManager " + className, e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (InstantiationException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Could not instantiate the LayoutManager: " + className, e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Cannot access non-public constructor " + className, e); + } catch (ClassCastException e) { + throw new IllegalStateException(attrs.getPositionDescription() + + ": Class is not a LayoutManager " + className, e); + } + } + } + } + + private String getFullClassName(Context context, String className) { + if (className.charAt(0) == '.') { + return context.getPackageName() + className; + } + if (className.contains(".")) { + return className; + } + return RecyclerView.class.getPackage().getName() + '.' + className; + } + + private void initChildrenHelper() { + mChildHelper = new ChildHelper(new ChildHelper.Callback() { + @Override + public int getChildCount() { + return RecyclerView.this.getChildCount(); + } + + @Override + public void addView(View child, int index) { + RecyclerView.this.addView(child, index); + 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); + } + RecyclerView.this.removeViewAt(index); + } + + @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++) { + dispatchChildDetached(getChildAt(i)); + } + 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); + } + if (DEBUG) { + Log.d(TAG, "reAttach " + vh); + } + vh.clearTmpDetachFlag(); + } + 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); + } + if (DEBUG) { + Log.d(TAG, "tmpDetach " + vh); + } + vh.addFlags(ViewHolder.FLAG_TMP_DETACHED); + } + } + 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 (DEBUG) { + 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(Adapter adapter, boolean removeAndRecycleExistingViews) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, true, removeAndRecycleExistingViews); + setDataSetChangedAfterLayout(); + 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(Adapter adapter) { + // bail out if layout is frozen + setLayoutFrozen(false); + setAdapterInternal(adapter, false, true); + 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(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; + markKnownViewsInvalid(); + } + + /** + * Retrieves the previously set adapter or null if no adapter is set. + * + * @return The previously set adapter + * @see #setAdapter(Adapter) + */ + 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 + */ + public void setRecyclerListener(RecyclerListener listener) { + mRecyclerListener = 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(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(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(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); + } + 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()); + if (mLayout != null && mPendingSavedState.mLayoutState != null) { + mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState); + } + } + + /** + * 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 + * @see #addAnimatingView(RecyclerView.ViewHolder) + * @return true if an animating view is removed + */ + boolean removeAnimatingView(View view) { + eatRequestLayout(); + final boolean removed = mChildHelper.removeViewIfHidden(view); + if (removed) { + final ViewHolder viewHolder = getChildViewHolderInt(view); + mRecycler.unscrapView(viewHolder); + mRecycler.recycleViewHolderInternal(viewHolder); + if (DEBUG) { + Log.d(TAG, "after removing animated view: " + view + ", " + this); + } + } + // only clear request eaten flag if we removed the view. + resumeRequestLayout(!removed); + return removed; + } + + /** + * Return the {@link LayoutManager} currently responsible for + * layout policy for this RecyclerView. + * + * @return The currently bound LayoutManager + */ + 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) + */ + 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 android.support.v4.view.ViewPager ViewPager}. + * + * @param pool Pool to set. If this parameter is null a new pool will be created and used. + */ + public void setRecycledViewPool(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 {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)} + */ + public void setViewCacheExtension(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 (DEBUG) { + 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(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(ItemDecoration decor) { + addItemDecoration(decor, -1); + } + + /** + * 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(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(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(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 or null to clear + */ + public void addOnScrollListener(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(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 com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int)} + * @param position Scroll to this adapter position + * @see com.android.internal.widget.RecyclerView.LayoutManager#scrollToPosition(int) + */ + public void scrollToPosition(int position) { + if (mLayoutFrozen) { + 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; + } + 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 (mLayoutFrozen) { + 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 (mLayoutFrozen) { + return; + } + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + if (canScrollHorizontal || canScrollVertical) { + scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null); + } + } + + /** + * 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) { + Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + Trace.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)) { + Trace.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); + eatRequestLayout(); + onEnterLayoutOrScroll(); + mAdapterHelper.preProcess(); + if (!mLayoutRequestEaten) { + if (hasUpdatedView()) { + dispatchLayout(); + } else { + // no need to layout, clean state + mAdapterHelper.consumePostponedUpdates(); + } + } + resumeRequestLayout(true); + onExitLayoutOrScroll(); + Trace.endSection(); + } else if (mAdapterHelper.hasPendingUpdates()) { + Trace.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + Trace.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. + * + * @return Whether any scroll was consumed in either direction. + */ + boolean scrollByInternal(int x, int y, MotionEvent ev) { + int unconsumedX = 0, unconsumedY = 0; + int consumedX = 0, consumedY = 0; + + consumePendingUpdateOperations(); + if (mAdapter != null) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + Trace.beginSection(TRACE_SCROLL_TAG); + if (x != 0) { + consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); + unconsumedX = x - consumedX; + } + if (y != 0) { + consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); + unconsumedY = y - consumedY; + } + Trace.endSection(); + repositionShadowingViews(); + onExitLayoutOrScroll(); + resumeRequestLayout(false); + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + + if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset)) { + // Update the last touch co-ords, taking any scroll offset into account + mLastTouchX -= mScrollOffset[0]; + mLastTouchY -= mScrollOffset[1]; + if (ev != null) { + ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + } + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (ev != null) { + pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY); + } + considerReleasingGlowsOnScroll(x, y); + } + if (consumedX != 0 || consumedY != 0) { + dispatchOnScrolled(consumedX, consumedY); + } + if (!awakenScrollBars()) { + invalidate(); + } + return consumedX != 0 || consumedY != 0; + } + + /** + *

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 com.android.internal.widget.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 com.android.internal.widget.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; + } + + + void eatRequestLayout() { + mEatRequestLayout++; + if (mEatRequestLayout == 1 && !mLayoutFrozen) { + mLayoutRequestEaten = false; + } + } + + void resumeRequestLayout(boolean performLayoutChildren) { + if (mEatRequestLayout < 1) { + //noinspection PointlessBooleanExpression + if (DEBUG) { + throw new IllegalStateException("invalid eat request layout count"); + } + mEatRequestLayout = 1; + } + if (!performLayoutChildren) { + // 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 + + mLayoutRequestEaten = false; + } + if (mEatRequestLayout == 1) { + // when layout is frozen we should delay dispatchLayout() + if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen + && mLayout != null && mAdapter != null) { + dispatchLayout(); + } + if (!mLayoutFrozen) { + mLayoutRequestEaten = false; + } + } + mEatRequestLayout--; + } + + /** + * 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. + */ + public void setLayoutFrozen(boolean frozen) { + if (frozen != mLayoutFrozen) { + assertNotInLayoutOrScroll("Do not setLayoutFrozen in layout or scroll"); + if (!frozen) { + mLayoutFrozen = false; + if (mLayoutRequestEaten && mLayout != null && mAdapter != null) { + requestLayout(); + } + mLayoutRequestEaten = false; + } else { + final long now = SystemClock.uptimeMillis(); + MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + onTouchEvent(cancelEvent); + mLayoutFrozen = true; + mIgnoreMotionEventTillDown = true; + stopScroll(); + } + } + } + + /** + * Returns true if layout and scroll are frozen. + * + * @return true if layout and scroll are frozen + * @see #setLayoutFrozen(boolean) + */ + public boolean isLayoutFrozen() { + return mLayoutFrozen; + } + + /** + * 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(int dx, 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 is going to use the default interpolator. + */ + public void smoothScrollBy(int dx, int dy, Interpolator interpolator) { + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (mLayoutFrozen) { + return; + } + if (!mLayout.canScrollHorizontally()) { + dx = 0; + } + if (!mLayout.canScrollVertically()) { + dy = 0; + } + if (dx != 0 || dy != 0) { + mViewFlinger.smoothScrollBy(dx, dy, interpolator); + } + } + + /** + * 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 (mLayoutFrozen) { + 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; + } + + if (!dispatchNestedPreFling(velocityX, velocityY)) { + final boolean canScroll = canScrollHorizontal || canScrollVertical; + dispatchNestedFling(velocityX, velocityY, canScroll); + + if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) { + return true; + } + + if (canScroll) { + velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); + velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); + mViewFlinger.fling(velocityX, velocityY); + return true; + } + } + return false; + } + + /** + * 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(); + mLeftGlow.onPull(-overscrollX / getWidth(), 1f - y / getHeight()); + invalidate = true; + } else if (overscrollX > 0) { + ensureRightGlow(); + mRightGlow.onPull(overscrollX / getWidth(), y / getHeight()); + invalidate = true; + } + + if (overscrollY < 0) { + ensureTopGlow(); + mTopGlow.onPull(-overscrollY / getHeight(), x / getWidth()); + invalidate = true; + } else if (overscrollY > 0) { + ensureBottomGlow(); + mBottomGlow.onPull(overscrollY / getHeight(), 1f - x / getWidth()); + invalidate = true; + } + + if (invalidate || overscrollX != 0 || overscrollY != 0) { + postInvalidateOnAnimation(); + } + } + + private void releaseGlows() { + boolean needsInvalidate = false; + if (mLeftGlow != null) { + mLeftGlow.onRelease(); + needsInvalidate = true; + } + if (mTopGlow != null) { + mTopGlow.onRelease(); + needsInvalidate = true; + } + if (mRightGlow != null) { + mRightGlow.onRelease(); + needsInvalidate = true; + } + if (mBottomGlow != null) { + mBottomGlow.onRelease(); + needsInvalidate = true; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + } + + void considerReleasingGlowsOnScroll(int dx, int dy) { + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) { + mLeftGlow.onRelease(); + needsInvalidate = true; + } + if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) { + mRightGlow.onRelease(); + needsInvalidate = true; + } + if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) { + mTopGlow.onRelease(); + needsInvalidate = true; + } + if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) { + mBottomGlow.onRelease(); + needsInvalidate = true; + } + if (needsInvalidate) { + postInvalidateOnAnimation(); + } + } + + void absorbGlows(int velocityX, int velocityY) { + if (velocityX < 0) { + ensureLeftGlow(); + mLeftGlow.onAbsorb(-velocityX); + } else if (velocityX > 0) { + ensureRightGlow(); + mRightGlow.onAbsorb(velocityX); + } + + if (velocityY < 0) { + ensureTopGlow(); + mTopGlow.onAbsorb(-velocityY); + } else if (velocityY > 0) { + ensureBottomGlow(); + mBottomGlow.onAbsorb(velocityY); + } + + if (velocityX != 0 || velocityY != 0) { + postInvalidateOnAnimation(); + } + } + + void ensureLeftGlow() { + if (mLeftGlow != null) { + return; + } + mLeftGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureRightGlow() { + if (mRightGlow != null) { + return; + } + mRightGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureTopGlow() { + if (mTopGlow != null) { + return; + } + mTopGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + + } + + void ensureBottomGlow() { + if (mBottomGlow != null) { + return; + } + mBottomGlow = new EdgeEffect(getContext()); + if (mClipToPadding) { + mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + } + + void invalidateGlows() { + mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null; + } + + /** + * 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() && !mLayoutFrozen; + + 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() == View.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; + } + eatRequestLayout(); + mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + resumeRequestLayout(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; + } + eatRequestLayout(); + result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + resumeRequestLayout(false); + } + } + 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) { + return false; + } + if (focused == null) { + return true; + } + + if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) { + final boolean rtl = mLayout.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + final int absHorizontal = (direction == View.FOCUS_FORWARD) ^ rtl + ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + if (isPreferredNextFocusAbsolute(focused, next, absHorizontal)) { + return true; + } + if (direction == View.FOCUS_FORWARD) { + return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_DOWN); + } else { + return isPreferredNextFocusAbsolute(focused, next, View.FOCUS_UP); + } + } else { + return isPreferredNextFocusAbsolute(focused, next, direction); + } + + } + + /** + * Logic taken from FocusSearch#isCandidate + */ + private boolean isPreferredNextFocusAbsolute(View focused, View next, int direction) { + mTempRect.set(0, 0, focused.getWidth(), focused.getHeight()); + mTempRect2.set(0, 0, next.getWidth(), next.getHeight()); + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetDescendantRectToMyCoords(next, mTempRect2); + switch (direction) { + case View.FOCUS_LEFT: + return (mTempRect.right > mTempRect2.right + || mTempRect.left >= mTempRect2.right) + && mTempRect.left > mTempRect2.left; + case View.FOCUS_RIGHT: + return (mTempRect.left < mTempRect2.left + || mTempRect.right <= mTempRect2.left) + && mTempRect.right < mTempRect2.right; + case View.FOCUS_UP: + return (mTempRect.bottom > mTempRect2.bottom + || mTempRect.top >= mTempRect2.bottom) + && mTempRect.top > mTempRect2.top; + case View.FOCUS_DOWN: + return (mTempRect.top < mTempRect2.top + || mTempRect.bottom <= mTempRect2.top) + && mTempRect.bottom < mTempRect2.bottom; + } + throw new IllegalArgumentException("direction must be absolute. received:" + direction); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) { + mTempRect.set(0, 0, focused.getWidth(), focused.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 = focused.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; + } + } + + offsetDescendantRectToMyCoords(focused, mTempRect); + offsetRectIntoDescendantCoords(child, mTempRect); + requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete); + } + super.requestChildFocus(child, focused); + } + + @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(); + 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 = getDisplay(); + 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(); + + if (ALLOW_THREAD_GAP_WORK) { + // 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"); + } + throw new IllegalStateException(message); + + } + } + + /** + * 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"); + } + 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("")); + } + } + + /** + * 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(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(OnItemTouchListener listener) { + mOnItemTouchListeners.remove(listener); + if (mActiveOnItemTouchListener == listener) { + mActiveOnItemTouchListener = null; + } + } + + private boolean dispatchOnItemTouchIntercept(MotionEvent e) { + final int action = e.getAction(); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) { + mActiveOnItemTouchListener = null; + } + + 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) { + mActiveOnItemTouchListener = listener; + return true; + } + } + return false; + } + + private boolean dispatchOnItemTouch(MotionEvent e) { + final int action = e.getAction(); + if (mActiveOnItemTouchListener != null) { + if (action == MotionEvent.ACTION_DOWN) { + // Stale state from a previous gesture, we're starting a new one. Clear it. + mActiveOnItemTouchListener = null; + } else { + mActiveOnItemTouchListener.onTouchEvent(this, e); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Clean up for the next gesture. + mActiveOnItemTouchListener = null; + } + return true; + } + } + + // Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept + // as called from onInterceptTouchEvent; skip it. + if (action != MotionEvent.ACTION_DOWN) { + final int listenerCount = mOnItemTouchListeners.size(); + for (int i = 0; i < listenerCount; i++) { + final OnItemTouchListener listener = mOnItemTouchListeners.get(i); + if (listener.onInterceptTouchEvent(this, e)) { + mActiveOnItemTouchListener = listener; + return true; + } + } + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent e) { + if (mLayoutFrozen) { + // When layout is frozen, RV does not intercept the motion event. + // A child view e.g. a button may still get the click. + return false; + } + if (dispatchOnItemTouchIntercept(e)) { + cancelTouch(); + 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 (mScrollState == SCROLL_STATE_SETTLING) { + getParent().requestDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } + + // Clear the nested offsets + mNestedOffsets[0] = mNestedOffsets[1] = 0; + + int nestedScrollAxis = View.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); + 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 = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1); + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1); + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + } break; + + case MotionEvent.ACTION_POINTER_UP: { + onPointerUp(e); + } break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.clear(); + stopNestedScroll(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } + } + return mScrollState == SCROLL_STATE_DRAGGING; + } + + @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 (mLayoutFrozen || mIgnoreMotionEventTillDown) { + return false; + } + if (dispatchOnItemTouch(e)) { + cancelTouch(); + 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 MotionEvent vtev = MotionEvent.obtain(e); + final int action = e.getActionMasked(); + final int actionIndex = e.getActionIndex(); + + if (action == MotionEvent.ACTION_DOWN) { + mNestedOffsets[0] = mNestedOffsets[1] = 0; + } + 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 = View.SCROLL_AXIS_NONE; + if (canScrollHorizontally) { + nestedScrollAxis |= View.SCROLL_AXIS_HORIZONTAL; + } + if (canScrollVertically) { + nestedScrollAxis |= View.SCROLL_AXIS_VERTICAL; + } + startNestedScroll(nestedScrollAxis); + } 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 (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + // Updated the nested offsets + mNestedOffsets[0] += mScrollOffset[0]; + mNestedOffsets[1] += mScrollOffset[1]; + } + + if (mScrollState != SCROLL_STATE_DRAGGING) { + boolean startScroll = false; + if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) { + if (dx > 0) { + dx -= mTouchSlop; + } else { + dx += mTouchSlop; + } + startScroll = true; + } + if (canScrollVertically && Math.abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop; + } else { + dy += mTouchSlop; + } + startScroll = true; + } + if (startScroll) { + setScrollState(SCROLL_STATE_DRAGGING); + } + } + + if (mScrollState == SCROLL_STATE_DRAGGING) { + mLastTouchX = x - mScrollOffset[0]; + mLastTouchY = y - mScrollOffset[1]; + + if (scrollByInternal( + canScrollHorizontally ? dx : 0, + canScrollVertically ? dy : 0, + vtev)) { + 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); + } + resetTouch(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } break; + } + + if (!eventAddedToVelocityTracker) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + + return true; + } + + private void resetTouch() { + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + stopNestedScroll(); + releaseGlows(); + } + + private void cancelTouch() { + resetTouch(); + 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 (mLayoutFrozen) { + return false; + } + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + if (event.getAction() == MotionEvent.ACTION_SCROLL) { + final float vScroll, hScroll; + 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; + } + + if (vScroll != 0 || hScroll != 0) { + final float scrollFactor = getScrollFactor(); + scrollByInternal((int) (hScroll * scrollFactor), + (int) (vScroll * scrollFactor), event); + } + } + } + return false; + } + + /** + * Ported from View.getVerticalScrollFactor. + */ + private float getScrollFactor() { + if (mScrollFactor == Float.MIN_VALUE) { + TypedValue outValue = new TypedValue(); + if (getContext().getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + mScrollFactor = outValue.getDimension( + getContext().getResources().getDisplayMetrics()); + } else { + return 0; //listPreferredItemHeight is not defined, no generic scrolling + } + } + return mScrollFactor; + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + if (mLayout == null) { + defaultOnMeasure(widthSpec, heightSpec); + return; + } + if (mLayout.mAutoMeasure) { + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); + final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY + && heightMode == MeasureSpec.EXACTLY; + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + if (skipMeasure || 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); + } + } else { + if (mHasFixedSize) { + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + return; + } + // custom onMeasure + if (mAdapterUpdateDuringMeasure) { + eatRequestLayout(); + 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; + resumeRequestLayout(false); + } + + if (mAdapter != null) { + mState.mItemCount = mAdapter.getItemCount(); + } else { + mState.mItemCount = 0; + } + eatRequestLayout(); + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + resumeRequestLayout(false); + mState.mInPreLayout = false; // clear + } + } + + /** + * Used when onMeasure is called before layout manager is set + */ + 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(), + getMinimumWidth()); + final int height = LayoutManager.chooseSize(heightSpec, + getPaddingTop() + getPaddingBottom(), + getMinimumHeight()); + + 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(ItemAnimator animator) { + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + mItemAnimator.setListener(null); + } + mItemAnimator = animator; + if (mItemAnimator != null) { + mItemAnimator.setListener(mItemAnimatorListener); + } + } + + void onEnterLayoutOrScroll() { + mLayoutOrScrollCounter++; + } + + void onExitLayoutOrScroll() { + mLayoutOrScrollCounter--; + if (mLayoutOrScrollCounter < 1) { + if (DEBUG && mLayoutOrScrollCounter < 0) { + throw new IllegalStateException("layout or scroll counter cannot go below zero." + + "Some calls are not matching"); + } + mLayoutOrScrollCounter = 0; + 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); + event.setContentChangeTypes(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 = event.getContentChangeTypes(); + } + if (type == 0) { + type = AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; + } + mEatenAccessibilityChangeFlags |= type; + return true; + } + return false; + } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + if (shouldDeferAccessibilityEvent(event)) { + return; + } + super.sendAccessibilityEventUnchecked(event); + } + + /** + * 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. + */ + 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) { + postOnAnimation(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(); + 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.e(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 (mState.mLayoutStep == State.STEP_START) { + dispatchLayoutStep1(); + mLayout.setExactMeasureSpecsFrom(this); + dispatchLayoutStep2(); + } else if (mAdapterHelper.hasUpdates() || 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. + 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.getAdapterPosition()); + 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; + } + + /** + * 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); + mState.mIsMeasuring = false; + eatRequestLayout(); + 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(); + resumeRequestLayout(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() { + eatRequestLayout(); + onEnterLayoutOrScroll(); + mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); + mAdapterHelper.consumeUpdatesInOnePass(); + mState.mItemCount = mAdapter.getItemCount(); + mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; + + // Step 2: Run layout + mState.mInPreLayout = false; + mLayout.onLayoutChildren(mRecycler, mState); + + mState.mStructureChanged = false; + mPendingSavedState = null; + + // onLayoutChildren may have caused client code to disable item animations; re-check + mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null; + mState.mLayoutStep = State.STEP_ANIMATIONS; + onExitLayoutOrScroll(); + resumeRequestLayout(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); + eatRequestLayout(); + 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; + 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(); + resumeRequestLayout(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); + } 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); + } + } + } + // 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); + } + + /** + * 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); + } + } + 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) { + Trace.beginSection(TRACE_ON_LAYOUT_TAG); + dispatchLayout(); + Trace.endSection(); + mFirstLayoutComplete = true; + } + + @Override + public void requestLayout() { + if (mEatRequestLayout == 0 && !mLayoutFrozen) { + super.requestLayout(); + } else { + mLayoutRequestEaten = 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) { + postInvalidateOnAnimation(); + } + } + + @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"); + } + return mLayout.generateDefaultLayoutParams(); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + return mLayout.generateLayoutParams(getContext(), attrs); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + 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 (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) { + throw new IllegalStateException("view holder cannot have position -1 unless it" + + " is removed"); + } + 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 (DEBUG) { + 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 (DEBUG) { + 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 (DEBUG) { + 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 (DEBUG) { + 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()); + } + + + /** + * Call this method to signal that *all* adapter content has changed (generally, because of + * swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should + * be discarded or animated. Note that this work is deferred because RecyclerView requires a + * layout to resolve non-incremental changes to the data set. + * + * Attached items are labeled as position unknown, and may no longer be cached. + * + * It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true, + * so calling this method *must* be associated with marking the cache invalid, so that the + * only valid items that remain in the cache, once layout occurs, are prefetched items. + */ + void setDataSetChangedAfterLayout() { + if (mDataSetHasChangedAfterLayout) { + return; + } + mDataSetHasChangedAfterLayout = true; + 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_ADAPTER_POSITION_UNKNOWN); + } + } + mRecycler.setAdapterPositionsAsUnknown(); + + // immediately mark all views as invalid, so prefetched views can be + // differentiated from views bound to previous data set - both in children, and cache + 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(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(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(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(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(View child) { + final ViewHolder holder = getChildViewHolderInt(child); + return holder != null ? holder.getAdapterPosition() : 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(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(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 + 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#getAdapterPosition()} to get the current adapter position of a 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 + */ + 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 + */ + 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() + && getAdapterPositionFor(holder) == position) { + if (mChildHelper.isHidden(holder.itemView)) { + hidden = holder; + } else { + return holder; + } + } + } + return hidden; + } + + 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 + */ + 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(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(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(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(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(View view, 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(int dx, int dy) { + // Do nothing + } + + void dispatchOnScrolled(int hresult, int vresult) { + mDispatchScrollCounter++; + // Pass the current scrollX/scrollY values; no actual change in these properties occurred + // but some general-purpose code may choose to respond to changes this way. + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + onScrollChanged(scrollX, scrollY, scrollX, scrollY); + + // 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 + } + + 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(); + } + + class ViewFlinger implements Runnable { + private int mLastFlingX; + private int mLastFlingY; + private OverScroller mScroller; + 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() { + mScroller = new OverScroller(getContext(), sQuinticInterpolator); + } + + @Override + public void run() { + if (mLayout == null) { + stop(); + return; // no layout, cannot scroll. + } + disableRunOnAnimationRequests(); + consumePendingUpdateOperations(); + // keep a local reference so that if it is changed during onAnimation method, it won't + // cause unexpected behaviors + final OverScroller scroller = mScroller; + final SmoothScroller smoothScroller = mLayout.mSmoothScroller; + if (scroller.computeScrollOffset()) { + final int x = scroller.getCurrX(); + final int y = scroller.getCurrY(); + final int dx = x - mLastFlingX; + final int dy = y - mLastFlingY; + int hresult = 0; + int vresult = 0; + mLastFlingX = x; + mLastFlingY = y; + int overscrollX = 0, overscrollY = 0; + if (mAdapter != null) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + Trace.beginSection(TRACE_SCROLL_TAG); + if (dx != 0) { + hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); + overscrollX = dx - hresult; + } + if (dy != 0) { + vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState); + overscrollY = dy - vresult; + } + Trace.endSection(); + repositionShadowingViews(); + + onExitLayoutOrScroll(); + resumeRequestLayout(false); + + 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(dx - overscrollX, dy - overscrollY); + } else { + smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY); + } + } + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + considerReleasingGlowsOnScroll(dx, dy); + } + if (overscrollX != 0 || overscrollY != 0) { + final int vel = (int) scroller.getCurrVelocity(); + + int velX = 0; + if (overscrollX != x) { + velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0; + } + + int velY = 0; + if (overscrollY != y) { + velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0; + } + + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + absorbGlows(velX, velY); + } + if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0) + && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) { + scroller.abortAnimation(); + } + } + if (hresult != 0 || vresult != 0) { + dispatchOnScrolled(hresult, vresult); + } + + if (!awakenScrollBars()) { + invalidate(); + } + + final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically() + && vresult == dy; + final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally() + && hresult == dx; + final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal + || fullyConsumedVertical; + + if (scroller.isFinished() || !fullyConsumedAny) { + setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this. + if (ALLOW_THREAD_GAP_WORK) { + mPrefetchRegistry.clearPrefetchPositions(); + } + } else { + postOnAnimation(); + if (mGapWorker != null) { + mGapWorker.postFromTraversal(RecyclerView.this, dx, dy); + } + } + } + // call this after the onAnimation is complete not to have inconsistent callbacks etc. + if (smoothScroller != null) { + if (smoothScroller.isPendingInitialRun()) { + smoothScroller.onAnimation(0, 0); + } + if (!mReSchedulePostAnimationCallback) { + smoothScroller.stop(); //stop if it does not trigger any scroll + } + } + enableRunOnAnimationRequests(); + } + + private void disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false; + mEatRunOnAnimationRequest = true; + } + + private void enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false; + if (mReSchedulePostAnimationCallback) { + postOnAnimation(); + } + } + + void postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true; + } else { + removeCallbacks(this); + RecyclerView.this.postOnAnimation(this); + } + } + + public void fling(int velocityX, int velocityY) { + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + mScroller.fling(0, 0, velocityX, velocityY, + Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE); + postOnAnimation(); + } + + public void smoothScrollBy(int dx, int dy) { + smoothScrollBy(dx, dy, 0, 0); + } + + public void smoothScrollBy(int dx, int dy, int vx, int vy) { + smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy)); + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + private int computeScrollDuration(int dx, int dy, int vx, int vy) { + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final boolean horizontal = absDx > absDy; + final int velocity = (int) Math.sqrt(vx * vx + vy * vy); + final int delta = (int) Math.sqrt(dx * dx + dy * dy); + final int containerSize = horizontal ? getWidth() : getHeight(); + final int halfContainerSize = containerSize / 2; + final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize); + final float distance = halfContainerSize + halfContainerSize + * distanceInfluenceForSnapDuration(distanceRatio); + + final int duration; + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + float absDelta = (float) (horizontal ? absDx : absDy); + duration = (int) (((absDelta / containerSize) + 1) * 300); + } + return Math.min(duration, MAX_SCROLL_DURATION); + } + + public void smoothScrollBy(int dx, int dy, int duration) { + smoothScrollBy(dx, dy, duration, sQuinticInterpolator); + } + + public void smoothScrollBy(int dx, int dy, Interpolator interpolator) { + smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, 0, 0), + interpolator == null ? sQuinticInterpolator : interpolator); + } + + public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) { + if (mInterpolator != interpolator) { + mInterpolator = interpolator; + mScroller = new OverScroller(getContext(), interpolator); + } + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + mScroller.startScroll(0, 0, dx, dy, duration); + postOnAnimation(); + } + + public void stop() { + removeCallbacks(this); + mScroller.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; + + setDataSetChangedAfterLayout(); + 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) { + RecyclerView.this.postOnAnimation(mUpdateChildViewsRunnable); + } else { + mAdapterUpdateDuringMeasure = true; + requestLayout(); + } + } + } + + /** + * 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 { + ArrayList mScrapHeap = new ArrayList<>(); + int mMaxScrap = DEFAULT_MAX_SCRAP; + long mCreateRunningAverageNs = 0; + long mBindRunningAverageNs = 0; + } + SparseArray mScrap = new SparseArray<>(); + + private int mAttachCount = 0; + + public void clear() { + for (int i = 0; i < mScrap.size(); i++) { + ScrapData data = mScrap.valueAt(i); + data.mScrapHeap.clear(); + } + } + + public void setMaxRecycledViews(int viewType, int max) { + ScrapData scrapData = getScrapDataForType(viewType); + scrapData.mMaxScrap = max; + final ArrayList scrapHeap = scrapData.mScrapHeap; + if (scrapHeap != null) { + 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(); + } + + public ViewHolder getRecycledView(int viewType) { + final ScrapData scrapData = mScrap.get(viewType); + if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { + final ArrayList scrapHeap = scrapData.mScrapHeap; + return scrapHeap.remove(scrapHeap.size() - 1); + } + return null; + } + + 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; + } + + public void putRecycledView(ViewHolder scrap) { + final int viewType = scrap.getItemViewType(); + final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap; + if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { + return; + } + if (DEBUG && 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(Adapter adapter) { + mAttachCount++; + } + + void detach() { + mAttachCount--; + } + + + /** + * 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 && mAttachCount == 0) { + clear(); + } + if (newAdapter != null) { + attach(newAdapter); + } + } + + 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. + */ + 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 (DEBUG && !mState.isPreLayout()) { + throw new IllegalStateException("should not receive a removed view unless it" + + " is pre layout"); + } + return mState.isPreLayout(); + } + if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { + throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + + "adapter position" + holder); + } + 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. + * @return + */ + private boolean tryBindViewHolderByDeadline(ViewHolder holder, int offsetPosition, + int position, long deadlineNs) { + 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; + } + mAdapter.bindViewHolder(holder, offsetPosition); + long endBindNs = getNanoTime(); + mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs); + attachAccessibilityDelegate(holder.itemView); + 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(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"); + } + 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()); + } + 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()); + } + 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 + */ + 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()); + } + 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()); + } + + 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"); + } else if (holder.shouldIgnore()) { + throw new IllegalArgumentException("getViewForPositionAndType returned" + + " a view that is ignored. You must call stopIgnoring before" + + " returning this view."); + } + } + } + if (holder == null) { // fallback to pool + if (DEBUG) { + 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 (DEBUG) { + 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 (DEBUG && holder.isRemoved()) { + throw new IllegalStateException("Removed holder should be bound and it should" + + " come here only in pre-layout. Holder: " + holder); + } + 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 attachAccessibilityDelegate(View itemView) { + if (isAccessibilityEnabled()) { + if (itemView.getImportantForAccessibility() + == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + itemView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + if (itemView.getAccessibilityDelegate() == null) { + itemView.setAccessibilityDelegate(mAccessibilityDelegate.getItemDelegate()); + } + } + } + + 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(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); + } + + /** + * Internally, use this method instead of {@link #recycleView(android.view.View)} to + * catch potential bugs. + * @param view + */ + void recycleViewInternal(View view) { + recycleViewHolderInternal(getChildViewHolderInt(view)); + } + + 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 (DEBUG) { + Log.d(TAG, "Recycling cached view at index " + cachedViewIndex); + } + ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); + if (DEBUG) { + 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)); + } + + if (holder.isTmpDetached()) { + throw new IllegalArgumentException("Tmp detached view should be removed " + + "from RecyclerView before it can be recycled: " + holder); + } + + if (holder.shouldIgnore()) { + throw new IllegalArgumentException("Trying to recycle an ignored view holder. You" + + " should first call stopIgnoringView(view) before calling recycle."); + } + //noinspection unchecked + final boolean transientStatePreventsRecycling = holder + .doesTransientStatePreventRecycling(); + final boolean forceRecycle = mAdapter != null + && transientStatePreventsRecycling + && mAdapter.onFailedToRecycleView(holder); + boolean cached = false; + boolean recycled = false; + if (DEBUG && mCachedViews.contains(holder)) { + throw new IllegalArgumentException("cached view received recycle internal? " + + holder); + } + 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 (DEBUG) { + Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will " + + "re-visit here. We are still removing it from animation lists"); + } + } + // 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) { + 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(ViewHolder holder, boolean dispatchRecycled) { + clearNestedRecyclerViewIfNotNested(holder); + holder.itemView.setAccessibilityDelegate(null); + if (dispatchRecycled) { + dispatchViewRecycled(holder); + } + 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."); + } + 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); + } + 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) { + if (!dryRun) { + mCachedViews.remove(i); + } + if (DEBUG) { + 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) { + if (type == holder.getItemViewType()) { + if (!dryRun) { + mCachedViews.remove(i); + } + return holder; + } else if (!dryRun) { + recycleCachedViewAt(i); + return null; + } + } + } + return null; + } + + void dispatchViewRecycled(ViewHolder holder) { + if (mRecyclerListener != null) { + mRecyclerListener.onViewRecycled(holder); + } + if (mAdapter != null) { + mAdapter.onViewRecycled(holder); + } + if (mState != null) { + mViewInfoStore.removeViewHolder(holder); + } + if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder); + } + + void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter, + boolean compatibleWithPrevious) { + clear(); + getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter, compatibleWithPrevious); + } + + 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 (DEBUG) { + 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 (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " + + holder + " now at position " + (holder.mPosition + count)); + } + holder.offsetPosition(count, true); + } + } + } + + /** + * @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 (DEBUG) { + 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) { + if (mRecyclerPool != null) { + mRecyclerPool.detach(); + } + mRecyclerPool = pool; + if (pool != null) { + mRecyclerPool.attach(getAdapter()); + } + } + + RecycledViewPool getRecycledViewPool() { + if (mRecyclerPool == null) { + mRecyclerPool = new RecycledViewPool(); + } + 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.getLayoutPosition(); + 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 setAdapterPositionsAsUnknown() { + 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_ADAPTER_POSITION_UNKNOWN); + } + } + } + + void markKnownViewsInvalid() { + if (mAdapter != null && mAdapter.hasStableIds()) { + 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); + } + } + } else { + // 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) + */ + public abstract View getViewForPositionAndType(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; + + /** + * 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) + */ + public abstract VH onCreateViewHolder(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#getAdapterPosition()} 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(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#getAdapterPosition()} 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(VH holder, int position, List payloads) { + onBindViewHolder(holder, 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) + */ + public final VH createViewHolder(ViewGroup parent, int viewType) { + Trace.beginSection(TRACE_CREATE_VIEW_TAG); + final VH holder = onCreateViewHolder(parent, viewType); + holder.mItemViewType = viewType; + Trace.endSection(); + return holder; + } + + /** + * 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. + * + * @see #onBindViewHolder(ViewHolder, int) + */ + public final void bindViewHolder(VH holder, int position) { + 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); + Trace.beginSection(TRACE_BIND_VIEW_TAG); + onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); + holder.clearPayload(); + final ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); + if (layoutParams instanceof RecyclerView.LayoutParams) { + ((LayoutParams) layoutParams).mInsetsDirty = true; + } + Trace.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#getAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder for the view being recycled + */ + public void onViewRecycled(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(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(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(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 com.android.internal.widget.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(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(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(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(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, 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, 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); + } + } + + 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); + } + } + } + + 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; + + @Nullable + SmoothScroller mSmoothScroller; + + boolean mRequestedSimpleAnimations = false; + + boolean mIsAttachedToWindow = false; + + 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 + * called after the RecyclerView calls + * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a measurement pass. + *

+ * This method should 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. + *

+ * This method is only called if the LayoutManager opted into the auto measurement API. + * + * @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 #setAutoMeasureEnabled(boolean) + */ + 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) + */ + public void assertNotInLayoutOrScroll(String message) { + if (mRecyclerView != null) { + mRecyclerView.assertNotInLayoutOrScroll(message); + } + } + + /** + * Defines whether the layout should be measured by the RecyclerView or the LayoutManager + * wants to handle the layout measurements itself. + *

+ * This method is usually called by the LayoutManager with value {@code true} if it wants + * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize + * the measurement logic, you can call this method with {@code false} and override + * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic. + *

+ * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or + * handle various specs provided by the RecyclerView's parent. + * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an + * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based + * on children's positions. It does this while supporting all existing animation + * capabilities of the RecyclerView. + *

+ * AutoMeasure works as follows: + *

    + *
  1. LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of + * the framework LayoutManagers use {@code auto-measure}.
  2. + *
  3. When {@link RecyclerView#onMeasure(int, int)} is called, if the provided specs are + * exact, RecyclerView will only call LayoutManager's {@code onMeasure} and return without + * doing any layout calculation.
  4. + *
  5. If one of the layout specs is not {@code EXACT}, the RecyclerView will start the + * layout process in {@code onMeasure} call. It will process all pending Adapter updates and + * decide whether to run a predictive layout or not. 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()}.

  6. + *
  7. 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.
  8. + *
  9. Any following on measure call to the RecyclerView 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. + *
  10. + *
  11. 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.
  12. + *
  13. Finally, animations are calculated and run as usual.
  14. + *
+ * + * @param enabled True if the Layout should be measured by the + * RecyclerView, false if the LayoutManager wants + * to measure itself. + * + * @see #setMeasuredDimension(Rect, int, int) + * @see #isAutoMeasureEnabled() + */ + public void setAutoMeasureEnabled(boolean enabled) { + mAutoMeasure = enabled; + } + + /** + * Returns whether the LayoutManager uses the automatic measurement API or not. + * + * @return True if the LayoutManager is measured by the RecyclerView or + * false if it measures itself. + * + * @see #setAutoMeasureEnabled(boolean) + */ + public boolean isAutoMeasureEnabled() { + return mAutoMeasure; + } + + /** + * Returns whether this LayoutManager supports automatic item animations. + * A LayoutManager wishing to support item animations should obey certain + * rules as outlined in {@link #onLayoutChildren(Recycler, State)}. + * The default return value is false, so subclasses of LayoutManager + * will not get predictive item animations by default. + * + *

Whether item animations are enabled in a RecyclerView is determined both + * by 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 simple item animations will be enabled, in which + * views that are moving onto or off of the screen are simply faded in/out. If + * the RecyclerView has a non-null ItemAnimator and this method returns true, + * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to + * setup up the information needed to more intelligently predict where appearing + * and disappearing views should be animated from/to.

+ * + * @return true if predictive item animations should be enabled, 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. + * + * @see #setItemPrefetchEnabled(boolean) + * + * @return true if item prefetch is enabled, false otherwise + */ + 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) + */ + 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) + */ + 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) { + mRecyclerView.postOnAnimation(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 + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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. + */ + 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. + */ + 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 (DEBUG) { + 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. + */ + 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 SmoothScroller.

+ *

Calling this method will cancel any previous smooth scroll request.

+ * @param smoothScroller Instance which defines how smooth scroll should be animated + */ + public void startSmoothScroll(SmoothScroller smoothScroller) { + if (mSmoothScroller != null && smoothScroller != mSmoothScroller + && mSmoothScroller.isRunning()) { + mSmoothScroller.stop(); + } + mSmoothScroller = smoothScroller; + mSmoothScroller.start(mRecyclerView, this); + } + + /** + * @return true if RecycylerView 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 android.view.View#LAYOUT_DIRECTION_RTL} if the layout + * direction is RTL or returns + * {@link android.view.View#LAYOUT_DIRECTION_LTR} if the layout direction + * is not RTL. + */ + public int getLayoutDirection() { + return mRecyclerView.getLayoutDirection(); + } + + /** + * 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() + */ + 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. + */ + 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. + */ + 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 + */ + 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 + */ + 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) { + child.dispatchFinishTemporaryDetach(); + } + } 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)); + } + 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 (DEBUG) { + 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 + */ + 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(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(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(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 + */ + 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(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, View view) { + if (DISPATCH_TEMP_DETACH) { + view.dispatchStartTemporaryDetach(); + } + 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(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) { + child.dispatchFinishTemporaryDetach(); + } + } + + /** + * 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(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(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(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); + } + 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(View child, 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, 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(View child, 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, 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 + */ + public View getChildAt(int index) { + return mChildHelper != null ? mChildHelper.getChildAt(index) : null; + } + + /** + * Return the width measurement spec mode of the RecyclerView. + *

+ * This value is set only if the LayoutManager opts into the auto measure 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) + * @see View#onMeasure(int, int) + */ + public int getWidthMode() { + return mWidthMode; + } + + /** + * Return the height measurement spec mode of the RecyclerView. + *

+ * This value is set only if the LayoutManager opts into the auto measure 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) + * @see View#onMeasure(int, int) + */ + public int getHeightMode() { + return mHeightMode; + } + + /** + * Return the width of the parent RecyclerView + * + * @return Width in pixels + */ + public int getWidth() { + return mWidth; + } + + /** + * Return the height of the parent RecyclerView + * + * @return Height in pixels + */ + public int getHeight() { + return mHeight; + } + + /** + * Return the left padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingLeft() { + return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0; + } + + /** + * Return the top padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingTop() { + return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0; + } + + /** + * Return the right padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingRight() { + return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0; + } + + /** + * Return the bottom padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingBottom() { + return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0; + } + + /** + * Return the start padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingStart() { + return mRecyclerView != null ? mRecyclerView.getPaddingStart() : 0; + } + + /** + * Return the end padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingEnd() { + return mRecyclerView != null ? mRecyclerView.getPaddingEnd() : 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. + */ + 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(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(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(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"); + } + 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(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(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 (DEBUG) { + 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 recycleable 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(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(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(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(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(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(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(View child, boolean includeDecorInsets, 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(View view, 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(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(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(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(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(View child, 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(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(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(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(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. 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 + */ + @Nullable + public View onFocusSearchFailed(View focused, int direction, Recycler recycler, + 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. + */ + public View onInterceptFocusSearch(View focused, int direction) { + return null; + } + + /** + * 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(RecyclerView parent, View child, Rect rect, + boolean immediate) { + 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() == View.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); + + if (dx != 0 || dy != 0) { + if (immediate) { + parent.scrollBy(dx, dy); + } else { + parent.smoothScrollBy(dx, dy); + } + return true; + } + return false; + } + + /** + * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)} + */ + @Deprecated + public boolean onRequestChildFocus(RecyclerView parent, View child, 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(RecyclerView parent, State state, View child, + View focused) { + return onRequestChildFocus(parent, child, focused); + } + + /** + * Called if the RecyclerView this LayoutManager is bound to has a different adapter set. + * 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 #setAdapter(RecyclerView.Adapter)} is called with {@code null}. + */ + public void onAdapterChanged(Adapter oldAdapter, 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(RecyclerView recyclerView, ArrayList views, + int direction, int focusableMode) { + return false; + } + + /** + * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving + * detailed information on what has actually changed. + * + * @param recyclerView + */ + public void onItemsChanged(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.) + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) { + } + + /** + * Called when items have been removed from the adapter. + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsRemoved(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. + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsUpdated(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)}. + * + * @param recyclerView + * @param positionStart + * @param itemCount + * @param payload + */ + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, + 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. + * + * @param recyclerView + * @param from + * @param to + * @param itemCount + */ + public void onItemsMoved(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(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(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(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(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(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(State state) { + return 0; + } + + /** + * Measure the attached RecyclerView. Implementations must call + * {@link #setMeasuredDimension(int, int)} before returning. + * + *

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} + */ + public void onMeasure(Recycler recycler, 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()} + */ + public int getMinimumWidth() { + return mRecyclerView.getMinimumWidth(); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumHeight()} + */ + public int getMinimumHeight() { + return mRecyclerView.getMinimumHeight(); + } + /** + *

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 + */ + public Parcelable onSaveInstanceState() { + return null; + } + + + public void onRestoreInstanceState(Parcelable state) { + + } + + void stopSmoothScroller() { + if (mSmoothScroller != null) { + mSmoothScroller.stop(); + } + } + + private 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(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(AccessibilityNodeInfo 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 + * android.view.accessibility.AccessibilityNodeInfo.CollectionInfo}. + *

+ * 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(Recycler recycler, State state, + AccessibilityNodeInfo info) { + if (mRecyclerView.canScrollVertically(-1) + || mRecyclerView.canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); + } + if (mRecyclerView.canScrollVertically(1) + || mRecyclerView.canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.setScrollable(true); + } + final AccessibilityNodeInfo.CollectionInfo collectionInfo = + AccessibilityNodeInfo.CollectionInfo + .obtain(getRowCountForAccessibility(recycler, state), + getColumnCountForAccessibility(recycler, state), + isLayoutHierarchical(recycler, state), + getSelectionModeForAccessibility(recycler, state)); + info.setCollectionInfo(collectionInfo); + } + + // called by accessibility delegate + public void onInitializeAccessibilityEvent(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(Recycler recycler, State state, + 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, AccessibilityNodeInfo 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(Recycler recycler, State state, + View host, AccessibilityNodeInfo info) { + int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0; + int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0; + final AccessibilityNodeInfo.CollectionItemInfo itemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndexGuess, 1, + columnIndexGuess, 1, false, false); + info.setCollectionItemInfo(itemInfo); + } + + /** + * 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 AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}, + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_SINGLE} or + * {@link AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_MULTIPLE}. + *

+ * Default implementation returns + * {@link AccessibilityNodeInfo.CollectionInfo#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 AccessibilityNodeInfo.CollectionInfo#SELECTION_MODE_NONE}. + */ + public int getSelectionModeForAccessibility(Recycler recycler, State state) { + return AccessibilityNodeInfo.CollectionInfo.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(Recycler recycler, State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollVertically() ? mRecyclerView.mAdapter.getItemCount() : 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(Recycler recycler, State state) { + if (mRecyclerView == null || mRecyclerView.mAdapter == null) { + return 1; + } + return canScrollHorizontally() ? mRecyclerView.mAdapter.getItemCount() : 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(Recycler recycler, State state) { + return false; + } + + // called by accessibility delegate + boolean performAccessibilityAction(int action, 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(Recycler recycler, State state, int action, + Bundle args) { + if (mRecyclerView == null) { + return false; + } + int vScroll = 0, hScroll = 0; + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + if (mRecyclerView.canScrollVertically(-1)) { + vScroll = -(getHeight() - getPaddingTop() - getPaddingBottom()); + } + if (mRecyclerView.canScrollHorizontally(-1)) { + hScroll = -(getWidth() - getPaddingLeft() - getPaddingRight()); + } + break; + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + if (mRecyclerView.canScrollVertically(1)) { + vScroll = getHeight() - getPaddingTop() - getPaddingBottom(); + } + if (mRecyclerView.canScrollHorizontally(1)) { + hScroll = getWidth() - getPaddingLeft() - getPaddingRight(); + } + break; + } + if (vScroll == 0 && hScroll == 0) { + return false; + } + mRecyclerView.scrollBy(hScroll, vScroll); + return true; + } + + // called by accessibility delegate + boolean performAccessibilityActionForItem(View view, int action, 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(Recycler recycler, State state, View view, + int action, Bundle args) { + return false; + } + + /** + * Parse the xml attributes to get the most common properties used by layout managers. + * + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout + * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd + * + * @return an object containing the properties as specified in the attrs. + */ + public static Properties getProperties(Context context, 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_orientation, VERTICAL); + 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 { + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation */ + public int orientation; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_spanCount */ + public int spanCount; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout */ + public boolean reverseLayout; + /** @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_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(Canvas c, RecyclerView parent, State state) { + onDraw(c, parent); + } + + /** + * @deprecated + * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDraw(Canvas c, 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(Canvas c, RecyclerView parent, State state) { + onDrawOver(c, parent); + } + + /** + * @deprecated + * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)} + */ + @Deprecated + public void onDrawOver(Canvas c, RecyclerView parent) { + } + + + /** + * @deprecated + * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)} + */ + @Deprecated + public void getItemOffsets(Rect outRect, int itemPosition, 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(Rect outRect, View view, RecyclerView parent, 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(RecyclerView rv, 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(RecyclerView rv, 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(RecyclerView rv, MotionEvent e) { + return false; + } + + @Override + public void onTouchEvent(RecyclerView rv, 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(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(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#getAdapterPosition()} to get + * its adapter position. + * + * @param holder The ViewHolder containing the view that was recycled + */ + void onViewRecycled(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(View view); + + /** + * Called when a view is detached from RecyclerView. + * + * @param view The View which is being detached from the RecyclerView + */ + void onChildViewDetachedFromWindow(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 { + 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; + + private int mFlags; + + private static final List FULLUPDATE_PAYLOADS = Collections.EMPTY_LIST; + + 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. + private Recycler mScrapContainer = null; + // Keeps whether this ViewHolder lives in Change scrap or Attached scrap + private 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 = + View.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; + + public ViewHolder(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; + } + + /** + * @deprecated This method is deprecated because its meaning is ambiguous due to the async + * handling of adapter updates. Please use {@link #getLayoutPosition()} or + * {@link #getAdapterPosition()} depending on your use case. + * + * @see #getLayoutPosition() + * @see #getAdapterPosition() + */ + @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 #getAdapterPosition()} or + * {@link RecyclerView.Recycler#convertPreLayoutPositionToPostLayout(int)}. + * + * @return Returns the adapter position of the ViewHolder in the latest layout pass. + * @see #getAdapterPosition() + */ + public final int getLayoutPosition() { + return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition; + } + + /** + * Returns the Adapter position of the item represented by this ViewHolder. + *

+ * 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}. + * + * @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. + */ + public final int getAdapterPosition() { + if (mOwnerRecyclerView == null) { + return NO_POSITION; + } + return mOwnerRecyclerView.getAdapterPositionFor(this); + } + + /** + * 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 isAdapterPositionUnknown() { + return (mFlags & FLAG_ADAPTER_POSITION_UNKNOWN) != 0 || isInvalid(); + } + + void setFlags(int flags, int mask) { + mFlags = (mFlags & ~mask) | (flags & mask); + } + + void addFlags(int flags) { + mFlags |= flags; + } + + void addChangePayload(Object payload) { + if (payload == null) { + addFlags(FLAG_ADAPTER_FULLUPDATE); + } else if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + createPayloadsIfNeeded(); + mPayloads.add(payload); + } + } + + private void createPayloadsIfNeeded() { + if (mPayloads == null) { + mPayloads = new ArrayList(); + mUnmodifiedPayloads = Collections.unmodifiableList(mPayloads); + } + } + + void clearPayload() { + if (mPayloads != null) { + mPayloads.clear(); + } + mFlags = mFlags & ~FLAG_ADAPTER_FULLUPDATE; + } + + List getUnmodifiedPayloads() { + if ((mFlags & FLAG_ADAPTER_FULLUPDATE) == 0) { + if (mPayloads == null || mPayloads.size() == 0) { + // Initial state, no update being called. + return FULLUPDATE_PAYLOADS; + } + // there are none-null payloads + return mUnmodifiedPayloads; + } else { + // a full update has been called. + return FULLUPDATE_PAYLOADS; + } + } + + void resetInternal() { + mFlags = 0; + mPosition = NO_POSITION; + mOldPosition = NO_POSITION; + mItemId = NO_ID; + mPreLayoutPosition = NO_POSITION; + mIsRecyclableCount = 0; + mShadowedHolder = null; + mShadowingHolder = null; + clearPayload(); + mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET; + clearNestedRecyclerViewIfNotNested(this); + } + + /** + * Called when the child view enters the hidden state + */ + private void onEnteredHiddenState(RecyclerView parent) { + // While the view item is in hidden state, make it invisible for the accessibility. + mWasImportantForAccessibilityBeforeHidden = + itemView.getImportantForAccessibility(); + parent.setChildImportantForAccessibilityInternal(this, + View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + + /** + * Called when the child view leaves the hidden state + */ + private void onLeftHiddenState(RecyclerView parent) { + parent.setChildImportantForAccessibilityInternal(this, + mWasImportantForAccessibilityBeforeHidden); + mWasImportantForAccessibilityBeforeHidden = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ViewHolder{" + + Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId + + ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition); + if (isScrap()) { + sb.append(" scrap ") + .append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); + } + if (isInvalid()) sb.append(" invalid"); + if (!isBound()) sb.append(" unbound"); + if (needsUpdate()) sb.append(" update"); + if (isRemoved()) sb.append(" removed"); + if (shouldIgnore()) sb.append(" ignored"); + if (isTmpDetached()) sb.append(" tmpDetached"); + if (!isRecyclable()) sb.append(" not recyclable(" + mIsRecyclableCount + ")"); + if (isAdapterPositionUnknown()) sb.append(" undefined adapter position"); + + if (itemView.getParent() == null) sb.append(" no parent"); + sb.append("}"); + return sb.toString(); + } + + /** + * Informs the recycler whether this item can be recycled. Views which are not + * recyclable will not be reused for other items until setIsRecyclable() is + * later set to true. Calls to setIsRecyclable() should always be paired (one + * call to setIsRecyclabe(false) should always be matched with a later call to + * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally + * reference-counted. + * + * @param recyclable Whether this item is available to be recycled. Default value + * is true. + * + * @see #isRecyclable() + */ + public final void setIsRecyclable(boolean recyclable) { + mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1; + if (mIsRecyclableCount < 0) { + mIsRecyclableCount = 0; + if (DEBUG) { + throw new RuntimeException("isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } + Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " + + "unmatched pair of setIsRecyable() calls for " + this); + } else if (!recyclable && mIsRecyclableCount == 1) { + mFlags |= FLAG_NOT_RECYCLABLE; + } else if (recyclable && mIsRecyclableCount == 0) { + mFlags &= ~FLAG_NOT_RECYCLABLE; + } + if (DEBUG) { + Log.d(TAG, "setIsRecyclable val:" + recyclable + ":" + this); + } + } + + /** + * @return true if this item is available to be recycled, false otherwise. + * + * @see #setIsRecyclable(boolean) + */ + public final boolean isRecyclable() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 + && !itemView.hasTransientState(); + } + + /** + * Returns whether we have animations referring to this view holder or not. + * This is similar to isRecyclable flag but does not check transient state. + */ + private boolean shouldBeKeptAsChild() { + return (mFlags & FLAG_NOT_RECYCLABLE) != 0; + } + + /** + * @return True if ViewHolder is not referenced by RecyclerView animations but has + * transient state which will prevent it from being recycled. + */ + private boolean doesTransientStatePreventRecycling() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && itemView.hasTransientState(); + } + + boolean isUpdated() { + return (mFlags & FLAG_UPDATE) != 0; + } + } + + /** + * This method is here so that we can control the important for a11y changes and test it. + */ + @VisibleForTesting + boolean setChildImportantForAccessibilityInternal(ViewHolder viewHolder, + int importantForAccessibility) { + if (isComputingLayout()) { + viewHolder.mPendingAccessibilityState = importantForAccessibility; + mPendingAccessibilityImportanceChange.add(viewHolder); + return false; + } + viewHolder.itemView.setImportantForAccessibility(importantForAccessibility); + return true; + } + + void dispatchPendingImportantForAccessibilityChanges() { + for (int i = mPendingAccessibilityImportanceChange.size() - 1; i >= 0; i--) { + ViewHolder viewHolder = mPendingAccessibilityImportanceChange.get(i); + if (viewHolder.itemView.getParent() != this || viewHolder.shouldIgnore()) { + continue; + } + int state = viewHolder.mPendingAccessibilityState; + if (state != ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET) { + //noinspection WrongConstant + viewHolder.itemView.setImportantForAccessibility(state); + viewHolder.mPendingAccessibilityState = + ViewHolder.PENDING_ACCESSIBILITY_STATE_NOT_SET; + } + } + mPendingAccessibilityImportanceChange.clear(); + } + + int getAdapterPositionFor(ViewHolder viewHolder) { + if (viewHolder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID + | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN) + || !viewHolder.isBound()) { + return RecyclerView.NO_POSITION; + } + return mAdapterHelper.applyPendingUpdatesToPosition(viewHolder.mPosition); + } + + /** + * {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of + * {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged + * to create their own subclass of this LayoutParams class + * to store any additional required per-child view metadata about the layout. + */ + public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams { + ViewHolder mViewHolder; + final Rect mDecorInsets = new Rect(); + boolean mInsetsDirty = true; + // Flag is set to true if the view is bound while it is detached from RV. + // In this case, we need to manually call invalidate after view is added to guarantee that + // invalidation is populated through the View hierarchy + boolean mPendingInvalidate = false; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super((ViewGroup.LayoutParams) source); + } + + /** + * Returns true if the view this LayoutParams is attached to needs to have its content + * updated from the corresponding adapter. + * + * @return true if the view should have its content updated + */ + public boolean viewNeedsUpdate() { + return mViewHolder.needsUpdate(); + } + + /** + * Returns true if the view this LayoutParams is attached to is now representing + * potentially invalid data. A LayoutManager should scrap/recycle it. + * + * @return true if the view is invalid + */ + public boolean isViewInvalid() { + return mViewHolder.isInvalid(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been removed from the data set. A LayoutManager may choose to + * treat it differently in order to animate its outgoing or disappearing state. + * + * @return true if the item the view corresponds to was removed from the data set + */ + public boolean isItemRemoved() { + return mViewHolder.isRemoved(); + } + + /** + * Returns true if the adapter data item corresponding to the view this LayoutParams + * is attached to has been changed in the data set. A LayoutManager may choose to + * treat it differently in order to animate its changing state. + * + * @return true if the item the view corresponds to was changed in the data set + */ + public boolean isItemChanged() { + return mViewHolder.isUpdated(); + } + + /** + * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} + */ + @Deprecated + public int getViewPosition() { + return mViewHolder.getPosition(); + } + + /** + * Returns the adapter position that the view this LayoutParams is attached to corresponds + * to as of latest layout calculation. + * + * @return the adapter position this view as of latest layout pass + */ + public int getViewLayoutPosition() { + return mViewHolder.getLayoutPosition(); + } + + /** + * Returns the up-to-date adapter position that the view this LayoutParams is attached to + * corresponds to. + * + * @return the up-to-date adapter position this view. It may return + * {@link RecyclerView#NO_POSITION} if item represented by this View has been removed or + * its up-to-date position cannot be calculated. + */ + public int getViewAdapterPosition() { + return mViewHolder.getAdapterPosition(); + } + } + + /** + * Observer base class for watching changes to an {@link Adapter}. + * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}. + */ + public abstract static class AdapterDataObserver { + public void onChanged() { + // Do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + // fallback to onItemRangeChanged(positionStart, itemCount) if app + // does not override this method. + onItemRangeChanged(positionStart, itemCount); + } + + public void onItemRangeInserted(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeRemoved(int positionStart, int itemCount) { + // do nothing + } + + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + // do nothing + } + } + + /** + *

Base class for smooth scrolling. Handles basic tracking of the target view position and + * provides methods to trigger a programmatic scroll.

+ * + * @see LinearSmoothScroller + */ + public abstract static class SmoothScroller { + + private int mTargetPosition = RecyclerView.NO_POSITION; + + private RecyclerView mRecyclerView; + + private LayoutManager mLayoutManager; + + private boolean mPendingInitialRun; + + private boolean mRunning; + + private View mTargetView; + + private final Action mRecyclingAction; + + public SmoothScroller() { + mRecyclingAction = new Action(0, 0); + } + + /** + * Starts a smooth scroll for the given target position. + *

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

+ * + *

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

+ */ + void start(RecyclerView recyclerView, LayoutManager layoutManager) { + mRecyclerView = recyclerView; + mLayoutManager = layoutManager; + if (mTargetPosition == RecyclerView.NO_POSITION) { + throw new IllegalArgumentException("Invalid target position"); + } + mRecyclerView.mState.mTargetPosition = mTargetPosition; + mRunning = true; + mPendingInitialRun = true; + mTargetView = findViewByPosition(getTargetPosition()); + onStart(); + mRecyclerView.mViewFlinger.postOnAnimation(); + } + + public void setTargetPosition(int targetPosition) { + mTargetPosition = targetPosition; + } + + /** + * @return The LayoutManager to which this SmoothScroller is attached. Will return + * null after the SmoothScroller is stopped. + */ + @Nullable + public LayoutManager getLayoutManager() { + return mLayoutManager; + } + + /** + * Stops running the SmoothScroller in each animation callback. Note that this does not + * cancel any existing {@link Action} updated by + * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or + * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}. + */ + protected final void stop() { + if (!mRunning) { + return; + } + onStop(); + mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION; + mTargetView = null; + mTargetPosition = RecyclerView.NO_POSITION; + mPendingInitialRun = false; + mRunning = false; + // trigger a cleanup + mLayoutManager.onSmoothScrollerStopped(this); + // clear references to avoid any potential leak by a custom smooth scroller + mLayoutManager = null; + mRecyclerView = null; + } + + /** + * Returns true if SmoothScroller has been started but has not received the first + * animation + * callback yet. + * + * @return True if this SmoothScroller is waiting to start + */ + public boolean isPendingInitialRun() { + return mPendingInitialRun; + } + + + /** + * @return True if SmoothScroller is currently active + */ + public boolean isRunning() { + return mRunning; + } + + /** + * Returns the adapter position of the target item + * + * @return Adapter position of the target item or + * {@link RecyclerView#NO_POSITION} if no target view is set. + */ + public int getTargetPosition() { + return mTargetPosition; + } + + private void onAnimation(int dx, int dy) { + final RecyclerView recyclerView = mRecyclerView; + if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION || recyclerView == null) { + stop(); + } + mPendingInitialRun = false; + if (mTargetView != null) { + // verify target position + if (getChildPosition(mTargetView) == mTargetPosition) { + onTargetFound(mTargetView, recyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(recyclerView); + stop(); + } else { + Log.e(TAG, "Passed over target position while smooth scrolling."); + mTargetView = null; + } + } + if (mRunning) { + onSeekTargetStep(dx, dy, recyclerView.mState, mRecyclingAction); + boolean hadJumpTarget = mRecyclingAction.hasJumpTarget(); + mRecyclingAction.runIfNecessary(recyclerView); + if (hadJumpTarget) { + // It is not stopped so needs to be restarted + if (mRunning) { + mPendingInitialRun = true; + recyclerView.mViewFlinger.postOnAnimation(); + } else { + stop(); // done + } + } + } + } + + /** + * @see RecyclerView#getChildLayoutPosition(android.view.View) + */ + public int getChildPosition(View view) { + return mRecyclerView.getChildLayoutPosition(view); + } + + /** + * @see RecyclerView.LayoutManager#getChildCount() + */ + public int getChildCount() { + return mRecyclerView.mLayout.getChildCount(); + } + + /** + * @see RecyclerView.LayoutManager#findViewByPosition(int) + */ + public View findViewByPosition(int position) { + return mRecyclerView.mLayout.findViewByPosition(position); + } + + /** + * @see RecyclerView#scrollToPosition(int) + * @deprecated Use {@link Action#jumpTo(int)}. + */ + @Deprecated + public void instantScrollToPosition(int position) { + mRecyclerView.scrollToPosition(position); + } + + protected void onChildAttachedToWindow(View child) { + if (getChildPosition(child) == getTargetPosition()) { + mTargetView = child; + if (DEBUG) { + Log.d(TAG, "smooth scroll target view has been attached"); + } + } + } + + /** + * Normalizes the vector. + * @param scrollVector The vector that points to the target scroll position + */ + protected void normalize(PointF scrollVector) { + final double magnitude = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y + * scrollVector.y); + scrollVector.x /= magnitude; + scrollVector.y /= magnitude; + } + + /** + * Called when smooth scroll is started. This might be a good time to do setup. + */ + protected abstract void onStart(); + + /** + * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. + * @see #stop() + */ + protected abstract void onStop(); + + /** + *

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

+ *

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

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

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

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

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

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

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

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

+ *

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

+ *

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

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

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

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

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

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

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

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

+ * You can get the adapter's item count via {@link LayoutManager#getItemCount()} method. + * + * @return The number of items currently available + * @see LayoutManager#getItemCount() + */ + public int getItemCount() { + return mInPreLayout + ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) + : mItemCount; + } + + @Override + public String toString() { + return "State{" + + "mTargetPosition=" + mTargetPosition + + ", mData=" + mData + + ", mItemCount=" + mItemCount + + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + + ", mDeletedInvisibleItemCountSincePreviousLayout=" + + mDeletedInvisibleItemCountSincePreviousLayout + + ", mStructureChanged=" + mStructureChanged + + ", mInPreLayout=" + mInPreLayout + + ", mRunSimpleAnimations=" + mRunSimpleAnimations + + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations + + '}'; + } + } + + /** + * This class defines the behavior of fling if the developer wishes to handle it. + *

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

By default, RecyclerView uses {@link DefaultItemAnimator}.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * It works using the {@link ViewGroup#getChildDrawingOrder(int, int)} method, so any case + * that applies to that method also applies to this callback. For example, changing the drawing + * order of two views will not have any effect if their elevation values are different since + * elevation overrides the result of this callback. + */ + public interface ChildDrawingOrderCallback { + /** + * Returns the index of the child to draw for this iteration. Override this + * if you want to change the drawing order of children. By default, it + * returns i. + * + * @param i The current iteration. + * @return The index of the child to draw this iteration. + * + * @see RecyclerView#setChildDrawingOrderCallback(RecyclerView.ChildDrawingOrderCallback) + */ + int onGetChildDrawingOrder(int childCount, int i); + } +} diff --git a/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java b/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java new file mode 100644 index 0000000000000..282da644ac8b1 --- /dev/null +++ b/core/java/com/android/internal/widget/RecyclerViewAccessibilityDelegate.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.os.Bundle; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * The AccessibilityDelegate used by RecyclerView. + *

+ * This class handles basic accessibility actions and delegates them to LayoutManager. + */ +public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegate { + final RecyclerView mRecyclerView; + + + public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + } + + boolean shouldIgnore() { + return mRecyclerView.hasPendingAdapterUpdates(); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args); + } + + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(RecyclerView.class.getName()); + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info); + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(RecyclerView.class.getName()); + if (host instanceof RecyclerView && !shouldIgnore()) { + RecyclerView rv = (RecyclerView) host; + if (rv.getLayoutManager() != null) { + rv.getLayoutManager().onInitializeAccessibilityEvent(event); + } + } + } + + /** + * Gets the AccessibilityDelegate for an individual item in the RecyclerView. + * A basic item delegate is provided by default, but you can override this + * method to provide a custom per-item delegate. + */ + public AccessibilityDelegate getItemDelegate() { + return mItemDelegate; + } + + final AccessibilityDelegate mItemDelegate = new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + mRecyclerView.getLayoutManager() + .onInitializeAccessibilityNodeInfoForItem(host, info); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) { + return mRecyclerView.getLayoutManager() + .performAccessibilityActionForItem(host, action, args); + } + return false; + } + }; +} + diff --git a/core/java/com/android/internal/widget/ScrollbarHelper.java b/core/java/com/android/internal/widget/ScrollbarHelper.java new file mode 100644 index 0000000000000..ae34e4c558d6a --- /dev/null +++ b/core/java/com/android/internal/widget/ScrollbarHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.view.View; + +/** + * A helper class to do scroll offset calculations. + */ +class ScrollbarHelper { + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled, boolean reverseLayout) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + final int minPosition = Math.min(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int maxPosition = Math.max(lm.getPosition(startChild), + lm.getPosition(endChild)); + final int itemsBefore = reverseLayout + ? Math.max(0, state.getItemCount() - maxPosition - 1) + : Math.max(0, minPosition); + if (!smoothScrollbarEnabled) { + return itemsBefore; + } + final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild)); + final int itemRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + 1; + final float avgSizePerRow = (float) laidOutArea / itemRange; + + return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding() + - orientation.getDecoratedStart(startChild))); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1; + } + final int extend = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + return Math.min(orientation.getTotalSpace(), extend); + } + + /** + * @param startChild View closest to start of the list. (top or left) + * @param endChild View closest to end of the list (bottom or right) + */ + static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation, + View startChild, View endChild, RecyclerView.LayoutManager lm, + boolean smoothScrollbarEnabled) { + if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null + || endChild == null) { + return 0; + } + if (!smoothScrollbarEnabled) { + return state.getItemCount(); + } + // smooth scrollbar enabled. try to estimate better. + final int laidOutArea = orientation.getDecoratedEnd(endChild) + - orientation.getDecoratedStart(startChild); + final int laidOutRange = Math.abs(lm.getPosition(startChild) + - lm.getPosition(endChild)) + + 1; + // estimate a size for full list. + return (int) ((float) laidOutArea / laidOutRange * state.getItemCount()); + } +} diff --git a/core/java/com/android/internal/widget/ScrollingView.java b/core/java/com/android/internal/widget/ScrollingView.java new file mode 100644 index 0000000000000..a0205e7771e77 --- /dev/null +++ b/core/java/com/android/internal/widget/ScrollingView.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +/** + * An interface that can be implemented by Views to provide scroll related APIs. + */ +public interface ScrollingView { + /** + *

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()}.

+ * + *

The default range is the drawing width of this view.

+ * + * @return the total horizontal range represented by the horizontal + * scrollbar + * + * @see #computeHorizontalScrollExtent() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollRange(); + + /** + *

Compute the horizontal offset of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * 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()}.

+ * + *

The default offset is the scroll offset of this view.

+ * + * @return the horizontal offset of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollOffset(); + + /** + *

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()}.

+ * + *

The default extent is the drawing width of this view.

+ * + * @return the horizontal extent of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollExtent(); + + /** + *

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()}.

+ * + * @return the total vertical range represented by the vertical scrollbar + * + *

The default range is the drawing height of this view.

+ * + * @see #computeVerticalScrollExtent() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollRange(); + + /** + *

Compute the vertical offset of the vertical scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * 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()}.

+ * + *

The default offset is the scroll offset of this view.

+ * + * @return the vertical offset of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollOffset(); + + /** + *

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()}.

+ * + *

The default extent is the drawing height of this view.

+ * + * @return the vertical extent of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollExtent(); +} diff --git a/core/java/com/android/internal/widget/SimpleItemAnimator.java b/core/java/com/android/internal/widget/SimpleItemAnimator.java new file mode 100644 index 0000000000000..f4cc7536ba4bf --- /dev/null +++ b/core/java/com/android/internal/widget/SimpleItemAnimator.java @@ -0,0 +1,457 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Log; +import android.view.View; + +import com.android.internal.widget.RecyclerView.Adapter; +import com.android.internal.widget.RecyclerView.ViewHolder; + +/** + * A wrapper class for ItemAnimator that records View bounds and decides whether it should run + * move, change, add or remove animations. This class also replicates the original ItemAnimator + * API. + *

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

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

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

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

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

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

+ * The return value indicates whether an animation has been set up and whether the + * ItemAnimator's {@link #runPendingAnimations()} method should be called at the + * next opportunity. This mechanism allows ItemAnimator to set up individual animations + * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, + * {@link #animateRemove(ViewHolder) animateRemove()}, and + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one, + * then start the animations together in the later call to {@link #runPendingAnimations()}. + * + * @param oldHolder The original item that changed. + * @param newHolder The new item that was created with the changed content. Might be null + * @param fromLeft Left of the old view holder + * @param fromTop Top of the old view holder + * @param toLeft Left of the new view holder + * @param toTop Top of the new view holder + * @return true if a later call to {@link #runPendingAnimations()} is requested, + * false otherwise. + */ + public abstract boolean animateChange(ViewHolder oldHolder, + ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop); + + /** + * Method to be called by subclasses when a remove animation is done. + * + * @param item The item which has been removed + * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + */ + public final void dispatchRemoveFinished(ViewHolder item) { + onRemoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a move animation is done. + * + * @param item The item which has been moved + * @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo, + * ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo) + * @see RecyclerView.ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo) + */ + public final void dispatchMoveFinished(ViewHolder item) { + onMoveFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when an add animation is done. + * + * @param item The item which has been added + */ + public final void dispatchAddFinished(ViewHolder item) { + onAddFinished(item); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a change animation is done. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + * @see #animateChange(ViewHolder, ViewHolder, int, int, int, int) + */ + public final void dispatchChangeFinished(ViewHolder item, boolean oldItem) { + onChangeFinished(item, oldItem); + dispatchAnimationFinished(item); + } + + /** + * Method to be called by subclasses when a remove animation is being started. + * + * @param item The item being removed + */ + public final void dispatchRemoveStarting(ViewHolder item) { + onRemoveStarting(item); + } + + /** + * Method to be called by subclasses when a move animation is being started. + * + * @param item The item being moved + */ + public final void dispatchMoveStarting(ViewHolder item) { + onMoveStarting(item); + } + + /** + * Method to be called by subclasses when an add animation is being started. + * + * @param item The item being added + */ + public final void dispatchAddStarting(ViewHolder item) { + onAddStarting(item); + } + + /** + * Method to be called by subclasses when a change animation is being started. + * + * @param item The item which has been changed (this method must be called for + * each non-null ViewHolder passed into + * {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}). + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + public final void dispatchChangeStarting(ViewHolder item, boolean oldItem) { + onChangeStarting(item, oldItem); + } + + /** + * Called when a remove animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onRemoveStarting(ViewHolder item) { + } + + /** + * Called when a remove animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onRemoveFinished(ViewHolder item) { + } + + /** + * Called when an add animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onAddStarting(ViewHolder item) { + } + + /** + * Called when an add animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onAddFinished(ViewHolder item) { + } + + /** + * Called when a move animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + @SuppressWarnings("UnusedParameters") + public void onMoveStarting(ViewHolder item) { + } + + /** + * Called when a move animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + */ + public void onMoveFinished(ViewHolder item) { + } + + /** + * Called when a change animation is being started on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + @SuppressWarnings("UnusedParameters") + public void onChangeStarting(ViewHolder item, boolean oldItem) { + } + + /** + * Called when a change animation has ended on the given ViewHolder. + * The default implementation does nothing. Subclasses may wish to override + * this method to handle any ViewHolder-specific operations linked to animation + * lifecycles. + * + * @param item The ViewHolder being animated. + * @param oldItem true if this is the old item that was changed, false if + * it is the new item that replaced the old item. + */ + public void onChangeFinished(ViewHolder item, boolean oldItem) { + } +} + diff --git a/core/java/com/android/internal/widget/ViewInfoStore.java b/core/java/com/android/internal/widget/ViewInfoStore.java new file mode 100644 index 0000000000000..6784a853d0d19 --- /dev/null +++ b/core/java/com/android/internal/widget/ViewInfoStore.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.LongSparseArray; +import android.util.Pools; + +import static com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo; +import static com.android.internal.widget.RecyclerView.ViewHolder; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_POST; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE; +import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * This class abstracts all tracking for Views to run animations. + */ +class ViewInfoStore { + + private static final boolean DEBUG = false; + + /** + * View data records for pre-layout + */ + @VisibleForTesting + final ArrayMap mLayoutHolderMap = new ArrayMap<>(); + + @VisibleForTesting + final LongSparseArray mOldChangedHolders = new LongSparseArray<>(); + + /** + * Clears the state and all existing tracking data + */ + void clear() { + mLayoutHolderMap.clear(); + mOldChangedHolders.clear(); + } + + /** + * Adds the item information to the prelayout tracking + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPreLayout(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.preInfo = info; + record.flags |= FLAG_PRE; + } + + boolean isDisappearing(ViewHolder holder) { + final InfoRecord record = mLayoutHolderMap.get(holder); + return record != null && ((record.flags & FLAG_DISAPPEARED) != 0); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + ItemHolderInfo popFromPreLayout(ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_PRE); + } + + /** + * Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it. + * + * @param vh The ViewHolder whose information is being queried + * @return The ItemHolderInfo for the given ViewHolder or null if it does not exist + */ + @Nullable + ItemHolderInfo popFromPostLayout(ViewHolder vh) { + return popFromLayoutStep(vh, FLAG_POST); + } + + private ItemHolderInfo popFromLayoutStep(ViewHolder vh, int flag) { + int index = mLayoutHolderMap.indexOfKey(vh); + if (index < 0) { + return null; + } + final InfoRecord record = mLayoutHolderMap.valueAt(index); + if (record != null && (record.flags & flag) != 0) { + record.flags &= ~flag; + final ItemHolderInfo info; + if (flag == FLAG_PRE) { + info = record.preInfo; + } else if (flag == FLAG_POST) { + info = record.postInfo; + } else { + throw new IllegalArgumentException("Must provide flag PRE or POST"); + } + // if not pre-post flag is left, clear. + if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) { + mLayoutHolderMap.removeAt(index); + InfoRecord.recycle(record); + } + return info; + } + return null; + } + + /** + * Adds the given ViewHolder to the oldChangeHolders list + * @param key The key to identify the ViewHolder. + * @param holder The ViewHolder to store + */ + void addToOldChangeHolders(long key, ViewHolder holder) { + mOldChangedHolders.put(key, holder); + } + + /** + * Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the + * LayoutManager during a pre-layout pass. We distinguish them from other views that were + * already in the pre-layout so that ItemAnimator can choose to run a different animation for + * them. + * + * @param holder The ViewHolder to store + * @param info The information to save + */ + void addToAppearedInPreLayoutHolders(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_APPEAR; + record.preInfo = info; + } + + /** + * Checks whether the given ViewHolder is in preLayout list + * @param viewHolder The ViewHolder to query + * + * @return True if the ViewHolder is present in preLayout, false otherwise + */ + boolean isInPreLayout(ViewHolder viewHolder) { + final InfoRecord record = mLayoutHolderMap.get(viewHolder); + return record != null && (record.flags & FLAG_PRE) != 0; + } + + /** + * Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns + * null. + * @param key The key to be used to find the ViewHolder. + * + * @return A ViewHolder if exists or null if it does not exist. + */ + ViewHolder getFromOldChangeHolders(long key) { + return mOldChangedHolders.get(key); + } + + /** + * Adds the item information to the post layout list + * @param holder The ViewHolder whose information is being saved + * @param info The information to save + */ + void addToPostLayout(ViewHolder holder, ItemHolderInfo info) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.postInfo = info; + record.flags |= FLAG_POST; + } + + /** + * A ViewHolder might be added by the LayoutManager just to animate its disappearance. + * This list holds such items so that we can animate / recycle these ViewHolders properly. + * + * @param holder The ViewHolder which disappeared during a layout. + */ + void addToDisappearedInLayout(ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + record = InfoRecord.obtain(); + mLayoutHolderMap.put(holder, record); + } + record.flags |= FLAG_DISAPPEARED; + } + + /** + * Removes a ViewHolder from disappearing list. + * @param holder The ViewHolder to be removed from the disappearing list. + */ + void removeFromDisappearedInLayout(ViewHolder holder) { + InfoRecord record = mLayoutHolderMap.get(holder); + if (record == null) { + return; + } + record.flags &= ~FLAG_DISAPPEARED; + } + + void process(ProcessCallback callback) { + for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) { + final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index); + final InfoRecord record = mLayoutHolderMap.removeAt(index); + if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) { + // Appeared then disappeared. Not useful for animations. + callback.unused(viewHolder); + } else if ((record.flags & FLAG_DISAPPEARED) != 0) { + // Set as "disappeared" by the LayoutManager (addDisappearingView) + if (record.preInfo == null) { + // similar to appear disappear but happened between different layout passes. + // this can happen when the layout manager is using auto-measure + callback.unused(viewHolder); + } else { + callback.processDisappeared(viewHolder, record.preInfo, record.postInfo); + } + } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) { + // Appeared in the layout but not in the adapter (e.g. entered the viewport) + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) { + // Persistent in both passes. Animate persistence + callback.processPersistent(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_PRE) != 0) { + // Was in pre-layout, never been added to post layout + callback.processDisappeared(viewHolder, record.preInfo, null); + } else if ((record.flags & FLAG_POST) != 0) { + // Was not in pre-layout, been added to post layout + callback.processAppeared(viewHolder, record.preInfo, record.postInfo); + } else if ((record.flags & FLAG_APPEAR) != 0) { + // Scrap view. RecyclerView will handle removing/recycling this. + } else if (DEBUG) { + throw new IllegalStateException("record without any reasonable flag combination:/"); + } + InfoRecord.recycle(record); + } + } + + /** + * Removes the ViewHolder from all list + * @param holder The ViewHolder which we should stop tracking + */ + void removeViewHolder(ViewHolder holder) { + for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) { + if (holder == mOldChangedHolders.valueAt(i)) { + mOldChangedHolders.removeAt(i); + break; + } + } + final InfoRecord info = mLayoutHolderMap.remove(holder); + if (info != null) { + InfoRecord.recycle(info); + } + } + + void onDetach() { + InfoRecord.drainCache(); + } + + public void onViewDetached(ViewHolder viewHolder) { + removeFromDisappearedInLayout(viewHolder); + } + + interface ProcessCallback { + void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, + @Nullable ItemHolderInfo postInfo); + void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo, + ItemHolderInfo postInfo); + void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo, + @NonNull ItemHolderInfo postInfo); + void unused(ViewHolder holder); + } + + static class InfoRecord { + // disappearing list + static final int FLAG_DISAPPEARED = 1; + // appear in pre layout list + static final int FLAG_APPEAR = 1 << 1; + // pre layout, this is necessary to distinguish null item info + static final int FLAG_PRE = 1 << 2; + // post layout, this is necessary to distinguish null item info + static final int FLAG_POST = 1 << 3; + static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED; + static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST; + static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST; + int flags; + @Nullable ItemHolderInfo preInfo; + @Nullable ItemHolderInfo postInfo; + static Pools.Pool sPool = new Pools.SimplePool<>(20); + + private InfoRecord() { + } + + static InfoRecord obtain() { + InfoRecord record = sPool.acquire(); + return record == null ? new InfoRecord() : record; + } + + static void recycle(InfoRecord record) { + record.flags = 0; + record.preInfo = null; + record.postInfo = null; + sPool.release(record); + } + + static void drainCache() { + //noinspection StatementWithEmptyBody + while (sPool.acquire() != null); + } + } +} diff --git a/core/java/com/android/internal/widget/helper/ItemTouchHelper.java b/core/java/com/android/internal/widget/helper/ItemTouchHelper.java new file mode 100644 index 0000000000000..9636ed87696ac --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchHelper.java @@ -0,0 +1,2391 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget.helper; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +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 com.android.internal.R; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; +import com.android.internal.widget.RecyclerView.OnItemTouchListener; +import com.android.internal.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. On + * platforms older than Honeycomb, ItemTouchHelper uses canvas translations and View's visibility + * property to move items in response to touch events. 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 but due to limitations of + * platform prior to Honeycomb, you may need to implement onChildDrawOver as well. + */ +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. + */ + public static final int ACTION_STATE_SWIPE = 1; + + /** + * A View is currently being dragged. + */ + public static final int ACTION_STATE_DRAG = 2; + + /** + * Animation type for views which are swiped successfully. + */ + 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. + */ + 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. + */ + public static final int ANIMATION_TYPE_DRAG = 1 << 3; + + static final String TAG = "ItemTouchHelper"; + + static final boolean DEBUG = false; + + 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 + */ + 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. + */ + float mSwipeEscapeVelocity; + + /** + * Set when ItemTouchHelper is assigned to a RecyclerView. + */ + 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. + */ + float mSelectedStartX; + + float mSelectedStartY; + + /** + * The pointer we are tracking. + */ + int mActivePointerId = ACTIVE_POINTER_ID_NONE; + + /** + * Developer callback which controls the behavior of ItemTouchHelper. + */ + Callback mCallback; + + /** + * Current mode. + */ + int mActionState = ACTION_STATE_IDLE; + + /** + * The direction flags obtained from unmasking + * {@link Callback#getAbsoluteMovementFlags(RecyclerView, ViewHolder)} for the current + * action state. + */ + 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. + */ + 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. + */ + 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); + mRecyclerView.postOnAnimation(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. + */ + 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. + */ + int mOverdrawChildPosition = -1; + + /** + * Used to detect long press. + */ + GestureDetector mGestureDetector; + + private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() { + @Override + public boolean onInterceptTouchEvent(RecyclerView recyclerView, 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(RecyclerView recyclerView, 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(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 (mRecyclerView != 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); + initGestureDetector(); + } + + 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); + mCallback.clearView(mRecyclerView, recoverAnimation.mViewHolder); + } + mRecoverAnimations.clear(); + mOverdrawChild = null; + mOverdrawChildPosition = -1; + releaseVelocityTracker(); + } + + private void initGestureDetector() { + if (mGestureDetector != null) { + return; + } + mGestureDetector = new GestureDetector(mRecyclerView.getContext(), + new ItemTouchHelperGestureListener()); + } + + 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(Canvas c, RecyclerView parent, 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 + 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 + * @param actionState The type of action + */ + void select(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) { + // 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(); + } + + 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.getAdapterPosition() != 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); + } + } + } + }); + } + + 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. + */ + 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. + */ + 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.getAdapterPosition(); + final int fromPosition = viewHolder.getAdapterPosition(); + if (mCallback.onMove(mRecyclerView, viewHolder, target)) { + // keep target visible + mCallback.onMoved(mRecyclerView, viewHolder, fromPosition, + target, toPosition, x, y); + } + } + + @Override + public void onChildViewAttachedToWindow(View view) { + } + + @Override + public void onChildViewDetachedFromWindow(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. + */ + int 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 anim.mAnimationType; + } + } + return 0; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + outRect.setEmpty(); + } + + 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. + */ + boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) { + if (mSelected != null || action != MotionEvent.ACTION_MOVE + || mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) { + return false; + } + if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) { + return false; + } + final ViewHolder vh = findSwipedView(motionEvent); + if (vh == null) { + return false; + } + 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 false; + } + + // 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 false; + } + if (absDx > absDy) { + if (dx < 0 && (swipeFlags & LEFT) == 0) { + return false; + } + if (dx > 0 && (swipeFlags & RIGHT) == 0) { + return false; + } + } else { + if (dy < 0 && (swipeFlags & UP) == 0) { + return false; + } + if (dy > 0 && (swipeFlags & DOWN) == 0) { + return false; + } + } + mDx = mDy = 0f; + mActivePointerId = motionEvent.getPointerId(0); + select(vh, ACTION_STATE_SWIPE); + return true; + } + + 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(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(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); + } + + 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; + } + + 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, + mRecyclerView.getLayoutDirection()); + 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, + mRecyclerView.getLayoutDirection()); + } + 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, + mRecyclerView.getLayoutDirection()); + } + 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); + } + + 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 + * {@link #prepareForDrop(View, View, int, int)} 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(View view, 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 { + + public static final int DEFAULT_DRAG_ANIMATION_DURATION = 200; + + 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 ItemTouchUIUtil sUICallback = new ItemTouchUIUtilImpl(); + + 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} + */ + public static ItemTouchUIUtil getDefaultUIUtil() { + return sUICallback; + } + + /** + * 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 View#getLayoutDirection()}. + * @return Updated flags which uses relative flags ({@link #START}, {@link #END}) instead + * of {@link #LEFT}, {@link #RIGHT}. + * @see #convertToAbsoluteDirection(int, int) + */ + 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 == View.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. + */ + 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(RecyclerView recyclerView, + 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. + */ + 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 == View.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, recyclerView.getLayoutDirection()); + } + + 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. + */ + public boolean canDropOver(RecyclerView recyclerView, ViewHolder current, + 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#getAdapterPosition() + * ViewHolder#getAdapterPosition()}). + *

+ * 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(RecyclerView recyclerView, + ViewHolder viewHolder, 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. + */ + 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 . + */ + public float getSwipeThreshold(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 . + */ + public float getMoveThreshold(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) + */ + 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) + */ + 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. + */ + public ViewHolder chooseDropTarget(ViewHolder selected, + 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(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(ViewHolder viewHolder, int actionState) { + if (viewHolder != null) { + sUICallback.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(final RecyclerView recyclerView, + final ViewHolder viewHolder, int fromPos, 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(RecyclerView recyclerView, ViewHolder viewHolder) { + sUICallback.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(Canvas c, RecyclerView recyclerView, + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + sUICallback.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(Canvas c, RecyclerView recyclerView, + ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + sUICallback.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 + */ + public long getAnimationDuration(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. + */ + public int interpolateOutOfBoundsScroll(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 abstract 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. + */ + public void setDefaultSwipeDirs(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. + */ + public void setDefaultDragDirs(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 RecyclerView for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + public int getSwipeDirs(RecyclerView recyclerView, 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 RecyclerView for which the swipe direction is queried. + * @return A binary OR of direction flags. + */ + public int getDragDirs(RecyclerView recyclerView, ViewHolder viewHolder) { + return mDefaultDragDirs; + } + + @Override + public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { + return makeMovementFlags(getDragDirs(recyclerView, viewHolder), + getSwipeDirs(recyclerView, viewHolder)); + } + } + + private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener { + + ItemTouchHelperGestureListener() { + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + 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); + } + } + } + } + } + } + + private class RecoverAnimation implements Animator.AnimatorListener { + + final float mStartDx; + + final float mStartDy; + + final float mTargetX; + + final float mTargetY; + + final ViewHolder mViewHolder; + + final int mActionState; + + private final ValueAnimator mValueAnimator; + + final int mAnimationType; + + public 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/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java b/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java new file mode 100644 index 0000000000000..e368a6d41f09d --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchUIUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget.helper; + +import android.graphics.Canvas; +import android.view.View; + +import com.android.internal.widget.RecyclerView; + +/** + * 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)} + */ + 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)} + */ + 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)} + */ + void clearView(View view); + + /** + * The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged( + * RecyclerView.ViewHolder, int)} + */ + void onSelected(View view); +} + diff --git a/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java b/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java new file mode 100644 index 0000000000000..0de240b8c6833 --- /dev/null +++ b/core/java/com/android/internal/widget/helper/ItemTouchUIUtilImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 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 com.android.internal.widget.helper; + +import android.graphics.Canvas; +import android.view.View; + +import com.android.internal.R; +import com.android.internal.widget.RecyclerView; + +/** + * 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 { + @Override + public void onDraw(Canvas c, RecyclerView recyclerView, View view, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (isCurrentlyActive) { + Object originalElevation = view.getTag( + R.id.item_touch_helper_previous_elevation); + if (originalElevation == null) { + originalElevation = view.getElevation(); + float newElevation = 1f + findMaxElevation(recyclerView, view); + view.setElevation(newElevation); + view.setTag(R.id.item_touch_helper_previous_elevation, + originalElevation); + } + } + view.setTranslationX(dX); + view.setTranslationY(dY); + } + + private 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 = child.getElevation(); + if (elevation > max) { + max = elevation; + } + } + return max; + } + + @Override + public void clearView(View view) { + final Object tag = view.getTag( + R.id.item_touch_helper_previous_elevation); + if (tag != null && tag instanceof Float) { + view.setElevation((Float) tag); + } + view.setTag(R.id.item_touch_helper_previous_elevation, null); + view.setTranslationX(0f); + view.setTranslationY(0f); + } + + @Override + public void onSelected(View view) { + } + + @Override + public void onDrawOver(Canvas c, RecyclerView recyclerView, + View view, float dX, float dY, int actionState, boolean isCurrentlyActive) { + } +} diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 8e9959f5664d5..9452335c8d5b2 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8430,7 +8430,7 @@ - + + + + + + + + diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index bd195216ae4ec..e6358a37bdedb 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -509,4 +509,10 @@ 96dp 8dp + + + 20dp + 120dp + 800dp diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml index 55477063a71a4..f351b70085477 100644 --- a/core/res/res/values/ids.xml +++ b/core/res/res/values/ids.xml @@ -131,4 +131,7 @@ + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c370ef74f6a9c..6eb3bee67741b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2806,4 +2806,10 @@ + + + + + +