From dae22d02845a2cb07c8e8c225f4c0592cb1bc026 Mon Sep 17 00:00:00 2001 From: seven332 Date: Tue, 12 May 2015 19:35:29 +0800 Subject: [PATCH] Make RecyclerView smooth scroll speed customizable --- app/app.iml | 1 - app/build.gradle | 1 - .../support/v7/widget/AdapterHelper.java | 733 ++ .../support/v7/widget/ChildHelper.java | 487 + .../v7/widget/DefaultItemAnimator.java | 631 ++ .../support/v7/widget/LayoutState.java | 87 + .../v7/widget/LinearLayoutManager.java | 2131 ++++ .../v7/widget/LinearSmoothScroller.java | 338 + .../support/v7/widget/OpReorderer.java | 238 + .../support/v7/widget/OrientationHelper.java | 338 + .../support/v7/widget/RecyclerView.java | 9334 +++++++++++++++++ .../RecyclerViewAccessibilityDelegate.java | 101 + .../support/v7/widget/ScrollbarHelper.java | 98 + .../v7/widget/StaggeredGridLayoutManager.java | 2763 +++++ .../hippo/ehviewer/widget/ContentLayout.java | 7 +- .../com/hippo/util/LayoutManagerUtils.java | 45 + .../recyclerview/SimpleSmoothScroller.java | 38 + 17 files changed, 17367 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/android/support/v7/widget/AdapterHelper.java create mode 100644 app/src/main/java/android/support/v7/widget/ChildHelper.java create mode 100644 app/src/main/java/android/support/v7/widget/DefaultItemAnimator.java create mode 100644 app/src/main/java/android/support/v7/widget/LayoutState.java create mode 100644 app/src/main/java/android/support/v7/widget/LinearLayoutManager.java create mode 100644 app/src/main/java/android/support/v7/widget/LinearSmoothScroller.java create mode 100644 app/src/main/java/android/support/v7/widget/OpReorderer.java create mode 100644 app/src/main/java/android/support/v7/widget/OrientationHelper.java create mode 100644 app/src/main/java/android/support/v7/widget/RecyclerView.java create mode 100644 app/src/main/java/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java create mode 100644 app/src/main/java/android/support/v7/widget/ScrollbarHelper.java create mode 100644 app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java create mode 100644 app/src/main/java/com/hippo/widget/recyclerview/SimpleSmoothScroller.java diff --git a/app/app.iml b/app/app.iml index 421c9fdf9..4d7a115ac 100644 --- a/app/app.iml +++ b/app/app.iml @@ -93,7 +93,6 @@ - diff --git a/app/build.gradle b/app/build.gradle index eb83cb6e3..efcb643ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,7 +26,6 @@ android { dependencies { compile 'com.android.support:support-v4:22.1.1' compile 'com.android.support:appcompat-v7:22.1.1' - compile 'com.android.support:recyclerview-v7:22.1.1' compile 'com.github.japgolly.android:svg-android:2.0.6' compile 'com.facebook.fresco:fresco:0.4.0' compile fileTree(dir: 'libs', include: ['*.jar']) diff --git a/app/src/main/java/android/support/v7/widget/AdapterHelper.java b/app/src/main/java/android/support/v7/widget/AdapterHelper.java new file mode 100644 index 000000000..032449ccc --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/AdapterHelper.java @@ -0,0 +1,733 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.support.v4.util.Pools; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.support.v7.widget.RecyclerView.*; + +/** + * Helper class that can enqueue and process adapter update operations. + *

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

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

+ * Although operations may be forwarded to LayoutManager in different orders, resulting data set + * is guaranteed to be the consistent. + */ +class AdapterHelper implements OpReorderer.Callback { + + final static int POSITION_TYPE_INVISIBLE = 0; + + final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1; + + private static final boolean DEBUG = false; + + private static final String TAG = "AHT"; + + private Pools.Pool mUpdateOpPool = new Pools.SimplePool(UpdateOp.POOL_SIZE); + + final ArrayList mPendingUpdates = new ArrayList(); + + final ArrayList mPostponedList = new ArrayList(); + + final Callback mCallback; + + Runnable mOnItemProcessedCallback; + + final boolean mDisableRecycler; + + final OpReorderer mOpReorderer; + + AdapterHelper(Callback callback) { + this(callback, false); + } + + AdapterHelper(Callback callback, boolean disableRecycler) { + mCallback = callback; + mDisableRecycler = disableRecycler; + mOpReorderer = new OpReorderer(this); + } + + AdapterHelper addUpdateOp(UpdateOp... ops) { + Collections.addAll(mPendingUpdates, ops); + return this; + } + + void reset() { + recycleUpdateOpsAndClearList(mPendingUpdates); + recycleUpdateOpsAndClearList(mPostponedList); + } + + void preProcess() { + mOpReorderer.reorderOps(mPendingUpdates); + final int count = mPendingUpdates.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + applyAdd(op); + break; + case UpdateOp.REMOVE: + applyRemove(op); + break; + case UpdateOp.UPDATE: + applyUpdate(op); + break; + case UpdateOp.MOVE: + applyMove(op); + break; + } + if (mOnItemProcessedCallback != null) { + mOnItemProcessedCallback.run(); + } + } + mPendingUpdates.clear(); + } + + void consumePostponedUpdates() { + final int count = mPostponedList.size(); + for (int i = 0; i < count; i++) { + mCallback.onDispatchSecondPass(mPostponedList.get(i)); + } + recycleUpdateOpsAndClearList(mPostponedList); + } + + private void applyMove(UpdateOp op) { + // MOVE ops are pre-processed so at this point, we know that item is still in the adapter. + // otherwise, it would be converted into a REMOVE operation + postponeAndUpdateViewHolders(op); + } + + private void applyRemove(UpdateOp op) { + int tmpStart = op.positionStart; + int tmpCount = 0; + int tmpEnd = op.positionStart + op.itemCount; + int type = -1; + for (int position = op.positionStart; position < tmpEnd; position++) { + boolean typeChanged = false; + ViewHolder vh = mCallback.findViewHolder(position); + if (vh != null || canFindInPreLayout(position)) { + // If a ViewHolder exists or this is a newly added item, we can defer this update + // to post layout stage. + // * For existing ViewHolders, we'll fake its existence in the pre-layout phase. + // * For items that are added and removed in the same process cycle, they won't + // have any effect in pre-layout since their add ops are already deferred to + // post-layout pass. + if (type == POSITION_TYPE_INVISIBLE) { + // Looks like we have other updates that we cannot merge with this one. + // Create an UpdateOp and dispatch it to LayoutManager. + UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); + dispatchAndUpdateViewHolders(newOp); + typeChanged = true; + } + type = POSITION_TYPE_NEW_OR_LAID_OUT; + } else { + // This update cannot be recovered because we don't have a ViewHolder representing + // this position. Instead, post it to LayoutManager immediately + if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { + // Looks like we have other updates that we cannot merge with this one. + // Create UpdateOp op and dispatch it to LayoutManager. + UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); + postponeAndUpdateViewHolders(newOp); + typeChanged = true; + } + type = POSITION_TYPE_INVISIBLE; + } + if (typeChanged) { + position -= tmpCount; // also equal to tmpStart + tmpEnd -= tmpCount; + tmpCount = 1; + } else { + tmpCount++; + } + } + if (tmpCount != op.itemCount) { // all 1 effect + recycleUpdateOp(op); + op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount); + } + if (type == POSITION_TYPE_INVISIBLE) { + dispatchAndUpdateViewHolders(op); + } else { + postponeAndUpdateViewHolders(op); + } + } + + private void applyUpdate(UpdateOp op) { + int tmpStart = op.positionStart; + int tmpCount = 0; + int tmpEnd = op.positionStart + op.itemCount; + int type = -1; + for (int position = op.positionStart; position < tmpEnd; position++) { + ViewHolder vh = mCallback.findViewHolder(position); + if (vh != null || canFindInPreLayout(position)) { // deferred + if (type == POSITION_TYPE_INVISIBLE) { + UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); + dispatchAndUpdateViewHolders(newOp); + tmpCount = 0; + tmpStart = position; + } + type = POSITION_TYPE_NEW_OR_LAID_OUT; + } else { // applied + if (type == POSITION_TYPE_NEW_OR_LAID_OUT) { + UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); + postponeAndUpdateViewHolders(newOp); + tmpCount = 0; + tmpStart = position; + } + type = POSITION_TYPE_INVISIBLE; + } + tmpCount++; + } + if (tmpCount != op.itemCount) { // all 1 effect + recycleUpdateOp(op); + op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount); + } + if (type == POSITION_TYPE_INVISIBLE) { + dispatchAndUpdateViewHolders(op); + } else { + postponeAndUpdateViewHolders(op); + } + } + + private void dispatchAndUpdateViewHolders(UpdateOp op) { + // tricky part. + // traverse all postpones and revert their changes on this op if necessary, apply updated + // dispatch to them since now they are after this op. + if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) { + throw new IllegalArgumentException("should not dispatch add or move for pre layout"); + } + if (DEBUG) { + Log.d(TAG, "dispatch (pre)" + op); + Log.d(TAG, "postponed state before:"); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + + // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial + // TODO Since move ops are pushed to end, we should not need this anymore + int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd); + if (DEBUG) { + Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart); + } + int tmpCnt = 1; + int offsetPositionForPartial = op.positionStart; + final int positionMultiplier; + switch (op.cmd) { + case UpdateOp.UPDATE: + positionMultiplier = 1; + break; + case UpdateOp.REMOVE: + positionMultiplier = 0; + break; + default: + throw new IllegalArgumentException("op should be remove or update." + op); + } + for (int p = 1; p < op.itemCount; p++) { + final int pos = op.positionStart + (positionMultiplier * p); + int updatedPos = updatePositionWithPostponed(pos, op.cmd); + if (DEBUG) { + Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos); + } + boolean continuous = false; + switch (op.cmd) { + case UpdateOp.UPDATE: + continuous = updatedPos == tmpStart + 1; + break; + case UpdateOp.REMOVE: + continuous = updatedPos == tmpStart; + break; + } + if (continuous) { + tmpCnt++; + } else { + // need to dispatch this separately + UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt); + if (DEBUG) { + Log.d(TAG, "need to dispatch separately " + tmp); + } + dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); + recycleUpdateOp(tmp); + if (op.cmd == UpdateOp.UPDATE) { + offsetPositionForPartial += tmpCnt; + } + tmpStart = updatedPos;// need to remove previously dispatched + tmpCnt = 1; + } + } + recycleUpdateOp(op); + if (tmpCnt > 0) { + UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt); + if (DEBUG) { + Log.d(TAG, "dispatching:" + tmp); + } + dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial); + recycleUpdateOp(tmp); + } + if (DEBUG) { + Log.d(TAG, "post dispatch"); + Log.d(TAG, "postponed state after:"); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + } + + void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) { + mCallback.onDispatchFirstPass(op); + switch (op.cmd) { + case UpdateOp.REMOVE: + mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.markViewHoldersUpdated(offsetStart, op.itemCount); + break; + default: + throw new IllegalArgumentException("only remove and update ops can be dispatched" + + " in first pass"); + } + } + + private int updatePositionWithPostponed(int pos, int cmd) { + final int count = mPostponedList.size(); + for (int i = count - 1; i >= 0; i--) { + UpdateOp postponed = mPostponedList.get(i); + if (postponed.cmd == UpdateOp.MOVE) { + int start, end; + if (postponed.positionStart < postponed.itemCount) { + start = postponed.positionStart; + end = postponed.itemCount; + } else { + start = postponed.itemCount; + end = postponed.positionStart; + } + if (pos >= start && pos <= end) { + //i'm affected + if (start == postponed.positionStart) { + if (cmd == UpdateOp.ADD) { + postponed.itemCount++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.itemCount--; + } + // op moved to left, move it right to revert + pos++; + } else { + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + } + // op was moved right, move left to revert + pos--; + } + } else if (pos < postponed.positionStart) { + // postponed MV is outside the dispatched OP. if it is before, offset + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + postponed.itemCount++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + postponed.itemCount--; + } + } + } else { + if (postponed.positionStart <= pos) { + if (postponed.cmd == UpdateOp.ADD) { + pos -= postponed.itemCount; + } else if (postponed.cmd == UpdateOp.REMOVE) { + pos += postponed.itemCount; + } + } else { + if (cmd == UpdateOp.ADD) { + postponed.positionStart++; + } else if (cmd == UpdateOp.REMOVE) { + postponed.positionStart--; + } + } + } + if (DEBUG) { + Log.d(TAG, "dispath (step" + i + ")"); + Log.d(TAG, "postponed state:" + i + ", pos:" + pos); + for (UpdateOp updateOp : mPostponedList) { + Log.d(TAG, updateOp.toString()); + } + Log.d(TAG, "----"); + } + } + for (int i = mPostponedList.size() - 1; i >= 0; i--) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (op.itemCount == op.positionStart || op.itemCount < 0) { + mPostponedList.remove(i); + recycleUpdateOp(op); + } + } else if (op.itemCount <= 0) { + mPostponedList.remove(i); + recycleUpdateOp(op); + } + } + return pos; + } + + private boolean canFindInPreLayout(int position) { + final int count = mPostponedList.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (findPositionOffset(op.itemCount, i + 1) == position) { + return true; + } + } else if (op.cmd == UpdateOp.ADD) { + // TODO optimize. + final int end = op.positionStart + op.itemCount; + for (int pos = op.positionStart; pos < end; pos++) { + if (findPositionOffset(pos, i + 1) == position) { + return true; + } + } + } + } + return false; + } + + private void applyAdd(UpdateOp op) { + postponeAndUpdateViewHolders(op); + } + + private void postponeAndUpdateViewHolders(UpdateOp op) { + if (DEBUG) { + Log.d(TAG, "postponing " + op); + } + mPostponedList.add(op); + switch (op.cmd) { + case UpdateOp.ADD: + mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); + break; + case UpdateOp.MOVE: + mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); + break; + case UpdateOp.REMOVE: + mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, + op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount); + break; + default: + throw new IllegalArgumentException("Unknown update op type for " + op); + } + } + + boolean hasPendingUpdates() { + return mPendingUpdates.size() > 0; + } + + int findPositionOffset(int position) { + return findPositionOffset(position, 0); + } + + int findPositionOffset(int position, int firstPostponedItem) { + int count = mPostponedList.size(); + for (int i = firstPostponedItem; i < count; ++i) { + UpdateOp op = mPostponedList.get(i); + if (op.cmd == UpdateOp.MOVE) { + if (op.positionStart == position) { + position = op.itemCount; + } else { + if (op.positionStart < position) { + position--; // like a remove + } + if (op.itemCount <= position) { + position++; // like an add + } + } + } else if (op.positionStart <= position) { + if (op.cmd == UpdateOp.REMOVE) { + if (position < op.positionStart + op.itemCount) { + return -1; + } + position -= op.itemCount; + } else if (op.cmd == UpdateOp.ADD) { + position += op.itemCount; + } + } + } + return position; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeChanged(int positionStart, int itemCount) { + mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount)); + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeInserted(int positionStart, int itemCount) { + mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount)); + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeRemoved(int positionStart, int itemCount) { + mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount)); + return mPendingUpdates.size() == 1; + } + + /** + * @return True if updates should be processed. + */ + boolean onItemRangeMoved(int from, int to, int itemCount) { + if (from == to) { + return false;//no-op + } + if (itemCount != 1) { + throw new IllegalArgumentException("Moving more than 1 item is not supported yet"); + } + mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to)); + return mPendingUpdates.size() == 1; + } + + /** + * Skips pre-processing and applies all updates in one pass. + */ + void consumeUpdatesInOnePass() { + // we still consume postponed updates (if there is) in case there was a pre-process call + // w/o a matching consumePostponedUpdates. + consumePostponedUpdates(); + final int count = mPendingUpdates.size(); + for (int i = 0; i < count; i++) { + UpdateOp op = mPendingUpdates.get(i); + switch (op.cmd) { + case UpdateOp.ADD: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount); + break; + case UpdateOp.REMOVE: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount); + break; + case UpdateOp.UPDATE: + mCallback.onDispatchSecondPass(op); + mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount); + break; + case UpdateOp.MOVE: + mCallback.onDispatchSecondPass(op); + mCallback.offsetPositionsForMove(op.positionStart, op.itemCount); + break; + } + if (mOnItemProcessedCallback != null) { + mOnItemProcessedCallback.run(); + } + } + recycleUpdateOpsAndClearList(mPendingUpdates); + } + + 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; + } + + /** + * Queued operation to happen when child views are updated. + */ + static class UpdateOp { + + static final int ADD = 0; + + static final int REMOVE = 1; + + static final int UPDATE = 2; + + static final int MOVE = 3; + + static final int POOL_SIZE = 30; + + int cmd; + + int positionStart; + + // holds the target position if this is a MOVE + int itemCount; + + UpdateOp(int cmd, int positionStart, int itemCount) { + this.cmd = cmd; + this.positionStart = positionStart; + this.itemCount = itemCount; + } + + String cmdToString() { + switch (cmd) { + case ADD: + return "add"; + case REMOVE: + return "rm"; + case UPDATE: + return "up"; + case MOVE: + return "mv"; + } + return "??"; + } + + @Override + public String toString() { + return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + UpdateOp op = (UpdateOp) o; + + if (cmd != op.cmd) { + return false; + } + if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) { + // reverse of this is also true + if (itemCount == op.positionStart && positionStart == op.itemCount) { + return true; + } + } + if (itemCount != op.itemCount) { + return false; + } + if (positionStart != op.positionStart) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = cmd; + result = 31 * result + positionStart; + result = 31 * result + itemCount; + return result; + } + } + + @Override + public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) { + UpdateOp op = mUpdateOpPool.acquire(); + if (op == null) { + op = new UpdateOp(cmd, positionStart, itemCount); + } else { + op.cmd = cmd; + op.positionStart = positionStart; + op.itemCount = itemCount; + } + return op; + } + + @Override + public void recycleUpdateOp(UpdateOp op) { + if (!mDisableRecycler) { + mUpdateOpPool.release(op); + } + } + + void recycleUpdateOpsAndClearList(List ops) { + final int count = ops.size(); + for (int i = 0; i < count; i++) { + recycleUpdateOp(ops.get(i)); + } + ops.clear(); + } + + /** + * Contract between AdapterHelper and RecyclerView. + */ + static interface Callback { + + ViewHolder findViewHolder(int position); + + void offsetPositionsForRemovingInvisible(int positionStart, int itemCount); + + void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount); + + void markViewHoldersUpdated(int positionStart, int itemCount); + + void onDispatchFirstPass(UpdateOp updateOp); + + void onDispatchSecondPass(UpdateOp updateOp); + + void offsetPositionsForAdd(int positionStart, int itemCount); + + void offsetPositionsForMove(int from, int to); + } +} diff --git a/app/src/main/java/android/support/v7/widget/ChildHelper.java b/app/src/main/java/android/support/v7/widget/ChildHelper.java new file mode 100644 index 000000000..bf6014ed0 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/ChildHelper.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.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(); + } + + /** + * 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) { + mHiddenViews.add(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)) { + mHiddenViews.remove(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)) { + mHiddenViews.remove(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(); + mHiddenViews.clear(); + 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. + * @param type View type, can be {@link RecyclerView#INVALID_TYPE}. + * @return A hidden view with a valid ViewHolder that matches the position and type. + */ + View findHiddenNonRemovedView(int position, int type) { + 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() && + (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) { + 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) { + mHiddenViews.add(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); + mHiddenViews.add(view); + if (DEBUG) { + Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this); + } + } + + @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 (mHiddenViews.remove(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 (!mHiddenViews.remove(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 { + + final static int BITS_PER_WORD = Long.SIZE; + + final static long LAST_BIT = 1L << (Long.SIZE - 1); + + long mData = 0; + + Bucket next; + + void set(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + next.set(index - BITS_PER_WORD); + } else { + mData |= 1L << index; + } + } + + private void ensureNext() { + if (next == null) { + next = new Bucket(); + } + } + + void clear(int index) { + if (index >= BITS_PER_WORD) { + if (next != null) { + next.clear(index - BITS_PER_WORD); + } + } else { + mData &= ~(1L << index); + } + + } + + boolean get(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return next.get(index - BITS_PER_WORD); + } else { + return (mData & (1L << index)) != 0; + } + } + + void reset() { + mData = 0; + if (next != null) { + next.reset(); + } + } + + void insert(int index, boolean value) { + if (index >= BITS_PER_WORD) { + ensureNext(); + next.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 || next != null) { + ensureNext(); + next.insert(0, lastBit); + } + } + } + + boolean remove(int index) { + if (index >= BITS_PER_WORD) { + ensureNext(); + return next.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 (next != null) { + if (next.get(0)) { + set(BITS_PER_WORD - 1); + } + next.remove(0); + } + return value; + } + } + + int countOnesBefore(int index) { + if (next == 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 next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData); + } + } + + @Override + public String toString() { + return next == null ? Long.toBinaryString(mData) + : next.toString() + "xx" + Long.toBinaryString(mData); + } + } + + static 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); + } +} diff --git a/app/src/main/java/android/support/v7/widget/DefaultItemAnimator.java b/app/src/main/java/android/support/v7/widget/DefaultItemAnimator.java new file mode 100644 index 000000000..cdc541809 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/DefaultItemAnimator.java @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v7.widget; + +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.ViewPropertyAnimatorListener; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.view.View; + +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 RecyclerView.ItemAnimator { + private static final boolean DEBUG = false; + + private ArrayList mPendingRemovals = new ArrayList(); + private ArrayList mPendingAdditions = new ArrayList(); + private ArrayList mPendingMoves = new ArrayList(); + private ArrayList mPendingChanges = new ArrayList(); + + private ArrayList> mAdditionsList = + new ArrayList>(); + private ArrayList> mMovesList = new ArrayList>(); + private ArrayList> mChangesList = new ArrayList>(); + + private ArrayList mAddAnimations = new ArrayList(); + private ArrayList mMoveAnimations = new ArrayList(); + private ArrayList mRemoveAnimations = new ArrayList(); + private ArrayList mChangeAnimations = new ArrayList(); + + private static class MoveInfo { + public ViewHolder holder; + public int fromX, fromY, toX, toY; + + private 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; + } + + private 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; + ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); + } else { + mover.run(); + } + } + // Next, change stuff, to run in parallel with move animations + if (changesPending) { + final ArrayList changes = new ArrayList(); + changes.addAll(mPendingChanges); + mChangesList.add(changes); + mPendingChanges.clear(); + Runnable changer = new Runnable() { + @Override + public void run() { + for (ChangeInfo change : changes) { + animateChangeImpl(change); + } + changes.clear(); + mChangesList.remove(changes); + } + }; + if (removalsPending) { + ViewHolder holder = changes.get(0).oldHolder; + ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); + } else { + changer.run(); + } + } + // Next, add stuff + if (additionsPending) { + final ArrayList additions = new ArrayList(); + additions.addAll(mPendingAdditions); + mAdditionsList.add(additions); + mPendingAdditions.clear(); + Runnable adder = new Runnable() { + 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; + ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); + } else { + adder.run(); + } + } + } + + @Override + public boolean animateRemove(final ViewHolder holder) { + endAnimation(holder); + mPendingRemovals.add(holder); + return true; + } + + private void animateRemoveImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mRemoveAnimations.add(holder); + animation.setDuration(getRemoveDuration()) + .alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchRemoveStarting(holder); + } + + @Override + public void onAnimationEnd(View view) { + animation.setListener(null); + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(holder); + mRemoveAnimations.remove(holder); + dispatchFinishedWhenDone(); + } + }).start(); + } + + @Override + public boolean animateAdd(final ViewHolder holder) { + endAnimation(holder); + ViewCompat.setAlpha(holder.itemView, 0); + mPendingAdditions.add(holder); + return true; + } + + private void animateAddImpl(final ViewHolder holder) { + final View view = holder.itemView; + final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mAddAnimations.add(holder); + animation.alpha(1).setDuration(getAddDuration()). + setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchAddStarting(holder); + } + @Override + public void onAnimationCancel(View view) { + ViewCompat.setAlpha(view, 1); + } + + @Override + public void onAnimationEnd(View view) { + 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 += ViewCompat.getTranslationX(holder.itemView); + fromY += ViewCompat.getTranslationY(holder.itemView); + endAnimation(holder); + int deltaX = toX - fromX; + int deltaY = toY - fromY; + if (deltaX == 0 && deltaY == 0) { + dispatchMoveFinished(holder); + return false; + } + if (deltaX != 0) { + ViewCompat.setTranslationX(view, -deltaX); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, -deltaY); + } + mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); + return true; + } + + private 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) { + ViewCompat.animate(view).translationX(0); + } + if (deltaY != 0) { + ViewCompat.animate(view).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 ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); + mMoveAnimations.add(holder); + animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchMoveStarting(holder); + } + @Override + public void onAnimationCancel(View view) { + if (deltaX != 0) { + ViewCompat.setTranslationX(view, 0); + } + if (deltaY != 0) { + ViewCompat.setTranslationY(view, 0); + } + } + @Override + public void onAnimationEnd(View view) { + 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) { + final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); + final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); + final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); + endAnimation(oldHolder); + int deltaX = (int) (toX - fromX - prevTranslationX); + int deltaY = (int) (toY - fromY - prevTranslationY); + // recover prev translation state after ending animation + ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); + ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); + ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); + if (newHolder != null && newHolder.itemView != null) { + // carry over translation values + endAnimation(newHolder); + ViewCompat.setTranslationX(newHolder.itemView, -deltaX); + ViewCompat.setTranslationY(newHolder.itemView, -deltaY); + ViewCompat.setAlpha(newHolder.itemView, 0); + } + mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); + return true; + } + + private 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 ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( + getChangeDuration()); + mChangeAnimations.add(changeInfo.oldHolder); + oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); + oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); + oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.oldHolder, true); + } + + @Override + public void onAnimationEnd(View view) { + oldViewAnim.setListener(null); + ViewCompat.setAlpha(view, 1); + ViewCompat.setTranslationX(view, 0); + ViewCompat.setTranslationY(view, 0); + dispatchChangeFinished(changeInfo.oldHolder, true); + mChangeAnimations.remove(changeInfo.oldHolder); + dispatchFinishedWhenDone(); + } + }).start(); + } + if (newView != null) { + final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); + mChangeAnimations.add(changeInfo.newHolder); + newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). + alpha(1).setListener(new VpaListenerAdapter() { + @Override + public void onAnimationStart(View view) { + dispatchChangeStarting(changeInfo.newHolder, false); + } + @Override + public void onAnimationEnd(View view) { + newViewAnimation.setListener(null); + ViewCompat.setAlpha(newView, 1); + ViewCompat.setTranslationX(newView, 0); + ViewCompat.setTranslationY(newView, 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; + } + ViewCompat.setAlpha(item.itemView, 1); + ViewCompat.setTranslationX(item.itemView, 0); + ViewCompat.setTranslationY(item.itemView, 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. + ViewCompat.animate(view).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) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 0); + dispatchMoveFinished(item); + mPendingMoves.remove(i); + } + } + endChangeAnimation(mPendingChanges, item); + if (mPendingRemovals.remove(item)) { + ViewCompat.setAlpha(view, 1); + dispatchRemoveFinished(item); + } + if (mPendingAdditions.remove(item)) { + ViewCompat.setAlpha(view, 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) { + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 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)) { + ViewCompat.setAlpha(view, 1); + dispatchAddFinished(item); + if (additions.isEmpty()) { + mAdditionsList.remove(i); + } + } + } + + // animations should be ended by the cancel above. + if (mRemoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mRemoveAnimations list"); + } + + if (mAddAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mAddAnimations list"); + } + + if (mChangeAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mChangeAnimations list"); + } + + if (mMoveAnimations.remove(item) && DEBUG) { + throw new IllegalStateException("after animation is cancelled, item should not be in " + + "mMoveAnimations list"); + } + dispatchFinishedWhenDone(); + } + + @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. + */ + private 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; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 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); + View view = item.itemView; + ViewCompat.setAlpha(view, 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; + ViewCompat.setTranslationY(view, 0); + ViewCompat.setTranslationX(view, 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; + ViewCompat.setAlpha(view, 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--) { + ViewCompat.animate(viewHolders.get(i).itemView).cancel(); + } + } + + private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { + @Override + public void onAnimationStart(View view) {} + + @Override + public void onAnimationEnd(View view) {} + + @Override + public void onAnimationCancel(View view) {} + }; +} diff --git a/app/src/main/java/android/support/v7/widget/LayoutState.java b/app/src/main/java/android/support/v7/widget/LayoutState.java new file mode 100644 index 000000000..e62a80a28 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/LayoutState.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific languag`e governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; +import android.view.View; + +/** + * Helper class that keeps temporary state while {LayoutManager} is filling out the empty + * space. + */ +class LayoutState { + + final static String TAG = "LayoutState"; + + final static int LAYOUT_START = -1; + + final static int LAYOUT_END = 1; + + final static int INVALID_LAYOUT = Integer.MIN_VALUE; + + final static int ITEM_DIRECTION_HEAD = -1; + + final static int ITEM_DIRECTION_TAIL = 1; + + final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE; + + /** + * 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 if you want to pre-layout items that are not yet visible. + * The difference with {@link #mAvailable} is that, when recycling, distance rendered for + * {@link #mExtra} is not considered not to recycle visible children. + */ + int mExtra = 0; + + /** + * @return true if there are more items in the data adapter + */ + boolean hasMore(RecyclerView.State state) { + return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); + } + + /** + * Gets the view for the next element that we should render. + * Also updates current item index to the next item, based on {@link #mItemDirection} + * + * @return The next element that we should render. + */ + View next(RecyclerView.Recycler recycler) { + final View view = recycler.getViewForPosition(mCurrentPosition); + mCurrentPosition += mItemDirection; + return view; + } +} diff --git a/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java b/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java new file mode 100644 index 000000000..04d5c2d21 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/LinearLayoutManager.java @@ -0,0 +1,2131 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific languag`e governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.content.Context; +import android.graphics.PointF; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import java.util.List; + +import static android.support.v7.widget.RecyclerView.NO_POSITION; + +/** + * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides + * similar functionality to {@link android.widget.ListView}. + */ +public class LinearLayoutManager extends RecyclerView.LayoutManager { + + private static final String TAG = "LinearLayoutManager"; + + private 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 = 0.33f; + + + /** + * 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; + + /** + * 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) { + mAnchorInfo = new AnchorInfo(); + setOrientation(orientation); + setReverseLayout(reverseLayout); + } + + /** + * {@inheritDoc} + */ + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + /** + * Returns whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + * + * @return true if LayoutManager will recycle its children when it is detached from + * RecyclerView. + */ + public boolean getRecycleChildrenOnDetach() { + return mRecycleChildrenOnDetach; + } + + /** + * Set whether LayoutManager will recycle its children when it is detached from + * RecyclerView. + *

+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set + * this flag to true so that views will be avilable 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) { + final AccessibilityRecordCompat record = AccessibilityEventCompat + .asRecord(event); + record.setFromIndex(findFirstVisibleItemPosition()); + record.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 orientaion of the layout. + * + * @return Current orientation. + * @see #mOrientation + * @see #setOrientation(int) + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Sets the orientation of the layout. {@link android.support.v7.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 {@link #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 android.support.v7.widget.RecyclerView} is LTR, than it will + * layout from RTL, if {@link android.support.v7.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) { + return getChildAt(viewPosition); + } + return null; + } + + /** + *

Returns the amount of extra space that should be laid out by LayoutManager. + * By default, {@link android.support.v7.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.

+ *

Laying out invisible elements will eventually come with performance cost. On the other + * hand, in places like smooth scrolling to an unknown location, this extra content helps + * LayoutManager to calculate a much smoother scrolling; which improves user experience.

+ *

You can also use this if you are trying to pre-layout your upcoming views.

+ * + * @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()) { + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + return LinearLayoutManager.this + .computeScrollVectorForPosition(targetPosition); + } + }; + linearSmoothScroller.setTargetPosition(position); + startSmoothScroll(linearSmoothScroller); + } + + 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 && mPendingSavedState.hasValidAnchor()) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + } + + ensureLayoutState(); + mLayoutState.mRecycle = false; + // resolve layout direction + resolveShouldLayoutReverse(); + + mAnchorInfo.reset(); + mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; + // calculate anchor position and coordinate + updateAnchorInfoForLayout(state, mAnchorInfo); + 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; + onAnchorReady(state, mAnchorInfo); + detachAndScrapAttachedViews(recycler); + 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()) { + mPendingScrollPosition = NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + mOrientationHelper.onLayoutComplete(); + } + mLastStackFromEnd = mStackFromEnd; + mPendingSavedState = null; // we don't need this anymore + if (DEBUG) { + validateChildOrder(); + } + } + + /** + * Method called when Anchor position is decided. Extending class can setup accordingly or + * even update anchor info if necessary. + * + * @param state + * @param anchorInfo Simple data structure to keep anchor point information for the next layout + */ + void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) { + } + + /** + * 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.State state, AnchorInfo anchorInfo) { + if (updateAnchorFromPendingData(state, anchorInfo)) { + if (DEBUG) { + Log.d(TAG, "updated anchor info from pending information"); + } + return; + } + + if (updateAnchorFromChildren(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.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(state) + : findReferenceChildClosestToStart(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 (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.SCOLLING_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.SCOLLING_OFFSET_NaN; + + } + + protected boolean isLayoutRTL() { + return getLayoutDirection() == ViewCompat.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 ViewCompat#getLayoutDirection(android.view.View)} and {@link #getStackFromEnd()}. + *

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

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

+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. + * + * @param position Index (starting at 0) of the reference item. + * @param offset The distance (in pixels) between the start edge of the item view and + * start edge of the RecyclerView. + * @see #setReverseLayout(boolean) + * @see #scrollToPosition(int) + */ + public void scrollToPositionWithOffset(int position, int offset) { + mPendingScrollPosition = position; + mPendingScrollPositionOffset = offset; + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchor(); + } + requestLayout(); + } + + + /** + * {@inheritDoc} + */ + @Override + 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) { + mLayoutState.mExtra = getExtraLayoutSpace(state); + mLayoutState.mLayoutDirection = layoutDirection; + int fastScrollSpace; + 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) + fastScrollSpace = 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); + fastScrollSpace = -mOrientationHelper.getDecoratedStart(child) + + mOrientationHelper.getStartAfterPadding(); + } + mLayoutState.mAvailable = requiredSpace; + if (canUseExistingSpace) { + mLayoutState.mAvailable -= fastScrollSpace; + } + mLayoutState.mScrollingOffset = fastScrollSpace; + } + + 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 freeScroll = mLayoutState.mScrollingOffset; + final int consumed = freeScroll + 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. + * + * @param recycler Recycler instance of {@link android.support.v7.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) {// 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) {// stop here + recycleChildren(recycler, 0, i); + return; + } + } + } + } + + + /** + * Recycles views that went out of bounds after scrolling towards the start of the layout. + * + * @param recycler Recycler instance of {@link android.support.v7.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) {// 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) {// 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(android.support.v7.widget.RecyclerView.Recycler, int) + * @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int) + * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection + */ + private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { + if (!layoutState.mRecycle) { + 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 android.support.v7.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 scoll 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.SCOLLING_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 = new LayoutChunkResult(); + while (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.SCOLLING_OFFSET_NaN) { + layoutState.mScrollingOffset += layoutChunkResult.mConsumed; + if (layoutState.mAvailable < 0) { + layoutState.mScrollingOffset += layoutState.mAvailable; + } + recycleByLayoutState(recycler, layoutState); + } + if (stopOnFocusable && layoutChunkResult.mFocusable) { + break; + } + } + if (DEBUG) { + validateChildOrder(); + } + return start - layoutState.mAvailable; + } + + void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, + LayoutState layoutState, LayoutChunkResult result) { + View view = layoutState.next(recycler); + if (view == null) { + if (DEBUG && layoutState.mScrapList == null) { + throw new RuntimeException("received null view when unexpected"); + } + // if we are laying out views in scrap, this may return null which means there is + // no more items to layout. + result.mFinished = true; + return; + } + RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); + if (layoutState.mScrapList == null) { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addView(view); + } else { + addView(view, 0); + } + } else { + if (mShouldReverseLayout == (layoutState.mLayoutDirection + == LayoutState.LAYOUT_START)) { + addDisappearingView(view); + } else { + addDisappearingView(view, 0); + } + } + measureChildWithMargins(view, 0, 0); + result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); + int left, top, right, bottom; + if (mOrientation == VERTICAL) { + if (isLayoutRTL()) { + right = getWidth() - getPaddingRight(); + left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); + } else { + left = getPaddingLeft(); + right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); + } + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + bottom = layoutState.mOffset; + top = layoutState.mOffset - result.mConsumed; + } else { + top = layoutState.mOffset; + bottom = layoutState.mOffset + result.mConsumed; + } + } else { + top = getPaddingTop(); + bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); + + if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { + right = layoutState.mOffset; + left = layoutState.mOffset - result.mConsumed; + } else { + left = layoutState.mOffset; + right = layoutState.mOffset + result.mConsumed; + } + } + // We calculate everything with View's bounding box (which includes decor and margins) + // To calculate correct layout position, we subtract margins. + layoutDecorated(view, left + params.leftMargin, top + params.topMargin, + right - params.rightMargin, bottom - params.bottomMargin); + 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(); + } + + /** + * Converts a focusDirection to orientation. + * + * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} + * or 0 for not applicable + * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction + * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. + */ + private int convertFocusDirectionToLayoutDirection(int focusDirection) { + switch (focusDirection) { + case View.FOCUS_BACKWARD: + return LayoutState.LAYOUT_START; + case View.FOCUS_FORWARD: + 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.State state) { + return mShouldReverseLayout ? findFirstReferenceChild(state.getItemCount()) : + findLastReferenceChild(state.getItemCount()); + } + + /** + * 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.State state) { + return mShouldReverseLayout ? findLastReferenceChild(state.getItemCount()) : + findFirstReferenceChild(state.getItemCount()); + } + + private View findFirstReferenceChild(int itemCount) { + return findReferenceChild(0, getChildCount(), itemCount); + } + + private View findLastReferenceChild(int itemCount) { + return findReferenceChild(getChildCount() - 1, -1, itemCount); + } + + // overridden by GridLayoutManager + View findReferenceChild(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 (((RecyclerView.LayoutParams) view.getLayoutParams()).isItemRemoved()) { + if (invalidMatch == null) { + invalidMatch = view; // removed item, least preferred + } + } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd || + mOrientationHelper.getDecoratedEnd(view) < boundsStart) { + if (outOfBoundsMatch == null) { + outOfBoundsMatch = view; // item is not visible, less preferred + } + } else { + return view; + } + } + } + return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; + } + + /** + * 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(state); + } else { + referenceChild = findReferenceChildClosestToEnd(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.SCOLLING_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; + } + + /** + * Helper class that keeps temporary state while {LayoutManager} is filling out the empty + * space. + */ + static class LayoutState { + + final static String TAG = "LinearLayoutManager#LayoutState"; + + final static int LAYOUT_START = -1; + + final static int LAYOUT_END = 1; + + final static int INVALID_LAYOUT = Integer.MIN_VALUE; + + final static int ITEM_DIRECTION_HEAD = -1; + + final static int ITEM_DIRECTION_TAIL = 1; + + final static int SCOLLING_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; + + /** + * @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 RecyclerView.ViewHolder viewHolder = mScrapList.get(i); + if (viewHolder.isRemoved()) { + continue; + } + if (mCurrentPosition == viewHolder.getLayoutPosition()) { + assignPositionFromScrapList(viewHolder); + return viewHolder.itemView; + } + } + return null; + } + + public void assignPositionFromScrapList() { + assignPositionFromScrapList(null); + } + + public void assignPositionFromScrapList(RecyclerView.ViewHolder ignore) { + RecyclerView.ViewHolder closest = nextViewHolderInLimitedList(ignore); + mCurrentPosition = closest == null ? RecyclerView.NO_POSITION : + closest.getLayoutPosition(); + } + + public RecyclerView.ViewHolder nextViewHolderInLimitedList(RecyclerView.ViewHolder ignore) { + int size = mScrapList.size(); + RecyclerView.ViewHolder 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++) { + RecyclerView.ViewHolder viewHolder = mScrapList.get(i); + if (viewHolder == ignore || viewHolder.isRemoved()) { + continue; + } + final int distance = (viewHolder.getLayoutPosition() - mCurrentPosition) * + mItemDirection; + if (distance < 0) { + continue; // item is not in current direction + } + if (distance < closestDistance) { + closest = viewHolder; + closestDistance = distance; + if (distance == 0) { + break; + } + } + } + return closest; + } + + void log() { + Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); + } + } + + 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; + void reset() { + mPosition = NO_POSITION; + mCoordinate = INVALID_OFFSET; + mLayoutFromEnd = 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 + + '}'; + } + + private boolean isViewValidAsAnchor(View child, RecyclerView.State state) { + RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams(); + return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0 + && lp.getViewLayoutPosition() < state.getItemCount(); + } + + public void assignFromViewAndKeepVisibleRect(View child) { + 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/app/src/main/java/android/support/v7/widget/LinearSmoothScroller.java b/app/src/main/java/android/support/v7/widget/LinearSmoothScroller.java new file mode 100644 index 000000000..ed4c95015 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/LinearSmoothScroller.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.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 + * {@link android.view.animation.LinearInterpolator} until the target position becames a child of + * the RecyclerView and then uses + * {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position. + */ +abstract 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; + } + 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)) { + Log.e(TAG, "To support smooth scrolling, you should override \n" + + "LayoutManager#computeScrollVectorForPosition.\n" + + "Falling back to instant scroll"); + final int target = getTargetPosition(); + stop(); + instantScrollToPosition(target); + 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_END}. + * @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.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.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); + } + + abstract public PointF computeScrollVectorForPosition(int targetPosition); +} diff --git a/app/src/main/java/android/support/v7/widget/OpReorderer.java b/app/src/main/java/android/support/v7/widget/OpReorderer.java new file mode 100644 index 000000000..1a34cba3e --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/OpReorderer.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.support.v7.widget.AdapterHelper.UpdateOp; + +import java.util.List; + +import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD; +import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE; +import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE; +import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE; + +class OpReorderer { + + final Callback mCallback; + + public 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); + 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); + } + // 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.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; + } + + static interface Callback { + + UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount); + + void recycleUpdateOp(UpdateOp op); + } +} diff --git a/app/src/main/java/android/support/v7/widget/OrientationHelper.java b/app/src/main/java/android/support/v7/widget/OrientationHelper.java new file mode 100644 index 000000000..8ca9851e3 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/OrientationHelper.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.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; + + 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 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(); + + /** + * 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 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(); + } + }; + } + + /** + * 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 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(); + } + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/android/support/v7/widget/RecyclerView.java b/app/src/main/java/android/support/v7/widget/RecyclerView.java new file mode 100644 index 000000000..c610c480b --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/RecyclerView.java @@ -0,0 +1,9334 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v7.widget; + +import android.content.Context; +import android.database.Observable; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v4.os.TraceCompat; +import android.support.v4.util.ArrayMap; +import android.support.v4.view.InputDeviceCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ScrollingView; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewConfigurationCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.support.v4.widget.ScrollerCompat; +import static android.support.v7.widget.AdapterHelper.UpdateOp; +import static android.support.v7.widget.AdapterHelper.Callback; + +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Interpolator; + +import 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. + */ +public class RecyclerView extends ViewGroup implements ScrollingView { + + private static final String TAG = "RecyclerView"; + + private static final boolean DEBUG = false; + + /** + * 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. + */ + private static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18 + || Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20; + + private 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; + + private 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. + */ + private 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. + */ + private static final String TRACE_BIND_VIEW_TAG = "RV OnBindView"; + + /** + * 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. + */ + private static final String TRACE_CREATE_VIEW_TAG = "RV CreateView"; + + private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver(); + + final Recycler mRecycler = new Recycler(); + + private SavedState mPendingSavedState; + + AdapterHelper mAdapterHelper; + + ChildHelper mChildHelper; + + /** + * Prior to L, there is no way to query this variable which is why we override the setter and + * track it here. + */ + private 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 + */ + private final Runnable mUpdateChildViewsRunnable = new Runnable() { + public void run() { + if (!mFirstLayoutComplete) { + // a layout request will happen, we should not do layout here. + return; + } + if (mDataSetHasChangedAfterLayout) { + TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG); + dispatchLayout(); + TraceCompat.endSection(); + } else if (mAdapterHelper.hasPendingUpdates()) { + TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG); + eatRequestLayout(); + mAdapterHelper.preProcess(); + if (!mLayoutRequestEaten) { + // We run this after pre-processing is complete so that ViewHolders have their + // final adapter positions. No need to run it if a layout is already requested. + rebindUpdatedViewHolders(); + } + resumeRequestLayout(true); + TraceCompat.endSection(); + } + } + }; + + private final Rect mTempRect = new Rect(); + private Adapter mAdapter; + private LayoutManager mLayout; + private RecyclerListener mRecyclerListener; + private final ArrayList mItemDecorations = new ArrayList(); + private final ArrayList mOnItemTouchListeners = + new ArrayList(); + private OnItemTouchListener mActiveOnItemTouchListener; + private boolean mIsAttached; + private boolean mHasFixedSize; + private boolean mFirstLayoutComplete; + private boolean mEatRequestLayout; + private boolean mLayoutRequestEaten; + // binary OR of change events that were eaten during a layout or scroll. + private int mEatenAccessibilityChangeFlags; + private boolean mAdapterUpdateDuringMeasure; + private final boolean mPostUpdatesOnAnimation; + private final AccessibilityManager mAccessibilityManager; + + /** + * 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. + */ + private 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; + + private EdgeEffectCompat 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; + + // 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 final int mMinFlingVelocity; + private final int mMaxFlingVelocity; + // This value is used when handling generic motion events. + private float mScrollFactor = Float.MIN_VALUE; + + private final ViewFlinger mViewFlinger = new ViewFlinger(); + + 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(); + private boolean mPostedAnimatorRunner = false; + private RecyclerViewAccessibilityDelegate mAccessibilityDelegate; + + // 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 Runnable mItemAnimatorRunner = new Runnable() { + @Override + public void run() { + if (mItemAnimator != null) { + mItemAnimator.runPendingAnimations(); + } + mPostedAnimatorRunner = false; + } + }; + + private static final Interpolator sQuinticInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + 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); + setFocusableInTouchMode(true); + final int version = Build.VERSION.SDK_INT; + mPostUpdatesOnAnimation = version >= 16; + + final ViewConfiguration vc = ViewConfiguration.get(context); + mTouchSlop = vc.getScaledTouchSlop(); + mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); + mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); + setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER); + + mItemAnimator.setListener(mItemAnimatorListener); + initAdapterManager(); + initChildrenHelper(); + // If not explicitly specified this view is important for accessibility. + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + mAccessibilityManager = (AccessibilityManager) getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); + } + + /** + * 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; + ViewCompat.setAccessibilityDelegate(this, mAccessibilityDelegate); + } + + 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); + } + }); + } + + void initAdapterManager() { + mAdapterHelper = new AdapterHelper(new 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) { + viewRangeUpdate(positionStart, itemCount); + mItemsChanged = true; + } + + @Override + public void onDispatchFirstPass(UpdateOp op) { + dispatchUpdate(op); + } + + void dispatchUpdate(UpdateOp op) { + switch (op.cmd) { + case UpdateOp.ADD: + mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount); + break; + case UpdateOp.REMOVE: + mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount); + break; + case UpdateOp.UPDATE: + mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount); + break; + case UpdateOp.MOVE: + mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1); + break; + } + } + + @Override + public void onDispatchSecondPass(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 changes in + * adapter content cannot change the size of the RecyclerView itself. + * If your use of RecyclerView falls into this category, set this to true. + * + * @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(); + } + } + + /** + * 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 = ViewConfigurationCompat.getScaledPagingTouchSlop(vc); + 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) { + 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) { + setAdapterInternal(adapter, false, true); + requestLayout(); + } + + /** + * 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) { + // 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(); + } + 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(); + } + } + + /** + * 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; + } + // TODO We should do this switch a dispachLayout pass and animate children. There is a good + // chance that LayoutManagers will re-use views. + if (mLayout != null) { + if (mIsAttached) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + mLayout.setRecyclerView(null); + } + mRecycler.clear(); + 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); + } + } + requestLayout(); + } + + @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) { + 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 + */ + private 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); + } + } + resumeRequestLayout(false); + 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; + } + + private 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(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER); + } + markItemDecorInsetsDirty(); + requestLayout(); + } + + /** + * 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 android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)} + * @param position Scroll to this adapter position + * @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int) + */ + public void scrollToPosition(int position) { + 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(); + } + + /** + * 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 (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) { + throw new UnsupportedOperationException( + "RecyclerView does not support scrolling to an absolute position."); + } + + @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; + } + final boolean canScrollHorizontal = mLayout.canScrollHorizontally(); + final boolean canScrollVertical = mLayout.canScrollVertically(); + if (canScrollHorizontal || canScrollVertical) { + scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, false, 0, 0); + } + } + + /** + * 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. + */ + private void consumePendingUpdateOperations() { + mUpdateChildViewsRunnable.run(); + } + + /** + * 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 fromMotionEvent If request is originated from a MotionEvent, this should be set to + * true and motionX/motionY should be provided, false otherwise. + * @param motionX The x coordinate of the MotionEvent which triggered this scroll. Unused if + * fromMotionEvent is false. + * @param motionY The y coordinate of the MotionEvent which triggered this scroll. Unused if + * fromMotionEvent is false. + * + * @return Whether any scroll was consumed in either direction. + */ + boolean scrollByInternal(int x, int y, boolean fromMotionEvent, int motionX, int motionY) { + int overscrollX = 0, overscrollY = 0; + int hresult = 0, vresult = 0; + consumePendingUpdateOperations(); + if (mAdapter != null) { + eatRequestLayout(); + onEnterLayoutOrScroll(); + TraceCompat.beginSection(TRACE_SCROLL_TAG); + if (x != 0) { + hresult = mLayout.scrollHorizontallyBy(x, mRecycler, mState); + overscrollX = x - hresult; + } + if (y != 0) { + vresult = mLayout.scrollVerticallyBy(y, mRecycler, mState); + overscrollY = y - vresult; + } + TraceCompat.endSection(); + if (supportsChangeAnimations()) { + // Fix up shadow views used by changing 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) { + ViewHolder shadowingHolder = holder.mShadowingHolder; + View shadowingView = shadowingHolder != null ? shadowingHolder.itemView : null; + if (shadowingView != null) { + 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()); + } + } + } + } + } + onExitLayoutOrScroll(); + resumeRequestLayout(false); + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { + if (fromMotionEvent) { + pullGlows(motionX, overscrollX, motionY, overscrollY); + } + considerReleasingGlowsOnScroll(x, y); + } + if (hresult != 0 || vresult != 0) { + dispatchOnScrolled(hresult, vresult); + } + if (!awakenScrollBars()) { + invalidate(); + } + return hresult != 0 || vresult != 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 android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset + * (RecyclerView.Adapter) + */ + @Override + public int computeHorizontalScrollOffset() { + 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() { + 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() { + 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 android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset + * (RecyclerView.Adapter) + */ + @Override + public int computeVerticalScrollOffset() { + 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() { + 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() { + return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mState) : 0; + } + + + void eatRequestLayout() { + if (!mEatRequestLayout) { + mEatRequestLayout = true; + mLayoutRequestEaten = false; + } + } + + void resumeRequestLayout(boolean performLayoutChildren) { + if (mEatRequestLayout) { + if (performLayoutChildren && mLayoutRequestEaten && + mLayout != null && mAdapter != null) { + dispatchLayout(); + } + mEatRequestLayout = false; + mLayoutRequestEaten = false; + } + } + + /** + * 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) { + if (mLayout == null) { + Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return; + } + if (!mLayout.canScrollHorizontally()) { + dx = 0; + } + if (!mLayout.canScrollVertically()) { + dy = 0; + } + if (dx != 0 || dy != 0) { + mViewFlinger.smoothScrollBy(dx, dy); + } + } + + /** + * Begin a standard fling with an initial velocity along each axis in pixels per second. + * If the velocity given is below the system-defined minimum this method will return false + * and no fling will occur. + * + * @param velocityX Initial horizontal velocity in pixels per second + * @param velocityY Initial vertical velocity in pixels per second + * @return true if the fling was started, false if the velocity was too low to fling or + * LayoutManager does not support scrolling in the axis fling is issued. + * + * @see LayoutManager#canScrollVertically() + * @see LayoutManager#canScrollHorizontally() + */ + public boolean fling(int velocityX, int velocityY) { + if (mLayout == null) { + Log.e(TAG, "Cannot fling without a LayoutManager set. " + + "Call setLayoutManager with a non-null argument."); + return false; + } + 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; + } + velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity)); + velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity)); + if (velocityX != 0 || velocityY != 0) { + 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(); + } + } + + /** + * Apply a pull to relevant overscroll glow effects + */ + private void pullGlows(int x, int overscrollX, int y, int overscrollY) { + boolean invalidate = false; + if (overscrollX < 0) { + ensureLeftGlow(); + invalidate = mLeftGlow.onPull(-overscrollX / (float) getWidth(), + 1f - y / (float) getHeight()) || invalidate; + } else if (overscrollX > 0) { + ensureRightGlow(); + invalidate = mRightGlow.onPull(overscrollX / (float) getWidth(), + y / (float) getHeight()) || invalidate; + } + + if (overscrollY < 0) { + ensureTopGlow(); + invalidate = mTopGlow.onPull(-overscrollY / (float) getHeight(), + x / (float) getWidth()) || invalidate; + } else if (overscrollY > 0) { + ensureBottomGlow(); + invalidate = mBottomGlow.onPull(overscrollY / (float) getHeight(), + 1f - x / (float) getWidth()) || invalidate; + } + + if (invalidate || overscrollX != 0 || overscrollY != 0) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void releaseGlows() { + boolean needsInvalidate = false; + if (mLeftGlow != null) needsInvalidate = mLeftGlow.onRelease(); + if (mTopGlow != null) needsInvalidate |= mTopGlow.onRelease(); + if (mRightGlow != null) needsInvalidate |= mRightGlow.onRelease(); + if (mBottomGlow != null) needsInvalidate |= mBottomGlow.onRelease(); + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void considerReleasingGlowsOnScroll(int dx, int dy) { + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) { + needsInvalidate = mLeftGlow.onRelease(); + } + if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) { + needsInvalidate |= mRightGlow.onRelease(); + } + if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) { + needsInvalidate |= mTopGlow.onRelease(); + } + if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) { + needsInvalidate |= mBottomGlow.onRelease(); + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + 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) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + void ensureLeftGlow() { + if (mLeftGlow != null) { + return; + } + mLeftGlow = new EdgeEffectCompat(getContext()); + if (mClipToPadding) { + mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mLeftGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureRightGlow() { + if (mRightGlow != null) { + return; + } + mRightGlow = new EdgeEffectCompat(getContext()); + if (mClipToPadding) { + mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), + getMeasuredWidth() - getPaddingLeft() - getPaddingRight()); + } else { + mRightGlow.setSize(getMeasuredHeight(), getMeasuredWidth()); + } + } + + void ensureTopGlow() { + if (mTopGlow != null) { + return; + } + mTopGlow = new EdgeEffectCompat(getContext()); + if (mClipToPadding) { + mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mTopGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + + } + + void ensureBottomGlow() { + if (mBottomGlow != null) { + return; + } + mBottomGlow = new EdgeEffectCompat(getContext()); + if (mClipToPadding) { + mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); + } else { + mBottomGlow.setSize(getMeasuredWidth(), getMeasuredHeight()); + } + } + + void invalidateGlows() { + mLeftGlow = mRightGlow = mTopGlow = mBottomGlow = null; + } + + // Focus handling + + @Override + public View focusSearch(View focused, int direction) { + View result = mLayout.onInterceptFocusSearch(focused, direction); + if (result != null) { + return result; + } + final FocusFinder ff = FocusFinder.getInstance(); + result = ff.findNextFocus(this, focused, direction); + if (result == null && mAdapter != null && mLayout != null) { + eatRequestLayout(); + result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState); + resumeRequestLayout(false); + } + return result != null ? result : super.focusSearch(focused, 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 void onAttachedToWindow() { + super.onAttachedToWindow(); + mLayoutOrScrollCounter = 0; + mIsAttached = true; + mFirstLayoutComplete = false; + if (mLayout != null) { + mLayout.dispatchAttachedToWindow(this); + } + mPostedAnimatorRunner = false; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mItemAnimator != null) { + mItemAnimator.endAnimations(); + } + mFirstLayoutComplete = false; + + stopScroll(); + mIsAttached = false; + if (mLayout != null) { + mLayout.dispatchDetachedFromWindow(this, mRecycler); + } + removeCallbacks(mItemAnimatorRunner); + } + + /** + * 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 (!isRunningLayoutOrScroll()) { + 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 (isRunningLayoutOrScroll()) { + if (message == null) { + throw new IllegalStateException("Cannot call this method while RecyclerView is " + + "computing a layout or scrolling"); + } + throw new IllegalStateException(message); + } + } + + /** + * 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 + */ + 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 (dispatchOnItemTouchIntercept(e)) { + cancelTouch(); + return true; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(e); + + final int action = MotionEventCompat.getActionMasked(e); + final int actionIndex = MotionEventCompat.getActionIndex(e); + + switch (action) { + case MotionEvent.ACTION_DOWN: + mScrollPointerId = MotionEventCompat.getPointerId(e, 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); + } + break; + + case MotionEventCompat.ACTION_POINTER_DOWN: + mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex); + mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f); + break; + + case MotionEvent.ACTION_MOVE: { + final int index = MotionEventCompat.findPointerIndex(e, 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) (MotionEventCompat.getX(e, index) + 0.5f); + final int y = (int) (MotionEventCompat.getY(e, 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 MotionEventCompat.ACTION_POINTER_UP: { + onPointerUp(e); + } break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.clear(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } + } + return mScrollState == SCROLL_STATE_DRAGGING; + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + if (dispatchOnItemTouch(e)) { + cancelTouch(); + return true; + } + + final boolean canScrollHorizontally = mLayout.canScrollHorizontally(); + final boolean canScrollVertically = mLayout.canScrollVertically(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(e); + + final int action = MotionEventCompat.getActionMasked(e); + final int actionIndex = MotionEventCompat.getActionIndex(e); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mScrollPointerId = MotionEventCompat.getPointerId(e, 0); + mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f); + mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); + } break; + + case MotionEventCompat.ACTION_POINTER_DOWN: { + mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex); + mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f); + } break; + + case MotionEvent.ACTION_MOVE: { + final int index = MotionEventCompat.findPointerIndex(e, 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) (MotionEventCompat.getX(e, index) + 0.5f); + final int y = (int) (MotionEventCompat.getY(e, 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); + } + } + if (mScrollState == SCROLL_STATE_DRAGGING) { + final int dx = x - mLastTouchX; + final int dy = y - mLastTouchY; + if (scrollByInternal(canScrollHorizontally ? -dx : 0, + canScrollVertically ? -dy : 0, true, x, y)) { + getParent().requestDisallowInterceptTouchEvent(true); + } + } + mLastTouchX = x; + mLastTouchY = y; + } break; + + case MotionEventCompat.ACTION_POINTER_UP: { + onPointerUp(e); + } break; + + case MotionEvent.ACTION_UP: { + mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity); + final float xvel = canScrollHorizontally ? + -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0; + final float yvel = canScrollVertically ? + -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0; + if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) { + setScrollState(SCROLL_STATE_IDLE); + } + mVelocityTracker.clear(); + releaseGlows(); + } break; + + case MotionEvent.ACTION_CANCEL: { + cancelTouch(); + } break; + } + + return true; + } + + private void cancelTouch() { + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + releaseGlows(); + setScrollState(SCROLL_STATE_IDLE); + } + + private void onPointerUp(MotionEvent e) { + final int actionIndex = MotionEventCompat.getActionIndex(e); + if (MotionEventCompat.getPointerId(e, actionIndex) == mScrollPointerId) { + // Pick a new pointer to pick up the slack. + final int newIndex = actionIndex == 0 ? 1 : 0; + mScrollPointerId = MotionEventCompat.getPointerId(e, newIndex); + mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, newIndex) + 0.5f); + mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, newIndex) + 0.5f); + } + } + + // @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if (mLayout == null) { + return false; + } + if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + if (event.getAction() == MotionEventCompat.ACTION_SCROLL) { + final float vScroll, hScroll; + if (mLayout.canScrollVertically()) { + vScroll = MotionEventCompat + .getAxisValue(event, MotionEventCompat.AXIS_VSCROLL); + } else { + vScroll = 0f; + } + if (mLayout.canScrollHorizontally()) { + hScroll = MotionEventCompat + .getAxisValue(event, MotionEventCompat.AXIS_HSCROLL); + } else { + hScroll = 0f; + } + + if (vScroll != 0 || hScroll != 0) { + final float scrollFactor = getScrollFactor(); + scrollBy((int) (hScroll * scrollFactor), (int) (vScroll * scrollFactor)); + } + } + } + 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 (mAdapterUpdateDuringMeasure) { + eatRequestLayout(); + processAdapterUpdatesAndSetAnimationFlags(); + + if (mState.mRunPredictiveAnimations) { + // TODO: try to provide a better approach. + // When RV decides to run predictive animations, we need to measure in pre-layout + // state so that pre-layout pass results in correct layout. + // On the other hand, this will prevent the layout manager from resizing properly. + 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; + } + if (mLayout == null) { + defaultOnMeasure(widthSpec, heightSpec); + } else { + mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); + } + + mState.mInPreLayout = false; // clear + } + + /** + * Used when onMeasure is called before layout manager is set + */ + private void defaultOnMeasure(int widthSpec, int heightSpec) { + final int widthMode = MeasureSpec.getMode(widthSpec); + final int heightMode = MeasureSpec.getMode(heightSpec); + final int widthSize = MeasureSpec.getSize(widthSpec); + final int heightSize = MeasureSpec.getSize(heightSpec); + + int width = 0; + int height = 0; + + switch (widthMode) { + case MeasureSpec.EXACTLY: + case MeasureSpec.AT_MOST: + width = widthSize; + break; + case MeasureSpec.UNSPECIFIED: + default: + width = ViewCompat.getMinimumWidth(this); + break; + } + + switch (heightMode) { + case MeasureSpec.EXACTLY: + case MeasureSpec.AT_MOST: + height = heightSize; + break; + case MeasureSpec.UNSPECIFIED: + default: + height = ViewCompat.getMinimumHeight(this); + break; + } + + 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(); + } + } + + /** + * 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); + } + } + + private void onEnterLayoutOrScroll() { + mLayoutOrScrollCounter ++; + } + + private 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(); + } + } + + private void dispatchContentChangedIfNecessary() { + final int flags = mEatenAccessibilityChangeFlags; + mEatenAccessibilityChangeFlags = 0; + if (flags != 0 && mAccessibilityManager != null && mAccessibilityManager.isEnabled()) { + final AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + AccessibilityEventCompat.setContentChangeTypes(event, flags); + sendAccessibilityEventUnchecked(event); + } + } + + boolean isRunningLayoutOrScroll() { + 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 (isRunningLayoutOrScroll()) { + int type = 0; + if (event != null) { + type = AccessibilityEventCompat.getContentChangeTypes(event); + } + if (type == 0) { + type = AccessibilityEventCompat.CONTENT_CHANGE_TYPE_UNDEFINED; + } + mEatenAccessibilityChangeFlags |= type; + return true; + } + return false; + } + + @Override + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + if (shouldDeferAccessibilityEvent(event)) { + return; + } + super.sendAccessibilityEventUnchecked(event); + } + + /** + * 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; + } + + private boolean supportsChangeAnimations() { + return mItemAnimator != null && mItemAnimator.getSupportsChangeAnimations(); + } + + /** + * 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. + */ + private void postAnimationRunner() { + if (!mPostedAnimatorRunner && mIsAttached) { + ViewCompat.postOnAnimation(this, mItemAnimatorRunner); + mPostedAnimatorRunner = true; + } + } + + private boolean predictiveItemAnimationsEnabled() { + return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations()); + } + + /** + * Consumes adapter updates and calculates which type of animations we want to run. + * Called in onMeasure and dispatchLayout. + *

+ * This method may process only the pre-layout state of updates or all of them. + */ + private void processAdapterUpdatesAndSetAnimationFlags() { + if (mDataSetHasChangedAfterLayout) { + // Processing these items have no value since data set changed unexpectedly. + // Instead, we just reset it. + mAdapterHelper.reset(); + markKnownViewsInvalid(); + 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 (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations()) { + mAdapterHelper.preProcess(); + } else { + mAdapterHelper.consumeUpdatesInOnePass(); + } + boolean animationTypeSupported = (mItemsAddedOrRemoved && !mItemsChanged) || + (mItemsAddedOrRemoved || (mItemsChanged && supportsChangeAnimations())); + 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 moved ({@link ItemAnimator#animateMove(ViewHolder, int, int, int, int)}) + * REMOVED views are removed ({@link ItemAnimator#animateRemove(ViewHolder)}) + * ADDED views are added ({@link ItemAnimator#animateAdd(ViewHolder)}) + * DISAPPEARING views are moved off screen + * APPEARING views are moved on screen + */ + void dispatchLayout() { + if (mAdapter == null) { + Log.e(TAG, "No adapter attached; skipping layout"); + return; + } + if (mLayout == null) { + Log.e(TAG, "No layout manager attached; skipping layout"); + return; + } + mState.mDisappearingViewsInLayoutPass.clear(); + eatRequestLayout(); + onEnterLayoutOrScroll(); + + processAdapterUpdatesAndSetAnimationFlags(); + + mState.mOldChangedHolders = mState.mRunSimpleAnimations && mItemsChanged + && supportsChangeAnimations() ? new ArrayMap() : null; + mItemsAddedOrRemoved = mItemsChanged = false; + ArrayMap appearingViewInitialBounds = null; + 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 + mState.mPreLayoutHolderMap.clear(); + mState.mPostLayoutHolderMap.clear(); + 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 View view = holder.itemView; + mState.mPreLayoutHolderMap.put(holder, new ItemHolderInfo(holder, + view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); + } + } + 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(); + // processAdapterUpdatesAndSetAnimationFlags already run pre-layout animations. + if (mState.mOldChangedHolders != null) { + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.isChanged() && !holder.isRemoved() && !holder.shouldIgnore()) { + long key = getChangedHolderKey(holder); + mState.mOldChangedHolders.put(key, holder); + mState.mPreLayoutHolderMap.remove(holder); + } + } + } + + 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; + + appearingViewInitialBounds = new ArrayMap(); + for (int i = 0; i < mChildHelper.getChildCount(); ++i) { + boolean found = false; + View child = mChildHelper.getChildAt(i); + if (getChildViewHolderInt(child).shouldIgnore()) { + continue; + } + for (int j = 0; j < mState.mPreLayoutHolderMap.size(); ++j) { + ViewHolder holder = mState.mPreLayoutHolderMap.keyAt(j); + if (holder.itemView == child) { + found = true; + break; + } + } + if (!found) { + appearingViewInitialBounds.put(child, new Rect(child.getLeft(), child.getTop(), + child.getRight(), child.getBottom())); + } + } + // we don't process disappearing list because they may re-appear in post layout pass. + clearOldPositions(); + mAdapterHelper.consumePostponedUpdates(); + } else { + clearOldPositions(); + // in case pre layout did run but we decided not to run predictive animations. + mAdapterHelper.consumeUpdatesInOnePass(); + if (mState.mOldChangedHolders != null) { + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; ++i) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.isChanged() && !holder.isRemoved() && !holder.shouldIgnore()) { + long key = getChangedHolderKey(holder); + mState.mOldChangedHolders.put(key, holder); + mState.mPreLayoutHolderMap.remove(holder); + } + } + } + } + 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; + + if (mState.mRunSimpleAnimations) { + // Step 3: Find out where things are now, post-layout + ArrayMap newChangedHolders = mState.mOldChangedHolders != null ? + new ArrayMap() : null; + int count = mChildHelper.getChildCount(); + for (int i = 0; i < count; ++i) { + ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + if (holder.shouldIgnore()) { + continue; + } + final View view = holder.itemView; + long key = getChangedHolderKey(holder); + if (newChangedHolders != null && mState.mOldChangedHolders.get(key) != null) { + newChangedHolders.put(key, holder); + } else { + mState.mPostLayoutHolderMap.put(holder, new ItemHolderInfo(holder, + view.getLeft(), view.getTop(), view.getRight(), view.getBottom())); + } + } + processDisappearingList(appearingViewInitialBounds); + // Step 4: Animate DISAPPEARING and REMOVED items + int preLayoutCount = mState.mPreLayoutHolderMap.size(); + for (int i = preLayoutCount - 1; i >= 0; i--) { + ViewHolder itemHolder = mState.mPreLayoutHolderMap.keyAt(i); + if (!mState.mPostLayoutHolderMap.containsKey(itemHolder)) { + ItemHolderInfo disappearingItem = mState.mPreLayoutHolderMap.valueAt(i); + mState.mPreLayoutHolderMap.removeAt(i); + + View disappearingItemView = disappearingItem.holder.itemView; + mRecycler.unscrapView(disappearingItem.holder); + animateDisappearance(disappearingItem); + } + } + // Step 5: Animate APPEARING and ADDED items + int postLayoutCount = mState.mPostLayoutHolderMap.size(); + if (postLayoutCount > 0) { + for (int i = postLayoutCount - 1; i >= 0; i--) { + ViewHolder itemHolder = mState.mPostLayoutHolderMap.keyAt(i); + ItemHolderInfo info = mState.mPostLayoutHolderMap.valueAt(i); + if ((mState.mPreLayoutHolderMap.isEmpty() || + !mState.mPreLayoutHolderMap.containsKey(itemHolder))) { + mState.mPostLayoutHolderMap.removeAt(i); + Rect initialBounds = (appearingViewInitialBounds != null) ? + appearingViewInitialBounds.get(itemHolder.itemView) : null; + animateAppearance(itemHolder, initialBounds, + info.left, info.top); + } + } + } + // Step 6: Animate PERSISTENT items + count = mState.mPostLayoutHolderMap.size(); + for (int i = 0; i < count; ++i) { + ViewHolder postHolder = mState.mPostLayoutHolderMap.keyAt(i); + ItemHolderInfo postInfo = mState.mPostLayoutHolderMap.valueAt(i); + ItemHolderInfo preInfo = mState.mPreLayoutHolderMap.get(postHolder); + if (preInfo != null && postInfo != null) { + if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) { + postHolder.setIsRecyclable(false); + if (DEBUG) { + Log.d(TAG, "PERSISTENT: " + postHolder + + " with view " + postHolder.itemView); + } + if (mItemAnimator.animateMove(postHolder, + preInfo.left, preInfo.top, postInfo.left, postInfo.top)) { + postAnimationRunner(); + } + } + } + } + // Step 7: Animate CHANGING items + count = mState.mOldChangedHolders != null ? mState.mOldChangedHolders.size() : 0; + // traverse reverse in case view gets recycled while we are traversing the list. + for (int i = count - 1; i >= 0; i--) { + long key = mState.mOldChangedHolders.keyAt(i); + ViewHolder oldHolder = mState.mOldChangedHolders.get(key); + View oldView = oldHolder.itemView; + if (oldHolder.shouldIgnore()) { + continue; + } + // We probably don't need this check anymore since these views are removed from + // the list if they are recycled. + if (mRecycler.mChangedScrap != null && + mRecycler.mChangedScrap.contains(oldHolder)) { + animateChange(oldHolder, newChangedHolders.get(key)); + } else if (DEBUG) { + Log.e(TAG, "cannot find old changed holder in changed scrap :/" + oldHolder); + } + } + } + resumeRequestLayout(false); + mLayout.removeAndRecycleScrapInt(mRecycler); + mState.mPreviousLayoutItemCount = mState.mItemCount; + mDataSetHasChangedAfterLayout = false; + mState.mRunSimpleAnimations = false; + mState.mRunPredictiveAnimations = false; + onExitLayoutOrScroll(); + mLayout.mRequestedSimpleAnimations = false; + if (mRecycler.mChangedScrap != null) { + mRecycler.mChangedScrap.clear(); + } + mState.mOldChangedHolders = null; + + if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) { + dispatchOnScrolled(0, 0); + } + } + + private void findMinMaxChildLayoutPositions(int[] into) { + final int count = mChildHelper.getChildCount(); + if (count == 0) { + into[0] = 0; + into[1] = 0; + 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) { + int count = mChildHelper.getChildCount(); + if (count == 0) { + return minPositionPreLayout != 0 || maxPositionPreLayout != 0; + } + 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 || pos > maxPositionPreLayout) { + return true; + } + } + return false; + } + + @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; + } + + /** + * A LayoutManager may want to layout a view just to animate disappearance. + * This method handles those views and triggers remove animation on them. + */ + private void processDisappearingList(ArrayMap appearingViews) { + final List disappearingList = mState.mDisappearingViewsInLayoutPass; + for (int i = disappearingList.size() - 1; i >= 0; i --) { + View view = disappearingList.get(i); + ViewHolder vh = getChildViewHolderInt(view); + final ItemHolderInfo info = mState.mPreLayoutHolderMap.remove(vh); + if (!mState.isPreLayout()) { + mState.mPostLayoutHolderMap.remove(vh); + } + if (appearingViews.remove(view) != null) { + mLayout.removeAndRecycleView(view, mRecycler); + continue; + } + if (info != null) { + animateDisappearance(info); + } else { + // let it disappear from the position it becomes visible + animateDisappearance(new ItemHolderInfo(vh, view.getLeft(), view.getTop(), + view.getRight(), view.getBottom())); + } + } + disappearingList.clear(); + } + + private void animateAppearance(ViewHolder itemHolder, Rect beforeBounds, int afterLeft, + int afterTop) { + View newItemView = itemHolder.itemView; + if (beforeBounds != null && + (beforeBounds.left != afterLeft || beforeBounds.top != afterTop)) { + // slide items in if before/after locations differ + itemHolder.setIsRecyclable(false); + if (DEBUG) { + Log.d(TAG, "APPEARING: " + itemHolder + " with view " + newItemView); + } + if (mItemAnimator.animateMove(itemHolder, + beforeBounds.left, beforeBounds.top, + afterLeft, afterTop)) { + postAnimationRunner(); + } + } else { + if (DEBUG) { + Log.d(TAG, "ADDED: " + itemHolder + " with view " + newItemView); + } + itemHolder.setIsRecyclable(false); + if (mItemAnimator.animateAdd(itemHolder)) { + postAnimationRunner(); + } + } + } + + private void animateDisappearance(ItemHolderInfo disappearingItem) { + View disappearingItemView = disappearingItem.holder.itemView; + addAnimatingView(disappearingItem.holder); + int oldLeft = disappearingItem.left; + int oldTop = disappearingItem.top; + int newLeft = disappearingItemView.getLeft(); + int newTop = disappearingItemView.getTop(); + if (oldLeft != newLeft || oldTop != newTop) { + disappearingItem.holder.setIsRecyclable(false); + disappearingItemView.layout(newLeft, newTop, + newLeft + disappearingItemView.getWidth(), + newTop + disappearingItemView.getHeight()); + if (DEBUG) { + Log.d(TAG, "DISAPPEARING: " + disappearingItem.holder + + " with view " + disappearingItemView); + } + if (mItemAnimator.animateMove(disappearingItem.holder, oldLeft, oldTop, + newLeft, newTop)) { + postAnimationRunner(); + } + } else { + if (DEBUG) { + Log.d(TAG, "REMOVED: " + disappearingItem.holder + + " with view " + disappearingItemView); + } + disappearingItem.holder.setIsRecyclable(false); + if (mItemAnimator.animateRemove(disappearingItem.holder)) { + postAnimationRunner(); + } + } + } + + private void animateChange(ViewHolder oldHolder, ViewHolder newHolder) { + oldHolder.setIsRecyclable(false); + addAnimatingView(oldHolder); + oldHolder.mShadowedHolder = newHolder; + mRecycler.unscrapView(oldHolder); + if (DEBUG) { + Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView); + } + final int fromLeft = oldHolder.itemView.getLeft(); + final int fromTop = oldHolder.itemView.getTop(); + final int toLeft, toTop; + if (newHolder == null || newHolder.shouldIgnore()) { + toLeft = fromLeft; + toTop = fromTop; + } else { + toLeft = newHolder.itemView.getLeft(); + toTop = newHolder.itemView.getTop(); + newHolder.setIsRecyclable(false); + newHolder.mShadowingHolder = oldHolder; + } + if(mItemAnimator.animateChange(oldHolder, newHolder, + fromLeft, fromTop, toLeft, toTop)) { + postAnimationRunner(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + eatRequestLayout(); + TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); + dispatchLayout(); + TraceCompat.endSection(); + resumeRequestLayout(false); + mFirstLayoutComplete = true; + } + + @Override + public void requestLayout() { + if (!mEatRequestLayout) { + 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 chilChildrenToPadding is false, to draw glows properly, we + // need find children closest to edges. Not sure if it is worth the effort. + boolean needsInvalidate = false; + if (mLeftGlow != null && !mLeftGlow.isFinished()) { + final int restore = c.save(); + final int padding = mClipToPadding ? getPaddingBottom() : 0; + c.rotate(270); + c.translate(-getHeight() + padding, 0); + needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c); + c.restoreToCount(restore); + } + if (mTopGlow != null && !mTopGlow.isFinished()) { + final int restore = c.save(); + if (mClipToPadding) { + c.translate(getPaddingLeft(), getPaddingTop()); + } + needsInvalidate |= mTopGlow != null && mTopGlow.draw(c); + c.restoreToCount(restore); + } + if (mRightGlow != null && !mRightGlow.isFinished()) { + final int restore = c.save(); + final int width = getWidth(); + final int padding = mClipToPadding ? getPaddingTop() : 0; + c.rotate(90); + c.translate(-padding, -width); + needsInvalidate |= mRightGlow != null && mRightGlow.draw(c); + c.restoreToCount(restore); + } + if (mBottomGlow != null && !mBottomGlow.isFinished()) { + final int restore = c.save(); + c.rotate(180); + if (mClipToPadding) { + c.translate(-getWidth() + getPaddingRight(), -getHeight() + getPaddingBottom()); + } else { + c.translate(-getWidth(), -getHeight()); + } + needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c); + c.restoreToCount(restore); + } + + // If some views are animating, ItemDecorators are likely to move/change with them. + // Invalidate RecyclerView to re-draw decorators. This is still efficient because children's + // display lists are not invalidated. + if (!needsInvalidate && mItemAnimator != null && mItemDecorations.size() > 0 && + mItemAnimator.isRunning()) { + needsInvalidate = true; + } + + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + + final int count = mItemDecorations.size(); + for (int i = 0; i < count; i++) { + mItemDecorations.get(i).onDraw(c, this, mState); + } + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + if (mLayout == null) { + throw new IllegalStateException("RecyclerView has no LayoutManager"); + } + 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); + } + + 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) { + 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); + if (supportsChangeAnimations()) { + holder.addFlags(ViewHolder.FLAG_CHANGED); + } + // lp cannot be null since we get ViewHolder from it. + ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; + } + } + mRecycler.viewRangeUpdate(positionStart, itemCount); + } + + void rebindUpdatedViewHolders() { + final int childCount = mChildHelper.getChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); + // validate type is correct + if (holder == null || holder.shouldIgnore()) { + continue; + } + if (holder.isRemoved() || holder.isInvalid()) { + requestLayout(); + } else if (holder.needsUpdate()) { + final int type = mAdapter.getItemViewType(holder.mPosition); + if (holder.getItemViewType() == type) { + // Binding an attached view will request a layout if needed. + if (!holder.isChanged() || !supportsChangeAnimations()) { + mAdapter.bindViewHolder(holder, holder.mPosition); + } else { + // Don't rebind changed holders if change animations are enabled. + // We want the old contents for the animation and will get a new + // holder for the new contents. + requestLayout(); + } + } else { + // binding to a new view will need re-layout anyways. We can as well trigger + // it here so that it happens during layout + requestLayout(); + break; + } + } + } + } + + private 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(); + } + + /** + * 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(); + } + + /** + * 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); + } + + 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. + * + * @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. + * + * @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(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && !holder.isRemoved() && getAdapterPositionFor(holder) == position) { + return holder; + } + } + return null; + } + + ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + 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) { + return holder; + } + } else if (holder.getLayoutPosition() == position) { + 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 null; + } + + /** + * 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. + * + * @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) { + final int childCount = mChildHelper.getUnfilteredChildCount(); + for (int i = 0; i < childCount; i++) { + final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i)); + if (holder != null && holder.getItemId() == id) { + return holder; + } + } + // this method should not query cached views. They are not children so they + // should not be returned in this public method + return null; + } + + /** + * 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 = ViewCompat.getTranslationX(child); + final float translationY = ViewCompat.getTranslationY(child); + if (x >= child.getLeft() + translationX && + x <= child.getRight() + translationX && + y >= child.getTop() + translationY && + y <= child.getBottom() + translationY) { + return child; + } + } + return null; + } + + /** + * 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); + } + } + + Rect getItemDecorInsetsForChild(View child) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.mInsetsDirty) { + 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) { + // 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); + } + } + } + + /** + * 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(); + } + + private class ViewFlinger implements Runnable { + private int mLastFlingX; + private int mLastFlingY; + private ScrollerCompat mScroller; + private 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; + + public ViewFlinger() { + mScroller = ScrollerCompat.create(getContext(), sQuinticInterpolator); + } + + @Override + public void run() { + disableRunOnAnimationRequests(); + consumePendingUpdateOperations(); + // keep a local reference so that if it is changed during onAnimation method, it won't + // cause unexpected behaviors + final ScrollerCompat 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(); + TraceCompat.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; + } + TraceCompat.endSection(); + if (supportsChangeAnimations()) { + // Fix up shadow views used by changing 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()); + } + } + } + } + + 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); + } + } + onExitLayoutOrScroll(); + resumeRequestLayout(false); + } + if (!mItemDecorations.isEmpty()) { + invalidate(); + } + if (ViewCompat.getOverScrollMode(RecyclerView.this) != + ViewCompat.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 (ViewCompat.getOverScrollMode(RecyclerView.this) != + ViewCompat.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. + } else { + postOnAnimation(); + } + } + // call this after the onAnimation is complete not to have inconsistent callbacks etc. + if (smoothScroller != null && smoothScroller.isPendingInitialRun()) { + smoothScroller.onAnimation(0, 0); + } + 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); + ViewCompat.postOnAnimation(RecyclerView.this, 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, int duration, Interpolator interpolator) { + if (mInterpolator != interpolator) { + mInterpolator = interpolator; + mScroller = ScrollerCompat.create(getContext(), interpolator); + } + setScrollState(SCROLL_STATE_SETTLING); + mLastFlingX = mLastFlingY = 0; + mScroller.startScroll(0, 0, dx, dy, duration); + postOnAnimation(); + } + + public void stop() { + removeCallbacks(this); + mScroller.abortAnimation(); + } + + } + + private class RecyclerViewDataObserver extends AdapterDataObserver { + @Override + public void onChanged() { + assertNotInLayoutOrScroll(null); + if (mAdapter.hasStableIds()) { + // TODO Determine what actually changed. + // This is more important to implement now since this callback will disable all + // animations because we cannot rely on positions. + mState.mStructureChanged = true; + setDataSetChangedAfterLayout(); + } else { + mState.mStructureChanged = true; + setDataSetChangedAfterLayout(); + } + if (!mAdapterHelper.hasPendingUpdates()) { + requestLayout(); + } + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + assertNotInLayoutOrScroll(null); + if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount)) { + 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 (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) { + ViewCompat.postOnAnimation(RecyclerView.this, 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 SparseArray> mScrap = + new SparseArray>(); + private SparseIntArray mMaxScrap = new SparseIntArray(); + private int mAttachCount = 0; + + private static final int DEFAULT_MAX_SCRAP = 5; + + public void clear() { + mScrap.clear(); + } + + public void setMaxRecycledViews(int viewType, int max) { + mMaxScrap.put(viewType, max); + final ArrayList scrapHeap = mScrap.get(viewType); + if (scrapHeap != null) { + while (scrapHeap.size() > max) { + scrapHeap.remove(scrapHeap.size() - 1); + } + } + } + + public ViewHolder getRecycledView(int viewType) { + final ArrayList scrapHeap = mScrap.get(viewType); + if (scrapHeap != null && !scrapHeap.isEmpty()) { + final int index = scrapHeap.size() - 1; + final ViewHolder scrap = scrapHeap.get(index); + scrapHeap.remove(index); + return scrap; + } + return null; + } + + int size() { + int count = 0; + for (int i = 0; i < mScrap.size(); i ++) { + ArrayList viewHolders = mScrap.valueAt(i); + if (viewHolders != null) { + count += viewHolders.size(); + } + } + return count; + } + + public void putRecycledView(ViewHolder scrap) { + final int viewType = scrap.getItemViewType(); + final ArrayList scrapHeap = getScrapHeapForType(viewType); + if (mMaxScrap.get(viewType) <= scrapHeap.size()) { + return; + } + scrap.resetInternal(); + scrapHeap.add(scrap); + } + + 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 ArrayList getScrapHeapForType(int viewType) { + ArrayList scrap = mScrap.get(viewType); + if (scrap == null) { + scrap = new ArrayList(); + mScrap.put(viewType, scrap); + if (mMaxScrap.indexOfKey(viewType) < 0) { + mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP); + } + } + return scrap; + } + } + + /** + * 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(); + private ArrayList mChangedScrap = null; + + final ArrayList mCachedViews = new ArrayList(); + + private final List + mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap); + + private int mViewCacheMax = DEFAULT_CACHE_SIZE; + + private RecycledViewPool mRecyclerPool; + + private ViewCacheExtension mViewCacheExtension; + + private 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) { + mViewCacheMax = viewCount; + // first, try the views that can be recycled + for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > viewCount; 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()) { + return true; + } + 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; + } + + /** + * 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()); + } + holder.mOwnerRecyclerView = RecyclerView.this; + mAdapter.bindViewHolder(holder, offsetPosition); + attachAccessibilityDelegate(view); + if (mState.isPreLayout()) { + holder.mPreLayoutPosition = position; + } + + 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) { + if (position < 0 || position >= mState.getItemCount()) { + throw new IndexOutOfBoundsException("Invalid item position " + position + + "(" + position + "). Item count:" + mState.getItemCount()); + } + boolean fromScrap = false; + ViewHolder holder = null; + // 0) If there is a changed scrap, try to find from there + if (mState.isPreLayout()) { + holder = getChangedScrapViewForPosition(position); + fromScrap = holder != null; + } + // 1) Find from scrap by position + if (holder == null) { + holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun); + if (holder != null) { + if (!validateViewHolderForOffsetPosition(holder)) { + // recycle this scrap + 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 { + fromScrap = 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 via stable ids, if exists + if (mAdapter.hasStableIds()) { + holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); + if (holder != null) { + // update position + holder.mPosition = offsetPosition; + fromScrap = 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 recycler + // try recycler. + // Head to the shared pool. + if (DEBUG) { + Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared " + + "pool"); + } + holder = getRecycledViewPool().getRecycledView(type); + if (holder != null) { + holder.resetInternal(); + if (FORCE_INVALIDATE_DISPLAY_LIST) { + invalidateDisplayListInt(holder); + } + } + } + if (holder == null) { + holder = mAdapter.createViewHolder(RecyclerView.this, type); + if (DEBUG) { + Log.d(TAG, "getViewForPosition created new ViewHolder"); + } + } + } + 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); + holder.mOwnerRecyclerView = RecyclerView.this; + mAdapter.bindViewHolder(holder, offsetPosition); + attachAccessibilityDelegate(holder.itemView); + bound = true; + if (mState.isPreLayout()) { + holder.mPreLayoutPosition = position; + } + } + + 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 = fromScrap && bound; + return holder.itemView; + } + + private void attachAccessibilityDelegate(View itemView) { + if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()) { + if (ViewCompat.getImportantForAccessibility(itemView) == + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(itemView, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + if (!ViewCompat.hasAccessibilityDelegate(itemView)) { + ViewCompat.setAccessibilityDelegate(itemView, + 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(); + } + + /** + * 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); + 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 (forceRecycle || holder.isRecyclable()) { + if (!holder.isInvalid() && !holder.isRemoved() && !holder.isChanged()) { + // Retire oldest cached view + final int cachedViewSize = mCachedViews.size(); + if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) { + recycleCachedViewAt(0); + } + if (cachedViewSize < mViewCacheMax) { + mCachedViews.add(holder); + cached = true; + } + } + if (!cached) { + addViewHolderToRecycledViewPool(holder); + recycled = true; + } + } else 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. + mState.onViewRecycled(holder); + if (!cached && !recycled && transientStatePreventsRecycling) { + holder.mOwnerRecyclerView = null; + } + } + + void addViewHolderToRecycledViewPool(ViewHolder holder) { + ViewCompat.setAccessibilityDelegate(holder.itemView, null); + 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.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); + holder.setScrapContainer(this); + if (!holder.isChanged() || !supportsChangeAnimations()) { + 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."); + } + mAttachedScrap.add(holder); + } else { + if (mChangedScrap == null) { + mChangedScrap = new ArrayList(); + } + 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.isChanged() || !supportsChangeAnimations() || mChangedScrap == null) { + mAttachedScrap.remove(holder); + } else { + mChangedScrap.remove(holder); + } + holder.mScrapContainer = null; + holder.clearReturnedFromScrapFlag(); + } + + int getScrapCount() { + return mAttachedScrap.size(); + } + + View getScrapViewAt(int index) { + return mAttachedScrap.get(index).itemView; + } + + void clearScrap() { + mAttachedScrap.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 scrap view for the position. If type is not INVALID_TYPE, it also checks if + * ViewHolder's type matches the provided type. + * + * @param position Item position + * @param type View type + * @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 getScrapViewForPosition(int position, int type, 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())) { + if (type != INVALID_TYPE && holder.getItemViewType() != type) { + Log.e(TAG, "Scrap view for position " + position + " isn't dirty but has" + + " wrong view type! (found " + holder.getItemViewType() + + " but expected " + type + ")"); + break; + } + holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP); + return holder; + } + } + + if (!dryRun) { + View view = mChildHelper.findHiddenNonRemovedView(position, type); + if (view != null) { + // ending the animation should cause it to get recycled before we reuse it + mItemAnimator.endAnimation(getChildViewHolder(view)); + } + } + + // 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 getScrapViewForId + if (!holder.isInvalid() && holder.getLayoutPosition() == position) { + if (!dryRun) { + mCachedViews.remove(i); + } + if (DEBUG) { + Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type + + ") found match in cache: " + holder); + } + return holder; + } + } + return null; + } + + ViewHolder getScrapViewForId(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) { + // 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; + } + + void dispatchViewRecycled(ViewHolder holder) { + if (mRecyclerListener != null) { + mRecyclerListener.onViewRecycled(holder); + } + if (mAdapter != null) { + mAdapter.onViewRecycled(holder); + } + if (mState != null) { + mState.onViewRecycled(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.getLayoutPosition() >= 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.getLayoutPosition() >= removedEnd) { + if (DEBUG) { + Log.d(TAG, "offsetPositionRecordsForRemove cached " + i + + " holder " + holder + " now at position " + + (holder.mPosition - count)); + } + holder.offsetPosition(-count, applyToPreLayout); + } else if (holder.getLayoutPosition() >= 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 = 0; i < cachedCount; 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); + // 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); + } + } + } 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 + * ben 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) + */ + abstract public 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}.

+ */ + public static abstract 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)}. 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. + * + * @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); + + /** + * 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) { + TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG); + final VH holder = onCreateViewHolder(parent, viewType); + holder.mItemViewType = viewType; + TraceCompat.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); + TraceCompat.beginSection(TRACE_BIND_VIEW_TAG); + onBindViewHolder(holder, position); + TraceCompat.endSection(); + } + + /** + * Return the view type of the item at position for the purposes + * of view recycling. + * + *

The default implementation of this method returns 0, making the assumption of + * a single view type for the adapter. Unlike ListView adapters, types need not + * be contiguous. Consider using id resources to uniquely identify item view types. + * + * @param position position to query + * @return integer value identifying the type of the view needed to represent the item at + * position. Type codes need not be contiguous. + */ + public int getItemViewType(int position) { + return 0; + } + + /** + * Indicates whether each item in the data set can be represented with a unique identifier + * of type {@link java.lang.Long}. + * + * @param hasStableIds Whether items in data set have unique identifiers or not. + * @see #hasStableIds() + * @see #getItemId(int) + */ + public void setHasStableIds(boolean hasStableIds) { + if (hasObservers()) { + throw new IllegalStateException("Cannot change whether this adapter has " + + "stable IDs while the adapter has registered observers."); + } + mHasStableIds = hasStableIds; + } + + /** + * Return the stable ID for the item at position. If {@link #hasStableIds()} + * would return false this method should return {@link #NO_ID}. The default implementation + * of this method returns {@link #NO_ID}. + * + * @param position Adapter position to query + * @return the stable ID of the item at position + */ + public long getItemId(int position) { + return NO_ID; + } + + /** + * Returns the total number of items in the data set hold 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 an 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 android.support.v7.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. + * + *

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 itemCount items starting at + * position positionStart have changed. + * + *

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 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); + } + } + + private void dispatchChildDetached(View child) { + if (mAdapter != null) { + mAdapter.onViewDetachedFromWindow(getChildViewHolderInt(child)); + } + onChildDetachedFromWindow(child); + } + + private void dispatchChildAttached(View child) { + if (mAdapter != null) { + mAdapter.onViewAttachedToWindow(getChildViewHolderInt(child)); + } + onChildAttachedToWindow(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. + */ + public static abstract class LayoutManager { + ChildHelper mChildHelper; + RecyclerView mRecyclerView; + + @Nullable + SmoothScroller mSmoothScroller; + + private boolean mRequestedSimpleAnimations = false; + + private boolean mIsAttachedToWindow = false; + + void setRecyclerView(RecyclerView recyclerView) { + if (recyclerView == null) { + mRecyclerView = null; + mChildHelper = null; + } else { + mRecyclerView = recyclerView; + mChildHelper = recyclerView.mChildHelper; + } + + } + + /** + * 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); + } + } + + /** + * 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); + } + } + + /** + * 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; + } + + void dispatchAttachedToWindow(RecyclerView view) { + mIsAttachedToWindow = true; + onAttachedToWindow(view); + } + + void dispatchDetachedFromWindow(RecyclerView view, Recycler recycler) { + mIsAttachedToWindow = false; + onDetachedFromWindow(view, recycler); + } + + /** + * Returns whether LayoutManager is currently attached to a RecyclerView which is attached + * to a window. + * + * @return True if this LayoutManager is controlling a RecyclerView and the RecyclerView + * is attached to window. + */ + public boolean isAttachedToWindow() { + return mIsAttachedToWindow; + } + + /** + * Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread. + *

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

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

Subclass implementations should always call through to the superclass implementation. + *

+ * + * @param view The RecyclerView this LayoutManager is bound to + */ + 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. + * + *

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. + */ + 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 + * simple fade views in and out, whether they are actuall 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) "); + } + + /** + * 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 Unstance 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.support.v4.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout + * direction is RTL or returns + * {@link android.support.v4.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction + * is not RTL. + */ + public int getLayoutDirection() { + return ViewCompat.getLayoutDirection(mRecyclerView); + } + + /** + * Ends all animations on the view created by the {@link ItemAnimator}. + * + * @param view The View for which the animations should be ended. + * @see RecyclerView.ItemAnimator#endAnimations() + */ + 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.mState.addToDisappearingList(child); + } 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.mState.removeFromDisappearingList(child); + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (holder.wasReturnedFromScrap() || holder.isScrap()) { + if (holder.isScrap()) { + holder.unScrap(); + } else { + holder.clearReturnedFromScrapFlag(); + } + mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } + } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child + // ensure in correct position + int currentIndex = mChildHelper.indexOfChild(child); + if (index == -1) { + index = mChildHelper.getChildCount(); + } + if (currentIndex == -1) { + throw new IllegalStateException("Added View has RecyclerView as parent but" + + " view is not a real child. Unfiltered index:" + + mRecyclerView.indexOfChild(child)); + } + 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(); + } + + /** + * 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) { + ViewCompat.dispatchStartTemporaryDetach(view); + } + mChildHelper.detachViewFromParent(index); + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + * @param lp LayoutParams for child + */ + public void attachView(View child, int index, LayoutParams lp) { + ViewHolder vh = getChildViewHolderInt(child); + if (vh.isRemoved()) { + mRecyclerView.mState.addToDisappearingList(child); + } else { + mRecyclerView.mState.removeFromDisappearingList(child); + } + mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved()); + if (DISPATCH_TEMP_DETACH) { + ViewCompat.dispatchFinishTemporaryDetach(child); + } + } + + /** + * Reattach a previously {@link #detachView(android.view.View) detached} view. + * This method should not be used to reattach views that were previously + * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}. + * + * @param child Child to reattach + * @param index Intended child index for child + */ + public void attachView(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 of the parent RecyclerView + * + * @return Width in pixels + */ + public int getWidth() { + return mRecyclerView != null ? mRecyclerView.getWidth() : 0; + } + + /** + * Return the height of the parent RecyclerView + * + * @return Height in pixels + */ + public int getHeight() { + return mRecyclerView != null ? mRecyclerView.getHeight() : 0; + } + + /** + * 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 ? ViewCompat.getPaddingStart(mRecyclerView) : 0; + } + + /** + * Return the end padding of the parent RecyclerView + * + * @return Padding in pixels + */ + public int getPaddingEnd() { + return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0; + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has focus. + * + * @return True if the RecyclerView has focus, false otherwise. + * @see View#isFocused() + */ + public boolean isFocused() { + return mRecyclerView != null && mRecyclerView.isFocused(); + } + + /** + * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus. + * + * @return true if the RecyclerView has or contains focus + * @see View#hasFocus() + */ + public boolean hasFocus() { + return mRecyclerView != null && mRecyclerView.hasFocus(); + } + + /** + * Returns the item View which has or contains focus. + * + * @return A direct child of RecyclerView which has focus or contains the focused child. + */ + 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()}. In + * methods where State is available, you should use {@link State#getItemCount()} instead. + * For more details, check the documentation for {@link 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.mState.onViewIgnored(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() && !viewHolder.isChanged() && + !mRecyclerView.mAdapter.hasStableIds()) { + removeViewAt(index); + recycler.recycleViewHolderInternal(viewHolder); + } else { + detachViewAt(index); + recycler.scrapView(view); + } + } + + /** + * 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 (vh.isTmpDetached()) { + mRecyclerView.removeDetachedView(scrap, false); + } + 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(), + getPaddingLeft() + getPaddingRight() + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), + getPaddingTop() + getPaddingBottom() + heightUsed, lp.height, + canScrollVertically()); + child.measure(widthSpec, heightSpec); + } + + /** + * 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(), + getPaddingLeft() + getPaddingRight() + + lp.leftMargin + lp.rightMargin + widthUsed, lp.width, + canScrollHorizontally()); + final int heightSpec = getChildMeasureSpec(getHeight(), + getPaddingTop() + getPaddingBottom() + + lp.topMargin + lp.bottomMargin + heightUsed, lp.height, + canScrollVertically()); + 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 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 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.FILL_PARENT) { + resultSize = size; + resultMode = MeasureSpec.EXACTLY; + } else if (childDimension == LayoutParams.WRAP_CONTENT) { + resultSize = size; + resultMode = MeasureSpec.AT_MOST; + } + } + 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 #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) + */ + 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); + } + + /** + * 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; + final int childTop = child.getTop() + rect.top; + final int childRight = childLeft + rect.width(); + final int childBottom = childTop + rect.height(); + + final int offScreenLeft = Math.min(0, childLeft - parentLeft); + final int offScreenTop = Math.min(0, childTop - parentTop); + final int offScreenRight = Math.max(0, childRight - parentRight); + final int offScreenBottom = Math.max(0, childBottom - parentBottom); + + // Favor the "start" layout direction over the end when bringing one side or the other + // of a large rect into view. If we decide to bring in end because start is already + // visible, limit the scroll such that start won't go out of bounds. + final int dx; + if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { + dx = offScreenRight != 0 ? offScreenRight + : Math.max(offScreenLeft, childRight - parentRight); + } else { + dx = offScreenLeft != 0 ? offScreenLeft + : Math.min(childLeft - parentLeft, offScreenRight); + } + + // Favor bringing the top into view over the bottom. If top is already visible and + // we should scroll to make bottom visible, make sure top does not go out of bounds. + final int dy = offScreenTop != 0 ? offScreenTop + : Math.min(childTop - parentTop, offScreenBottom); + + 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.isRunningLayoutOrScroll(); + } + + /** + * 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. + * + * @param recyclerView + * @param positionStart + * @param itemCount + */ + public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int 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 ViewCompat.getMinimumWidth(mRecyclerView); + } + + /** + * @return The host RecyclerView's {@link View#getMinimumHeight()} + */ + public int getMinimumHeight() { + return ViewCompat.getMinimumHeight(mRecyclerView); + } + /** + *

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

+ *

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

+ * + * @return Necessary information for LayoutManager to be able to restore its state + */ + 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(AccessibilityNodeInfoCompat info) { + onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler, mRecyclerView.mState, info); + } + + /** + * Called by the AccessibilityDelegate when the information about the current layout should + * be populated. + *

+ * Default implementation adds a {@link + * android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat}. + *

+ * You should override + * {@link #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State)}, + * {@link #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State)} and + * {@link #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)} for + * more accurate accessibility information. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @param info The info that should be filled by the LayoutManager + * @see View#onInitializeAccessibilityNodeInfo( + *android.view.accessibility.AccessibilityNodeInfo) + * @see #getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State) + * @see #isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) + * @see #getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) + */ + public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state, + AccessibilityNodeInfoCompat info) { + if (ViewCompat.canScrollVertically(mRecyclerView, -1) || + ViewCompat.canScrollHorizontally(mRecyclerView, -1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + info.setScrollable(true); + } + if (ViewCompat.canScrollVertically(mRecyclerView, 1) || + ViewCompat.canScrollHorizontally(mRecyclerView, 1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + info.setScrollable(true); + } + final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo + = AccessibilityNodeInfoCompat.CollectionInfoCompat + .obtain(getRowCountForAccessibility(recycler, state), + getColumnCountForAccessibility(recycler, state), + isLayoutHierarchical(recycler, state), + getSelectionModeForAccessibility(recycler, state)); + info.setCollectionInfo(collectionInfo); + } + + // called by accessibility delegate + public void onInitializeAccessibilityEvent(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) { + final AccessibilityRecordCompat record = AccessibilityEventCompat + .asRecord(event); + if (mRecyclerView == null || record == null) { + return; + } + record.setScrollable(ViewCompat.canScrollVertically(mRecyclerView, 1) + || ViewCompat.canScrollVertically(mRecyclerView, -1) + || ViewCompat.canScrollHorizontally(mRecyclerView, -1) + || ViewCompat.canScrollHorizontally(mRecyclerView, 1)); + + if (mRecyclerView.mAdapter != null) { + record.setItemCount(mRecyclerView.mAdapter.getItemCount()); + } + } + + // called by accessibility delegate + void onInitializeAccessibilityNodeInfoForItem(View host, AccessibilityNodeInfoCompat info) { + final ViewHolder vh = getChildViewHolderInt(host); + // avoid trying to create accessibility node info for removed children + if (vh != null && !vh.isRemoved()) { + 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, AccessibilityNodeInfoCompat info) { + int rowIndexGuess = canScrollVertically() ? getPosition(host) : 0; + int columnIndexGuess = canScrollHorizontally() ? getPosition(host) : 0; + final AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo + = AccessibilityNodeInfoCompat.CollectionItemInfoCompat.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 AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_NONE}, + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_SINGLE} or + * {@link AccessibilityNodeInfoCompat.CollectionInfoCompat#SELECTION_MODE_MULTIPLE}. + *

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

+ * Default implementation returns the number of items in the adapter if LayoutManager + * supports vertical scrolling or 1 if LayoutManager does not support vertical + * scrolling. + * + * @param recycler The Recycler that can be used to convert view positions into adapter + * positions + * @param state The current state of RecyclerView + * @return The number of rows in LayoutManager for accessibility. + */ + public int getRowCountForAccessibility(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 AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: + if (ViewCompat.canScrollVertically(mRecyclerView, -1)) { + vScroll = -(getHeight() - getPaddingTop() - getPaddingBottom()); + } + if (ViewCompat.canScrollHorizontally(mRecyclerView, -1)) { + hScroll = -(getWidth() - getPaddingLeft() - getPaddingRight()); + } + break; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: + if (ViewCompat.canScrollVertically(mRecyclerView, 1)) { + vScroll = getHeight() - getPaddingTop() - getPaddingBottom(); + } + if (ViewCompat.canScrollHorizontally(mRecyclerView, 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; + } + } + + /** + * 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 static abstract 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.

+ */ + 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. + */ + public 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. + */ + public void onTouchEvent(RecyclerView rv, MotionEvent e); + } + + /** + * An OnScrollListener can be set on a RecyclerView to receive messages + * when a scrolling event has occurred on that RecyclerView. + * + * @see RecyclerView#setOnScrollListener(OnScrollListener) and + * RecyclerView#addOnScrollListener(OnScrollListener) + * + * If you are planning to have several listeners at the same time, use + * RecyclerView#addOnScrollListener. If there will be only one listener at the time and you + * want your components to be able to easily replace the listener use + * RecyclerView#setOnScrollListener. + */ + abstract static public 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 + */ + public void onViewRecycled(ViewHolder holder); + } + + /** + * 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 static abstract class ViewHolder { + public final View itemView; + 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's contents have changed. This flag is used as an indication that + * change animations may be used, if supported by the ItemAnimator. + */ + static final int FLAG_CHANGED = 1 << 6; + + /** + * 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; + + private int mFlags; + + 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; + + /** + * 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 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) { + mScrapContainer = recycler; + } + + boolean isInvalid() { + return (mFlags & FLAG_INVALID) != 0; + } + + boolean needsUpdate() { + return (mFlags & FLAG_UPDATE) != 0; + } + + boolean isChanged() { + return (mFlags & FLAG_CHANGED) != 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 resetInternal() { + mFlags = 0; + mPosition = NO_POSITION; + mOldPosition = NO_POSITION; + mItemId = NO_ID; + mPreLayoutPosition = NO_POSITION; + mIsRecyclableCount = 0; + mShadowedHolder = null; + mShadowingHolder = null; + } + + @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"); + 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 (isChanged()) sb.append(" changed"); + 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. + */ + 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); + } + } + + /** + * @see {@link #setIsRecyclable(boolean)} + * + * @return true if this item is available to be recycled, false otherwise. + */ + public final boolean isRecyclable() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && + !ViewCompat.hasTransientState(itemView); + } + + /** + * Returns whether we have animations referring to this view holder or not. + * This is similar to isRecyclable flag but does not check transient state. + */ + private boolean shouldBeKeptAsChild() { + return (mFlags & FLAG_NOT_RECYCLABLE) != 0; + } + + /** + * @return True if ViewHolder is not refenrenced by RecyclerView animations but has + * transient state which will prevent it from being recycled. + */ + private boolean doesTransientStatePreventRecycling() { + return (mFlags & FLAG_NOT_RECYCLABLE) == 0 && ViewCompat.hasTransientState(itemView); + } + } + + private 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.isChanged(); + } + + /** + * @deprecated use {@link #getViewLayoutPosition()} or {@link #getViewAdapterPosition()} + */ + 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 static abstract class AdapterDataObserver { + public void onChanged() { + // Do nothing + } + + public void onItemRangeChanged(int positionStart, int itemCount) { + // do nothing + } + + 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 static abstract 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 + */ + 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)}. + */ + final protected 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) { + if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION) { + stop(); + } + mPendingInitialRun = false; + if (mTargetView != null) { + // verify target position + if (getChildPosition(mTargetView) == mTargetPosition) { + onTargetFound(mTargetView, mRecyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(mRecyclerView); + stop(); + } else { + Log.e(TAG, "Passed over target position while smooth scrolling."); + mTargetView = null; + } + } + if (mRunning) { + onSeekTargetStep(dx, dy, mRecyclerView.mState, mRecyclingAction); + mRecyclingAction.runIfNecessary(mRecyclerView); + } + } + + /** + * @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) + */ + 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 magnitute = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y * + scrollVector.y); + scrollVector.x /= magnitute; + scrollVector.y /= magnitute; + } + + /** + * Called when smooth scroll is started. This might be a good time to do setup. + */ + abstract protected void onStart(); + + /** + * Called when smooth scroller is stopped. This is a good place to cleanup your state etc. + * @see #stop() + */ + abstract protected 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 verticaully + * @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. + */ + abstract protected 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 + */ + abstract protected 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 Interpolator mInterpolator; + + private boolean changed = false; + + // we track this variable to inform custom implementer if they are updating the action + // in every animation callback + private int consecutiveUpdates = 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; + } + private void runIfNecessary(RecyclerView recyclerView) { + if (changed) { + 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); + } + consecutiveUpdates ++; + if (consecutiveUpdates > 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"); + } + changed = false; + } else { + consecutiveUpdates = 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) { + changed = true; + mDx = dx; + } + + public int getDy() { + return mDy; + } + + public void setDy(int dy) { + changed = true; + mDy = dy; + } + + public int getDuration() { + return mDuration; + } + + public void setDuration(int duration) { + changed = 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) { + changed = 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; + changed = true; + } + } + } + + 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) { + // 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); + } + } + + 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); + } + } + } + + static class SavedState extends android.view.View.BaseSavedState { + + 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); + } + + private 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 { + + private int mTargetPosition = RecyclerView.NO_POSITION; + ArrayMap mPreLayoutHolderMap = + new ArrayMap(); + ArrayMap mPostLayoutHolderMap = + new ArrayMap(); + // nullable + ArrayMap mOldChangedHolders = new ArrayMap(); + + // we use this like a set + final List mDisappearingViewsInLayoutPass = new ArrayList(); + + private SparseArray mData; + + /** + * Number of items adapter has. + */ + int mItemCount = 0; + + /** + * Number of items adapter had in the previous layout. + */ + private int mPreviousLayoutItemCount = 0; + + /** + * Number of items that were NOT laid out but has been deleted from the adapter after the + * previous layout. + */ + private int mDeletedInvisibleItemCountSincePreviousLayout = 0; + + private boolean mStructureChanged = false; + + private boolean mInPreLayout = false; + + private boolean mRunSimpleAnimations = false; + + private boolean mRunPredictiveAnimations = false; + + State reset() { + mTargetPosition = RecyclerView.NO_POSITION; + if (mData != null) { + mData.clear(); + } + mItemCount = 0; + mStructureChanged = false; + return this; + } + + 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; + } + + void onViewRecycled(ViewHolder holder) { + mPreLayoutHolderMap.remove(holder); + mPostLayoutHolderMap.remove(holder); + if (mOldChangedHolders != null) { + removeFrom(mOldChangedHolders, holder); + } + mDisappearingViewsInLayoutPass.remove(holder.itemView); + // holder cannot be in new list. + } + + public void onViewIgnored(ViewHolder holder) { + onViewRecycled(holder); + } + + private void removeFrom(ArrayMap holderMap, ViewHolder holder) { + for (int i = holderMap.size() - 1; i >= 0; i --) { + if (holder == holderMap.valueAt(i)) { + holderMap.removeAt(i); + return; + } + } + } + + void removeFromDisappearingList(View child) { + mDisappearingViewsInLayoutPass.remove(child); + } + + void addToDisappearingList(View child) { + if (!mDisappearingViewsInLayoutPass.contains(child)) { + mDisappearingViewsInLayoutPass.add(child); + } + } + + @Override + public String toString() { + return "State{" + + "mTargetPosition=" + mTargetPosition + + ", mPreLayoutHolderMap=" + mPreLayoutHolderMap + + ", mPostLayoutHolderMap=" + mPostLayoutHolderMap + + ", mData=" + mData + + ", mItemCount=" + mItemCount + + ", mPreviousLayoutItemCount=" + mPreviousLayoutItemCount + + ", mDeletedInvisibleItemCountSincePreviousLayout=" + + mDeletedInvisibleItemCountSincePreviousLayout + + ", mStructureChanged=" + mStructureChanged + + ", mInPreLayout=" + mInPreLayout + + ", mRunSimpleAnimations=" + mRunSimpleAnimations + + ", mRunPredictiveAnimations=" + mRunPredictiveAnimations + + '}'; + } + } + + /** + * 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 { + + @Override + public void onRemoveFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) { + removeDetachedView(item.itemView, false); + } + } + + @Override + public void onAddFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (!item.shouldBeKeptAsChild()) { + removeAnimatingView(item.itemView); + } + } + + @Override + public void onMoveFinished(ViewHolder item) { + item.setIsRecyclable(true); + if (!item.shouldBeKeptAsChild()) { + removeAnimatingView(item.itemView); + } + } + + @Override + public void onChangeFinished(ViewHolder item) { + item.setIsRecyclable(true); + /** + * We check both shadowed and shadowing because a ViewHolder may get both roles at the + * same time. + * + * Assume this flow: + * item X is represented by VH_1. Then itemX changes, so we create VH_2 . + * RV sets the following and calls item animator: + * VH_1.shadowed = VH_2; + * VH_1.mChanged = true; + * VH_2.shadowing =VH_1; + * + * Then, before the first change finishes, item changes again so we create VH_3. + * RV sets the following and calls item animator: + * VH_2.shadowed = VH_3 + * VH_2.mChanged = true + * VH_3.shadowing = VH_2 + * + * Because VH_2 already has an animation, it will be cancelled. At this point VH_2 has + * both shadowing and shadowed fields set. Shadowing information is obsolete now + * because the first animation where VH_2 is newViewHolder is not valid anymore. + * We ended up in this case because VH_2 played both roles. On the other hand, + * we DO NOT want to clear its changed flag. + * + * If second change was simply reverting first change, we would find VH_1 in + * {@link Recycler#getScrapViewForPosition(int, int, boolean)} and recycle it before + * re-using + */ + if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh + item.mShadowedHolder = null; + item.setFlags(~ViewHolder.FLAG_CHANGED, item.mFlags); + } + // always null this because an OldViewHolder can never become NewViewHolder w/o being + // recycled. + item.mShadowingHolder = null; + if (!item.shouldBeKeptAsChild()) { + removeAnimatingView(item.itemView); + } + } + }; + + /** + * 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 the appropriate "Starting" + * ({@link #dispatchRemoveStarting(ViewHolder)}, {@link #dispatchMoveStarting(ViewHolder)}, + * {@link #dispatchChangeStarting(ViewHolder, boolean)}, or + * {@link #dispatchAddStarting(ViewHolder)}) + * and "Finished" ({@link #dispatchRemoveFinished(ViewHolder)}, + * {@link #dispatchMoveFinished(ViewHolder)}, + * {@link #dispatchChangeFinished(ViewHolder, boolean)}, + * or {@link #dispatchAddFinished(ViewHolder)}) methods when each item animation is + * being started and ended. + * + *

By default, RecyclerView uses {@link DefaultItemAnimator}

+ * + * @see #setItemAnimator(ItemAnimator) + */ + public static abstract class ItemAnimator { + + 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; + + private boolean mSupportsChangeAnimations = true; + + /** + * 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; + } + + /** + * Returns whether this ItemAnimator supports animations of change events. + * + * @return true if change animations are supported, false otherwise + */ + 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 are 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. + * + * @see Adapter#notifyItemChanged(int) + * @see Adapter#notifyItemRangeChanged(int, int) + * + * @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. + */ + public void setSupportsChangeAnimations(boolean supportsChangeAnimations) { + mSupportsChangeAnimations = supportsChangeAnimations; + } + + /** + * 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 when there are pending animations waiting to be started. This state + * is governed by the return values from {@link #animateAdd(ViewHolder) animateAdd()}, + * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, and + * {@link #animateRemove(ViewHolder) animateRemove()}, 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. + */ + abstract public void runPendingAnimations(); + + /** + * 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. + */ + abstract public 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. + */ + abstract public 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. + */ + abstract public 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 ViewHolder, + * 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 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. + */ + abstract public 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 + */ + public final void dispatchRemoveFinished(ViewHolder item) { + onRemoveFinished(item); + if (mListener != null) { + mListener.onRemoveFinished(item); + } + } + + /** + * Method to be called by subclasses when a move animation is done. + * + * @param item The item which has been moved + */ + public final void dispatchMoveFinished(ViewHolder item) { + onMoveFinished(item); + if (mListener != null) { + mListener.onMoveFinished(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); + if (mListener != null) { + mListener.onAddFinished(item); + } + } + + /** + * Method to be called by subclasses when a change animation is done. + * + * @see #animateChange(ViewHolder, ViewHolder, int, int, int, int) + * @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 dispatchChangeFinished(ViewHolder item, boolean oldItem) { + onChangeFinished(item, oldItem); + if (mListener != null) { + mListener.onChangeFinished(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); + } + + /** + * 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, appropriate dispatch methods (e.g., {@link #dispatchAddFinished(ViewHolder)} + * should be called since the animations are effectively done when this + * method is called. + * + * @param item The item for which an animation should be stopped. + */ + abstract public 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, appropriate dispatch methods (e.g., {@link #dispatchAddFinished(ViewHolder)} + * should be called since the animations are effectively done when this + * method is called. + */ + abstract public 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. + */ + abstract public boolean isRunning(); + + /** + * Like {@link #isRunning()}, this method returns whether there are any item + * animations currently running. Addtionally, 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; + } + + /** + * 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 onRemoveFinished(ViewHolder item); + void onAddFinished(ViewHolder item); + void onMoveFinished(ViewHolder item); + void onChangeFinished(ViewHolder item); + } + + /** + * 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(); + } + + /** + * 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 { + void onAnimationsFinished(); + } + + /** + * 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. + */ + 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. + */ + 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. + */ + 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. + */ + 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) {} + + } + + /** + * Internal data structure that holds information about an item's bounds. + * This information is used in calculating item animations. + */ + private static class ItemHolderInfo { + ViewHolder holder; + int left, top, right, bottom; + + ItemHolderInfo(ViewHolder holder, int left, int top, int right, int bottom) { + this.holder = holder; + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + } +} diff --git a/app/src/main/java/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java b/app/src/main/java/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java new file mode 100644 index 000000000..3fe9abc84 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.os.Bundle; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +/** + * The AccessibilityDelegate used by RecyclerView. + *

+ * This class handles basic accessibility actions and delegates them to LayoutManager. + */ +public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat { + final RecyclerView mRecyclerView; + + + public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + } + + private 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, AccessibilityNodeInfoCompat 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); + } + } + } + + AccessibilityDelegateCompat getItemDelegate() { + return mItemDelegate; + } + + final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat 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/app/src/main/java/android/support/v7/widget/ScrollbarHelper.java b/app/src/main/java/android/support/v7/widget/ScrollbarHelper.java new file mode 100644 index 000000000..724fac8a6 --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/ScrollbarHelper.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v7.widget; + +import android.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/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java b/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java new file mode 100644 index 000000000..f98b7f2cd --- /dev/null +++ b/app/src/main/java/android/support/v7/widget/StaggeredGridLayoutManager.java @@ -0,0 +1,2763 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v7.widget; + +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.List; + +import static android.support.v7.widget.LayoutState.LAYOUT_START; +import static android.support.v7.widget.LayoutState.LAYOUT_END; +import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD; +import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL; +import static android.support.v7.widget.RecyclerView.NO_POSITION; + +/** + * A LayoutManager that lays out children in a staggered grid formation. + * It supports horizontal & vertical layout as well as an ability to layout children in reverse. + *

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

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

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

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

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

+ * After scrolling stops, we check this flag and if it is set, re-layout. + */ + private boolean mLaidOutInvalidFullSpan = false; + + /** + * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. + * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} + */ + private boolean mSmoothScrollbarEnabled = true; + + private final Runnable mCheckForGapsRunnable = new Runnable() { + @Override + public void run() { + checkForGaps(); + } + }; + + /** + * Creates a StaggeredGridLayoutManager with given parameters. + * + * @param spanCount If orientation is vertical, spanCount is number of columns. If + * orientation is horizontal, spanCount is number of rows. + * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL} + */ + public StaggeredGridLayoutManager(int spanCount, int orientation) { + mOrientation = orientation; + setSpanCount(spanCount); + } + + /** + * Checks for gaps in the UI that may be caused by adapter changes. + *

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

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

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

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

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

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

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

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

+ * Not that this value is not affected by RecyclerView's layout direction. + * + * @return True if layout is reversed, false otherwise + * @see #setReverseLayout(boolean) + */ + public boolean getReverseLayout() { + return mReverseLayout; + } + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + ensureOrientationHelper(); + + final AnchorInfo anchorInfo = mAnchorInfo; + anchorInfo.reset(); + + if (mPendingSavedState != null) { + applyPendingSavedState(anchorInfo); + } else { + resolveShouldLayoutReverse(); + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + } + + updateAnchorInfoForLayout(state, anchorInfo); + + if (mPendingSavedState == null) { + if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd || + isLayoutRTL() != mLastLayoutRTL) { + mLazySpanLookup.clear(); + anchorInfo.mInvalidateOffsets = true; + } + } + + if (getChildCount() > 0 && (mPendingSavedState == null || + mPendingSavedState.mSpanOffsetsSize < 1)) { + if (anchorInfo.mInvalidateOffsets) { + for (int i = 0; i < mSpanCount; i++) { + // Scroll to position is set, clear. + mSpans[i].clear(); + if (anchorInfo.mOffset != INVALID_OFFSET) { + mSpans[i].setLine(anchorInfo.mOffset); + } + } + } else { + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorInfo.mOffset); + } + } + } + detachAndScrapAttachedViews(recycler); + mLaidOutInvalidFullSpan = false; + updateMeasureSpecs(); + if (anchorInfo.mLayoutFromEnd) { + // Layout start. + updateLayoutStateToFillStart(anchorInfo.mPosition, state); + fill(recycler, mLayoutState, state); + // Layout end. + updateLayoutStateToFillEnd(anchorInfo.mPosition, state); + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); + } else { + // Layout end. + updateLayoutStateToFillEnd(anchorInfo.mPosition, state); + fill(recycler, mLayoutState, state); + // Layout start. + updateLayoutStateToFillStart(anchorInfo.mPosition, state); + mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; + fill(recycler, mLayoutState, state); + } + + if (getChildCount() > 0) { + if (mShouldReverseLayout) { + fixEndGap(recycler, state, true); + fixStartGap(recycler, state, false); + } else { + fixStartGap(recycler, state, true); + fixEndGap(recycler, state, false); + } + } + + if (!state.isPreLayout()) { + final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE + && getChildCount() > 0 + && (mLaidOutInvalidFullSpan || hasGapsToFix() != null); + if (needToCheckForGaps) { + removeCallbacks(mCheckForGapsRunnable); + postOnAnimation(mCheckForGapsRunnable); + } + mPendingScrollPosition = NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + } + mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd; + mLastLayoutRTL = isLayoutRTL(); + mPendingSavedState = null; // we don't need this anymore + } + + private void applyPendingSavedState(AnchorInfo anchorInfo) { + if (DEBUG) { + Log.d(TAG, "found saved state: " + mPendingSavedState); + } + if (mPendingSavedState.mSpanOffsetsSize > 0) { + if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) { + for (int i = 0; i < mSpanCount; i++) { + mSpans[i].clear(); + int line = mPendingSavedState.mSpanOffsets[i]; + if (line != Span.INVALID_LINE) { + if (mPendingSavedState.mAnchorLayoutFromEnd) { + line += mPrimaryOrientation.getEndAfterPadding(); + } else { + line += mPrimaryOrientation.getStartAfterPadding(); + } + } + mSpans[i].setLine(line); + } + } else { + mPendingSavedState.invalidateSpanInfo(); + mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition; + } + } + mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL; + setReverseLayout(mPendingSavedState.mReverseLayout); + resolveShouldLayoutReverse(); + + if (mPendingSavedState.mAnchorPosition != NO_POSITION) { + mPendingScrollPosition = mPendingSavedState.mAnchorPosition; + anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; + } else { + anchorInfo.mLayoutFromEnd = mShouldReverseLayout; + } + if (mPendingSavedState.mSpanLookupSize > 1) { + mLazySpanLookup.mData = mPendingSavedState.mSpanLookup; + mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems; + } + } + + void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) { + if (updateAnchorFromPendingData(state, anchorInfo)) { + return; + } + if (updateAnchorFromChildren(state, anchorInfo)) { + return; + } + if (DEBUG) { + Log.d(TAG, "Deciding anchor info from fresh state"); + } + anchorInfo.assignCoordinateFromPadding(); + anchorInfo.mPosition = 0; + } + + private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) { + // We don't recycle views out of adapter order. This way, we can rely on the first or + // last child as the anchor position. + // Layout direction may change but we should select the child depending on the latest + // layout direction. Otherwise, we'll choose the wrong child. + anchorInfo.mPosition = mLastLayoutFromEnd + ? findLastReferenceChildPosition(state.getItemCount()) + : findFirstReferenceChildPosition(state.getItemCount()); + anchorInfo.mOffset = INVALID_OFFSET; + return true; + } + + boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { + // Validate scroll position if exists. + if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) { + return false; + } + // Validate it. + if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { + mPendingScrollPosition = NO_POSITION; + mPendingScrollPositionOffset = INVALID_OFFSET; + return false; + } + + if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == NO_POSITION + || mPendingSavedState.mSpanOffsetsSize < 1) { + // If item is visible, make it fully visible. + final View child = findViewByPosition(mPendingScrollPosition); + if (child != null) { + // Use regular anchor position, offset according to pending offset and target + // child + anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition() + : getFirstChildPosition(); + + if (mPendingScrollPositionOffset != INVALID_OFFSET) { + if (anchorInfo.mLayoutFromEnd) { + final int target = mPrimaryOrientation.getEndAfterPadding() - + mPendingScrollPositionOffset; + anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child); + } else { + final int target = mPrimaryOrientation.getStartAfterPadding() + + mPendingScrollPositionOffset; + anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child); + } + return true; + } + + // no offset provided. Decide according to the child location + final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child); + if (childSize > mPrimaryOrientation.getTotalSpace()) { + // Item does not fit. Fix depending on layout direction. + anchorInfo.mOffset = anchorInfo.mLayoutFromEnd + ? mPrimaryOrientation.getEndAfterPadding() + : mPrimaryOrientation.getStartAfterPadding(); + return true; + } + + final int startGap = mPrimaryOrientation.getDecoratedStart(child) + - mPrimaryOrientation.getStartAfterPadding(); + if (startGap < 0) { + anchorInfo.mOffset = -startGap; + return true; + } + final int endGap = mPrimaryOrientation.getEndAfterPadding() - + mPrimaryOrientation.getDecoratedEnd(child); + if (endGap < 0) { + anchorInfo.mOffset = endGap; + return true; + } + // child already visible. just layout as usual + anchorInfo.mOffset = INVALID_OFFSET; + } else { + // Child is not visible. Set anchor coordinate depending on in which direction + // child will be visible. + anchorInfo.mPosition = mPendingScrollPosition; + if (mPendingScrollPositionOffset == INVALID_OFFSET) { + final int position = calculateScrollDirectionForPosition( + anchorInfo.mPosition); + anchorInfo.mLayoutFromEnd = position == LAYOUT_END; + anchorInfo.assignCoordinateFromPadding(); + } else { + anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset); + } + anchorInfo.mInvalidateOffsets = true; + } + } else { + anchorInfo.mOffset = INVALID_OFFSET; + anchorInfo.mPosition = mPendingScrollPosition; + } + return true; + } + + void updateMeasureSpecs() { + mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount; + mFullSizeSpec = View.MeasureSpec.makeMeasureSpec( + mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY); + if (mOrientation == VERTICAL) { + mWidthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); + mHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } else { + mHeightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY); + mWidthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + } + + @Override + public boolean supportsPredictiveItemAnimations() { + return mPendingSavedState == null; + } + + /** + * Returns the adapter position of the first visible view for each span. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. + * + * @param position Index (starting at 0) of the reference item. + * @param offset The distance (in pixels) between the start edge of the item view and + * start edge of the RecyclerView. + * @see #setReverseLayout(boolean) + * @see #scrollToPosition(int) + */ + public void scrollToPositionWithOffset(int position, int offset) { + if (mPendingSavedState != null) { + mPendingSavedState.invalidateAnchorPositionInfo(); + } + mPendingScrollPosition = position; + mPendingScrollPositionOffset = offset; + requestLayout(); + } + + int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { + ensureOrientationHelper(); + final int referenceChildPosition; + if (dt > 0) { // layout towards end + mLayoutState.mLayoutDirection = LAYOUT_END; + mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD + : ITEM_DIRECTION_TAIL; + referenceChildPosition = getLastChildPosition(); + } else { + mLayoutState.mLayoutDirection = LAYOUT_START; + mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL + : ITEM_DIRECTION_HEAD; + referenceChildPosition = getFirstChildPosition(); + } + mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection; + final int absDt = Math.abs(dt); + mLayoutState.mAvailable = absDt; + mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0; + int consumed = fill(recycler, mLayoutState, state); + final int totalScroll; + if (absDt < consumed) { + totalScroll = dt; + } else if (dt < 0) { + totalScroll = -consumed; + } else { // dt > 0 + totalScroll = consumed; + } + if (DEBUG) { + Log.d(TAG, "asked " + dt + " scrolled" + totalScroll); + } + + mPrimaryOrientation.offsetChildren(-totalScroll); + // always reset this if we scroll for a proper save instance state + mLastLayoutFromEnd = mShouldReverseLayout; + return totalScroll; + } + + private int getLastChildPosition() { + final int childCount = getChildCount(); + return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1)); + } + + private int getFirstChildPosition() { + final int childCount = getChildCount(); + return childCount == 0 ? 0 : getPosition(getChildAt(0)); + } + + /** + * Finds the first View that can be used as an anchor View. + * + * @return Position of the View or 0 if it cannot find any such View. + */ + private int findFirstReferenceChildPosition(int itemCount) { + final int limit = getChildCount(); + for (int i = 0; i < limit; i++) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + return position; + } + } + return 0; + } + + /** + * Finds the last View that can be used as an anchor View. + * + * @return Position of the View or 0 if it cannot find any such View. + */ + private int findLastReferenceChildPosition(int itemCount) { + for (int i = getChildCount() - 1; i >= 0; i--) { + final View view = getChildAt(i); + final int position = getPosition(view); + if (position >= 0 && position < itemCount) { + return position; + } + } + return 0; + } + + @Override + public RecyclerView.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) { + return new LayoutParams(c, attrs); + } + + @Override + public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { + if (lp instanceof ViewGroup.MarginLayoutParams) { + return new LayoutParams((ViewGroup.MarginLayoutParams) lp); + } else { + return new LayoutParams(lp); + } + } + + @Override + public boolean checkLayoutParams(RecyclerView.LayoutParams lp) { + return lp instanceof LayoutParams; + } + + public int getOrientation() { + return mOrientation; + } + + + /** + * LayoutParams used by StaggeredGridLayoutManager. + *

+ * Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the + * orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is + * expected to fill all of the space given to it. + */ + public static class LayoutParams extends RecyclerView.LayoutParams { + + /** + * Span Id for Views that are not laid out yet. + */ + public static final int INVALID_SPAN_ID = -1; + + // Package scope to be able to access from tests. + Span mSpan; + + boolean mFullSpan; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(RecyclerView.LayoutParams source) { + super(source); + } + + /** + * When set to true, the item will layout using all span area. That means, if orientation + * is vertical, the view will have full width; if orientation is horizontal, the view will + * have full height. + * + * @param fullSpan True if this item should traverse all spans. + * @see #isFullSpan() + */ + public void setFullSpan(boolean fullSpan) { + mFullSpan = fullSpan; + } + + /** + * Returns whether this View occupies all available spans or just one. + * + * @return True if the View occupies all spans or false otherwise. + * @see #setFullSpan(boolean) + */ + public boolean isFullSpan() { + return mFullSpan; + } + + /** + * Returns the Span index to which this View is assigned. + * + * @return The Span index of the View. If View is not yet assigned to any span, returns + * {@link #INVALID_SPAN_ID}. + */ + public final int getSpanIndex() { + if (mSpan == null) { + return INVALID_SPAN_ID; + } + return mSpan.mIndex; + } + } + + // Package scoped to access from tests. + class Span { + + static final int INVALID_LINE = Integer.MIN_VALUE; + private ArrayList mViews = new ArrayList(); + int mCachedStart = INVALID_LINE; + int mCachedEnd = INVALID_LINE; + int mDeletedSize = 0; + final int mIndex; + + private Span(int index) { + mIndex = index; + } + + int getStartLine(int def) { + if (mCachedStart != INVALID_LINE) { + return mCachedStart; + } + if (mViews.size() == 0) { + return def; + } + calculateCachedStart(); + return mCachedStart; + } + + void calculateCachedStart() { + final View startView = mViews.get(0); + final LayoutParams lp = getLayoutParams(startView); + mCachedStart = mPrimaryOrientation.getDecoratedStart(startView); + if (lp.mFullSpan) { + LazySpanLookup.FullSpanItem fsi = mLazySpanLookup + .getFullSpanItem(lp.getViewLayoutPosition()); + if (fsi != null && fsi.mGapDir == LAYOUT_START) { + mCachedStart -= fsi.getGapForSpan(mIndex); + } + } + } + + // Use this one when default value does not make sense and not having a value means a bug. + int getStartLine() { + if (mCachedStart != INVALID_LINE) { + return mCachedStart; + } + calculateCachedStart(); + return mCachedStart; + } + + int getEndLine(int def) { + if (mCachedEnd != INVALID_LINE) { + return mCachedEnd; + } + final int size = mViews.size(); + if (size == 0) { + return def; + } + calculateCachedEnd(); + return mCachedEnd; + } + + void calculateCachedEnd() { + final View endView = mViews.get(mViews.size() - 1); + final LayoutParams lp = getLayoutParams(endView); + mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView); + if (lp.mFullSpan) { + LazySpanLookup.FullSpanItem fsi = mLazySpanLookup + .getFullSpanItem(lp.getViewLayoutPosition()); + if (fsi != null && fsi.mGapDir == LAYOUT_END) { + mCachedEnd += fsi.getGapForSpan(mIndex); + } + } + } + + // Use this one when default value does not make sense and not having a value means a bug. + int getEndLine() { + if (mCachedEnd != INVALID_LINE) { + return mCachedEnd; + } + calculateCachedEnd(); + return mCachedEnd; + } + + void prependToSpan(View view) { + LayoutParams lp = getLayoutParams(view); + lp.mSpan = this; + mViews.add(0, view); + mCachedStart = INVALID_LINE; + if (mViews.size() == 1) { + mCachedEnd = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); + } + } + + void appendToSpan(View view) { + LayoutParams lp = getLayoutParams(view); + lp.mSpan = this; + mViews.add(view); + mCachedEnd = INVALID_LINE; + if (mViews.size() == 1) { + mCachedStart = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view); + } + } + + // Useful method to preserve positions on a re-layout. + void cacheReferenceLineAndClear(boolean reverseLayout, int offset) { + int reference; + if (reverseLayout) { + reference = getEndLine(INVALID_LINE); + } else { + reference = getStartLine(INVALID_LINE); + } + clear(); + if (reference == INVALID_LINE) { + return; + } + if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding()) || + (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) { + return; + } + if (offset != INVALID_OFFSET) { + reference += offset; + } + mCachedStart = mCachedEnd = reference; + } + + void clear() { + mViews.clear(); + invalidateCache(); + mDeletedSize = 0; + } + + void invalidateCache() { + mCachedStart = INVALID_LINE; + mCachedEnd = INVALID_LINE; + } + + void setLine(int line) { + mCachedEnd = mCachedStart = line; + } + + void popEnd() { + final int size = mViews.size(); + View end = mViews.remove(size - 1); + final LayoutParams lp = getLayoutParams(end); + lp.mSpan = null; + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end); + } + if (size == 1) { + mCachedStart = INVALID_LINE; + } + mCachedEnd = INVALID_LINE; + } + + void popStart() { + View start = mViews.remove(0); + final LayoutParams lp = getLayoutParams(start); + lp.mSpan = null; + if (mViews.size() == 0) { + mCachedEnd = INVALID_LINE; + } + if (lp.isItemRemoved() || lp.isItemChanged()) { + mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start); + } + mCachedStart = INVALID_LINE; + } + + public int getDeletedSize() { + return mDeletedSize; + } + + LayoutParams getLayoutParams(View view) { + return (LayoutParams) view.getLayoutParams(); + } + + void onOffset(int dt) { + if (mCachedStart != INVALID_LINE) { + mCachedStart += dt; + } + if (mCachedEnd != INVALID_LINE) { + mCachedEnd += dt; + } + } + + // normalized offset is how much this span can scroll + int getNormalizedOffset(int dt, int targetStart, int targetEnd) { + if (mViews.size() == 0) { + return 0; + } + if (dt < 0) { + final int endSpace = getEndLine() - targetEnd; + if (endSpace <= 0) { + return 0; + } + return -dt > endSpace ? -endSpace : dt; + } else { + final int startSpace = targetStart - getStartLine(); + if (startSpace <= 0) { + return 0; + } + return startSpace < dt ? startSpace : dt; + } + } + + /** + * Returns if there is no child between start-end lines + * + * @param start The start line + * @param end The end line + * @return true if a new child can be added between start and end + */ + boolean isEmpty(int start, int end) { + final int count = mViews.size(); + for (int i = 0; i < count; i++) { + final View view = mViews.get(i); + if (mPrimaryOrientation.getDecoratedStart(view) < end && + mPrimaryOrientation.getDecoratedEnd(view) > start) { + return false; + } + } + return true; + } + + public int findFirstVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(mViews.size() - 1, -1, false) + : findOneVisibleChild(0, mViews.size(), false); + } + + public int findFirstCompletelyVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(mViews.size() - 1, -1, true) + : findOneVisibleChild(0, mViews.size(), true); + } + + public int findLastVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(0, mViews.size(), false) + : findOneVisibleChild(mViews.size() - 1, -1, false); + } + + public int findLastCompletelyVisibleItemPosition() { + return mReverseLayout + ? findOneVisibleChild(0, mViews.size(), true) + : findOneVisibleChild(mViews.size() - 1, -1, true); + } + + int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) { + final int start = mPrimaryOrientation.getStartAfterPadding(); + final int end = mPrimaryOrientation.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = mViews.get(i); + final int childStart = mPrimaryOrientation.getDecoratedStart(child); + final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); + if (childStart < end && childEnd > start) { + if (completelyVisible) { + if (childStart >= start && childEnd <= end) { + return getPosition(child); + } + } else { + return getPosition(child); + } + } + } + return NO_POSITION; + } + } + + /** + * An array of mappings from adapter position to span. + * This only grows when a write happens and it grows up to the size of the adapter. + */ + static class LazySpanLookup { + + private static final int MIN_SIZE = 10; + int[] mData; + List mFullSpanItems; + + + /** + * Invalidates everything after this position, including full span information + */ + int forceInvalidateAfter(int position) { + if (mFullSpanItems != null) { + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + mFullSpanItems.remove(i); + } + } + } + return invalidateAfter(position); + } + + /** + * returns end position for invalidation. + */ + int invalidateAfter(int position) { + if (mData == null) { + return RecyclerView.NO_POSITION; + } + if (position >= mData.length) { + return RecyclerView.NO_POSITION; + } + int endPosition = invalidateFullSpansAfter(position); + if (endPosition == RecyclerView.NO_POSITION) { + Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID); + return mData.length; + } else { + // just invalidate items in between + Arrays.fill(mData, position, endPosition + 1, LayoutParams.INVALID_SPAN_ID); + return endPosition + 1; + } + } + + int getSpan(int position) { + if (mData == null || position >= mData.length) { + return LayoutParams.INVALID_SPAN_ID; + } else { + return mData[position]; + } + } + + void setSpan(int position, Span span) { + ensureSize(position); + mData[position] = span.mIndex; + } + + int sizeForPosition(int position) { + int len = mData.length; + while (len <= position) { + len *= 2; + } + return len; + } + + void ensureSize(int position) { + if (mData == null) { + mData = new int[Math.max(position, MIN_SIZE) + 1]; + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } else if (position >= mData.length) { + int[] old = mData; + mData = new int[sizeForPosition(position)]; + System.arraycopy(old, 0, mData, 0, old.length); + Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID); + } + } + + void clear() { + if (mData != null) { + Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID); + } + mFullSpanItems = null; + } + + void offsetForRemoval(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart + itemCount, mData, positionStart, + mData.length - positionStart - itemCount); + Arrays.fill(mData, mData.length - itemCount, mData.length, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForRemoval(positionStart, itemCount); + } + + private void offsetFullSpansForRemoval(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + final int end = positionStart + itemCount; + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + if (fsi.mPosition < end) { + mFullSpanItems.remove(i); + } else { + fsi.mPosition -= itemCount; + } + } + } + + void offsetForAddition(int positionStart, int itemCount) { + if (mData == null || positionStart >= mData.length) { + return; + } + ensureSize(positionStart + itemCount); + System.arraycopy(mData, positionStart, mData, positionStart + itemCount, + mData.length - positionStart - itemCount); + Arrays.fill(mData, positionStart, positionStart + itemCount, + LayoutParams.INVALID_SPAN_ID); + offsetFullSpansForAddition(positionStart, itemCount); + } + + private void offsetFullSpansForAddition(int positionStart, int itemCount) { + if (mFullSpanItems == null) { + return; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition < positionStart) { + continue; + } + fsi.mPosition += itemCount; + } + } + + /** + * Returns when invalidation should end. e.g. hitting a full span position. + * Returned position SHOULD BE invalidated. + */ + private int invalidateFullSpansAfter(int position) { + if (mFullSpanItems == null) { + return RecyclerView.NO_POSITION; + } + final FullSpanItem item = getFullSpanItem(position); + // if there is an fsi at this position, get rid of it. + if (item != null) { + mFullSpanItems.remove(item); + } + int nextFsiIndex = -1; + final int count = mFullSpanItems.size(); + for (int i = 0; i < count; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= position) { + nextFsiIndex = i; + break; + } + } + if (nextFsiIndex != -1) { + FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex); + mFullSpanItems.remove(nextFsiIndex); + return fsi.mPosition; + } + return RecyclerView.NO_POSITION; + } + + public void addFullSpanItem(FullSpanItem fullSpanItem) { + if (mFullSpanItems == null) { + mFullSpanItems = new ArrayList(); + } + final int size = mFullSpanItems.size(); + for (int i = 0; i < size; i++) { + FullSpanItem other = mFullSpanItems.get(i); + if (other.mPosition == fullSpanItem.mPosition) { + if (DEBUG) { + throw new IllegalStateException("two fsis for same position"); + } else { + mFullSpanItems.remove(i); + } + } + if (other.mPosition >= fullSpanItem.mPosition) { + mFullSpanItems.add(i, fullSpanItem); + return; + } + } + // if it is not added to a position. + mFullSpanItems.add(fullSpanItem); + } + + public FullSpanItem getFullSpanItem(int position) { + if (mFullSpanItems == null) { + return null; + } + for (int i = mFullSpanItems.size() - 1; i >= 0; i--) { + final FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition == position) { + return fsi; + } + } + return null; + } + + /** + * @param minPos inclusive + * @param maxPos exclusive + * @param gapDir if not 0, returns FSIs on in that direction + * @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be + * returned even if its gap direction does not match. + */ + public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir, + boolean hasUnwantedGapAfter) { + if (mFullSpanItems == null) { + return null; + } + final int limit = mFullSpanItems.size(); + for (int i = 0; i < limit; i++) { + FullSpanItem fsi = mFullSpanItems.get(i); + if (fsi.mPosition >= maxPos) { + return null; + } + if (fsi.mPosition >= minPos + && (gapDir == 0 || fsi.mGapDir == gapDir || + (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) { + return fsi; + } + } + return null; + } + + /** + * We keep information about full span items because they may create gaps in the UI. + */ + static class FullSpanItem implements Parcelable { + + int mPosition; + int mGapDir; + int[] mGapPerSpan; + // A full span may be laid out in primary direction but may have gaps due to + // invalidation of views after it. This is recorded during a reverse scroll and if + // view is still on the screen after scroll stops, we have to recalculate layout + boolean mHasUnwantedGapAfter; + + public FullSpanItem(Parcel in) { + mPosition = in.readInt(); + mGapDir = in.readInt(); + mHasUnwantedGapAfter = in.readInt() == 1; + int spanCount = in.readInt(); + if (spanCount > 0) { + mGapPerSpan = new int[spanCount]; + in.readIntArray(mGapPerSpan); + } + } + + public FullSpanItem() { + } + + int getGapForSpan(int spanIndex) { + return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex]; + } + + public void invalidateSpanGaps() { + mGapPerSpan = null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mPosition); + dest.writeInt(mGapDir); + dest.writeInt(mHasUnwantedGapAfter ? 1 : 0); + if (mGapPerSpan != null && mGapPerSpan.length > 0) { + dest.writeInt(mGapPerSpan.length); + dest.writeIntArray(mGapPerSpan); + } else { + dest.writeInt(0); + } + } + + @Override + public String toString() { + return "FullSpanItem{" + + "mPosition=" + mPosition + + ", mGapDir=" + mGapDir + + ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter + + ", mGapPerSpan=" + Arrays.toString(mGapPerSpan) + + '}'; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FullSpanItem createFromParcel(Parcel in) { + return new FullSpanItem(in); + } + + @Override + public FullSpanItem[] newArray(int size) { + return new FullSpanItem[size]; + } + }; + } + } + + static class SavedState implements Parcelable { + + int mAnchorPosition; + int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated + int mSpanOffsetsSize; + int[] mSpanOffsets; + int mSpanLookupSize; + int[] mSpanLookup; + List mFullSpanItems; + boolean mReverseLayout; + boolean mAnchorLayoutFromEnd; + boolean mLastLayoutRTL; + + public SavedState() { + } + + SavedState(Parcel in) { + mAnchorPosition = in.readInt(); + mVisibleAnchorPosition = in.readInt(); + mSpanOffsetsSize = in.readInt(); + if (mSpanOffsetsSize > 0) { + mSpanOffsets = new int[mSpanOffsetsSize]; + in.readIntArray(mSpanOffsets); + } + + mSpanLookupSize = in.readInt(); + if (mSpanLookupSize > 0) { + mSpanLookup = new int[mSpanLookupSize]; + in.readIntArray(mSpanLookup); + } + mReverseLayout = in.readInt() == 1; + mAnchorLayoutFromEnd = in.readInt() == 1; + mLastLayoutRTL = in.readInt() == 1; + mFullSpanItems = in.readArrayList( + LazySpanLookup.FullSpanItem.class.getClassLoader()); + } + + public SavedState(SavedState other) { + mSpanOffsetsSize = other.mSpanOffsetsSize; + mAnchorPosition = other.mAnchorPosition; + mVisibleAnchorPosition = other.mVisibleAnchorPosition; + mSpanOffsets = other.mSpanOffsets; + mSpanLookupSize = other.mSpanLookupSize; + mSpanLookup = other.mSpanLookup; + mReverseLayout = other.mReverseLayout; + mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; + mLastLayoutRTL = other.mLastLayoutRTL; + mFullSpanItems = other.mFullSpanItems; + } + + void invalidateSpanInfo() { + mSpanOffsets = null; + mSpanOffsetsSize = 0; + mSpanLookupSize = 0; + mSpanLookup = null; + mFullSpanItems = null; + } + + void invalidateAnchorPositionInfo() { + mSpanOffsets = null; + mSpanOffsetsSize = 0; + mAnchorPosition = NO_POSITION; + mVisibleAnchorPosition = NO_POSITION; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAnchorPosition); + dest.writeInt(mVisibleAnchorPosition); + dest.writeInt(mSpanOffsetsSize); + if (mSpanOffsetsSize > 0) { + dest.writeIntArray(mSpanOffsets); + } + dest.writeInt(mSpanLookupSize); + if (mSpanLookupSize > 0) { + dest.writeIntArray(mSpanLookup); + } + dest.writeInt(mReverseLayout ? 1 : 0); + dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); + dest.writeInt(mLastLayoutRTL ? 1 : 0); + dest.writeList(mFullSpanItems); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Data class to hold the information about an anchor position which is used in onLayout call. + */ + private class AnchorInfo { + + int mPosition; + int mOffset; + boolean mLayoutFromEnd; + boolean mInvalidateOffsets; + + void reset() { + mPosition = NO_POSITION; + mOffset = INVALID_OFFSET; + mLayoutFromEnd = false; + mInvalidateOffsets = false; + } + + void assignCoordinateFromPadding() { + mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding() + : mPrimaryOrientation.getStartAfterPadding(); + } + + void assignCoordinateFromPadding(int addedDistance) { + if (mLayoutFromEnd) { + mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance; + } else { + mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance; + } + } + } +} diff --git a/app/src/main/java/com/hippo/ehviewer/widget/ContentLayout.java b/app/src/main/java/com/hippo/ehviewer/widget/ContentLayout.java index 222cb759d..8a9fa3560 100644 --- a/app/src/main/java/com/hippo/ehviewer/widget/ContentLayout.java +++ b/app/src/main/java/com/hippo/ehviewer/widget/ContentLayout.java @@ -350,7 +350,9 @@ public class ContentLayout extends FrameLayout { if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) { mFirstIndex += pageVolume; mLastIndex += pageVolume; - // TODO + + mRecyclerView.stopScroll(); + LayoutManagerUtils.smoothScrollToPosition(mLayoutManager, mContext, mFirstIndex - 1, 100); } else { mCurrentPage = mFirstPage; mFirstIndex = 0; @@ -464,7 +466,8 @@ public class ContentLayout extends FrameLayout { if (mViewTransition.getShownViewIndex() == 0) { // Go to top mRecyclerView.stopScroll(); - mLayoutManager.smoothScrollToPosition(mRecyclerView, null, 0); + LayoutManagerUtils.scrollToPositionWithOffset(mLayoutManager, 0, 0); + onScrollToPosition(); // Show header refresh mRefreshLayout.setFooterRefreshing(false); mRefreshLayout.setHeaderRefreshing(true); diff --git a/app/src/main/java/com/hippo/util/LayoutManagerUtils.java b/app/src/main/java/com/hippo/util/LayoutManagerUtils.java index 9a1ba604b..aa73ed9ce 100644 --- a/app/src/main/java/com/hippo/util/LayoutManagerUtils.java +++ b/app/src/main/java/com/hippo/util/LayoutManagerUtils.java @@ -15,10 +15,14 @@ package com.hippo.util; +import android.content.Context; +import android.graphics.PointF; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; +import com.hippo.widget.recyclerview.SimpleSmoothScroller; + public final class LayoutManagerUtils { public static void scrollToPositionWithOffset( @@ -33,6 +37,47 @@ public final class LayoutManagerUtils { } } + public static void smoothScrollToPosition( + RecyclerView.LayoutManager layoutManager, Context context, int position) { + smoothScrollToPosition(layoutManager, context, position, -1); + } + + public static void smoothScrollToPosition( + RecyclerView.LayoutManager layoutManager, Context context, int position, + int millisecondsPerInch) { + SimpleSmoothScroller smoothScroller; + if (layoutManager instanceof LinearLayoutManager) { + final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; + smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) { + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + return linearLayoutManager.computeScrollVectorForPosition(targetPosition); + } + }; + } else if (layoutManager instanceof StaggeredGridLayoutManager) { + final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager; + smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) { + @Override + public PointF computeScrollVectorForPosition(int targetPosition) { + final int direction = staggeredGridLayoutManager.calculateScrollDirectionForPosition(targetPosition); + if (direction == 0) { + return null; + } + if (staggeredGridLayoutManager.getOrientation() == StaggeredGridLayoutManager.HORIZONTAL) { + return new PointF(direction, 0); + } else { + return new PointF(0, direction); + } + } + }; + } else { + throw new IllegalStateException("Can't do smoothScrollToPosition for " + + layoutManager.getClass().getName()); + } + smoothScroller.setTargetPosition(position); + layoutManager.startSmoothScroll(smoothScroller); + } + public static int getFirstVisibleItemPostion(RecyclerView.LayoutManager layoutManager) { if (layoutManager instanceof LinearLayoutManager) { return ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); diff --git a/app/src/main/java/com/hippo/widget/recyclerview/SimpleSmoothScroller.java b/app/src/main/java/com/hippo/widget/recyclerview/SimpleSmoothScroller.java new file mode 100644 index 000000000..26053b82e --- /dev/null +++ b/app/src/main/java/com/hippo/widget/recyclerview/SimpleSmoothScroller.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 Hippo Seven + * + * 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 + * 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.hippo.widget.recyclerview; + +import android.content.Context; +import android.support.v7.widget.LinearSmoothScroller; + +public abstract class SimpleSmoothScroller extends LinearSmoothScroller { + + private float mMillisecondsPerPx; + + public SimpleSmoothScroller(Context context, float millisecondsPerInch) { + super(context); + mMillisecondsPerPx = millisecondsPerInch / context.getResources().getDisplayMetrics().densityDpi; + } + + @Override + protected int calculateTimeForScrolling(int dx) { + if (mMillisecondsPerPx <= 0) { + return super.calculateTimeForScrolling(dx); + } else { + return (int) Math.ceil(Math.abs(dx) * mMillisecondsPerPx); + } + } +}