Build RecyclerView inline

pull/214/head
M66B 11 months ago
parent be2bbed659
commit 445dc9f25f

@ -608,7 +608,9 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview
// https://mvnrepository.com/artifact/androidx.recyclerview/recyclerview-selection
implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
//implementation "androidx.recyclerview:recyclerview:$recyclerview_version"
implementation "androidx.customview:customview:1.1.0"
implementation "androidx.customview:customview-poolingcontainer:1.0.0"
//implementation "androidx.recyclerview:recyclerview-selection:1.1.0" // 1.2.0-alpha01
// https://mvnrepository.com/artifact/androidx.coordinatorlayout/coordinatorlayout

@ -0,0 +1,776 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.util.Log;
import androidx.core.util.Pools;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Helper class that can enqueue and process adapter update operations.
* <p>
* 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.
* <p>
* 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.
* <p>
* Although operations may be forwarded to LayoutManager in different orders, resulting data set
* is guaranteed to be the consistent.
*/
final class AdapterHelper implements OpReorderer.Callback {
static final int POSITION_TYPE_INVISIBLE = 0;
static final int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
private static final boolean DEBUG = false;
private static final String TAG = "AHT";
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
final Callback mCallback;
Runnable mOnItemProcessedCallback;
final boolean mDisableRecycler;
final OpReorderer mOpReorderer;
private int mExistingUpdateTypes = 0;
AdapterHelper(Callback callback) {
this(callback, false);
}
AdapterHelper(Callback callback, boolean disableRecycler) {
mCallback = callback;
mDisableRecycler = disableRecycler;
mOpReorderer = new OpReorderer(this);
}
AdapterHelper addUpdateOp(UpdateOp... ops) {
Collections.addAll(mPendingUpdates, ops);
return this;
}
void reset() {
recycleUpdateOpsAndClearList(mPendingUpdates);
recycleUpdateOpsAndClearList(mPostponedList);
mExistingUpdateTypes = 0;
}
void preProcess() {
mOpReorderer.reorderOps(mPendingUpdates);
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
applyAdd(op);
break;
case UpdateOp.REMOVE:
applyRemove(op);
break;
case UpdateOp.UPDATE:
applyUpdate(op);
break;
case UpdateOp.MOVE:
applyMove(op);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
mPendingUpdates.clear();
}
void consumePostponedUpdates() {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
mCallback.onDispatchSecondPass(mPostponedList.get(i));
}
recycleUpdateOpsAndClearList(mPostponedList);
mExistingUpdateTypes = 0;
}
private void applyMove(UpdateOp op) {
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
// otherwise, it would be converted into a REMOVE operation
postponeAndUpdateViewHolders(op);
}
private void applyRemove(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
boolean typeChanged = false;
RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) {
// If a ViewHolder exists or this is a newly added item, we can defer this update
// to post layout stage.
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
// * For items that are added and removed in the same process cycle, they won't
// have any effect in pre-layout since their add ops are already deferred to
// post-layout pass.
if (type == POSITION_TYPE_INVISIBLE) {
// Looks like we have other updates that we cannot merge with this one.
// Create an UpdateOp and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
dispatchAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else {
// This update cannot be recovered because we don't have a ViewHolder representing
// this position. Instead, post it to LayoutManager immediately
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
// Looks like we have other updates that we cannot merge with this one.
// Create UpdateOp op and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
postponeAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_INVISIBLE;
}
if (typeChanged) {
position -= tmpCount; // also equal to tmpStart
tmpEnd -= tmpCount;
tmpCount = 1;
} else {
tmpCount++;
}
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount, null);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void applyUpdate(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
RecyclerView.ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) { // deferred
if (type == POSITION_TYPE_INVISIBLE) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
op.payload);
dispatchAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else { // applied
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount,
op.payload);
postponeAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_INVISIBLE;
}
tmpCount++;
}
if (tmpCount != op.itemCount) { // all 1 effect
Object payload = op.payload;
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void dispatchAndUpdateViewHolders(UpdateOp op) {
// tricky part.
// traverse all postpones and revert their changes on this op if necessary, apply updated
// dispatch to them since now they are after this op.
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
}
if (DEBUG) {
Log.d(TAG, "dispatch (pre)" + op);
Log.d(TAG, "postponed state before:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
// TODO Since move ops are pushed to end, we should not need this anymore
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
}
int tmpCnt = 1;
int offsetPositionForPartial = op.positionStart;
final int positionMultiplier;
switch (op.cmd) {
case UpdateOp.UPDATE:
positionMultiplier = 1;
break;
case UpdateOp.REMOVE:
positionMultiplier = 0;
break;
default:
throw new IllegalArgumentException("op should be remove or update." + op);
}
for (int p = 1; p < op.itemCount; p++) {
final int pos = op.positionStart + (positionMultiplier * p);
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
}
boolean continuous = false;
switch (op.cmd) {
case UpdateOp.UPDATE:
continuous = updatedPos == tmpStart + 1;
break;
case UpdateOp.REMOVE:
continuous = updatedPos == tmpStart;
break;
}
if (continuous) {
tmpCnt++;
} else {
// need to dispatch this separately
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, op.payload);
if (DEBUG) {
Log.d(TAG, "need to dispatch separately " + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
if (op.cmd == UpdateOp.UPDATE) {
offsetPositionForPartial += tmpCnt;
}
tmpStart = updatedPos; // need to remove previously dispatched
tmpCnt = 1;
}
}
Object payload = op.payload;
recycleUpdateOp(op);
if (tmpCnt > 0) {
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt, payload);
if (DEBUG) {
Log.d(TAG, "dispatching:" + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
}
if (DEBUG) {
Log.d(TAG, "post dispatch");
Log.d(TAG, "postponed state after:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
mCallback.onDispatchFirstPass(op);
switch (op.cmd) {
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("only remove and update ops can be dispatched"
+ " in first pass");
}
}
private int updatePositionWithPostponed(int pos, int cmd) {
final int count = mPostponedList.size();
for (int i = count - 1; i >= 0; i--) {
UpdateOp postponed = mPostponedList.get(i);
if (postponed.cmd == UpdateOp.MOVE) {
int start, end;
if (postponed.positionStart < postponed.itemCount) {
start = postponed.positionStart;
end = postponed.itemCount;
} else {
start = postponed.itemCount;
end = postponed.positionStart;
}
if (pos >= start && pos <= end) {
//i'm affected
if (start == postponed.positionStart) {
if (cmd == UpdateOp.ADD) {
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.itemCount--;
}
// op moved to left, move it right to revert
pos++;
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
// op was moved right, move left to revert
pos--;
}
} else if (pos < postponed.positionStart) {
// postponed MV is outside the dispatched OP. if it is before, offset
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
postponed.itemCount--;
}
}
} else {
if (postponed.positionStart <= pos) {
if (postponed.cmd == UpdateOp.ADD) {
pos -= postponed.itemCount;
} else if (postponed.cmd == UpdateOp.REMOVE) {
pos += postponed.itemCount;
}
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
}
}
if (DEBUG) {
Log.d(TAG, "dispath (step" + i + ")");
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.itemCount == op.positionStart || op.itemCount < 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
} else if (op.itemCount <= 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
}
return pos;
}
private boolean canFindInPreLayout(int position) {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (findPositionOffset(op.itemCount, i + 1) == position) {
return true;
}
} else if (op.cmd == UpdateOp.ADD) {
// TODO optimize.
final int end = op.positionStart + op.itemCount;
for (int pos = op.positionStart; pos < end; pos++) {
if (findPositionOffset(pos, i + 1) == position) {
return true;
}
}
}
}
return false;
}
private void applyAdd(UpdateOp op) {
postponeAndUpdateViewHolders(op);
}
private void postponeAndUpdateViewHolders(UpdateOp op) {
if (DEBUG) {
Log.d(TAG, "postponing " + op);
}
mPostponedList.add(op);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
}
}
boolean hasPendingUpdates() {
return mPendingUpdates.size() > 0;
}
boolean hasAnyUpdateTypes(int updateTypes) {
return (mExistingUpdateTypes & updateTypes) != 0;
}
int findPositionOffset(int position) {
return findPositionOffset(position, 0);
}
int findPositionOffset(int position, int firstPostponedItem) {
int count = mPostponedList.size();
for (int i = firstPostponedItem; i < count; ++i) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.positionStart == position) {
position = op.itemCount;
} else {
if (op.positionStart < position) {
position--; // like a remove
}
if (op.itemCount <= position) {
position++; // like an add
}
}
} else if (op.positionStart <= position) {
if (op.cmd == UpdateOp.REMOVE) {
if (position < op.positionStart + op.itemCount) {
return -1;
}
position -= op.itemCount;
} else if (op.cmd == UpdateOp.ADD) {
position += op.itemCount;
}
}
}
return position;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
mExistingUpdateTypes |= UpdateOp.UPDATE;
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeInserted(int positionStart, int itemCount) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount, null));
mExistingUpdateTypes |= UpdateOp.ADD;
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeRemoved(int positionStart, int itemCount) {
if (itemCount < 1) {
return false;
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
mExistingUpdateTypes |= UpdateOp.REMOVE;
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeMoved(int from, int to, int itemCount) {
if (from == to) {
return false; // no-op
}
if (itemCount != 1) {
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
mExistingUpdateTypes |= UpdateOp.MOVE;
return mPendingUpdates.size() == 1;
}
/**
* Skips pre-processing and applies all updates in one pass.
*/
void consumeUpdatesInOnePass() {
// we still consume postponed updates (if there is) in case there was a pre-process call
// w/o a matching consumePostponedUpdates.
consumePostponedUpdates();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
break;
case UpdateOp.MOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
recycleUpdateOpsAndClearList(mPendingUpdates);
mExistingUpdateTypes = 0;
}
public int applyPendingUpdatesToPosition(int position) {
final int size = mPendingUpdates.size();
for (int i = 0; i < size; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
if (op.positionStart <= position) {
position += op.itemCount;
}
break;
case UpdateOp.REMOVE:
if (op.positionStart <= position) {
final int end = op.positionStart + op.itemCount;
if (end > position) {
return RecyclerView.NO_POSITION;
}
position -= op.itemCount;
}
break;
case UpdateOp.MOVE:
if (op.positionStart == position) {
position = op.itemCount; //position end
} else {
if (op.positionStart < position) {
position -= 1;
}
if (op.itemCount <= position) {
position += 1;
}
}
break;
}
}
return position;
}
boolean hasUpdates() {
return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty();
}
/**
* Queued operation to happen when child views are updated.
*/
static final class UpdateOp {
static final int ADD = 1;
static final int REMOVE = 1 << 1;
static final int UPDATE = 1 << 2;
static final int MOVE = 1 << 3;
static final int POOL_SIZE = 30;
int cmd;
int positionStart;
Object payload;
// holds the target position if this is a MOVE
int itemCount;
UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
this.cmd = cmd;
this.positionStart = positionStart;
this.itemCount = itemCount;
this.payload = payload;
}
String cmdToString() {
switch (cmd) {
case ADD:
return "add";
case REMOVE:
return "rm";
case UPDATE:
return "up";
case MOVE:
return "mv";
}
return "??";
}
@Override
public String toString() {
return Integer.toHexString(System.identityHashCode(this))
+ "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
+ ",p:" + payload + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof UpdateOp)) {
return false;
}
UpdateOp op = (UpdateOp) o;
if (cmd != op.cmd) {
return false;
}
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
// reverse of this is also true
if (itemCount == op.positionStart && positionStart == op.itemCount) {
return true;
}
}
if (itemCount != op.itemCount) {
return false;
}
if (positionStart != op.positionStart) {
return false;
}
if (payload != null) {
if (!payload.equals(op.payload)) {
return false;
}
} else if (op.payload != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = cmd;
result = 31 * result + positionStart;
result = 31 * result + itemCount;
return result;
}
}
@Override
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
UpdateOp op = mUpdateOpPool.acquire();
if (op == null) {
op = new UpdateOp(cmd, positionStart, itemCount, payload);
} else {
op.cmd = cmd;
op.positionStart = positionStart;
op.itemCount = itemCount;
op.payload = payload;
}
return op;
}
@Override
public void recycleUpdateOp(UpdateOp op) {
if (!mDisableRecycler) {
op.payload = null;
mUpdateOpPool.release(op);
}
}
void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
final int count = ops.size();
for (int i = 0; i < count; i++) {
recycleUpdateOp(ops.get(i));
}
ops.clear();
}
/**
* Contract between AdapterHelper and RecyclerView.
*/
interface Callback {
RecyclerView.ViewHolder findViewHolder(int position);
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
void markViewHoldersUpdated(int positionStart, int itemCount, Object payloads);
void onDispatchFirstPass(UpdateOp updateOp);
void onDispatchSecondPass(UpdateOp updateOp);
void offsetPositionsForAdd(int positionStart, int itemCount);
void offsetPositionsForMove(int from, int to);
}
}

@ -0,0 +1,65 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
/**
* ListUpdateCallback that dispatches update events to the given adapter.
*
* @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
public final class AdapterListUpdateCallback implements ListUpdateCallback {
@NonNull
private final RecyclerView.Adapter mAdapter;
/**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
*
* @param adapter The Adapter to send updates to.
*/
public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
/** {@inheritDoc} */
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
/** {@inheritDoc} */
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
/** {@inheritDoc} */
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
/** {@inheritDoc} */
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}

@ -0,0 +1,148 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* Configuration object for {@link ListAdapter}, {@link AsyncListDiffer}, and similar
* background-thread list diffing adapter logic.
* <p>
* At minimum, defines item diffing behavior with a {@link DiffUtil.ItemCallback}, used to compute
* item differences to pass to a RecyclerView adapter.
*
* @param <T> Type of items in the lists, and being compared.
*/
public final class AsyncDifferConfig<T> {
@Nullable
private final Executor mMainThreadExecutor;
@NonNull
private final Executor mBackgroundThreadExecutor;
@NonNull
private final DiffUtil.ItemCallback<T> mDiffCallback;
@SuppressWarnings("WeakerAccess") /* synthetic access */
AsyncDifferConfig(
@Nullable Executor mainThreadExecutor,
@NonNull Executor backgroundThreadExecutor,
@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mDiffCallback = diffCallback;
}
/** @hide */
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Nullable
public Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public Executor getBackgroundThreadExecutor() {
return mBackgroundThreadExecutor;
}
@SuppressWarnings("WeakerAccess")
@NonNull
public DiffUtil.ItemCallback<T> getDiffCallback() {
return mDiffCallback;
}
/**
* Builder class for {@link AsyncDifferConfig}.
*
* @param <T>
*/
public static final class Builder<T> {
@Nullable
private Executor mMainThreadExecutor;
private Executor mBackgroundThreadExecutor;
private final DiffUtil.ItemCallback<T> mDiffCallback;
public Builder(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffCallback = diffCallback;
}
/**
* If provided, defines the main thread executor used to dispatch adapter update
* notifications on the main thread.
* <p>
* If not provided or null, it will default to the main thread.
*
* @param executor The executor which can run tasks in the UI thread.
* @return this
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@NonNull
public Builder<T> setMainThreadExecutor(@Nullable Executor executor) {
mMainThreadExecutor = executor;
return this;
}
/**
* If provided, defines the background executor used to calculate the diff between an old
* and a new list.
* <p>
* If not provided or null, defaults to two thread pool executor, shared by all
* ListAdapterConfigs.
*
* @param executor The background executor to run list diffing.
* @return this
*/
@SuppressWarnings({"unused", "WeakerAccess"})
@NonNull
public Builder<T> setBackgroundThreadExecutor(@Nullable Executor executor) {
mBackgroundThreadExecutor = executor;
return this;
}
/**
* Creates a {@link AsyncListDiffer} with the given parameters.
*
* @return A new AsyncDifferConfig.
*/
@NonNull
public AsyncDifferConfig<T> build() {
if (mBackgroundThreadExecutor == null) {
synchronized (sExecutorLock) {
if (sDiffExecutor == null) {
sDiffExecutor = Executors.newFixedThreadPool(2);
}
}
mBackgroundThreadExecutor = sDiffExecutor;
}
return new AsyncDifferConfig<>(
mMainThreadExecutor,
mBackgroundThreadExecutor,
mDiffCallback);
}
// TODO: remove the below once supportlib has its own appropriate executors
private static final Object sExecutorLock = new Object();
private static Executor sDiffExecutor = null;
}
}

@ -0,0 +1,405 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
/**
* Helper for computing the difference between two lists via {@link DiffUtil} on a background
* thread.
* <p>
* It can be connected to a
* {@link RecyclerView.Adapter RecyclerView.Adapter}, and will signal the
* adapter of changes between sumbitted lists.
* <p>
* For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the
* AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an
* adapter base class to support asynchronous List diffing isn't convenient.
* <p>
* The AsyncListDiffer can consume the values from a LiveData of <code>List</code> and present the
* data simply for an adapter. It computes differences in list contents via {@link DiffUtil} on a
* background thread as new <code>List</code>s are received.
* <p>
* Use {@link #getCurrentList()} to access the current List, and present its data objects. Diff
* results will be dispatched to the ListUpdateCallback immediately before the current list is
* updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can
* safely access list items and total size via {@link #getCurrentList()}.
* <p>
* A complete usage pattern with Room would look like this:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract LiveData&lt;List&lt;User>> usersByLastName();
* }
*
* class MyViewModel extends ViewModel {
* public final LiveData&lt;List&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = userDao.usersByLastName();
* }
* }
*
* class MyActivity extends AppCompatActivity {
* {@literal @}Override
* public void onCreate(Bundle savedState) {
* super.onCreate(savedState);
* MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
* RecyclerView recyclerView = findViewById(R.id.user_list);
* UserAdapter adapter = new UserAdapter();
* viewModel.usersList.observe(this, list -> adapter.submitList(list));
* recyclerView.setAdapter(adapter);
* }
* }
*
* class UserAdapter extends RecyclerView.Adapter&lt;UserViewHolder> {
* private final AsyncListDiffer&lt;User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mDiffer.getCurrentList().size();
* }
* public void submitList(List&lt;User> list) {
* mDiffer.submitList(list);
* }
* {@literal @}Override
* public void onBindViewHolder(UserViewHolder holder, int position) {
* User user = mDiffer.getCurrentList().get(position);
* holder.bindTo(user);
* }
* public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK
* = new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // User properties may have changed if reloaded from the DB, but ID is fixed
* return oldUser.getId() == newUser.getId();
* }
* {@literal @}Override
* public boolean areContentsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // NOTE: if you use equals, your object must properly override Object#equals()
* // Incorrectly returning false here will result in too many animations.
* return oldUser.equals(newUser);
* }
* }
* }</pre>
*
* @param <T> Type of the lists this AsyncListDiffer will receive.
*
* @see DiffUtil
* @see AdapterListUpdateCallback
*/
public class AsyncListDiffer<T> {
private final ListUpdateCallback mUpdateCallback;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final AsyncDifferConfig<T> mConfig;
Executor mMainThreadExecutor;
private static class MainThreadExecutor implements Executor {
final Handler mHandler = new Handler(Looper.getMainLooper());
MainThreadExecutor() {}
@Override
public void execute(@NonNull Runnable command) {
mHandler.post(command);
}
}
// TODO: use MainThreadExecutor from supportlib once one exists
private static final Executor sMainThreadExecutor = new MainThreadExecutor();
/**
* Listener for when the current List is updated.
*
* @param <T> Type of items in List
*/
public interface ListListener<T> {
/**
* Called after the current List has been updated.
*
* @param previousList The previous list.
* @param currentList The new current list.
*/
void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList);
}
private final List<ListListener<T>> mListeners = new CopyOnWriteArrayList<>();
/**
* Convenience for
* {@code AsyncListDiffer(new AdapterListUpdateCallback(adapter),
* new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());}
*
* @param adapter Adapter to dispatch position updates to.
* @param diffCallback ItemCallback that compares items to dispatch appropriate animations when
*
* @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
public AsyncListDiffer(@NonNull RecyclerView.Adapter adapter,
@NonNull DiffUtil.ItemCallback<T> diffCallback) {
this(new AdapterListUpdateCallback(adapter),
new AsyncDifferConfig.Builder<>(diffCallback).build());
}
/**
* Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch
* updates to.
*
* @param listUpdateCallback Callback to dispatch updates to.
* @param config Config to define background work Executor, and DiffUtil.ItemCallback for
* computing List diffs.
*
* @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
@SuppressWarnings("WeakerAccess")
public AsyncListDiffer(@NonNull ListUpdateCallback listUpdateCallback,
@NonNull AsyncDifferConfig<T> config) {
mUpdateCallback = listUpdateCallback;
mConfig = config;
if (config.getMainThreadExecutor() != null) {
mMainThreadExecutor = config.getMainThreadExecutor();
} else {
mMainThreadExecutor = sMainThreadExecutor;
}
}
@Nullable
private List<T> mList;
/**
* Non-null, unmodifiable version of mList.
* <p>
* Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise
*/
@NonNull
private List<T> mReadOnlyList = Collections.emptyList();
// Max generation of currently scheduled runnable
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mMaxScheduledGeneration;
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
* <p>
* If a <code>null</code> List, or no List has been submitted, an empty list will be returned.
* <p>
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return current List.
*/
@NonNull
public List<T> getCurrentList() {
return mReadOnlyList;
}
/**
* Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
* thread.
* <p>
* If a List is already present, a diff will be computed asynchronously on a background thread.
* When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}),
* and the new List will be swapped in.
*
* @param newList The new List.
*/
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList) {
submitList(newList, null);
}
/**
* Pass a new List to the AdapterHelper. Adapter updates will be computed on a background
* thread.
* <p>
* If a List is already present, a diff will be computed asynchronously on a background thread.
* When the diff is computed, it will be applied (dispatched to the {@link ListUpdateCallback}),
* and the new List will be swapped in.
* <p>
* The commit callback can be used to know when the List is committed, but note that it
* may not be executed. If List B is submitted immediately after List A, and is
* committed directly, the callback associated with List A will not be run.
*
* @param newList The new List.
* @param commitCallback Optional runnable that is executed when the List is committed, if
* it is committed.
*/
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
// fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// fast simple first insert
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
final List<T> oldList = mList;
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
}
// If both items are null we consider them the same.
return oldItem == null && newItem == null;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
}
if (oldItem == null && newItem == null) {
return true;
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true. That
// only occurs when both items are non-null or both are null and both of
// those cases are handled above.
throw new AssertionError();
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true AND
// areContentsTheSame returns false. That only occurs when both items are
// non-null which is the only case handled above.
throw new AssertionError();
}
});
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void latchList(
@NonNull List<T> newList,
@NonNull DiffUtil.DiffResult diffResult,
@Nullable Runnable commitCallback) {
final List<T> previousList = mReadOnlyList;
mList = newList;
// notify last, after list is updated
mReadOnlyList = Collections.unmodifiableList(newList);
diffResult.dispatchUpdatesTo(mUpdateCallback);
onCurrentListChanged(previousList, commitCallback);
}
private void onCurrentListChanged(@NonNull List<T> previousList,
@Nullable Runnable commitCallback) {
// current list is always mReadOnlyList
for (ListListener<T> listener : mListeners) {
listener.onCurrentListChanged(previousList, mReadOnlyList);
}
if (commitCallback != null) {
commitCallback.run();
}
}
/**
* Add a ListListener to receive updates when the current List changes.
*
* @param listener Listener to receive updates.
*
* @see #getCurrentList()
* @see #removeListListener(ListListener)
*/
public void addListListener(@NonNull ListListener<T> listener) {
mListeners.add(listener);
}
/**
* Remove a previously registered ListListener.
*
* @param listener Previously registered listener.
* @see #getCurrentList()
* @see #addListListener(ListListener)
*/
public void removeListListener(@NonNull ListListener<T> listener) {
mListeners.remove(listener);
}
}

@ -0,0 +1,596 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
/**
* A utility class that supports asynchronous content loading.
* <p>
* It can be used to load Cursor data in chunks without querying the Cursor on the UI Thread while
* keeping UI and cache synchronous for better user experience.
* <p>
* It loads the data on a background thread and keeps only a limited number of fixed sized
* chunks in memory at all times.
* <p>
* {@link AsyncListUtil} queries the currently visible range through {@link ViewCallback},
* loads the required data items in the background through {@link DataCallback}, and notifies a
* {@link ViewCallback} when the data is loaded. It may load some extra items for smoother
* scrolling.
* <p>
* Note that this class uses a single thread to load the data, so it suitable to load data from
* secondary storage such as disk, but not from network.
* <p>
* This class is designed to work with {@link RecyclerView}, but it does
* not depend on it and can be used with other list views.
*
*/
public class AsyncListUtil<T> {
static final String TAG = "AsyncListUtil";
static final boolean DEBUG = false;
final Class<T> mTClass;
final int mTileSize;
final DataCallback<T> mDataCallback;
final ViewCallback mViewCallback;
final TileList<T> mTileList;
final ThreadUtil.MainThreadCallback<T> mMainThreadProxy;
final ThreadUtil.BackgroundCallback<T> mBackgroundProxy;
final int[] mTmpRange = new int[2];
final int[] mPrevRange = new int[2];
final int[] mTmpRangeExtended = new int[2];
boolean mAllowScrollHints;
private int mScrollHint = ViewCallback.HINT_SCROLL_NONE;
int mItemCount = 0;
int mDisplayedGeneration = 0;
int mRequestedGeneration = mDisplayedGeneration;
final SparseIntArray mMissingPositions = new SparseIntArray();
void log(String s, Object... args) {
Log.d(TAG, "[MAIN] " + String.format(s, args));
}
/**
* Creates an AsyncListUtil.
*
* @param klass Class of the data item.
* @param tileSize Number of item per chunk loaded at once.
* @param dataCallback Data access callback.
* @param viewCallback Callback for querying visible item range and update notifications.
*/
public AsyncListUtil(@NonNull Class<T> klass, int tileSize,
@NonNull DataCallback<T> dataCallback, @NonNull ViewCallback viewCallback) {
mTClass = klass;
mTileSize = tileSize;
mDataCallback = dataCallback;
mViewCallback = viewCallback;
mTileList = new TileList<T>(mTileSize);
ThreadUtil<T> threadUtil = new MessageThreadUtil<T>();
mMainThreadProxy = threadUtil.getMainThreadProxy(mMainThreadCallback);
mBackgroundProxy = threadUtil.getBackgroundProxy(mBackgroundCallback);
refresh();
}
private boolean isRefreshPending() {
return mRequestedGeneration != mDisplayedGeneration;
}
/**
* Updates the currently visible item range.
*
* <p>
* Identifies the data items that have not been loaded yet and initiates loading them in the
* background. Should be called from the view's scroll listener (such as
* {@link RecyclerView.OnScrollListener#onScrolled}).
*/
public void onRangeChanged() {
if (isRefreshPending()) {
return; // Will update range will the refresh result arrives.
}
updateRange();
mAllowScrollHints = true;
}
/**
* Forces reloading the data.
* <p>
* Discards all the cached data and reloads all required data items for the currently visible
* range. To be called when the data item count and/or contents has changed.
*/
public void refresh() {
mMissingPositions.clear();
mBackgroundProxy.refresh(++mRequestedGeneration);
}
/**
* Returns the data item at the given position or <code>null</code> if it has not been loaded
* yet.
*
* <p>
* If this method has been called for a specific position and returned <code>null</code>, then
* {@link ViewCallback#onItemLoaded(int)} will be called when it finally loads. Note that if
* this position stays outside of the cached item range (as defined by
* {@link ViewCallback#extendRangeInto} method), then the callback will never be called for
* this position.
*
* @param position Item position.
*
* @return The data item at the given position or <code>null</code> if it has not been loaded
* yet.
*/
@Nullable
public T getItem(int position) {
if (position < 0 || position >= mItemCount) {
throw new IndexOutOfBoundsException(position + " is not within 0 and " + mItemCount);
}
T item = mTileList.getItemAt(position);
if (item == null && !isRefreshPending()) {
mMissingPositions.put(position, 0);
}
return item;
}
/**
* Returns the number of items in the data set.
*
* <p>
* This is the number returned by a recent call to
* {@link DataCallback#refreshData()}.
*
* @return Number of items.
*/
public int getItemCount() {
return mItemCount;
}
void updateRange() {
mViewCallback.getItemRangeInto(mTmpRange);
if (mTmpRange[0] > mTmpRange[1] || mTmpRange[0] < 0) {
return;
}
if (mTmpRange[1] >= mItemCount) {
// Invalid range may arrive soon after the refresh.
return;
}
if (!mAllowScrollHints) {
mScrollHint = ViewCallback.HINT_SCROLL_NONE;
} else if (mTmpRange[0] > mPrevRange[1] || mPrevRange[0] > mTmpRange[1]) {
// Ranges do not intersect, long leap not a scroll.
mScrollHint = ViewCallback.HINT_SCROLL_NONE;
} else if (mTmpRange[0] < mPrevRange[0]) {
mScrollHint = ViewCallback.HINT_SCROLL_DESC;
} else if (mTmpRange[0] > mPrevRange[0]) {
mScrollHint = ViewCallback.HINT_SCROLL_ASC;
}
mPrevRange[0] = mTmpRange[0];
mPrevRange[1] = mTmpRange[1];
mViewCallback.extendRangeInto(mTmpRange, mTmpRangeExtended, mScrollHint);
mTmpRangeExtended[0] = Math.min(mTmpRange[0], Math.max(mTmpRangeExtended[0], 0));
mTmpRangeExtended[1] =
Math.max(mTmpRange[1], Math.min(mTmpRangeExtended[1], mItemCount - 1));
mBackgroundProxy.updateRange(mTmpRange[0], mTmpRange[1],
mTmpRangeExtended[0], mTmpRangeExtended[1], mScrollHint);
}
private final ThreadUtil.MainThreadCallback<T>
mMainThreadCallback = new ThreadUtil.MainThreadCallback<T>() {
@Override
public void updateItemCount(int generation, int itemCount) {
if (DEBUG) {
log("updateItemCount: size=%d, gen #%d", itemCount, generation);
}
if (!isRequestedGeneration(generation)) {
return;
}
mItemCount = itemCount;
mViewCallback.onDataRefresh();
mDisplayedGeneration = mRequestedGeneration;
recycleAllTiles();
mAllowScrollHints = false; // Will be set to true after a first real scroll.
// There will be no scroll event if the size change does not affect the current range.
updateRange();
}
@Override
public void addTile(int generation, TileList.Tile<T> tile) {
if (!isRequestedGeneration(generation)) {
if (DEBUG) {
log("recycling an older generation tile @%d", tile.mStartPosition);
}
mBackgroundProxy.recycleTile(tile);
return;
}
TileList.Tile<T> duplicate = mTileList.addOrReplace(tile);
if (duplicate != null) {
Log.e(TAG, "duplicate tile @" + duplicate.mStartPosition);
mBackgroundProxy.recycleTile(duplicate);
}
if (DEBUG) {
log("gen #%d, added tile @%d, total tiles: %d",
generation, tile.mStartPosition, mTileList.size());
}
int endPosition = tile.mStartPosition + tile.mItemCount;
int index = 0;
while (index < mMissingPositions.size()) {
final int position = mMissingPositions.keyAt(index);
if (tile.mStartPosition <= position && position < endPosition) {
mMissingPositions.removeAt(index);
mViewCallback.onItemLoaded(position);
} else {
index++;
}
}
}
@Override
public void removeTile(int generation, int position) {
if (!isRequestedGeneration(generation)) {
return;
}
TileList.Tile<T> tile = mTileList.removeAtPos(position);
if (tile == null) {
Log.e(TAG, "tile not found @" + position);
return;
}
if (DEBUG) {
log("recycling tile @%d, total tiles: %d", tile.mStartPosition, mTileList.size());
}
mBackgroundProxy.recycleTile(tile);
}
private void recycleAllTiles() {
if (DEBUG) {
log("recycling all %d tiles", mTileList.size());
}
for (int i = 0; i < mTileList.size(); i++) {
mBackgroundProxy.recycleTile(mTileList.getAtIndex(i));
}
mTileList.clear();
}
private boolean isRequestedGeneration(int generation) {
return generation == mRequestedGeneration;
}
};
private final ThreadUtil.BackgroundCallback<T>
mBackgroundCallback = new ThreadUtil.BackgroundCallback<T>() {
private TileList.Tile<T> mRecycledRoot;
final SparseBooleanArray mLoadedTiles = new SparseBooleanArray();
private int mGeneration;
private int mItemCount;
private int mFirstRequiredTileStart;
private int mLastRequiredTileStart;
@Override
public void refresh(int generation) {
mGeneration = generation;
mLoadedTiles.clear();
mItemCount = mDataCallback.refreshData();
mMainThreadProxy.updateItemCount(mGeneration, mItemCount);
}
@Override
public void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd,
int scrollHint) {
if (DEBUG) {
log("updateRange: %d..%d extended to %d..%d, scroll hint: %d",
rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint);
}
if (rangeStart > rangeEnd) {
return;
}
final int firstVisibleTileStart = getTileStart(rangeStart);
final int lastVisibleTileStart = getTileStart(rangeEnd);
mFirstRequiredTileStart = getTileStart(extRangeStart);
mLastRequiredTileStart = getTileStart(extRangeEnd);
if (DEBUG) {
log("requesting tile range: %d..%d",
mFirstRequiredTileStart, mLastRequiredTileStart);
}
// All pending tile requests are removed by ThreadUtil at this point.
// Re-request all required tiles in the most optimal order.
if (scrollHint == ViewCallback.HINT_SCROLL_DESC) {
requestTiles(mFirstRequiredTileStart, lastVisibleTileStart, scrollHint, true);
requestTiles(lastVisibleTileStart + mTileSize, mLastRequiredTileStart, scrollHint,
false);
} else {
requestTiles(firstVisibleTileStart, mLastRequiredTileStart, scrollHint, false);
requestTiles(mFirstRequiredTileStart, firstVisibleTileStart - mTileSize, scrollHint,
true);
}
}
private int getTileStart(int position) {
return position - position % mTileSize;
}
private void requestTiles(int firstTileStart, int lastTileStart, int scrollHint,
boolean backwards) {
for (int i = firstTileStart; i <= lastTileStart; i += mTileSize) {
int tileStart = backwards ? (lastTileStart + firstTileStart - i) : i;
if (DEBUG) {
log("requesting tile @%d", tileStart);
}
mBackgroundProxy.loadTile(tileStart, scrollHint);
}
}
@Override
public void loadTile(int position, int scrollHint) {
if (isTileLoaded(position)) {
if (DEBUG) {
log("already loaded tile @%d", position);
}
return;
}
TileList.Tile<T> tile = acquireTile();
tile.mStartPosition = position;
tile.mItemCount = Math.min(mTileSize, mItemCount - tile.mStartPosition);
mDataCallback.fillData(tile.mItems, tile.mStartPosition, tile.mItemCount);
flushTileCache(scrollHint);
addTile(tile);
}
@Override
public void recycleTile(TileList.Tile<T> tile) {
if (DEBUG) {
log("recycling tile @%d", tile.mStartPosition);
}
mDataCallback.recycleData(tile.mItems, tile.mItemCount);
tile.mNext = mRecycledRoot;
mRecycledRoot = tile;
}
private TileList.Tile<T> acquireTile() {
if (mRecycledRoot != null) {
TileList.Tile<T> result = mRecycledRoot;
mRecycledRoot = mRecycledRoot.mNext;
return result;
}
return new TileList.Tile<T>(mTClass, mTileSize);
}
private boolean isTileLoaded(int position) {
return mLoadedTiles.get(position);
}
private void addTile(TileList.Tile<T> tile) {
mLoadedTiles.put(tile.mStartPosition, true);
mMainThreadProxy.addTile(mGeneration, tile);
if (DEBUG) {
log("loaded tile @%d, total tiles: %d", tile.mStartPosition, mLoadedTiles.size());
}
}
private void removeTile(int position) {
mLoadedTiles.delete(position);
mMainThreadProxy.removeTile(mGeneration, position);
if (DEBUG) {
log("flushed tile @%d, total tiles: %s", position, mLoadedTiles.size());
}
}
private void flushTileCache(int scrollHint) {
final int cacheSizeLimit = mDataCallback.getMaxCachedTiles();
while (mLoadedTiles.size() >= cacheSizeLimit) {
int firstLoadedTileStart = mLoadedTiles.keyAt(0);
int lastLoadedTileStart = mLoadedTiles.keyAt(mLoadedTiles.size() - 1);
int startMargin = mFirstRequiredTileStart - firstLoadedTileStart;
int endMargin = lastLoadedTileStart - mLastRequiredTileStart;
if (startMargin > 0 && (startMargin >= endMargin ||
(scrollHint == ViewCallback.HINT_SCROLL_ASC))) {
removeTile(firstLoadedTileStart);
} else if (endMargin > 0 && (startMargin < endMargin ||
(scrollHint == ViewCallback.HINT_SCROLL_DESC))){
removeTile(lastLoadedTileStart);
} else {
// Could not flush on either side, bail out.
return;
}
}
}
private void log(String s, Object... args) {
Log.d(TAG, "[BKGR] " + String.format(s, args));
}
};
/**
* The callback that provides data access for {@link AsyncListUtil}.
*
* <p>
* All methods are called on the background thread.
*/
public static abstract class DataCallback<T> {
/**
* Refresh the data set and return the new data item count.
*
* <p>
* If the data is being accessed through {@link android.database.Cursor} this is where
* the new cursor should be created.
*
* @return Data item count.
*/
@WorkerThread
public abstract int refreshData();
/**
* Fill the given tile.
*
* <p>
* The provided tile might be a recycled tile, in which case it will already have objects.
* It is suggested to re-use these objects if possible in your use case.
*
* @param startPosition The start position in the list.
* @param itemCount The data item count.
* @param data The data item array to fill into. Should not be accessed beyond
* <code>itemCount</code>.
*/
@WorkerThread
public abstract void fillData(@NonNull T[] data, int startPosition, int itemCount);
/**
* Recycle the objects created in {@link #fillData} if necessary.
*
*
* @param data Array of data items. Should not be accessed beyond <code>itemCount</code>.
* @param itemCount The data item count.
*/
@WorkerThread
public void recycleData(@NonNull T[] data, int itemCount) {
}
/**
* Returns tile cache size limit (in tiles).
*
* <p>
* The actual number of cached tiles will be the maximum of this value and the number of
* tiles that is required to cover the range returned by
* {@link ViewCallback#extendRangeInto(int[], int[], int)}.
* <p>
* For example, if this method returns 10, and the most
* recent call to {@link ViewCallback#extendRangeInto(int[], int[], int)} returned
* {100, 179}, and the tile size is 5, then the maximum number of cached tiles will be 16.
* <p>
* However, if the tile size is 20, then the maximum number of cached tiles will be 10.
* <p>
* The default implementation returns 10.
*
* @return Maximum cache size.
*/
@WorkerThread
public int getMaxCachedTiles() {
return 10;
}
}
/**
* The callback that links {@link AsyncListUtil} with the list view.
*
* <p>
* All methods are called on the main thread.
*/
public static abstract class ViewCallback {
/**
* No scroll direction hint available.
*/
public static final int HINT_SCROLL_NONE = 0;
/**
* Scrolling in descending order (from higher to lower positions in the order of the backing
* storage).
*/
public static final int HINT_SCROLL_DESC = 1;
/**
* Scrolling in ascending order (from lower to higher positions in the order of the backing
* storage).
*/
public static final int HINT_SCROLL_ASC = 2;
/**
* Compute the range of visible item positions.
* <p>
* outRange[0] is the position of the first visible item (in the order of the backing
* storage).
* <p>
* outRange[1] is the position of the last visible item (in the order of the backing
* storage).
* <p>
* Negative positions and positions greater or equal to {@link #getItemCount} are invalid.
* If the returned range contains invalid positions it is ignored (no item will be loaded).
*
* @param outRange The visible item range.
*/
@UiThread
public abstract void getItemRangeInto(@NonNull int[] outRange);
/**
* Compute a wider range of items that will be loaded for smoother scrolling.
*
* <p>
* If there is no scroll hint, the default implementation extends the visible range by half
* its length in both directions. If there is a scroll hint, the range is extended by
* its full length in the scroll direction, and by half in the other direction.
* <p>
* For example, if <code>range</code> is <code>{100, 200}</code> and <code>scrollHint</code>
* is {@link #HINT_SCROLL_ASC}, then <code>outRange</code> will be <code>{50, 300}</code>.
* <p>
* However, if <code>scrollHint</code> is {@link #HINT_SCROLL_NONE}, then
* <code>outRange</code> will be <code>{50, 250}</code>
*
* @param range Visible item range.
* @param outRange Extended range.
* @param scrollHint The scroll direction hint.
*/
@UiThread
public void extendRangeInto(@NonNull int[] range, @NonNull int[] outRange, int scrollHint) {
final int fullRange = range[1] - range[0] + 1;
final int halfRange = fullRange / 2;
outRange[0] = range[0] - (scrollHint == HINT_SCROLL_DESC ? fullRange : halfRange);
outRange[1] = range[1] + (scrollHint == HINT_SCROLL_ASC ? fullRange : halfRange);
}
/**
* Called when the entire data set has changed.
*/
@UiThread
public abstract void onDataRefresh();
/**
* Called when an item at the given position is loaded.
* @param position Item position.
*/
@UiThread
public abstract void onItemLoaded(int position);
}
}

@ -0,0 +1,128 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
/**
* Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
* <p>
* For instance, when 2 add operations comes that adds 2 consecutive elements,
* BatchingListUpdateCallback merges them and calls the wrapped callback only once.
* <p>
* This is a general purpose class and is also used by
* {@link DiffUtil.DiffResult DiffResult} and
* {@link SortedList} to minimize the number of updates that are dispatched.
* <p>
* If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
* stream of update events drain.
*/
public class BatchingListUpdateCallback implements ListUpdateCallback {
private static final int TYPE_NONE = 0;
private static final int TYPE_ADD = 1;
private static final int TYPE_REMOVE = 2;
private static final int TYPE_CHANGE = 3;
final ListUpdateCallback mWrapped;
int mLastEventType = TYPE_NONE;
int mLastEventPosition = -1;
int mLastEventCount = -1;
Object mLastEventPayload = null;
public BatchingListUpdateCallback(@NonNull ListUpdateCallback callback) {
mWrapped = callback;
}
/**
* BatchingListUpdateCallback holds onto the last event to see if it can be merged with the
* next one. When stream of events finish, you should call this method to dispatch the last
* event.
*/
public void dispatchLastEvent() {
if (mLastEventType == TYPE_NONE) {
return;
}
switch (mLastEventType) {
case TYPE_ADD:
mWrapped.onInserted(mLastEventPosition, mLastEventCount);
break;
case TYPE_REMOVE:
mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
break;
case TYPE_CHANGE:
mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
break;
}
mLastEventPayload = null;
mLastEventType = TYPE_NONE;
}
@Override
public void onInserted(int position, int count) {
if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
&& position <= mLastEventPosition + mLastEventCount) {
mLastEventCount += count;
mLastEventPosition = Math.min(position, mLastEventPosition);
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_ADD;
}
@Override
public void onRemoved(int position, int count) {
if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position &&
mLastEventPosition <= position + count) {
mLastEventCount += count;
mLastEventPosition = position;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_REMOVE;
}
@Override
public void onMoved(int fromPosition, int toPosition) {
dispatchLastEvent(); // moves are not merged
mWrapped.onMoved(fromPosition, toPosition);
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onChanged(int position, int count, Object payload) {
if (mLastEventType == TYPE_CHANGE &&
!(position > mLastEventPosition + mLastEventCount
|| position + count < mLastEventPosition || mLastEventPayload != payload)) {
// take potential overlap into account
int previousEnd = mLastEventPosition + mLastEventCount;
mLastEventPosition = Math.min(position, mLastEventPosition);
mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventPayload = payload;
mLastEventType = TYPE_CHANGE;
}
}

@ -0,0 +1,601 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to manage children.
* <p>
* It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
* provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
* like getChildAt, getChildCount etc. These methods ignore hidden children.
* <p>
* When RecyclerView needs direct access to the view group children, it can call unfiltered
* methods like get getUnfilteredChildCount or getUnfilteredChildAt.
*/
class ChildHelper {
private static final boolean DEBUG = false;
private static final String TAG = "ChildrenHelper";
/** Not in call to removeView/removeViewAt/removeViewIfHidden. */
private static final int REMOVE_STATUS_NONE = 0;
/** Within a call to removeView/removeViewAt. */
private static final int REMOVE_STATUS_IN_REMOVE = 1;
/** Within a call to removeViewIfHidden. */
private static final int REMOVE_STATUS_IN_REMOVE_IF_HIDDEN = 2;
final Callback mCallback;
final Bucket mBucket;
final List<View> mHiddenViews;
/**
* One of REMOVE_STATUS_NONE, REMOVE_STATUS_IN_REMOVE, REMOVE_STATUS_IN_REMOVE_IF_HIDDEN.
* removeView and removeViewIfHidden may call each other:
* 1. removeView triggers removeViewIfHidden: this happens when removeView stops the item
* animation. removeViewIfHidden should do nothing.
* 2. removeView triggers removeView: this should not happen.
* 3. removeViewIfHidden triggers removeViewIfHidden: this should not happen, since the
* animation was stopped before the first removeViewIfHidden, it won't trigger another
* removeViewIfHidden.
* 4. removeViewIfHidden triggers removeView: this should not happen.
*/
private int mRemoveStatus = REMOVE_STATUS_NONE;
/** The view to remove in REMOVE_STATUS_IN_REMOVE. */
private View mViewInRemoveView;
ChildHelper(Callback callback) {
mCallback = callback;
mBucket = new Bucket();
mHiddenViews = new ArrayList<View>();
}
/**
* Marks a child view as hidden
*
* @param child View to hide.
*/
private void hideViewInternal(View child) {
mHiddenViews.add(child);
mCallback.onEnteredHiddenState(child);
}
/**
* Unmarks a child view as hidden.
*
* @param child View to hide.
*/
private boolean unhideViewInternal(View child) {
if (mHiddenViews.remove(child)) {
mCallback.onLeftHiddenState(child);
return true;
} else {
return false;
}
}
/**
* Adds a view to the ViewGroup
*
* @param child View to add.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, boolean hidden) {
addView(child, -1, hidden);
}
/**
* Add a view to the ViewGroup at an index
*
* @param child View to add.
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mBucket.insert(offset, hidden);
if (hidden) {
hideViewInternal(child);
}
mCallback.addView(child, offset);
if (DEBUG) {
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
}
}
private int getOffset(int index) {
if (index < 0) {
return -1; //anything below 0 won't work as diff will be undefined.
}
final int limit = mCallback.getChildCount();
int offset = index;
while (offset < limit) {
final int removedBefore = mBucket.countOnesBefore(offset);
final int diff = index - (offset - removedBefore);
if (diff == 0) {
while (mBucket.get(offset)) { // ensure this offset is not hidden
offset++;
}
return offset;
} else {
offset += diff;
}
}
return -1;
}
/**
* Removes the provided View from underlying RecyclerView.
*
* @param view The view to remove.
*/
void removeView(View view) {
if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
throw new IllegalStateException("Cannot call removeView(At) within removeView(At)");
} else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden");
}
try {
mRemoveStatus = REMOVE_STATUS_IN_REMOVE;
mViewInRemoveView = view;
int index = mCallback.indexOfChild(view);
if (index < 0) {
return;
}
if (mBucket.remove(index)) {
unhideViewInternal(view);
}
mCallback.removeViewAt(index);
if (DEBUG) {
Log.d(TAG, "remove View off:" + index + "," + this);
}
} finally {
mRemoveStatus = REMOVE_STATUS_NONE;
mViewInRemoveView = null;
}
}
/**
* Removes the view at the provided index from RecyclerView.
*
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
*/
void removeViewAt(int index) {
if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
throw new IllegalStateException("Cannot call removeView(At) within removeView(At)");
} else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
throw new IllegalStateException("Cannot call removeView(At) within removeViewIfHidden");
}
try {
final int offset = getOffset(index);
final View view = mCallback.getChildAt(offset);
if (view == null) {
return;
}
mRemoveStatus = REMOVE_STATUS_IN_REMOVE;
mViewInRemoveView = view;
if (mBucket.remove(offset)) {
unhideViewInternal(view);
}
mCallback.removeViewAt(offset);
if (DEBUG) {
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
}
} finally {
mRemoveStatus = REMOVE_STATUS_NONE;
mViewInRemoveView = null;
}
}
/**
* Returns the child at provided index.
*
* @param index Index of the child to return in regular perspective.
*/
View getChildAt(int index) {
final int offset = getOffset(index);
return mCallback.getChildAt(offset);
}
/**
* Removes all views from the ViewGroup including the hidden ones.
*/
void removeAllViewsUnfiltered() {
mBucket.reset();
for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
mCallback.onLeftHiddenState(mHiddenViews.get(i));
mHiddenViews.remove(i);
}
mCallback.removeAllViews();
if (DEBUG) {
Log.d(TAG, "removeAllViewsUnfiltered");
}
}
/**
* This can be used to find a disappearing view by position.
*
* @param position The adapter position of the item.
* @return A hidden view with a valid ViewHolder that matches the position.
*/
View findHiddenNonRemovedView(int position) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position
&& !holder.isInvalid()
&& !holder.isRemoved()) {
return view;
}
}
return null;
}
/**
* Attaches the provided view to the underlying ViewGroup.
*
* @param child Child to attach.
* @param index Index of the child to attach in regular perspective.
* @param layoutParams LayoutParams for the child.
* @param hidden If set to true, this item will be invisible to the regular methods.
*/
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mBucket.insert(offset, hidden);
if (hidden) {
hideViewInternal(child);
}
mCallback.attachViewToParent(child, offset, layoutParams);
if (DEBUG) {
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
+ "h:" + hidden + ", " + this);
}
}
/**
* Returns the number of children that are not hidden.
*
* @return Number of children that are not hidden.
* @see #getChildAt(int)
*/
int getChildCount() {
return mCallback.getChildCount() - mHiddenViews.size();
}
/**
* Returns the total number of children.
*
* @return The total number of children including the hidden views.
* @see #getUnfilteredChildAt(int)
*/
int getUnfilteredChildCount() {
return mCallback.getChildCount();
}
/**
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
*
* @param index ViewGroup index of the child to return.
* @return The view in the provided index.
*/
View getUnfilteredChildAt(int index) {
return mCallback.getChildAt(index);
}
/**
* Detaches the view at the provided index.
*
* @param index Index of the child to return in regular perspective.
*/
void detachViewFromParent(int index) {
final int offset = getOffset(index);
mBucket.remove(offset);
mCallback.detachViewFromParent(offset);
if (DEBUG) {
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
}
}
/**
* Returns the index of the child in regular perspective.
*
* @param child The child whose index will be returned.
* @return The regular perspective index of the child or -1 if it does not exists.
*/
int indexOfChild(View child) {
final int index = mCallback.indexOfChild(child);
if (index == -1) {
return -1;
}
if (mBucket.get(index)) {
if (DEBUG) {
throw new IllegalArgumentException("cannot get index of a hidden child");
} else {
return -1;
}
}
// reverse the index
return index - mBucket.countOnesBefore(index);
}
/**
* Returns whether a View is visible to LayoutManager or not.
*
* @param view The child view to check. Should be a child of the Callback.
* @return True if the View is not visible to LayoutManager
*/
boolean isHidden(View view) {
return mHiddenViews.contains(view);
}
/**
* Marks a child view as hidden.
*
* @param view The view to hide.
*/
void hide(View view) {
final int offset = mCallback.indexOfChild(view);
if (offset < 0) {
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
}
if (DEBUG && mBucket.get(offset)) {
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
}
mBucket.set(offset);
hideViewInternal(view);
if (DEBUG) {
Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
}
}
/**
* Moves a child view from hidden list to regular list.
* Calling this method should probably be followed by a detach, otherwise, it will suddenly
* show up in LayoutManager's children list.
*
* @param view The hidden View to unhide
*/
void unhide(View view) {
final int offset = mCallback.indexOfChild(view);
if (offset < 0) {
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
}
if (!mBucket.get(offset)) {
throw new RuntimeException("trying to unhide a view that was not hidden" + view);
}
mBucket.clear(offset);
unhideViewInternal(view);
}
@Override
public String toString() {
return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
}
/**
* Removes a view from the ViewGroup if it is hidden.
*
* @param view The view to remove.
* @return True if the View is found and it is hidden. False otherwise.
*/
boolean removeViewIfHidden(View view) {
if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE) {
if (mViewInRemoveView != view) {
throw new IllegalStateException("Cannot call removeViewIfHidden within removeView"
+ "(At) for a different view");
}
// removeView ends the ItemAnimation and triggers removeViewIfHidden
return false;
} else if (mRemoveStatus == REMOVE_STATUS_IN_REMOVE_IF_HIDDEN) {
throw new IllegalStateException("Cannot call removeViewIfHidden within"
+ " removeViewIfHidden");
}
try {
mRemoveStatus = REMOVE_STATUS_IN_REMOVE_IF_HIDDEN;
final int index = mCallback.indexOfChild(view);
if (index == -1) {
if (unhideViewInternal(view) && DEBUG) {
throw new IllegalStateException("view is in hidden list but not in view group");
}
return true;
}
if (mBucket.get(index)) {
mBucket.remove(index);
if (!unhideViewInternal(view) && DEBUG) {
throw new IllegalStateException(
"removed a hidden view but it is not in hidden views list");
}
mCallback.removeViewAt(index);
return true;
}
return false;
} finally {
mRemoveStatus = REMOVE_STATUS_NONE;
}
}
/**
* Bitset implementation that provides methods to offset indices.
*/
static class Bucket {
static final int BITS_PER_WORD = Long.SIZE;
static final long LAST_BIT = 1L << (Long.SIZE - 1);
long mData = 0;
Bucket mNext;
void set(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
mNext.set(index - BITS_PER_WORD);
} else {
mData |= 1L << index;
}
}
private void ensureNext() {
if (mNext == null) {
mNext = new Bucket();
}
}
void clear(int index) {
if (index >= BITS_PER_WORD) {
if (mNext != null) {
mNext.clear(index - BITS_PER_WORD);
}
} else {
mData &= ~(1L << index);
}
}
boolean get(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return mNext.get(index - BITS_PER_WORD);
} else {
return (mData & (1L << index)) != 0;
}
}
void reset() {
mData = 0;
if (mNext != null) {
mNext.reset();
}
}
void insert(int index, boolean value) {
if (index >= BITS_PER_WORD) {
ensureNext();
mNext.insert(index - BITS_PER_WORD, value);
} else {
final boolean lastBit = (mData & LAST_BIT) != 0;
long mask = (1L << index) - 1;
final long before = mData & mask;
final long after = (mData & ~mask) << 1;
mData = before | after;
if (value) {
set(index);
} else {
clear(index);
}
if (lastBit || mNext != null) {
ensureNext();
mNext.insert(0, lastBit);
}
}
}
boolean remove(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return mNext.remove(index - BITS_PER_WORD);
} else {
long mask = (1L << index);
final boolean value = (mData & mask) != 0;
mData &= ~mask;
mask = mask - 1;
final long before = mData & mask;
// cannot use >> because it adds one.
final long after = Long.rotateRight(mData & ~mask, 1);
mData = before | after;
if (mNext != null) {
if (mNext.get(0)) {
set(BITS_PER_WORD - 1);
}
mNext.remove(0);
}
return value;
}
}
int countOnesBefore(int index) {
if (mNext == null) {
if (index >= BITS_PER_WORD) {
return Long.bitCount(mData);
}
return Long.bitCount(mData & ((1L << index) - 1));
}
if (index < BITS_PER_WORD) {
return Long.bitCount(mData & ((1L << index) - 1));
} else {
return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
}
}
@Override
public String toString() {
return mNext == null ? Long.toBinaryString(mData)
: mNext.toString() + "xx" + Long.toBinaryString(mData);
}
}
interface Callback {
int getChildCount();
void addView(View child, int index);
int indexOfChild(View view);
void removeViewAt(int index);
View getChildAt(int offset);
void removeAllViews();
RecyclerView.ViewHolder getChildViewHolder(View view);
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
void detachViewFromParent(int offset);
void onEnteredHiddenState(View child);
void onLeftHiddenState(View child);
}
}

@ -0,0 +1,481 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS;
import android.util.Pair;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* An {@link Adapter} implementation that presents the contents of multiple adapters in sequence.
*
* <pre>
* MyAdapter adapter1 = ...;
* AnotherAdapter adapter2 = ...;
* ConcatAdapter concatenated = new ConcatAdapter(adapter1, adapter2);
* recyclerView.setAdapter(concatenated);
* </pre>
* <p>
* By default, {@link ConcatAdapter} isolates view types of nested adapters from each other such
* that
* it will change the view type before reporting it back to the {@link RecyclerView} to avoid any
* conflicts between the view types of added adapters. This also means each added adapter will have
* its own isolated pool of {@link ViewHolder}s, with no re-use in between added adapters.
* <p>
* If your {@link Adapter}s share the same view types, and can support sharing {@link ViewHolder}
* s between added adapters, provide an instance of {@link Config} where you set
* {@link Config#isolateViewTypes} to {@code false}. A common usage pattern for this is to return
* the {@code R.layout.<layout_name>} from the {@link Adapter#getItemViewType(int)} method.
* <p>
* When an added adapter calls one of the {@code notify} methods, {@link ConcatAdapter} properly
* offsets values before reporting it back to the {@link RecyclerView}.
* If an adapter calls {@link Adapter#notifyDataSetChanged()}, {@link ConcatAdapter} also calls
* {@link Adapter#notifyDataSetChanged()} as calling
* {@link Adapter#notifyItemRangeChanged(int, int)} will confuse the {@link RecyclerView}.
* You are highly encouraged to to use {@link SortedList} or {@link ListAdapter} to avoid
* calling {@link Adapter#notifyDataSetChanged()}.
* <p>
* Whether {@link ConcatAdapter} should support stable ids is defined in the {@link Config}
* object. Calling {@link Adapter#setHasStableIds(boolean)} has no effect. See documentation
* for {@link Config.StableIdMode} for details on how to configure {@link ConcatAdapter} to use
* stable ids. By default, it will not use stable ids and sub adapter stable ids will be ignored.
* Similar to the case above, you are highly encouraged to use {@link ListAdapter}, which will
* automatically calculate the changes in the data set for you so you won't need stable ids.
* <p>
* It is common to find the adapter position of a {@link ViewHolder} to handle user action on the
* {@link ViewHolder}. For those cases, instead of calling {@link ViewHolder#getAdapterPosition()},
* use {@link ViewHolder#getBindingAdapterPosition()}. If your adapters share {@link ViewHolder}s,
* you can use the {@link ViewHolder#getBindingAdapter()} method to find the adapter which last
* bound that {@link ViewHolder}.
*/
@SuppressWarnings("unchecked")
public final class ConcatAdapter extends Adapter<ViewHolder> {
static final String TAG = "ConcatAdapter";
/**
* Bulk of the logic is in the controller to keep this class isolated to the public API.
*/
private final ConcatAdapterController mController;
/**
* Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given
* order.
*
* @param adapters The list of adapters to add
*/
@SafeVarargs
public ConcatAdapter(@NonNull Adapter<? extends ViewHolder>... adapters) {
this(Config.DEFAULT, adapters);
}
/**
* Creates a ConcatAdapter with the given config and the given adapters in the given order.
*
* @param config The configuration for this ConcatAdapter
* @param adapters The list of adapters to add
* @see Config.Builder
*/
@SafeVarargs
public ConcatAdapter(
@NonNull Config config,
@NonNull Adapter<? extends ViewHolder>... adapters) {
this(config, Arrays.asList(adapters));
}
/**
* Creates a ConcatAdapter with {@link Config#DEFAULT} and the given adapters in the given
* order.
*
* @param adapters The list of adapters to add
*/
public ConcatAdapter(@NonNull List<? extends Adapter<? extends ViewHolder>> adapters) {
this(Config.DEFAULT, adapters);
}
/**
* Creates a ConcatAdapter with the given config and the given adapters in the given order.
*
* @param config The configuration for this ConcatAdapter
* @param adapters The list of adapters to add
* @see Config.Builder
*/
public ConcatAdapter(
@NonNull Config config,
@NonNull List<? extends Adapter<? extends ViewHolder>> adapters) {
mController = new ConcatAdapterController(this, config);
for (Adapter<? extends ViewHolder> adapter : adapters) {
addAdapter(adapter);
}
// go through super as we override it to be no-op
super.setHasStableIds(mController.hasStableIds());
}
/**
* Appends the given adapter to the existing list of adapters and notifies the observers of
* this {@link ConcatAdapter}.
*
* @param adapter The new adapter to add
* @return {@code true} if the adapter is successfully added because it did not already exist,
* {@code false} otherwise.
* @see #addAdapter(int, Adapter)
* @see #removeAdapter(Adapter)
*/
public boolean addAdapter(@NonNull Adapter<? extends ViewHolder> adapter) {
return mController.addAdapter((Adapter<ViewHolder>) adapter);
}
/**
* Adds the given adapter to the given index among other adapters that are already added.
*
* @param index The index into which to insert the adapter. ConcatAdapter will throw an
* {@link IndexOutOfBoundsException} if the index is not between 0 and current
* adapter count (inclusive).
* @param adapter The new adapter to add to the adapters list.
* @return {@code true} if the adapter is successfully added because it did not already exist,
* {@code false} otherwise.
* @see #addAdapter(Adapter)
* @see #removeAdapter(Adapter)
*/
public boolean addAdapter(int index, @NonNull Adapter<? extends ViewHolder> adapter) {
return mController.addAdapter(index, (Adapter<ViewHolder>) adapter);
}
/**
* Removes the given adapter from the adapters list if it exists
*
* @param adapter The adapter to remove
* @return {@code true} if the adapter was previously added to this {@code ConcatAdapter} and
* now removed or {@code false} if it couldn't be found.
*/
public boolean removeAdapter(@NonNull Adapter<? extends ViewHolder> adapter) {
return mController.removeAdapter((Adapter<ViewHolder>) adapter);
}
@Override
public int getItemViewType(int position) {
return mController.getItemViewType(position);
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return mController.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
mController.onBindViewHolder(holder, position);
}
/**
* Calling this method is an error and will result in an {@link UnsupportedOperationException}.
* You should use the {@link Config} object passed into the ConcatAdapter to configure this
* behavior.
*
* @param hasStableIds Whether items in data set have unique identifiers or not.
*/
@Override
public void setHasStableIds(boolean hasStableIds) {
throw new UnsupportedOperationException(
"Calling setHasStableIds is not allowed on the ConcatAdapter. "
+ "Use the Config object passed in the constructor to control this "
+ "behavior");
}
/**
* Calling this method is an error and will result in an {@link UnsupportedOperationException}.
*
* ConcatAdapter infers this value from added {@link Adapter}s.
*
* @param strategy The saved state restoration strategy for this Adapter such that
* {@link ConcatAdapter} will allow state restoration only if all added
* adapters allow it or
* there are no adapters.
*/
@Override
public void setStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) {
// do nothing
throw new UnsupportedOperationException(
"Calling setStateRestorationPolicy is not allowed on the ConcatAdapter."
+ " This value is inferred from added adapters");
}
@Override
public long getItemId(int position) {
return mController.getItemId(position);
}
/**
* Internal method called by the ConcatAdapterController.
*/
void internalSetStateRestorationPolicy(@NonNull StateRestorationPolicy strategy) {
super.setStateRestorationPolicy(strategy);
}
@Override
public int getItemCount() {
return mController.getTotalCount();
}
@Override
public boolean onFailedToRecycleView(@NonNull ViewHolder holder) {
return mController.onFailedToRecycleView(holder);
}
@Override
public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
mController.onViewAttachedToWindow(holder);
}
@Override
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
mController.onViewDetachedFromWindow(holder);
}
@Override
public void onViewRecycled(@NonNull ViewHolder holder) {
mController.onViewRecycled(holder);
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
mController.onAttachedToRecyclerView(recyclerView);
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
mController.onDetachedFromRecyclerView(recyclerView);
}
/**
* Returns an unmodifiable copy of the list of adapters in this {@link ConcatAdapter}.
* Note that this is a copy hence future changes in the ConcatAdapter are not reflected in
* this list.
*
* @return A copy of the list of adapters in this ConcatAdapter.
*/
@NonNull
public List<? extends Adapter<? extends ViewHolder>> getAdapters() {
return Collections.unmodifiableList(mController.getCopyOfAdapters());
}
/**
* Returns the position of the given {@link ViewHolder} in the given {@link Adapter}.
*
* If the given {@link Adapter} is not part of this {@link ConcatAdapter},
* {@link RecyclerView#NO_POSITION} is returned.
*
* @param adapter The adapter which is a sub adapter of this ConcatAdapter or itself.
* @param viewHolder The view holder whose local position in the given adapter will be
* returned.
* @param localPosition The position of the given {@link ViewHolder} in this {@link Adapter}.
* @return The local position of the given {@link ViewHolder} in the given {@link Adapter} or
* {@link RecyclerView#NO_POSITION} if the {@link ViewHolder} is not bound to an item or the
* given {@link Adapter} is not part of this ConcatAdapter.
*/
@Override
public int findRelativeAdapterPositionIn(
@NonNull Adapter<? extends ViewHolder> adapter,
@NonNull ViewHolder viewHolder,
int localPosition) {
return mController.getLocalAdapterPosition(adapter, viewHolder, localPosition);
}
/**
* Retrieve the adapter and local position for a given position in this {@code ConcatAdapter}.
*
* This allows for retrieving wrapped adapter information in situations where you don't have a
* {@link ViewHolder}, such as within a
* {@link androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup} in which you want to
* look up information from the source adapter.
*
* @param globalPosition The position in this {@code ConcatAdapter}.
* @return a Pair with the first element set to the wrapped {@code Adapter} containing that
* position and the second element set to the local position in the wrapped adapter
* @throws IllegalArgumentException if the specified {@code globalPosition} does not
* correspond to a valid element of this adapter. That is, if {@code globalPosition} is less
* than 0 or greater than the total number of items in the {@code ConcatAdapter}
*/
@NonNull
public Pair<Adapter<? extends ViewHolder>, Integer> getWrappedAdapterAndPosition(int
globalPosition) {
return mController.getWrappedAdapterAndPosition(globalPosition);
}
/**
* The configuration object for a {@link ConcatAdapter}.
*/
public static final class Config {
/**
* If {@code false}, {@link ConcatAdapter} assumes all assigned adapters share a global
* view type pool such that they use the same view types to refer to the same
* {@link ViewHolder}s.
* <p>
* Setting this to {@code false} will allow nested adapters to share {@link ViewHolder}s but
* it also means these adapters should not have conflicting view types
* ({@link Adapter#getItemViewType(int)}) such that two different adapters return the same
* view type for different {@link ViewHolder}s.
*
* By default, it is set to {@code true} which means {@link ConcatAdapter} will isolate
* view types across adapters, preventing them from using the same {@link ViewHolder}s.
*/
public final boolean isolateViewTypes;
/**
* Defines whether the {@link ConcatAdapter} should support stable ids or not
* ({@link Adapter#hasStableIds()}.
* <p>
* There are 3 possible options:
*
* {@link StableIdMode#NO_STABLE_IDS}: In this mode, {@link ConcatAdapter} ignores the
* stable
* ids reported by sub adapters. This is the default mode.
*
* {@link StableIdMode#ISOLATED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return
* {@code true} from {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
* {@link Adapter}s to have stable ids. As two different adapters may return same stable ids
* because they are unaware of each-other, {@link ConcatAdapter} will isolate each
* {@link Adapter}'s id pool from each other such that it will overwrite the reported stable
* id before reporting back to the {@link RecyclerView}. In this mode, the value returned
* from {@link ViewHolder#getItemId()} might differ from the value returned from
* {@link Adapter#getItemId(int)}.
*
* {@link StableIdMode#SHARED_STABLE_IDS}: In this mode, {@link ConcatAdapter} will return
* {@code true} from {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
* {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS},
* {@link ConcatAdapter} will not override the returned item ids. In this mode,
* child {@link Adapter}s must be aware of each-other and never return the same id unless
* an item is moved between {@link Adapter}s.
*
* Default value is {@link StableIdMode#NO_STABLE_IDS}.
*/
@NonNull
public final StableIdMode stableIdMode;
/**
* Default configuration for {@link ConcatAdapter} where {@link Config#isolateViewTypes}
* is set to {@code true} and {@link Config#stableIdMode} is set to
* {@link StableIdMode#NO_STABLE_IDS}.
*/
@NonNull
public static final Config DEFAULT = new Config(true, NO_STABLE_IDS);
Config(boolean isolateViewTypes, @NonNull StableIdMode stableIdMode) {
this.isolateViewTypes = isolateViewTypes;
this.stableIdMode = stableIdMode;
}
/**
* Defines how {@link ConcatAdapter} handle stable ids ({@link Adapter#hasStableIds()}).
*/
public enum StableIdMode {
/**
* In this mode, {@link ConcatAdapter} ignores the stable
* ids reported by sub adapters. This is the default mode.
* Adding an {@link Adapter} with stable ids will result in a warning as it will be
* ignored.
*/
NO_STABLE_IDS,
/**
* In this mode, {@link ConcatAdapter} will return {@code true} from
* {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
* {@link Adapter}s to have stable ids. As two different adapters may return
* same stable ids because they are unaware of each-other, {@link ConcatAdapter} will
* isolate each {@link Adapter}'s id pool from each other such that it will overwrite
* the reported stable id before reporting back to the {@link RecyclerView}. In this
* mode, the value returned from {@link ViewHolder#getItemId()} might differ from the
* value returned from {@link Adapter#getItemId(int)}.
*
* Adding an adapter without stable ids will result in an
* {@link IllegalArgumentException}.
*/
ISOLATED_STABLE_IDS,
/**
* In this mode, {@link ConcatAdapter} will return {@code true} from
* {@link ConcatAdapter#hasStableIds()} and will <b>require</b> all added
* {@link Adapter}s to have stable ids. Unlike {@link StableIdMode#ISOLATED_STABLE_IDS},
* {@link ConcatAdapter} will not override the returned item ids. In this mode,
* child {@link Adapter}s must be aware of each-other and never return the same id
* unless and item is moved between {@link Adapter}s.
* Adding an adapter without stable ids will result in an
* {@link IllegalArgumentException}.
*/
SHARED_STABLE_IDS
}
/**
* The builder for {@link Config} class.
*/
public static final class Builder {
private boolean mIsolateViewTypes = DEFAULT.isolateViewTypes;
private StableIdMode mStableIdMode = DEFAULT.stableIdMode;
/**
* Sets whether {@link ConcatAdapter} should isolate view types of nested adapters from
* each other.
*
* @param isolateViewTypes {@code true} if {@link ConcatAdapter} should override view
* types of nested adapters to avoid view type
* conflicts, {@code false} otherwise.
* Defaults to {@link Config#DEFAULT}'s
* {@link Config#isolateViewTypes} value ({@code true}).
* @return this
* @see Config#isolateViewTypes
*/
@NonNull
public Builder setIsolateViewTypes(boolean isolateViewTypes) {
mIsolateViewTypes = isolateViewTypes;
return this;
}
/**
* Sets how the {@link ConcatAdapter} should handle stable ids
* ({@link Adapter#hasStableIds()}). See documentation in {@link Config#stableIdMode}
* for details.
*
* @param stableIdMode The stable id mode for the {@link ConcatAdapter}. Defaults to
* {@link Config#DEFAULT}'s {@link Config#stableIdMode} value
* ({@link StableIdMode#NO_STABLE_IDS}).
* @return this
* @see Config#stableIdMode
*/
@NonNull
public Builder setStableIdMode(@NonNull StableIdMode stableIdMode) {
mStableIdMode = stableIdMode;
return this;
}
/**
* @return A new instance of {@link Config} with the given parameters.
*/
@NonNull
public Config build() {
return new Config(mIsolateViewTypes, mStableIdMode);
}
}
}
}

@ -0,0 +1,528 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS;
import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS;
import static androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS;
import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.ALLOW;
import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT;
import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import android.util.Log;
import android.util.Pair;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
/**
* All logic for the {@link ConcatAdapter} is here so that we can clearly see a separation
* between an adapter implementation and merging logic.
*/
class ConcatAdapterController implements NestedAdapterWrapper.Callback {
private final ConcatAdapter mConcatAdapter;
/**
* Holds the mapping from the view type to the adapter which reported that type.
*/
private final ViewTypeStorage mViewTypeStorage;
/**
* We hold onto the list of attached recyclerviews so that we can dispatch attach/detach to
* any adapter that was added later on.
* Probably does not need to be a weak reference but playing safe here.
*/
private List<WeakReference<RecyclerView>> mAttachedRecyclerViews = new ArrayList<>();
/**
* Keeps the information about which ViewHolder is bound by which adapter.
* It is set in onBind, reset at onRecycle.
*/
private final IdentityHashMap<ViewHolder, NestedAdapterWrapper>
mBinderLookup = new IdentityHashMap<>();
private List<NestedAdapterWrapper> mWrappers = new ArrayList<>();
// keep one of these around so that we can return wrapper & position w/o allocation ¯\_(ツ)_/¯
private WrapperAndLocalPosition mReusableHolder = new WrapperAndLocalPosition();
@NonNull
private final ConcatAdapter.Config.StableIdMode mStableIdMode;
/**
* This is where we keep stable ids, if supported
*/
private final StableIdStorage mStableIdStorage;
ConcatAdapterController(
ConcatAdapter concatAdapter,
ConcatAdapter.Config config) {
mConcatAdapter = concatAdapter;
// setup view type handling
if (config.isolateViewTypes) {
mViewTypeStorage = new ViewTypeStorage.IsolatedViewTypeStorage();
} else {
mViewTypeStorage = new ViewTypeStorage.SharedIdRangeViewTypeStorage();
}
// setup stable id handling
mStableIdMode = config.stableIdMode;
if (config.stableIdMode == NO_STABLE_IDS) {
mStableIdStorage = new StableIdStorage.NoStableIdStorage();
} else if (config.stableIdMode == ISOLATED_STABLE_IDS) {
mStableIdStorage = new StableIdStorage.IsolatedStableIdStorage();
} else if (config.stableIdMode == SHARED_STABLE_IDS) {
mStableIdStorage = new StableIdStorage.SharedPoolStableIdStorage();
} else {
throw new IllegalArgumentException("unknown stable id mode");
}
}
@Nullable
private NestedAdapterWrapper findWrapperFor(Adapter<ViewHolder> adapter) {
final int index = indexOfWrapper(adapter);
if (index == -1) {
return null;
}
return mWrappers.get(index);
}
private int indexOfWrapper(Adapter<ViewHolder> adapter) {
final int limit = mWrappers.size();
for (int i = 0; i < limit; i++) {
if (mWrappers.get(i).adapter == adapter) {
return i;
}
}
return -1;
}
/**
* return true if added, false otherwise.
*
* @see ConcatAdapter#addAdapter(Adapter)
*/
boolean addAdapter(Adapter<ViewHolder> adapter) {
return addAdapter(mWrappers.size(), adapter);
}
/**
* return true if added, false otherwise.
* throws exception if index is out of bounds
*
* @see ConcatAdapter#addAdapter(int, Adapter)
*/
boolean addAdapter(int index, Adapter<ViewHolder> adapter) {
if (index < 0 || index > mWrappers.size()) {
throw new IndexOutOfBoundsException("Index must be between 0 and "
+ mWrappers.size() + ". Given:" + index);
}
if (hasStableIds()) {
Preconditions.checkArgument(adapter.hasStableIds(),
"All sub adapters must have stable ids when stable id mode "
+ "is ISOLATED_STABLE_IDS or SHARED_STABLE_IDS");
} else {
if (adapter.hasStableIds()) {
Log.w(ConcatAdapter.TAG, "Stable ids in the adapter will be ignored as the"
+ " ConcatAdapter is configured not to have stable ids");
}
}
NestedAdapterWrapper existing = findWrapperFor(adapter);
if (existing != null) {
return false;
}
NestedAdapterWrapper wrapper = new NestedAdapterWrapper(adapter, this,
mViewTypeStorage, mStableIdStorage.createStableIdLookup());
mWrappers.add(index, wrapper);
// notify attach for all recyclerview
for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
RecyclerView recyclerView = reference.get();
if (recyclerView != null) {
adapter.onAttachedToRecyclerView(recyclerView);
}
}
// new items, notify add for them
if (wrapper.getCachedItemCount() > 0) {
mConcatAdapter.notifyItemRangeInserted(
countItemsBefore(wrapper),
wrapper.getCachedItemCount()
);
}
// reset state restoration strategy
calculateAndUpdateStateRestorationPolicy();
return true;
}
boolean removeAdapter(Adapter<ViewHolder> adapter) {
final int index = indexOfWrapper(adapter);
if (index == -1) {
return false;
}
NestedAdapterWrapper wrapper = mWrappers.get(index);
int offset = countItemsBefore(wrapper);
mWrappers.remove(index);
mConcatAdapter.notifyItemRangeRemoved(offset, wrapper.getCachedItemCount());
// notify detach for all recyclerviews
for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
RecyclerView recyclerView = reference.get();
if (recyclerView != null) {
adapter.onDetachedFromRecyclerView(recyclerView);
}
}
wrapper.dispose();
calculateAndUpdateStateRestorationPolicy();
return true;
}
private int countItemsBefore(NestedAdapterWrapper wrapper) {
int count = 0;
for (NestedAdapterWrapper item : mWrappers) {
if (item != wrapper) {
count += item.getCachedItemCount();
} else {
break;
}
}
return count;
}
public long getItemId(int globalPosition) {
WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
long globalItemId = wrapperAndPos.mWrapper.getItemId(wrapperAndPos.mLocalPosition);
releaseWrapperAndLocalPosition(wrapperAndPos);
return globalItemId;
}
@Override
public void onChanged(@NonNull NestedAdapterWrapper wrapper) {
// TODO should we notify more cleverly, maybe in v2
mConcatAdapter.notifyDataSetChanged();
calculateAndUpdateStateRestorationPolicy();
}
@Override
public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart, int itemCount) {
final int offset = countItemsBefore(nestedAdapterWrapper);
mConcatAdapter.notifyItemRangeChanged(
positionStart + offset,
itemCount
);
}
@Override
public void onItemRangeChanged(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart, int itemCount, @Nullable Object payload) {
final int offset = countItemsBefore(nestedAdapterWrapper);
mConcatAdapter.notifyItemRangeChanged(
positionStart + offset,
itemCount,
payload
);
}
@Override
public void onItemRangeInserted(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart, int itemCount) {
final int offset = countItemsBefore(nestedAdapterWrapper);
mConcatAdapter.notifyItemRangeInserted(
positionStart + offset,
itemCount
);
}
@Override
public void onItemRangeRemoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart, int itemCount) {
int offset = countItemsBefore(nestedAdapterWrapper);
mConcatAdapter.notifyItemRangeRemoved(
positionStart + offset,
itemCount
);
}
@Override
public void onItemRangeMoved(@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int fromPosition, int toPosition) {
int offset = countItemsBefore(nestedAdapterWrapper);
mConcatAdapter.notifyItemMoved(
fromPosition + offset,
toPosition + offset
);
}
@Override
public void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper) {
calculateAndUpdateStateRestorationPolicy();
}
private void calculateAndUpdateStateRestorationPolicy() {
StateRestorationPolicy newPolicy = computeStateRestorationPolicy();
if (newPolicy != mConcatAdapter.getStateRestorationPolicy()) {
mConcatAdapter.internalSetStateRestorationPolicy(newPolicy);
}
}
private StateRestorationPolicy computeStateRestorationPolicy() {
for (NestedAdapterWrapper wrapper : mWrappers) {
StateRestorationPolicy strategy =
wrapper.adapter.getStateRestorationPolicy();
if (strategy == PREVENT) {
// one adapter can block all
return PREVENT;
} else if (strategy == PREVENT_WHEN_EMPTY && wrapper.getCachedItemCount() == 0) {
// an adapter wants to allow w/ size but we need to make sure there is no prevent
return PREVENT;
}
}
return ALLOW;
}
public int getTotalCount() {
// should we cache this as well ?
int total = 0;
for (NestedAdapterWrapper wrapper : mWrappers) {
total += wrapper.getCachedItemCount();
}
return total;
}
public int getItemViewType(int globalPosition) {
WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
int itemViewType = wrapperAndPos.mWrapper.getItemViewType(wrapperAndPos.mLocalPosition);
releaseWrapperAndLocalPosition(wrapperAndPos);
return itemViewType;
}
public ViewHolder onCreateViewHolder(ViewGroup parent, int globalViewType) {
NestedAdapterWrapper wrapper = mViewTypeStorage.getWrapperForGlobalType(globalViewType);
return wrapper.onCreateViewHolder(parent, globalViewType);
}
public Pair<Adapter<? extends ViewHolder>, Integer> getWrappedAdapterAndPosition(
int globalPosition) {
WrapperAndLocalPosition wrapper = findWrapperAndLocalPosition(globalPosition);
Pair<Adapter<? extends ViewHolder>, Integer> pair = new Pair<>(wrapper.mWrapper.adapter,
wrapper.mLocalPosition);
releaseWrapperAndLocalPosition(wrapper);
return pair;
}
/**
* Always call {@link #releaseWrapperAndLocalPosition(WrapperAndLocalPosition)} when you are
* done with it
*/
@NonNull
private WrapperAndLocalPosition findWrapperAndLocalPosition(
int globalPosition
) {
WrapperAndLocalPosition result;
if (mReusableHolder.mInUse) {
result = new WrapperAndLocalPosition();
} else {
mReusableHolder.mInUse = true;
result = mReusableHolder;
}
int localPosition = globalPosition;
for (NestedAdapterWrapper wrapper : mWrappers) {
if (wrapper.getCachedItemCount() > localPosition) {
result.mWrapper = wrapper;
result.mLocalPosition = localPosition;
break;
}
localPosition -= wrapper.getCachedItemCount();
}
if (result.mWrapper == null) {
throw new IllegalArgumentException("Cannot find wrapper for " + globalPosition);
}
return result;
}
private void releaseWrapperAndLocalPosition(WrapperAndLocalPosition wrapperAndLocalPosition) {
wrapperAndLocalPosition.mInUse = false;
wrapperAndLocalPosition.mWrapper = null;
wrapperAndLocalPosition.mLocalPosition = -1;
mReusableHolder = wrapperAndLocalPosition;
}
public void onBindViewHolder(ViewHolder holder, int globalPosition) {
WrapperAndLocalPosition wrapperAndPos = findWrapperAndLocalPosition(globalPosition);
mBinderLookup.put(holder, wrapperAndPos.mWrapper);
wrapperAndPos.mWrapper.onBindViewHolder(holder, wrapperAndPos.mLocalPosition);
releaseWrapperAndLocalPosition(wrapperAndPos);
}
public boolean canRestoreState() {
for (NestedAdapterWrapper wrapper : mWrappers) {
if (!wrapper.adapter.canRestoreState()) {
return false;
}
}
return true;
}
public void onViewAttachedToWindow(ViewHolder holder) {
NestedAdapterWrapper wrapper = getWrapper(holder);
wrapper.adapter.onViewAttachedToWindow(holder);
}
public void onViewDetachedFromWindow(ViewHolder holder) {
NestedAdapterWrapper wrapper = getWrapper(holder);
wrapper.adapter.onViewDetachedFromWindow(holder);
}
public void onViewRecycled(ViewHolder holder) {
NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
if (wrapper == null) {
throw new IllegalStateException("Cannot find wrapper for " + holder
+ ", seems like it is not bound by this adapter: " + this);
}
wrapper.adapter.onViewRecycled(holder);
mBinderLookup.remove(holder);
}
public boolean onFailedToRecycleView(ViewHolder holder) {
NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
if (wrapper == null) {
throw new IllegalStateException("Cannot find wrapper for " + holder
+ ", seems like it is not bound by this adapter: " + this);
}
final boolean result = wrapper.adapter.onFailedToRecycleView(holder);
mBinderLookup.remove(holder);
return result;
}
@NonNull
private NestedAdapterWrapper getWrapper(ViewHolder holder) {
NestedAdapterWrapper wrapper = mBinderLookup.get(holder);
if (wrapper == null) {
throw new IllegalStateException("Cannot find wrapper for " + holder
+ ", seems like it is not bound by this adapter: " + this);
}
return wrapper;
}
private boolean isAttachedTo(RecyclerView recyclerView) {
for (WeakReference<RecyclerView> reference : mAttachedRecyclerViews) {
if (reference.get() == recyclerView) {
return true;
}
}
return false;
}
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
if (isAttachedTo(recyclerView)) {
return;
}
mAttachedRecyclerViews.add(new WeakReference<>(recyclerView));
for (NestedAdapterWrapper wrapper : mWrappers) {
wrapper.adapter.onAttachedToRecyclerView(recyclerView);
}
}
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
for (int i = mAttachedRecyclerViews.size() - 1; i >= 0; i--) {
WeakReference<RecyclerView> reference = mAttachedRecyclerViews.get(i);
if (reference.get() == null) {
mAttachedRecyclerViews.remove(i);
} else if (reference.get() == recyclerView) {
mAttachedRecyclerViews.remove(i);
break; // here we can break as we don't keep duplicates
}
}
for (NestedAdapterWrapper wrapper : mWrappers) {
wrapper.adapter.onDetachedFromRecyclerView(recyclerView);
}
}
public int getLocalAdapterPosition(
Adapter<? extends ViewHolder> adapter,
ViewHolder viewHolder,
int globalPosition
) {
NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder);
if (wrapper == null) {
return NO_POSITION;
}
int itemsBefore = countItemsBefore(wrapper);
// local position is globalPosition - itemsBefore
int localPosition = globalPosition - itemsBefore;
// Early error detection:
int itemCount = wrapper.adapter.getItemCount();
if (localPosition < 0 || localPosition >= itemCount) {
throw new IllegalStateException("Detected inconsistent adapter updates. The"
+ " local position of the view holder maps to " + localPosition + " which"
+ " is out of bounds for the adapter with size "
+ itemCount + "."
+ "Make sure to immediately call notify methods in your adapter when you "
+ "change the backing data"
+ "viewHolder:" + viewHolder
+ "adapter:" + adapter);
}
return wrapper.adapter.findRelativeAdapterPositionIn(adapter, viewHolder, localPosition);
}
@Nullable
public Adapter<? extends ViewHolder> getBoundAdapter(ViewHolder viewHolder) {
NestedAdapterWrapper wrapper = mBinderLookup.get(viewHolder);
if (wrapper == null) {
return null;
}
return wrapper.adapter;
}
@SuppressWarnings("MixedMutabilityReturnType")
public List<Adapter<? extends ViewHolder>> getCopyOfAdapters() {
if (mWrappers.isEmpty()) {
return Collections.emptyList();
}
List<Adapter<? extends ViewHolder>> adapters = new ArrayList<>(mWrappers.size());
for (NestedAdapterWrapper wrapper : mWrappers) {
adapters.add(wrapper.adapter);
}
return adapters;
}
public boolean hasStableIds() {
return mStableIdMode != NO_STABLE_IDS;
}
/**
* Helper class to hold onto wrapper and local position without allocating objects as this is
* a very common call.
*/
static class WrapperAndLocalPosition {
NestedAdapterWrapper mWrapper;
int mLocalPosition;
boolean mInUse;
}
}

@ -0,0 +1,674 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.view.View;
import android.view.ViewPropertyAnimator;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class DefaultItemAnimator extends SimpleItemAnimator {
private static final boolean DEBUG = false;
private static TimeInterpolator sDefaultInterpolator;
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList = new ArrayList<>();
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<>();
private static class MoveInfo {
public RecyclerView.ViewHolder holder;
public int fromX, fromY, toX, toY;
MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public RecyclerView.ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public String toString() {
return "ChangeInfo{"
+ "oldHolder=" + oldHolder
+ ", newHolder=" + newHolder
+ ", fromX=" + fromX
+ ", fromY=" + fromY
+ ", toX=" + toX
+ ", toY=" + toY
+ '}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (RecyclerView.ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> 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<ChangeInfo> changes = new ArrayList<>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
RecyclerView.ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
@Override
public void run() {
for (RecyclerView.ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimator animation = view.animate();
mRemoveAnimations.add(holder);
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
view.setAlpha(1);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public boolean animateAdd(final RecyclerView.ViewHolder holder) {
resetAnimation(holder);
holder.itemView.setAlpha(0);
mPendingAdditions.add(holder);
return true;
}
void animateAddImpl(final RecyclerView.ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimator animation = view.animate();
mAddAnimations.add(holder);
animation.alpha(1).setDuration(getAddDuration())
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(Animator animator) {
view.setAlpha(1);
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += (int) holder.itemView.getTranslationX();
fromY += (int) holder.itemView.getTranslationY();
resetAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
if (deltaX != 0) {
view.setTranslationX(-deltaX);
}
if (deltaY != 0) {
view.setTranslationY(-deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
view.animate().translationX(0);
}
if (deltaY != 0) {
view.animate().translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
final ViewPropertyAnimator animation = view.animate();
mMoveAnimations.add(holder);
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(Animator animator) {
if (deltaX != 0) {
view.setTranslationX(0);
}
if (deltaY != 0) {
view.setTranslationY(0);
}
}
@Override
public void onAnimationEnd(Animator animator) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public boolean animateChange(RecyclerView.ViewHolder oldHolder,
RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
if (oldHolder == newHolder) {
// Don't know how to run change animations when the same view holder is re-used.
// run a move animation to handle position changes.
return animateMove(oldHolder, fromLeft, fromTop, toLeft, toTop);
}
final float prevTranslationX = oldHolder.itemView.getTranslationX();
final float prevTranslationY = oldHolder.itemView.getTranslationY();
final float prevAlpha = oldHolder.itemView.getAlpha();
resetAnimation(oldHolder);
int deltaX = (int) (toLeft - fromLeft - prevTranslationX);
int deltaY = (int) (toTop - fromTop - prevTranslationY);
// recover prev translation state after ending animation
oldHolder.itemView.setTranslationX(prevTranslationX);
oldHolder.itemView.setTranslationY(prevTranslationY);
oldHolder.itemView.setAlpha(prevAlpha);
if (newHolder != null) {
// carry over translation values
resetAnimation(newHolder);
newHolder.itemView.setTranslationX(-deltaX);
newHolder.itemView.setTranslationY(-deltaY);
newHolder.itemView.setAlpha(0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop));
return true;
}
void animateChangeImpl(final ChangeInfo changeInfo) {
final RecyclerView.ViewHolder holder = changeInfo.oldHolder;
final View view = holder == null ? null : holder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view != null) {
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
getChangeDuration());
mChangeAnimations.add(changeInfo.oldHolder);
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(Animator animator) {
oldViewAnim.setListener(null);
view.setAlpha(1);
view.setTranslationX(0);
view.setTranslationY(0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
}
if (newView != null) {
final ViewPropertyAnimator newViewAnimation = newView.animate();
mChangeAnimations.add(changeInfo.newHolder);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
.alpha(1).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(Animator animator) {
newViewAnimation.setListener(null);
newView.setAlpha(1);
newView.setTranslationX(0);
newView.setTranslationY(0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
item.itemView.setAlpha(1);
item.itemView.setTranslationX(0);
item.itemView.setTranslationY(0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void endAnimation(RecyclerView.ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
view.animate().cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
view.setTranslationY(0);
view.setTranslationX(0);
dispatchMoveFinished(item);
mPendingMoves.remove(i);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
view.setAlpha(1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
view.setAlpha(1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(i);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
view.setTranslationY(0);
view.setTranslationX(0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(i);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
view.setAlpha(1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(i);
}
}
}
// animations should be ended by the cancel above.
//noinspection PointlessBooleanExpression,ConstantConditions
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
//noinspection PointlessBooleanExpression,ConstantConditions
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
private void resetAnimation(RecyclerView.ViewHolder holder) {
if (sDefaultInterpolator == null) {
sDefaultInterpolator = new ValueAnimator().getInterpolator();
}
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
endAnimation(holder);
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty()
|| !mPendingChanges.isEmpty()
|| !mPendingMoves.isEmpty()
|| !mPendingRemovals.isEmpty()
|| !mMoveAnimations.isEmpty()
|| !mRemoveAnimations.isEmpty()
|| !mAddAnimations.isEmpty()
|| !mChangeAnimations.isEmpty()
|| !mMovesList.isEmpty()
|| !mAdditionsList.isEmpty()
|| !mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
view.setTranslationY(0);
view.setTranslationX(0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
RecyclerView.ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
RecyclerView.ViewHolder item = mPendingAdditions.get(i);
item.itemView.setAlpha(1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
RecyclerView.ViewHolder item = moveInfo.holder;
View view = item.itemView;
view.setTranslationY(0);
view.setTranslationX(0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
RecyclerView.ViewHolder item = additions.get(j);
View view = item.itemView;
view.setAlpha(1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> 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<RecyclerView.ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
viewHolders.get(i).itemView.animate().cancel();
}
}
/**
* {@inheritDoc}
* <p>
* If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
* When this is the case:
* <ul>
* <li>If you override {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}, both
* ViewHolder arguments will be the same instance.
* </li>
* <li>
* If you are not overriding {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)},
* then DefaultItemAnimator will call {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int)} and
* run a move animation instead.
* </li>
* </ul>
*/
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,194 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* DividerItemDecoration is a {@link RecyclerView.ItemDecoration} that can be used as a divider
* between items of a {@link LinearLayoutManager}. It supports both {@link #HORIZONTAL} and
* {@link #VERTICAL} orientations.
*
* <pre>
* mDividerItemDecoration = new DividerItemDecoration(recyclerView.getContext(),
* mLayoutManager.getOrientation());
* recyclerView.addItemDecoration(mDividerItemDecoration);
* </pre>
*/
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final String TAG = "DividerItem";
private static final int[] ATTRS = new int[]{ android.R.attr.listDivider };
private Drawable mDivider;
/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;
private final Rect mBounds = new Rect();
/**
* Creates a divider {@link RecyclerView.ItemDecoration} that can be used with a
* {@link LinearLayoutManager}.
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public DividerItemDecoration(Context context, int orientation) {
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
if (mDivider == null) {
Log.w(TAG, "@android:attr/listDivider was not set in the theme used for this "
+ "DividerItemDecoration. Please set that attribute all call setDrawable()");
}
a.recycle();
setOrientation(orientation);
}
/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException(
"Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
}
/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public void setDrawable(@NonNull Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("Drawable cannot be null.");
}
mDivider = drawable;
}
/**
* @return the {@link Drawable} for this divider.
*/
@Nullable
public Drawable getDrawable() {
return mDivider;
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(c, parent);
} else {
drawHorizontal(c, parent);
}
}
private void drawVertical(Canvas canvas, RecyclerView parent) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right,
parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
private void drawHorizontal(Canvas canvas, RecyclerView parent) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getLayoutManager().getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
canvas.restore();
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
if (mDivider == null) {
outRect.set(0, 0, 0, 0);
return;
}
if (mOrientation == VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
} else {
outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
}
}
}

@ -0,0 +1,588 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.view.MotionEvent;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Class responsible to animate and provide a fast scroller.
*/
@VisibleForTesting
class FastScroller extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener {
@IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING})
@Retention(RetentionPolicy.SOURCE)
private @interface State { }
// Scroll thumb not showing
private static final int STATE_HIDDEN = 0;
// Scroll thumb visible and moving along with the scrollbar
private static final int STATE_VISIBLE = 1;
// Scroll thumb being dragged by user
private static final int STATE_DRAGGING = 2;
@IntDef({DRAG_X, DRAG_Y, DRAG_NONE})
@Retention(RetentionPolicy.SOURCE)
private @interface DragState{ }
private static final int DRAG_NONE = 0;
private static final int DRAG_X = 1;
private static final int DRAG_Y = 2;
@IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN,
ANIMATION_STATE_FADING_OUT})
@Retention(RetentionPolicy.SOURCE)
private @interface AnimationState { }
private static final int ANIMATION_STATE_OUT = 0;
private static final int ANIMATION_STATE_FADING_IN = 1;
private static final int ANIMATION_STATE_IN = 2;
private static final int ANIMATION_STATE_FADING_OUT = 3;
private static final int SHOW_DURATION_MS = 500;
private static final int HIDE_DELAY_AFTER_VISIBLE_MS = 1500;
private static final int HIDE_DELAY_AFTER_DRAGGING_MS = 1200;
private static final int HIDE_DURATION_MS = 500;
private static final int SCROLLBAR_FULL_OPAQUE = 255;
private static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed};
private static final int[] EMPTY_STATE_SET = new int[]{};
private final int mScrollbarMinimumRange;
private final int mMargin;
// Final values for the vertical scroll bar
@SuppressWarnings("WeakerAccess") /* synthetic access */
final StateListDrawable mVerticalThumbDrawable;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Drawable mVerticalTrackDrawable;
private final int mVerticalThumbWidth;
private final int mVerticalTrackWidth;
// Final values for the horizontal scroll bar
private final StateListDrawable mHorizontalThumbDrawable;
private final Drawable mHorizontalTrackDrawable;
private final int mHorizontalThumbHeight;
private final int mHorizontalTrackHeight;
// Dynamic values for the vertical scroll bar
@VisibleForTesting int mVerticalThumbHeight;
@VisibleForTesting int mVerticalThumbCenterY;
@VisibleForTesting float mVerticalDragY;
// Dynamic values for the horizontal scroll bar
@VisibleForTesting int mHorizontalThumbWidth;
@VisibleForTesting int mHorizontalThumbCenterX;
@VisibleForTesting float mHorizontalDragX;
private int mRecyclerViewWidth = 0;
private int mRecyclerViewHeight = 0;
private RecyclerView mRecyclerView;
/**
* Whether the document is long/wide enough to require scrolling. If not, we don't show the
* relevant scroller.
*/
private boolean mNeedVerticalScrollbar = false;
private boolean mNeedHorizontalScrollbar = false;
@State private int mState = STATE_HIDDEN;
@DragState private int mDragState = DRAG_NONE;
private final int[] mVerticalRange = new int[2];
private final int[] mHorizontalRange = new int[2];
@SuppressWarnings("WeakerAccess") /* synthetic access */
final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);
@SuppressWarnings("WeakerAccess") /* synthetic access */
@AnimationState int mAnimationState = ANIMATION_STATE_OUT;
private final Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
hide(HIDE_DURATION_MS);
}
};
private final RecyclerView.OnScrollListener
mOnScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
updateScrollPosition(recyclerView.computeHorizontalScrollOffset(),
recyclerView.computeVerticalScrollOffset());
}
};
FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable,
Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange,
int margin) {
mVerticalThumbDrawable = verticalThumbDrawable;
mVerticalTrackDrawable = verticalTrackDrawable;
mHorizontalThumbDrawable = horizontalThumbDrawable;
mHorizontalTrackDrawable = horizontalTrackDrawable;
mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth());
mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth());
mHorizontalThumbHeight = Math
.max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth());
mHorizontalTrackHeight = Math
.max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth());
mScrollbarMinimumRange = scrollbarMinimumRange;
mMargin = margin;
mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
mVerticalTrackDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
mShowHideAnimator.addListener(new AnimatorListener());
mShowHideAnimator.addUpdateListener(new AnimatorUpdater());
attachToRecyclerView(recyclerView);
}
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
}
}
private void setupCallbacks() {
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(this);
mRecyclerView.addOnScrollListener(mOnScrollListener);
}
private void destroyCallbacks() {
mRecyclerView.removeItemDecoration(this);
mRecyclerView.removeOnItemTouchListener(this);
mRecyclerView.removeOnScrollListener(mOnScrollListener);
cancelHide();
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void requestRedraw() {
mRecyclerView.invalidate();
}
void setState(@State int state) {
if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
cancelHide();
}
if (state == STATE_HIDDEN) {
requestRedraw();
} else {
show();
}
if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
} else if (state == STATE_VISIBLE) {
resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
}
mState = state;
}
private boolean isLayoutRTL() {
return ViewCompat.getLayoutDirection(mRecyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL;
}
public boolean isDragging() {
return mState == STATE_DRAGGING;
}
@VisibleForTesting boolean isVisible() {
return mState == STATE_VISIBLE;
}
public void show() {
switch (mAnimationState) {
case ANIMATION_STATE_FADING_OUT:
mShowHideAnimator.cancel();
// fall through
case ANIMATION_STATE_OUT:
mAnimationState = ANIMATION_STATE_FADING_IN;
mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1);
mShowHideAnimator.setDuration(SHOW_DURATION_MS);
mShowHideAnimator.setStartDelay(0);
mShowHideAnimator.start();
break;
}
}
@VisibleForTesting
void hide(int duration) {
switch (mAnimationState) {
case ANIMATION_STATE_FADING_IN:
mShowHideAnimator.cancel();
// fall through
case ANIMATION_STATE_IN:
mAnimationState = ANIMATION_STATE_FADING_OUT;
mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 0);
mShowHideAnimator.setDuration(duration);
mShowHideAnimator.start();
break;
}
}
private void cancelHide() {
mRecyclerView.removeCallbacks(mHideRunnable);
}
private void resetHideDelay(int delay) {
cancelHide();
mRecyclerView.postDelayed(mHideRunnable, delay);
}
@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
if (mRecyclerViewWidth != mRecyclerView.getWidth()
|| mRecyclerViewHeight != mRecyclerView.getHeight()) {
mRecyclerViewWidth = mRecyclerView.getWidth();
mRecyclerViewHeight = mRecyclerView.getHeight();
// This is due to the different events ordering when keyboard is opened or
// retracted vs rotate. Hence to avoid corner cases we just disable the
// scroller when size changed, and wait until the scroll position is recomputed
// before showing it back.
setState(STATE_HIDDEN);
return;
}
if (mAnimationState != ANIMATION_STATE_OUT) {
if (mNeedVerticalScrollbar) {
drawVerticalScrollbar(canvas);
}
if (mNeedHorizontalScrollbar) {
drawHorizontalScrollbar(canvas);
}
}
}
private void drawVerticalScrollbar(Canvas canvas) {
int viewWidth = mRecyclerViewWidth;
int left = viewWidth - mVerticalThumbWidth;
int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2;
mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight);
mVerticalTrackDrawable
.setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight);
if (isLayoutRTL()) {
mVerticalTrackDrawable.draw(canvas);
canvas.translate(mVerticalThumbWidth, top);
canvas.scale(-1, 1);
mVerticalThumbDrawable.draw(canvas);
canvas.scale(-1, 1);
canvas.translate(-mVerticalThumbWidth, -top);
} else {
canvas.translate(left, 0);
mVerticalTrackDrawable.draw(canvas);
canvas.translate(0, top);
mVerticalThumbDrawable.draw(canvas);
canvas.translate(-left, -top);
}
}
private void drawHorizontalScrollbar(Canvas canvas) {
int viewHeight = mRecyclerViewHeight;
int top = viewHeight - mHorizontalThumbHeight;
int left = mHorizontalThumbCenterX - mHorizontalThumbWidth / 2;
mHorizontalThumbDrawable.setBounds(0, 0, mHorizontalThumbWidth, mHorizontalThumbHeight);
mHorizontalTrackDrawable
.setBounds(0, 0, mRecyclerViewWidth, mHorizontalTrackHeight);
canvas.translate(0, top);
mHorizontalTrackDrawable.draw(canvas);
canvas.translate(left, 0);
mHorizontalThumbDrawable.draw(canvas);
canvas.translate(-left, -top);
}
/**
* Notify the scroller of external change of the scroll, e.g. through dragging or flinging on
* the view itself.
*
* @param offsetX The new scroll X offset.
* @param offsetY The new scroll Y offset.
*/
void updateScrollPosition(int offsetX, int offsetY) {
int verticalContentLength = mRecyclerView.computeVerticalScrollRange();
int verticalVisibleLength = mRecyclerViewHeight;
mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0
&& mRecyclerViewHeight >= mScrollbarMinimumRange;
int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange();
int horizontalVisibleLength = mRecyclerViewWidth;
mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0
&& mRecyclerViewWidth >= mScrollbarMinimumRange;
if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) {
if (mState != STATE_HIDDEN) {
setState(STATE_HIDDEN);
}
return;
}
if (mNeedVerticalScrollbar) {
float middleScreenPos = offsetY + verticalVisibleLength / 2.0f;
mVerticalThumbCenterY =
(int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength);
mVerticalThumbHeight = Math.min(verticalVisibleLength,
(verticalVisibleLength * verticalVisibleLength) / verticalContentLength);
}
if (mNeedHorizontalScrollbar) {
float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f;
mHorizontalThumbCenterX =
(int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength);
mHorizontalThumbWidth = Math.min(horizontalVisibleLength,
(horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength);
}
if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) {
setState(STATE_VISIBLE);
}
}
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent ev) {
final boolean handled;
if (mState == STATE_VISIBLE) {
boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY());
boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY());
if (ev.getAction() == MotionEvent.ACTION_DOWN
&& (insideVerticalThumb || insideHorizontalThumb)) {
if (insideHorizontalThumb) {
mDragState = DRAG_X;
mHorizontalDragX = (int) ev.getX();
} else if (insideVerticalThumb) {
mDragState = DRAG_Y;
mVerticalDragY = (int) ev.getY();
}
setState(STATE_DRAGGING);
handled = true;
} else {
handled = false;
}
} else if (mState == STATE_DRAGGING) {
handled = true;
} else {
handled = false;
}
return handled;
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent me) {
if (mState == STATE_HIDDEN) {
return;
}
if (me.getAction() == MotionEvent.ACTION_DOWN) {
boolean insideVerticalThumb = isPointInsideVerticalThumb(me.getX(), me.getY());
boolean insideHorizontalThumb = isPointInsideHorizontalThumb(me.getX(), me.getY());
if (insideVerticalThumb || insideHorizontalThumb) {
if (insideHorizontalThumb) {
mDragState = DRAG_X;
mHorizontalDragX = (int) me.getX();
} else if (insideVerticalThumb) {
mDragState = DRAG_Y;
mVerticalDragY = (int) me.getY();
}
setState(STATE_DRAGGING);
}
} else if (me.getAction() == MotionEvent.ACTION_UP && mState == STATE_DRAGGING) {
mVerticalDragY = 0;
mHorizontalDragX = 0;
setState(STATE_VISIBLE);
mDragState = DRAG_NONE;
} else if (me.getAction() == MotionEvent.ACTION_MOVE && mState == STATE_DRAGGING) {
show();
if (mDragState == DRAG_X) {
horizontalScrollTo(me.getX());
}
if (mDragState == DRAG_Y) {
verticalScrollTo(me.getY());
}
}
}
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
private void verticalScrollTo(float y) {
final int[] scrollbarRange = getVerticalRange();
y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y));
if (Math.abs(mVerticalThumbCenterY - y) < 2) {
return;
}
int scrollingBy = scrollTo(mVerticalDragY, y, scrollbarRange,
mRecyclerView.computeVerticalScrollRange(),
mRecyclerView.computeVerticalScrollOffset(), mRecyclerViewHeight);
if (scrollingBy != 0) {
mRecyclerView.scrollBy(0, scrollingBy);
}
mVerticalDragY = y;
}
private void horizontalScrollTo(float x) {
final int[] scrollbarRange = getHorizontalRange();
x = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], x));
if (Math.abs(mHorizontalThumbCenterX - x) < 2) {
return;
}
int scrollingBy = scrollTo(mHorizontalDragX, x, scrollbarRange,
mRecyclerView.computeHorizontalScrollRange(),
mRecyclerView.computeHorizontalScrollOffset(), mRecyclerViewWidth);
if (scrollingBy != 0) {
mRecyclerView.scrollBy(scrollingBy, 0);
}
mHorizontalDragX = x;
}
private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange,
int scrollOffset, int viewLength) {
int scrollbarLength = scrollbarRange[1] - scrollbarRange[0];
if (scrollbarLength == 0) {
return 0;
}
float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength);
int totalPossibleOffset = scrollRange - viewLength;
int scrollingBy = (int) (percentage * totalPossibleOffset);
int absoluteOffset = scrollOffset + scrollingBy;
if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) {
return scrollingBy;
} else {
return 0;
}
}
@VisibleForTesting
boolean isPointInsideVerticalThumb(float x, float y) {
return (isLayoutRTL() ? x <= mVerticalThumbWidth
: x >= mRecyclerViewWidth - mVerticalThumbWidth)
&& y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2
&& y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2;
}
@VisibleForTesting
boolean isPointInsideHorizontalThumb(float x, float y) {
return (y >= mRecyclerViewHeight - mHorizontalThumbHeight)
&& x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2
&& x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2;
}
@VisibleForTesting
Drawable getHorizontalTrackDrawable() {
return mHorizontalTrackDrawable;
}
@VisibleForTesting
Drawable getHorizontalThumbDrawable() {
return mHorizontalThumbDrawable;
}
@VisibleForTesting
Drawable getVerticalTrackDrawable() {
return mVerticalTrackDrawable;
}
@VisibleForTesting
Drawable getVerticalThumbDrawable() {
return mVerticalThumbDrawable;
}
/**
* Gets the (min, max) vertical positions of the vertical scroll bar.
*/
private int[] getVerticalRange() {
mVerticalRange[0] = mMargin;
mVerticalRange[1] = mRecyclerViewHeight - mMargin;
return mVerticalRange;
}
/**
* Gets the (min, max) horizontal positions of the horizontal scroll bar.
*/
private int[] getHorizontalRange() {
mHorizontalRange[0] = mMargin;
mHorizontalRange[1] = mRecyclerViewWidth - mMargin;
return mHorizontalRange;
}
private class AnimatorListener extends AnimatorListenerAdapter {
private boolean mCanceled = false;
AnimatorListener() {
}
@Override
public void onAnimationEnd(Animator animation) {
// Cancel is always followed by a new directive, so don't update state.
if (mCanceled) {
mCanceled = false;
return;
}
if ((float) mShowHideAnimator.getAnimatedValue() == 0) {
mAnimationState = ANIMATION_STATE_OUT;
setState(STATE_HIDDEN);
} else {
mAnimationState = ANIMATION_STATE_IN;
requestRedraw();
}
}
@Override
public void onAnimationCancel(Animator animation) {
mCanceled = true;
}
}
private class AnimatorUpdater implements AnimatorUpdateListener {
AnimatorUpdater() {
}
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue()));
mVerticalThumbDrawable.setAlpha(alpha);
mVerticalTrackDrawable.setAlpha(alpha);
requestRedraw();
}
}
}

@ -0,0 +1,407 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.core.os.TraceCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.TimeUnit;
final class GapWorker implements Runnable {
static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
long mPostTimeNs;
long mFrameIntervalNs;
static class Task {
public boolean immediate;
public int viewVelocity;
public int distanceToItem;
public RecyclerView view;
public int position;
public void clear() {
immediate = false;
viewVelocity = 0;
distanceToItem = 0;
view = null;
position = 0;
}
}
/**
* Temporary storage for prefetch Tasks that execute in {@link #prefetch(long)}. Task objects
* are pooled in the ArrayList, and never removed to avoid allocations, but always cleared
* in between calls.
*/
private ArrayList<Task> mTasks = new ArrayList<>();
/**
* Prefetch information associated with a specific RecyclerView.
*/
@SuppressLint("VisibleForTests")
static class LayoutPrefetchRegistryImpl
implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
int mPrefetchDx;
int mPrefetchDy;
int[] mPrefetchArray;
int mCount;
void setPrefetchVector(int dx, int dy) {
mPrefetchDx = dx;
mPrefetchDy = dy;
}
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
mCount = 0;
if (mPrefetchArray != null) {
Arrays.fill(mPrefetchArray, -1);
}
final RecyclerView.LayoutManager layout = view.mLayout;
if (view.mAdapter != null
&& layout != null
&& layout.isItemPrefetchEnabled()) {
if (nested) {
// nested prefetch, only if no adapter updates pending. Note: we don't query
// view.hasPendingAdapterUpdates(), as first layout may not have occurred
if (!view.mAdapterHelper.hasPendingUpdates()) {
layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
}
} else {
// momentum based prefetch, only if we trust current child/adapter state
if (!view.hasPendingAdapterUpdates()) {
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
view.mState, this);
}
}
if (mCount > layout.mPrefetchMaxCountObserved) {
layout.mPrefetchMaxCountObserved = mCount;
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
view.mRecycler.updateViewCacheSize();
}
}
}
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
if (layoutPosition < 0) {
throw new IllegalArgumentException("Layout positions must be non-negative");
}
if (pixelDistance < 0) {
throw new IllegalArgumentException("Pixel distance must be non-negative");
}
// allocate or expand array as needed, doubling when needed
final int storagePosition = mCount * 2;
if (mPrefetchArray == null) {
mPrefetchArray = new int[4];
Arrays.fill(mPrefetchArray, -1);
} else if (storagePosition >= mPrefetchArray.length) {
final int[] oldArray = mPrefetchArray;
mPrefetchArray = new int[storagePosition * 2];
System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
}
// add position
mPrefetchArray[storagePosition] = layoutPosition;
mPrefetchArray[storagePosition + 1] = pixelDistance;
mCount++;
}
boolean lastPrefetchIncludedPosition(int position) {
if (mPrefetchArray != null) {
final int count = mCount * 2;
for (int i = 0; i < count; i += 2) {
if (mPrefetchArray[i] == position) return true;
}
}
return false;
}
/**
* Called when prefetch indices are no longer valid for cache prioritization.
*/
void clearPrefetchPositions() {
if (mPrefetchArray != null) {
Arrays.fill(mPrefetchArray, -1);
}
mCount = 0;
}
}
public void add(RecyclerView recyclerView) {
if (RecyclerView.sDebugAssertionsEnabled && mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("RecyclerView already present in worker list!");
}
mRecyclerViews.add(recyclerView);
}
public void remove(RecyclerView recyclerView) {
boolean removeSuccess = mRecyclerViews.remove(recyclerView);
if (RecyclerView.sDebugAssertionsEnabled && !removeSuccess) {
throw new IllegalStateException("RecyclerView removal failed!");
}
}
/**
* Schedule a prefetch immediately after the current traversal.
*/
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
if (recyclerView.isAttachedToWindow()) {
if (RecyclerView.sDebugAssertionsEnabled && !mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("attempting to post unregistered view!");
}
if (mPostTimeNs == 0) {
mPostTimeNs = recyclerView.getNanoTime();
recyclerView.post(this);
}
}
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
static Comparator<Task> sTaskComparator = new Comparator<Task>() {
@Override
public int compare(Task lhs, Task rhs) {
// first, prioritize non-cleared tasks
if ((lhs.view == null) != (rhs.view == null)) {
return lhs.view == null ? 1 : -1;
}
// then prioritize immediate
if (lhs.immediate != rhs.immediate) {
return lhs.immediate ? -1 : 1;
}
// then prioritize _highest_ view velocity
int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
if (deltaViewVelocity != 0) return deltaViewVelocity;
// then prioritize _lowest_ distance to item
int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
if (deltaDistanceToItem != 0) return deltaDistanceToItem;
return 0;
}
};
private void buildTaskList() {
// Update PrefetchRegistry in each view
final int viewCount = mRecyclerViews.size();
int totalTaskCount = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
totalTaskCount += view.mPrefetchRegistry.mCount;
}
}
// Populate task list from prefetch data...
mTasks.ensureCapacity(totalTaskCount);
int totalTaskIndex = 0;
for (int i = 0; i < viewCount; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() != View.VISIBLE) {
// Invisible view, don't bother prefetching
continue;
}
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
+ Math.abs(prefetchRegistry.mPrefetchDy);
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
final Task task;
if (totalTaskIndex >= mTasks.size()) {
task = new Task();
mTasks.add(task);
} else {
task = mTasks.get(totalTaskIndex);
}
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
task.immediate = distanceToItem <= viewVelocity;
task.viewVelocity = viewVelocity;
task.distanceToItem = distanceToItem;
task.view = view;
task.position = prefetchRegistry.mPrefetchArray[j];
totalTaskIndex++;
}
}
// ... and priority sort
Collections.sort(mTasks, sTaskComparator);
}
static boolean isPrefetchPositionAttached(RecyclerView view, int position) {
final int childCount = view.mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
View attachedView = view.mChildHelper.getUnfilteredChildAt(i);
RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView);
// Note: can use mPosition here because adapter doesn't have pending updates
if (holder.mPosition == position && !holder.isInvalid()) {
return true;
}
}
return false;
}
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
int position, long deadlineNs) {
if (isPrefetchPositionAttached(view, position)) {
// don't attempt to prefetch attached views
return null;
}
RecyclerView.Recycler recycler = view.mRecycler;
RecyclerView.ViewHolder holder;
try {
view.onEnterLayoutOrScroll();
holder = recycler.tryGetViewHolderForPositionByDeadline(
position, false, deadlineNs);
if (holder != null) {
if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
recycler.recycleView(holder.itemView);
} else {
// Didn't bind, so we can't cache the view, but it will stay in the pool until
// next prefetch/traversal. If a View fails to bind, it means we didn't have
// enough time prior to the deadline (and won't for other instances of this
// type, during this GapWorker prefetch pass).
recycler.addViewHolderToRecycledViewPool(holder, false);
}
}
} finally {
view.onExitLayoutOrScroll(false);
}
return holder;
}
private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
long deadlineNs) {
if (innerView == null) {
return;
}
if (innerView.mDataSetHasChangedAfterLayout
&& innerView.mChildHelper.getUnfilteredChildCount() != 0) {
// RecyclerView has new data, but old attached views. Clear everything, so that
// we can prefetch without partially stale data.
innerView.removeAndRecycleViews();
}
// do nested prefetch!
final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
if (innerPrefetchRegistry.mCount != 0) {
try {
TraceCompat.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
// Note that we ignore immediate flag for inner items because
// we have lower confidence they're needed next frame.
final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
}
} finally {
TraceCompat.endSection();
}
}
}
private void flushTaskWithDeadline(Task task, long deadlineNs) {
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
if (holder != null
&& holder.mNestedRecyclerView != null
&& holder.isBound()
&& !holder.isInvalid()) {
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
}
}
private void flushTasksWithDeadline(long deadlineNs) {
for (int i = 0; i < mTasks.size(); i++) {
final Task task = mTasks.get(i);
if (task.view == null) {
break; // done with populated tasks
}
flushTaskWithDeadline(task, deadlineNs);
task.clear();
}
}
void prefetch(long deadlineNs) {
buildTaskList();
flushTasksWithDeadline(deadlineNs);
}
@Override
public void run() {
try {
TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
if (mRecyclerViews.isEmpty()) {
// abort - no work to do
return;
}
// Query most recent vsync so we can predict next one. Note that drawing time not yet
// valid in animation/input callbacks, so query it here to be safe.
final int size = mRecyclerViews.size();
long latestFrameVsyncMs = 0;
for (int i = 0; i < size; i++) {
RecyclerView view = mRecyclerViews.get(i);
if (view.getWindowVisibility() == View.VISIBLE) {
latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
}
}
if (latestFrameVsyncMs == 0) {
// abort - either no views visible, or couldn't get last vsync for estimating next
return;
}
long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
prefetch(nextFrameNs);
// TODO: consider rescheduling self, if there's more work to do
} finally {
mPostTimeNs = 0;
TraceCompat.endSection();
}
}
}

@ -0,0 +1,68 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.view.View;
/**
* Utility class for {@link ItemTouchHelper} which handles item transformations for different
* API versions.
* <p/>
* This class has methods that map to {@link ItemTouchHelper.Callback}'s drawing methods. Default
* implementations in {@link ItemTouchHelper.Callback} call these methods with
* {@link RecyclerView.ViewHolder#itemView} and {@link ItemTouchUIUtil} makes necessary changes
* on the View depending on the API level. You can access the instance of {@link ItemTouchUIUtil}
* via {@link ItemTouchHelper.Callback#getDefaultUIUtil()} and call its methods with the children
* of ViewHolder that you want to apply default effects.
*
* @see ItemTouchHelper.Callback#getDefaultUIUtil()
*/
public interface ItemTouchUIUtil {
/**
* The default implementation for {@link ItemTouchHelper.Callback#onChildDraw(Canvas,
* RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive);
/**
* The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas,
* RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive);
/**
* The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView,
* RecyclerView.ViewHolder)}
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void clearView(View view);
/**
* The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged(
* RecyclerView.ViewHolder, int)}
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void onSelected(View view);
}

@ -0,0 +1,105 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.graphics.Canvas;
import android.os.Build;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.R;
/**
* Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
* public API, which is not desired in this case.
*/
class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
@Override
public void onDraw(
@NonNull Canvas c,
@NonNull RecyclerView recyclerView,
@NonNull View view,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive
) {
if (Build.VERSION.SDK_INT >= 21) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);
float newElevation = 1f + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
}
view.setTranslationX(dX);
view.setTranslationY(dY);
}
private static float findMaxElevation(RecyclerView recyclerView, View itemView) {
final int childCount = recyclerView.getChildCount();
float max = 0;
for (int i = 0; i < childCount; i++) {
final View child = recyclerView.getChildAt(i);
if (child == itemView) {
continue;
}
final float elevation = ViewCompat.getElevation(child);
if (elevation > max) {
max = elevation;
}
}
return max;
}
@Override
public void onDrawOver(
@NonNull Canvas c,
@NonNull RecyclerView recyclerView,
@NonNull View view,
float dX,
float dY,
int actionState,
boolean isCurrentlyActive
) {
}
@Override
public void clearView(@NonNull View view) {
if (Build.VERSION.SDK_INT >= 21) {
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
if (tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}
view.setTag(R.id.item_touch_helper_previous_elevation, null);
}
view.setTranslationX(0f);
view.setTranslationY(0f);
}
@Override
public void onSelected(@NonNull View view) {
}
}

@ -0,0 +1,114 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.view.View;
/**
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
* space.
*/
class LayoutState {
static final int LAYOUT_START = -1;
static final int LAYOUT_END = 1;
static final int INVALID_LAYOUT = Integer.MIN_VALUE;
static final int ITEM_DIRECTION_HEAD = -1;
static final int ITEM_DIRECTION_TAIL = 1;
/**
* We may not want to recycle children in some cases (e.g. layout)
*/
boolean mRecycle = true;
/**
* Number of pixels that we should fill, in the layout direction.
*/
int mAvailable;
/**
* Current position on the adapter to get the next item.
*/
int mCurrentPosition;
/**
* Defines the direction in which the data adapter is traversed.
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
*/
int mItemDirection;
/**
* Defines the direction in which the layout is filled.
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
*/
int mLayoutDirection;
/**
* This is the target pixel closest to the start of the layout that we are trying to fill
*/
int mStartLine = 0;
/**
* This is the target pixel closest to the end of the layout that we are trying to fill
*/
int mEndLine = 0;
/**
* If true, layout should stop if a focusable view is added
*/
boolean mStopInFocusable;
/**
* If the content is not wrapped with any value
*/
boolean mInfinite;
/**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
/**
* Gets the view for the next element that we should render.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should render.
*/
View next(RecyclerView.Recycler recycler) {
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
@Override
public String toString() {
return "LayoutState{"
+ "mAvailable=" + mAvailable
+ ", mCurrentPosition=" + mCurrentPosition
+ ", mItemDirection=" + mItemDirection
+ ", mLayoutDirection=" + mLayoutDirection
+ ", mStartLine=" + mStartLine
+ ", mEndLine=" + mEndLine
+ '}';
}
}

@ -0,0 +1,360 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
/**
* {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
* the target position becomes a child of the RecyclerView and then uses a
* {@link DecelerateInterpolator} to slowly approach to target position.
* <p>
* If the {@link RecyclerView.LayoutManager} you are using does not implement the
* {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the
* {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with
* the support library implement this interface.
*/
public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
private static final boolean DEBUG = false;
private static final float MILLISECONDS_PER_INCH = 25f;
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
/**
* Align child view's left or top with parent view's left or top
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_START = -1;
/**
* Align child view's right or bottom with parent view's right or bottom
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_END = 1;
/**
* <p>Decides if the child should be snapped from start or end, depending on where it
* currently is in relation to its parent.</p>
* <p>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}</p>
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(android.view.View, int)
* @see #calculateDyToMakeVisible(android.view.View, int)
*/
public static final int SNAP_TO_ANY = 0;
// Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
// view is not laid out until interim target position is reached, we can detect the case before
// scrolling slows down and reschedule another interim target scroll
private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected PointF mTargetVector;
private final DisplayMetrics mDisplayMetrics;
private boolean mHasCalculatedMillisPerPixel = false;
private float mMillisPerPixel;
// Temporary variables to keep track of the interim scroll target. These values do not
// point to a real item position, rather point to an estimated location pixels.
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public LinearSmoothScroller(Context context) {
mDisplayMetrics = context.getResources().getDisplayMetrics();
}
/**
* {@inheritDoc}
*/
@Override
protected void onStart() {
}
/**
* {@inheritDoc}
*/
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
// TODO(b/72745539): Is there ever a time when onSeekTargetStep should be called when
// getChildCount returns 0? Should this logic be extracted out of this method such that
// this method is not called if getChildCount() returns 0?
if (getChildCount() == 0) {
stop();
return;
}
//noinspection PointlessBooleanExpression
if (DEBUG && mTargetVector != null
&& (mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0)) {
throw new IllegalStateException("Scroll happened in the opposite direction"
+ " of the target. Some calculations are wrong");
}
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
updateActionForInterimTarget(action);
} // everything is valid, keep going
}
/**
* {@inheritDoc}
*/
@Override
protected void onStop() {
mInterimTargetDx = mInterimTargetDy = 0;
mTargetVector = null;
}
/**
* Calculates the scroll speed.
*
* <p>By default, LinearSmoothScroller assumes this method always returns the same value and
* caches the result of calling it.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
private float getSpeedPerPixel() {
if (!mHasCalculatedMillisPerPixel) {
mMillisPerPixel = calculateSpeedPerPixel(mDisplayMetrics);
mHasCalculatedMillisPerPixel = true;
}
return mMillisPerPixel;
}
/**
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
* DecelerateInterpolator looks smooth.</p>
*
* @param dx Distance to scroll
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
* from LinearInterpolation
*/
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * getSpeedPerPixel());
}
/**
* When scrolling towards a child view, this method defines whether we should align the left
* or the right edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getHorizontalSnapPreference() {
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When scrolling towards a child view, this method defines whether we should align the top
* or the bottom edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getVerticalSnapPreference() {
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When the target scroll position is not a child of the RecyclerView, this method calculates
* a direction vector towards that child and triggers a smooth scroll.
*
* @see #computeScrollVectorForPosition(int)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
protected void updateActionForInterimTarget(Action action) {
// find an interim target position
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
final int target = getTargetPosition();
action.jumpTo(target);
stop();
return;
}
normalize(scrollVector);
mTargetVector = scrollVector;
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
// won't actually scroll more than what we need.
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
(int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
}
private int clampApplyScroll(int tmpDt, int dt) {
final int before = tmpDt;
tmpDt -= dt;
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
return 0;
}
return tmpDt;
}
/**
* Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
* {@link #calculateDyToMakeVisible(android.view.View, int)}
*/
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
snapPreference) {
switch (snapPreference) {
case SNAP_TO_START:
return boxStart - viewStart;
case SNAP_TO_END:
return boxEnd - viewEnd;
case SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
default:
throw new IllegalArgumentException("snap preference should be one of the"
+ " constants defined in SmoothScroller, starting with SNAP_");
}
return 0;
}
/**
* Calculates the vertical scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_ANY}.
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}
/**
* Calculates the horizontal scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}
}

@ -0,0 +1,276 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.graphics.PointF;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Implementation of the {@link SnapHelper} supporting snapping in either vertical or horizontal
* orientation.
* <p>
* The implementation will snap the center of the target child view to the center of
* the attached {@link RecyclerView}. If you intend to change this behavior then override
* {@link SnapHelper#calculateDistanceToFinalSnap}.
*/
public class LinearSnapHelper extends SnapHelper {
private static final float INVALID_DISTANCE = 1f;
// Orientation helpers are lazily created per LayoutManager.
@Nullable
private OrientationHelper mVerticalHelper;
@Nullable
private OrientationHelper mHorizontalHelper;
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
// cannot get a vector for the given position.
return RecyclerView.NO_POSITION;
}
int vDeltaJump, hDeltaJump;
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump;
}
} else {
hDeltaJump = 0;
}
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY);
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump;
}
} else {
vDeltaJump = 0;
}
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) {
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
return childCenter - containerCenter;
}
/**
* Estimates a position to which SnapHelper will try to scroll to in response to a fling.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The {@link OrientationHelper} that is created from the LayoutManager.
* @param velocityX The velocity on the x axis.
* @param velocityY The velocity on the y axis.
*
* @return The diff between the target scroll position and the current position.
*/
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
int[] distances = calculateScrollDistance(velocityX, velocityY);
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <= 0) {
return 0;
}
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
return (int) Math.round(distance / distancePerChild);
}
/**
* Return the child view that is currently closest to the center of this parent.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
*
* @return the child view that is currently closest to the center of this parent.
*/
@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
/** if child center is closer than previous closest, set it as closest **/
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
/**
* Computes an average pixel value to pass a single child.
* <p>
* Returns a negative value if it cannot be calculated.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The relevant {@link OrientationHelper} for the attached
* {@link RecyclerView.LayoutManager}.
*
* @return A float value that is the average number of pixels needed to scroll by one view in
* the relevant direction.
*/
private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
View minPosView = null;
View maxPosView = null;
int minPos = Integer.MAX_VALUE;
int maxPos = Integer.MIN_VALUE;
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return INVALID_DISTANCE;
}
for (int i = 0; i < childCount; i++) {
View child = layoutManager.getChildAt(i);
final int pos = layoutManager.getPosition(child);
if (pos == RecyclerView.NO_POSITION) {
continue;
}
if (pos < minPos) {
minPos = pos;
minPosView = child;
}
if (pos > maxPos) {
maxPos = pos;
maxPosView = child;
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE;
}
int start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView));
int end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView));
int distance = end - start;
if (distance == 0) {
return INVALID_DISTANCE;
}
return 1f * distance / ((maxPos - minPos) + 1);
}
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}

@ -0,0 +1,190 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
/**
* {@link RecyclerView.Adapter RecyclerView.Adapter} base class for presenting List data in a
* {@link RecyclerView}, including computing diffs between Lists on a background thread.
* <p>
* This class is a convenience wrapper around {@link AsyncListDiffer} that implements Adapter common
* default behavior for item access and counting.
* <p>
* While using a LiveData&lt;List> is an easy way to provide data to the adapter, it isn't required
* - you can use {@link #submitList(List)} when new lists are available.
* <p>
* A complete usage pattern with Room would look like this:
* <pre>
* {@literal @}Dao
* interface UserDao {
* {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
* public abstract LiveData&lt;List&lt;User>> usersByLastName();
* }
*
* class MyViewModel extends ViewModel {
* public final LiveData&lt;List&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
* usersList = userDao.usersByLastName();
* }
* }
*
* class MyActivity extends AppCompatActivity {
* {@literal @}Override
* public void onCreate(Bundle savedState) {
* super.onCreate(savedState);
* MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
* RecyclerView recyclerView = findViewById(R.id.user_list);
* UserAdapter&lt;User> adapter = new UserAdapter();
* viewModel.usersList.observe(this, list -> adapter.submitList(list));
* recyclerView.setAdapter(adapter);
* }
* }
*
* class UserAdapter extends ListAdapter&lt;User, UserViewHolder> {
* public UserAdapter() {
* super(User.DIFF_CALLBACK);
* }
* {@literal @}Override
* public void onBindViewHolder(UserViewHolder holder, int position) {
* holder.bindTo(getItem(position));
* }
* public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK =
* new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // User properties may have changed if reloaded from the DB, but ID is fixed
* return oldUser.getId() == newUser.getId();
* }
* {@literal @}Override
* public boolean areContentsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
* // NOTE: if you use equals, your object must properly override Object#equals()
* // Incorrectly returning false here will result in too many animations.
* return oldUser.equals(newUser);
* }
* }
* }</pre>
*
* Advanced users that wish for more control over adapter behavior, or to provide a specific base
* class should refer to {@link AsyncListDiffer}, which provides custom mapping from diff events
* to adapter positions.
*
* @param <T> Type of the Lists this Adapter will receive.
* @param <VH> A class that extends ViewHolder that will be used by the adapter.
*/
public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<VH> {
final AsyncListDiffer<T> mDiffer;
private final AsyncListDiffer.ListListener<T> mListener =
new AsyncListDiffer.ListListener<T>() {
@Override
public void onCurrentListChanged(
@NonNull List<T> previousList, @NonNull List<T> currentList) {
ListAdapter.this.onCurrentListChanged(previousList, currentList);
}
};
@SuppressWarnings("unused")
protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
mDiffer.addListListener(mListener);
}
@SuppressWarnings("unused")
protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addListListener(mListener);
}
/**
* Submits a new list to be diffed, and displayed.
* <p>
* If a list is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
*
* @param list The new list to be displayed.
*/
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list);
}
/**
* Set the new list to be displayed.
* <p>
* If a List is already being displayed, a diff will be computed on a background thread, which
* will dispatch Adapter.notifyItem events on the main thread.
* <p>
* The commit callback can be used to know when the List is committed, but note that it
* may not be executed. If List B is submitted immediately after List A, and is
* committed directly, the callback associated with List A will not be run.
*
* @param list The new list to be displayed.
* @param commitCallback Optional runnable that is executed when the List is committed, if
* it is committed.
*/
public void submitList(@Nullable List<T> list, @Nullable final Runnable commitCallback) {
mDiffer.submitList(list, commitCallback);
}
protected T getItem(int position) {
return mDiffer.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return mDiffer.getCurrentList().size();
}
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
* <p>
* If a <code>null</code> List, or no List has been submitted, an empty list will be returned.
* <p>
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return The list currently being displayed.
*
* @see #onCurrentListChanged(List, List)
*/
@NonNull
public List<T> getCurrentList() {
return mDiffer.getCurrentList();
}
/**
* Called when the current List is updated.
* <p>
* If a <code>null</code> List is passed to {@link #submitList(List)}, or no List has been
* submitted, the current List is represented as an empty List.
*
* @param previousList List that was displayed previously.
* @param currentList new List being displayed, will be empty if {@code null} was passed to
* {@link #submitList(List)}.
*
* @see #getCurrentList()
*/
public void onCurrentListChanged(@NonNull List<T> previousList, @NonNull List<T> currentList) {
}
}

@ -0,0 +1,57 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import androidx.annotation.Nullable;
/**
* An interface that can receive Update operations that are applied to a list.
* <p>
* This class can be used together with DiffUtil to detect changes between two lists.
*/
public interface ListUpdateCallback {
/**
* Called when {@code count} number of items are inserted at the given position.
*
* @param position The position of the new item.
* @param count The number of items that have been added.
*/
void onInserted(int position, int count);
/**
* Called when {@code count} number of items are removed from the given position.
*
* @param position The position of the item which has been removed.
* @param count The number of items which have been removed.
*/
void onRemoved(int position, int count);
/**
* Called when an item changes its position in the list.
*
* @param fromPosition The previous position of the item before the move.
* @param toPosition The new position of the item.
*/
void onMoved(int fromPosition, int toPosition);
/**
* Called when {@code count} number of items are updated at the given position.
*
* @param position The position of the item which has been updated.
* @param count The number of items which has changed.
*/
void onChanged(int position, int count, @Nullable Object payload);
}

@ -0,0 +1,294 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
class MessageThreadUtil<T> implements ThreadUtil<T> {
@Override
public MainThreadCallback<T> getMainThreadProxy(final MainThreadCallback<T> callback) {
return new MainThreadCallback<T>() {
final MessageQueue mQueue = new MessageQueue();
final private Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
static final int UPDATE_ITEM_COUNT = 1;
static final int ADD_TILE = 2;
static final int REMOVE_TILE = 3;
@Override
public void updateItemCount(int generation, int itemCount) {
sendMessage(SyncQueueItem.obtainMessage(UPDATE_ITEM_COUNT, generation, itemCount));
}
@Override
public void addTile(int generation, TileList.Tile<T> tile) {
sendMessage(SyncQueueItem.obtainMessage(ADD_TILE, generation, tile));
}
@Override
public void removeTile(int generation, int position) {
sendMessage(SyncQueueItem.obtainMessage(REMOVE_TILE, generation, position));
}
private void sendMessage(SyncQueueItem msg) {
mQueue.sendMessage(msg);
mMainThreadHandler.post(mMainThreadRunnable);
}
private Runnable mMainThreadRunnable = new Runnable() {
@Override
public void run() {
SyncQueueItem msg = mQueue.next();
while (msg != null) {
switch (msg.what) {
case UPDATE_ITEM_COUNT:
callback.updateItemCount(msg.arg1, msg.arg2);
break;
case ADD_TILE:
@SuppressWarnings("unchecked")
TileList.Tile<T> tile = (TileList.Tile<T>) msg.data;
callback.addTile(msg.arg1, tile);
break;
case REMOVE_TILE:
callback.removeTile(msg.arg1, msg.arg2);
break;
default:
Log.e("ThreadUtil", "Unsupported message, what=" + msg.what);
}
msg = mQueue.next();
}
}
};
};
}
@SuppressWarnings("deprecation") /* AsyncTask */
@Override
public BackgroundCallback<T> getBackgroundProxy(final BackgroundCallback<T> callback) {
return new BackgroundCallback<T>() {
final MessageQueue mQueue = new MessageQueue();
private final Executor mExecutor = android.os.AsyncTask.THREAD_POOL_EXECUTOR;
AtomicBoolean mBackgroundRunning = new AtomicBoolean(false);
static final int REFRESH = 1;
static final int UPDATE_RANGE = 2;
static final int LOAD_TILE = 3;
static final int RECYCLE_TILE = 4;
@Override
public void refresh(int generation) {
sendMessageAtFrontOfQueue(SyncQueueItem.obtainMessage(REFRESH, generation, null));
}
@Override
public void updateRange(int rangeStart, int rangeEnd,
int extRangeStart, int extRangeEnd, int scrollHint) {
sendMessageAtFrontOfQueue(SyncQueueItem.obtainMessage(UPDATE_RANGE,
rangeStart, rangeEnd, extRangeStart, extRangeEnd, scrollHint, null));
}
@Override
public void loadTile(int position, int scrollHint) {
sendMessage(SyncQueueItem.obtainMessage(LOAD_TILE, position, scrollHint));
}
@Override
public void recycleTile(TileList.Tile<T> tile) {
sendMessage(SyncQueueItem.obtainMessage(RECYCLE_TILE, 0, tile));
}
private void sendMessage(SyncQueueItem msg) {
mQueue.sendMessage(msg);
maybeExecuteBackgroundRunnable();
}
private void sendMessageAtFrontOfQueue(SyncQueueItem msg) {
mQueue.sendMessageAtFrontOfQueue(msg);
maybeExecuteBackgroundRunnable();
}
private void maybeExecuteBackgroundRunnable() {
if (mBackgroundRunning.compareAndSet(false, true)) {
mExecutor.execute(mBackgroundRunnable);
}
}
private Runnable mBackgroundRunnable = new Runnable() {
@Override
public void run() {
while (true) {
SyncQueueItem msg = mQueue.next();
if (msg == null) {
break;
}
switch (msg.what) {
case REFRESH:
mQueue.removeMessages(REFRESH);
callback.refresh(msg.arg1);
break;
case UPDATE_RANGE:
mQueue.removeMessages(UPDATE_RANGE);
mQueue.removeMessages(LOAD_TILE);
callback.updateRange(
msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5);
break;
case LOAD_TILE:
callback.loadTile(msg.arg1, msg.arg2);
break;
case RECYCLE_TILE:
@SuppressWarnings("unchecked")
TileList.Tile<T> tile = (TileList.Tile<T>) msg.data;
callback.recycleTile(tile);
break;
default:
Log.e("ThreadUtil", "Unsupported message, what=" + msg.what);
}
}
mBackgroundRunning.set(false);
}
};
};
}
/**
* Replica of android.os.Message. Unfortunately, cannot use it without a Handler and don't want
* to create a thread just for this component.
*/
static class SyncQueueItem {
private static SyncQueueItem sPool;
private static final Object sPoolLock = new Object();
SyncQueueItem next;
public int what;
public int arg1;
public int arg2;
public int arg3;
public int arg4;
public int arg5;
public Object data;
void recycle() {
next = null;
what = arg1 = arg2 = arg3 = arg4 = arg5 = 0;
data = null;
synchronized (sPoolLock) {
if (sPool != null) {
next = sPool;
}
sPool = this;
}
}
static SyncQueueItem obtainMessage(int what, int arg1, int arg2, int arg3, int arg4,
int arg5, Object data) {
synchronized (sPoolLock) {
final SyncQueueItem item;
if (sPool == null) {
item = new SyncQueueItem();
} else {
item = sPool;
sPool = sPool.next;
item.next = null;
}
item.what = what;
item.arg1 = arg1;
item.arg2 = arg2;
item.arg3 = arg3;
item.arg4 = arg4;
item.arg5 = arg5;
item.data = data;
return item;
}
}
static SyncQueueItem obtainMessage(int what, int arg1, int arg2) {
return obtainMessage(what, arg1, arg2, 0, 0, 0, null);
}
static SyncQueueItem obtainMessage(int what, int arg1, Object data) {
return obtainMessage(what, arg1, 0, 0, 0, 0, data);
}
}
static class MessageQueue {
private SyncQueueItem mRoot;
private final Object mLock = new Object();
SyncQueueItem next() {
synchronized (mLock) {
if (mRoot == null) {
return null;
}
final SyncQueueItem next = mRoot;
mRoot = mRoot.next;
return next;
}
}
void sendMessageAtFrontOfQueue(SyncQueueItem item) {
synchronized (mLock) {
item.next = mRoot;
mRoot = item;
}
}
void sendMessage(SyncQueueItem item) {
synchronized (mLock) {
if (mRoot == null) {
mRoot = item;
return;
}
SyncQueueItem last = mRoot;
while (last.next != null) {
last = last.next;
}
last.next = item;
}
}
void removeMessages(int what) {
synchronized (mLock) {
while (mRoot != null && mRoot.what == what) {
SyncQueueItem item = mRoot;
mRoot = mRoot.next;
item.recycle();
}
if (mRoot != null) {
SyncQueueItem prev = mRoot;
SyncQueueItem item = prev.next;
while (item != null) {
SyncQueueItem next = item.next;
if (item.what == what) {
prev.next = next;
item.recycle();
} else {
prev = item;
}
item = next;
}
}
}
}
}
}

@ -0,0 +1,201 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Preconditions;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
/**
* Wrapper for each adapter in {@link ConcatAdapter}.
*/
class NestedAdapterWrapper {
@NonNull
private final ViewTypeStorage.ViewTypeLookup mViewTypeLookup;
@NonNull
private final StableIdStorage.StableIdLookup mStableIdLookup;
public final Adapter<ViewHolder> adapter;
@SuppressWarnings("WeakerAccess")
final Callback mCallback;
// we cache this value so that we can know the previous size when change happens
// this is also important as getting real size while an adapter is dispatching possibly a
// a chain of events might create inconsistencies (as it happens in DiffUtil).
// Instead, we always calculate this value based on notify events.
@SuppressWarnings("WeakerAccess")
int mCachedItemCount;
private RecyclerView.AdapterDataObserver mAdapterObserver =
new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
mCachedItemCount = adapter.getItemCount();
mCallback.onChanged(NestedAdapterWrapper.this);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
mCallback.onItemRangeChanged(
NestedAdapterWrapper.this,
positionStart,
itemCount,
null
);
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount,
@Nullable Object payload) {
mCallback.onItemRangeChanged(
NestedAdapterWrapper.this,
positionStart,
itemCount,
payload
);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mCachedItemCount += itemCount;
mCallback.onItemRangeInserted(
NestedAdapterWrapper.this,
positionStart,
itemCount);
if (mCachedItemCount > 0
&& adapter.getStateRestorationPolicy() == PREVENT_WHEN_EMPTY) {
mCallback.onStateRestorationPolicyChanged(NestedAdapterWrapper.this);
}
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mCachedItemCount -= itemCount;
mCallback.onItemRangeRemoved(
NestedAdapterWrapper.this,
positionStart,
itemCount
);
if (mCachedItemCount < 1
&& adapter.getStateRestorationPolicy() == PREVENT_WHEN_EMPTY) {
mCallback.onStateRestorationPolicyChanged(NestedAdapterWrapper.this);
}
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
Preconditions.checkArgument(itemCount == 1,
"moving more than 1 item is not supported in RecyclerView");
mCallback.onItemRangeMoved(
NestedAdapterWrapper.this,
fromPosition,
toPosition
);
}
@Override
public void onStateRestorationPolicyChanged() {
mCallback.onStateRestorationPolicyChanged(
NestedAdapterWrapper.this
);
}
};
NestedAdapterWrapper(
Adapter<ViewHolder> adapter,
final Callback callback,
ViewTypeStorage viewTypeStorage,
StableIdStorage.StableIdLookup stableIdLookup) {
this.adapter = adapter;
mCallback = callback;
mViewTypeLookup = viewTypeStorage.createViewTypeWrapper(this);
mStableIdLookup = stableIdLookup;
mCachedItemCount = this.adapter.getItemCount();
this.adapter.registerAdapterDataObserver(mAdapterObserver);
}
void dispose() {
adapter.unregisterAdapterDataObserver(mAdapterObserver);
mViewTypeLookup.dispose();
}
int getCachedItemCount() {
return mCachedItemCount;
}
int getItemViewType(int localPosition) {
return mViewTypeLookup.localToGlobal(adapter.getItemViewType(localPosition));
}
ViewHolder onCreateViewHolder(
ViewGroup parent,
int globalViewType) {
int localType = mViewTypeLookup.globalToLocal(globalViewType);
return adapter.onCreateViewHolder(parent, localType);
}
void onBindViewHolder(ViewHolder viewHolder, int localPosition) {
adapter.bindViewHolder(viewHolder, localPosition);
}
public long getItemId(int localPosition) {
long localItemId = adapter.getItemId(localPosition);
return mStableIdLookup.localToGlobal(localItemId);
}
interface Callback {
void onChanged(@NonNull NestedAdapterWrapper wrapper);
void onItemRangeChanged(
@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart,
int itemCount
);
void onItemRangeChanged(
@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart,
int itemCount,
@Nullable Object payload
);
void onItemRangeInserted(
@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart,
int itemCount);
void onItemRangeRemoved(
@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int positionStart,
int itemCount
);
void onItemRangeMoved(
@NonNull NestedAdapterWrapper nestedAdapterWrapper,
int fromPosition,
int toPosition
);
void onStateRestorationPolicyChanged(NestedAdapterWrapper nestedAdapterWrapper);
}
}

@ -0,0 +1,233 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import java.util.List;
class OpReorderer {
final Callback mCallback;
OpReorderer(Callback callback) {
mCallback = callback;
}
void reorderOps(List<AdapterHelper.UpdateOp> 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<AdapterHelper.UpdateOp> list, int badMove, int next) {
final AdapterHelper.UpdateOp moveOp = list.get(badMove);
final AdapterHelper.UpdateOp nextOp = list.get(next);
switch (nextOp.cmd) {
case AdapterHelper.UpdateOp.REMOVE:
swapMoveRemove(list, badMove, moveOp, next, nextOp);
break;
case AdapterHelper.UpdateOp.ADD:
swapMoveAdd(list, badMove, moveOp, next, nextOp);
break;
case AdapterHelper.UpdateOp.UPDATE:
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
break;
}
}
void swapMoveRemove(List<AdapterHelper.UpdateOp> list, int movePos, AdapterHelper.UpdateOp moveOp,
int removePos, AdapterHelper.UpdateOp removeOp) {
AdapterHelper.UpdateOp extraRm = null;
// check if move is nulled out by remove
boolean revertedMove = false;
final boolean moveIsBackwards;
if (moveOp.positionStart < moveOp.itemCount) {
moveIsBackwards = false;
if (removeOp.positionStart == moveOp.positionStart
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
revertedMove = true;
}
} else {
moveIsBackwards = true;
if (removeOp.positionStart == moveOp.itemCount + 1
&& removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
revertedMove = true;
}
}
// going in reverse, first revert the effect of add
if (moveOp.itemCount < removeOp.positionStart) {
removeOp.positionStart--;
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
// move is removed.
removeOp.itemCount--;
moveOp.cmd = AdapterHelper.UpdateOp.REMOVE;
moveOp.itemCount = 1;
if (removeOp.itemCount == 0) {
list.remove(removePos);
mCallback.recycleUpdateOp(removeOp);
}
// no need to swap, it is already a remove
return;
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= removeOp.positionStart) {
removeOp.positionStart++;
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
final int remaining = removeOp.positionStart + removeOp.itemCount
- moveOp.positionStart;
extraRm = mCallback.obtainUpdateOp(AdapterHelper.UpdateOp.REMOVE, moveOp.positionStart + 1, remaining, null);
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
}
// if effects of move is reverted by remove, we are done.
if (revertedMove) {
list.set(movePos, removeOp);
list.remove(removePos);
mCallback.recycleUpdateOp(moveOp);
return;
}
// now find out the new locations for move actions
if (moveIsBackwards) {
if (extraRm != null) {
if (moveOp.positionStart > extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount > extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart > removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount > removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
} else {
if (extraRm != null) {
if (moveOp.positionStart >= extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount >= extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart >= removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount >= removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
}
list.set(movePos, removeOp);
if (moveOp.positionStart != moveOp.itemCount) {
list.set(removePos, moveOp);
} else {
list.remove(removePos);
}
if (extraRm != null) {
list.add(movePos, extraRm);
}
}
private void swapMoveAdd(List<AdapterHelper.UpdateOp> list, int move, AdapterHelper.UpdateOp moveOp, int add,
AdapterHelper.UpdateOp addOp) {
int offset = 0;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < addOp.positionStart) {
offset--;
}
if (moveOp.positionStart < addOp.positionStart) {
offset++;
}
if (addOp.positionStart <= moveOp.positionStart) {
moveOp.positionStart += addOp.itemCount;
}
if (addOp.positionStart <= moveOp.itemCount) {
moveOp.itemCount += addOp.itemCount;
}
addOp.positionStart += offset;
list.set(move, addOp);
list.set(add, moveOp);
}
void swapMoveUpdate(List<AdapterHelper.UpdateOp> list, int move, AdapterHelper.UpdateOp moveOp, int update,
AdapterHelper.UpdateOp updateOp) {
AdapterHelper.UpdateOp extraUp1 = null;
AdapterHelper.UpdateOp extraUp2 = null;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < updateOp.positionStart) {
updateOp.positionStart--;
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
// moved item is updated. add an update for it
updateOp.itemCount--;
extraUp1 = mCallback.obtainUpdateOp(AdapterHelper.UpdateOp.UPDATE, moveOp.positionStart, 1, updateOp.payload);
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= updateOp.positionStart) {
updateOp.positionStart++;
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
final int remaining = updateOp.positionStart + updateOp.itemCount
- moveOp.positionStart;
extraUp2 = mCallback.obtainUpdateOp(
AdapterHelper.UpdateOp.UPDATE, moveOp.positionStart + 1, remaining,
updateOp.payload);
updateOp.itemCount -= remaining;
}
list.set(update, moveOp);
if (updateOp.itemCount > 0) {
list.set(move, updateOp);
} else {
list.remove(move);
mCallback.recycleUpdateOp(updateOp);
}
if (extraUp1 != null) {
list.add(move, extraUp1);
}
if (extraUp2 != null) {
list.add(move, extraUp2);
}
}
private int getLastMoveOutOfOrder(List<AdapterHelper.UpdateOp> list) {
boolean foundNonMove = false;
for (int i = list.size() - 1; i >= 0; i--) {
final AdapterHelper.UpdateOp op1 = list.get(i);
if (op1.cmd == AdapterHelper.UpdateOp.MOVE) {
if (foundNonMove) {
return i;
}
} else {
foundNonMove = true;
}
}
return -1;
}
interface Callback {
AdapterHelper.UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload);
void recycleUpdateOp(AdapterHelper.UpdateOp op);
}
}

@ -0,0 +1,446 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.graphics.Rect;
import android.view.View;
/**
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
* <p>
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
* can also be used to abstract calls around view bounds and child measurements with margins and
* decorations.
*
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
* @see #createVerticalHelper(RecyclerView.LayoutManager)
*/
public abstract class OrientationHelper {
private static final int INVALID_SIZE = Integer.MIN_VALUE;
protected final RecyclerView.LayoutManager mLayoutManager;
public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
public static final int VERTICAL = RecyclerView.VERTICAL;
private int mLastTotalSpace = INVALID_SIZE;
final Rect mTmpRect = new Rect();
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}
/**
* Returns the {@link RecyclerView.LayoutManager LayoutManager} that
* is associated with this OrientationHelper.
*/
public RecyclerView.LayoutManager getLayoutManager() {
return mLayoutManager;
}
/**
* Call this method after onLayout method is complete if state is NOT pre-layout.
* This method records information like layout bounds that might be useful in the next layout
* calculations.
*/
public void onLayoutComplete() {
mLastTotalSpace = getTotalSpace();
}
/**
* Returns the layout space change between the previous layout pass and current layout pass.
* <p>
* 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.
* <p>
* 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.
* <p>
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
* decoration and 3px right margin, returned value will be 205.
*
* @param view The view element to check
* @return The last pixel of the element
* @see #getDecoratedStart(android.view.View)
*/
public abstract int getDecoratedEnd(View view);
/**
* Returns the end of the View after its matrix transformations are applied to its layout
* position.
* <p>
* This method is useful when trying to detect the visible edge of a View.
* <p>
* It includes the decorations but does not include the margins.
*
* @param view The view whose transformed end will be returned
* @return The end of the View after its decor insets and transformation matrix is applied to
* its position
*
* @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
*/
public abstract int getTransformedEndWithDecoration(View view);
/**
* Returns the start of the View after its matrix transformations are applied to its layout
* position.
* <p>
* This method is useful when trying to detect the visible edge of a View.
* <p>
* It includes the decorations but does not include the margins.
*
* @param view The view whose transformed start will be returned
* @return The start of the View after its decor insets and transformation matrix is applied to
* its position
*
* @see RecyclerView.LayoutManager#getTransformedBoundingBox(View, boolean, Rect)
*/
public abstract int getTransformedStartWithDecoration(View view);
/**
* Returns the space occupied by this View in the current orientation including decorations and
* margins.
*
* @param view The view element to check
* @return Total space occupied by this view
* @see #getDecoratedMeasurementInOther(View)
*/
public abstract int getDecoratedMeasurement(View view);
/**
* Returns the space occupied by this View in the perpendicular orientation including
* decorations and margins.
*
* @param view The view element to check
* @return Total space occupied by this view in the perpendicular orientation to current one
* @see #getDecoratedMeasurement(View)
*/
public abstract int getDecoratedMeasurementInOther(View view);
/**
* Returns the start position of the layout after the start padding is added.
*
* @return The very first pixel we can draw.
*/
public abstract int getStartAfterPadding();
/**
* Returns the end position of the layout after the end padding is removed.
*
* @return The end boundary for this layout.
*/
public abstract int getEndAfterPadding();
/**
* Returns the end position of the layout without taking padding into account.
*
* @return The end boundary for this layout without considering padding.
*/
public abstract int getEnd();
/**
* Offsets all children's positions by the given amount.
*
* @param amount Value to add to each child's layout parameters
*/
public abstract void offsetChildren(int amount);
/**
* Returns the total space to layout. This number is the difference between
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
*
* @return Total space to layout children
*/
public abstract int getTotalSpace();
/**
* Offsets the child in this orientation.
*
* @param view View to offset
* @param offset offset amount
*/
public abstract void offsetChild(View view, int offset);
/**
* Returns the padding at the end of the layout. For horizontal helper, this is the right
* padding and for vertical helper, this is the bottom padding. This method does not check
* whether the layout is RTL or not.
*
* @return The padding at the end of the layout.
*/
public abstract int getEndPadding();
/**
* Returns the MeasureSpec mode for the current orientation from the LayoutManager.
*
* @return The current measure spec mode.
*
* @see View.MeasureSpec
* @see RecyclerView.LayoutManager#getWidthMode()
* @see RecyclerView.LayoutManager#getHeightMode()
*/
public abstract int getMode();
/**
* Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager.
*
* @return The current measure spec mode.
*
* @see View.MeasureSpec
* @see RecyclerView.LayoutManager#getWidthMode()
* @see RecyclerView.LayoutManager#getHeightMode()
*/
public abstract int getModeInOther();
/**
* Creates an OrientationHelper for the given LayoutManager and orientation.
*
* @param layoutManager LayoutManager to attach to
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
* @return A new OrientationHelper
*/
public static OrientationHelper createOrientationHelper(
RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
switch (orientation) {
case HORIZONTAL:
return createHorizontalHelper(layoutManager);
case VERTICAL:
return createVerticalHelper(layoutManager);
}
throw new IllegalArgumentException("invalid orientation");
}
/**
* Creates a horizontal OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createHorizontalHelper(
RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}
@Override
public int getEnd() {
return mLayoutManager.getWidth();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenHorizontal(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingLeft();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
}
@Override
public int getTransformedEndWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.right;
}
@Override
public int getTransformedStartWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.left;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
- mLayoutManager.getPaddingRight();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetLeftAndRight(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingRight();
}
@Override
public int getMode() {
return mLayoutManager.getWidthMode();
}
@Override
public int getModeInOther() {
return mLayoutManager.getHeightMode();
}
};
}
/**
* Creates a vertical OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}
@Override
public int getEnd() {
return mLayoutManager.getHeight();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenVertical(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingTop();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
}
@Override
public int getTransformedEndWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.bottom;
}
@Override
public int getTransformedStartWithDecoration(View view) {
mLayoutManager.getTransformedBoundingBox(view, true, mTmpRect);
return mTmpRect.top;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
- mLayoutManager.getPaddingBottom();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetTopAndBottom(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingBottom();
}
@Override
public int getMode() {
return mLayoutManager.getHeightMode();
}
@Override
public int getModeInOther() {
return mLayoutManager.getWidthMode();
}
};
}
}

@ -0,0 +1,273 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or
* horizontal orientation.
*
* <p>
*
* PagerSnapHelper can help achieve a similar behavior to
* {@link androidx.viewpager.widget.ViewPager}. Set both {@link RecyclerView} and the items of the
* {@link RecyclerView.Adapter} to have
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} height and width and then attach
* PagerSnapHelper to the {@link RecyclerView} using {@link #attachToRecyclerView(RecyclerView)}.
*/
public class PagerSnapHelper extends SnapHelper {
private static final int MAX_SCROLL_ON_FLING_DURATION = 100; // ms
// Orientation helpers are lazily created per LayoutManager.
@Nullable
private OrientationHelper mVerticalHelper;
@Nullable
private OrientationHelper mHorizontalHelper;
@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
@Nullable
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
if (orientationHelper == null) {
return RecyclerView.NO_POSITION;
}
// A child that is exactly in the center is eligible for both before and after
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// Find the first view before the center, and the first view after the center
final int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) {
continue;
}
final int distance = distanceToCenter(child, orientationHelper);
if (distance <= 0 && distance > distanceBefore) {
// Child is before the center and closer then the previous best
distanceBefore = distance;
closestChildBeforeCenter = child;
}
if (distance >= 0 && distance < distanceAfter) {
// Child is after the center and closer then the previous best
distanceAfter = distance;
closestChildAfterCenter = child;
}
}
// Return the position of the first child from the center, in the direction of the fling
final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
if (forwardDirection && closestChildAfterCenter != null) {
return layoutManager.getPosition(closestChildAfterCenter);
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return layoutManager.getPosition(closestChildBeforeCenter);
}
// There is no child in the direction of the fling. Either it doesn't exist (start/end of
// the list), or it is not yet attached (very rare case when children are larger then the
// viewport). Extrapolate from the child that is visible to get the position of the view to
// snap to.
View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
if (visibleView == null) {
return RecyclerView.NO_POSITION;
}
int visiblePosition = layoutManager.getPosition(visibleView);
int snapToPosition = visiblePosition
+ (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);
if (snapToPosition < 0 || snapToPosition >= itemCount) {
return RecyclerView.NO_POSITION;
}
return snapToPosition;
}
private boolean isForwardFling(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (layoutManager.canScrollHorizontally()) {
return velocityX > 0;
} else {
return velocityY > 0;
}
}
private boolean isReverseLayout(RecyclerView.LayoutManager layoutManager) {
final int itemCount = layoutManager.getItemCount();
if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd != null) {
return vectorForEnd.x < 0 || vectorForEnd.y < 0;
}
}
return false;
}
@Nullable
@Override
protected RecyclerView.SmoothScroller createScroller(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(@NonNull View targetView,
@NonNull RecyclerView.State state, @NonNull Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(@NonNull DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
@Override
protected int calculateTimeForScrolling(int dx) {
return Math.min(MAX_SCROLL_ON_FLING_DURATION, super.calculateTimeForScrolling(dx));
}
};
}
private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) {
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
return childCenter - containerCenter;
}
/**
* Return the child view that is currently closest to the center of this parent.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
*
* @return the child view that is currently closest to the center of this parent.
*/
@Nullable
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
/* if child center is closer than previous closest, set it as closest */
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
@Nullable
private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return getVerticalHelper(layoutManager);
} else if (layoutManager.canScrollHorizontally()) {
return getHorizontalHelper(layoutManager);
} else {
return null;
}
}
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}

@ -0,0 +1,272 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
import java.util.Map;
import java.util.WeakHashMap;
/**
* The AccessibilityDelegate used by RecyclerView.
* <p>
* This class handles basic accessibility actions and delegates them to LayoutManager.
*/
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
final RecyclerView mRecyclerView;
private final ItemDelegate mItemDelegate;
public RecyclerViewAccessibilityDelegate(@NonNull RecyclerView recyclerView) {
mRecyclerView = recyclerView;
AccessibilityDelegateCompat itemDelegate = getItemDelegate();
if (itemDelegate != null && itemDelegate instanceof ItemDelegate) {
mItemDelegate = (ItemDelegate) itemDelegate;
} else {
mItemDelegate = new ItemDelegate(this);
}
}
boolean shouldIgnore() {
return mRecyclerView.hasPendingAdapterUpdates();
}
@Override
public boolean performAccessibilityAction(
@SuppressLint("InvalidNullabilityOverride") @NonNull View host,
int action,
@SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args
) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
}
return false;
}
@Override
public void onInitializeAccessibilityNodeInfo(
@SuppressLint("InvalidNullabilityOverride") @NonNull View host,
@SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityNodeInfoCompat info
) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
}
}
@Override
public void onInitializeAccessibilityEvent(
@SuppressLint("InvalidNullabilityOverride") @NonNull View host,
@SuppressLint("InvalidNullabilityOverride") @NonNull AccessibilityEvent event
) {
super.onInitializeAccessibilityEvent(host, event);
if (host instanceof RecyclerView && !shouldIgnore()) {
RecyclerView rv = (RecyclerView) host;
if (rv.getLayoutManager() != null) {
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
}
}
}
/**
* Gets the AccessibilityDelegate for an individual item in the RecyclerView.
* A basic item delegate is provided by default, but you can override this
* method to provide a custom per-item delegate.
* For now, returning an {@code AccessibilityDelegateCompat} as opposed to an
* {@code ItemDelegate} will prevent use of the {@code ViewCompat} accessibility API on
* item views.
*/
@NonNull
public AccessibilityDelegateCompat getItemDelegate() {
return mItemDelegate;
}
/**
* The default implementation of accessibility delegate for the individual items of the
* RecyclerView.
* <p>
* If you are overriding {@code RecyclerViewAccessibilityDelegate#getItemDelegate()} but still
* want to keep some default behavior, you can create an instance of this class and delegate to
* the parent as necessary.
*/
public static class ItemDelegate extends AccessibilityDelegateCompat {
final RecyclerViewAccessibilityDelegate mRecyclerViewDelegate;
private Map<View, AccessibilityDelegateCompat> mOriginalItemDelegates = new WeakHashMap<>();
/**
* Creates an item delegate for the given {@code RecyclerViewAccessibilityDelegate}.
*
* @param recyclerViewDelegate The parent RecyclerView's accessibility delegate.
*/
public ItemDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate) {
mRecyclerViewDelegate = recyclerViewDelegate;
}
/**
* Saves a reference to the original delegate of the itemView so that it's behavior can be
* combined with the ItemDelegate's behavior.
*/
void saveOriginalDelegate(View itemView) {
AccessibilityDelegateCompat delegate = ViewCompat.getAccessibilityDelegate(itemView);
if (delegate != null && delegate != this) {
mOriginalItemDelegates.put(itemView, delegate);
}
}
/**
* @return The delegate associated with itemView before the view was bound.
*/
AccessibilityDelegateCompat getAndRemoveOriginalDelegateForItem(View itemView) {
return mOriginalItemDelegates.remove(itemView);
}
@Override
public void onInitializeAccessibilityNodeInfo(
@SuppressLint("InvalidNullabilityOverride") @NonNull View host,
@SuppressLint("InvalidNullabilityOverride") @NonNull
AccessibilityNodeInfoCompat info
) {
if (!mRecyclerViewDelegate.shouldIgnore()
&& mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) {
mRecyclerViewDelegate.mRecyclerView.getLayoutManager()
.onInitializeAccessibilityNodeInfoForItem(host, info);
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
originalDelegate.onInitializeAccessibilityNodeInfo(host, info);
} else {
super.onInitializeAccessibilityNodeInfo(host, info);
}
} else {
super.onInitializeAccessibilityNodeInfo(host, info);
}
}
@Override
public boolean performAccessibilityAction(
@SuppressLint("InvalidNullabilityOverride") @NonNull View host,
int action,
@SuppressLint("InvalidNullabilityOverride") @Nullable Bundle args
) {
if (!mRecyclerViewDelegate.shouldIgnore()
&& mRecyclerViewDelegate.mRecyclerView.getLayoutManager() != null) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
if (originalDelegate.performAccessibilityAction(host, action, args)) {
return true;
}
} else if (super.performAccessibilityAction(host, action, args)) {
return true;
}
return mRecyclerViewDelegate.mRecyclerView.getLayoutManager()
.performAccessibilityActionForItem(host, action, args);
} else {
return super.performAccessibilityAction(host, action, args);
}
}
@Override
public void sendAccessibilityEvent(@NonNull View host, int eventType) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
originalDelegate.sendAccessibilityEvent(host, eventType);
} else {
super.sendAccessibilityEvent(host, eventType);
}
}
@Override
public void sendAccessibilityEventUnchecked(@NonNull View host,
@NonNull AccessibilityEvent event) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
originalDelegate.sendAccessibilityEventUnchecked(host, event);
} else {
super.sendAccessibilityEventUnchecked(host, event);
}
}
@Override
public boolean dispatchPopulateAccessibilityEvent(@NonNull View host,
@NonNull AccessibilityEvent event) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
return originalDelegate.dispatchPopulateAccessibilityEvent(host, event);
} else {
return super.dispatchPopulateAccessibilityEvent(host, event);
}
}
@Override
public void onPopulateAccessibilityEvent(@NonNull View host,
@NonNull AccessibilityEvent event) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
originalDelegate.onPopulateAccessibilityEvent(host, event);
} else {
super.onPopulateAccessibilityEvent(host, event);
}
}
@Override
public void onInitializeAccessibilityEvent(@NonNull View host,
@NonNull AccessibilityEvent event) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
originalDelegate.onInitializeAccessibilityEvent(host, event);
} else {
super.onInitializeAccessibilityEvent(host, event);
}
}
@Override
public boolean onRequestSendAccessibilityEvent(@NonNull ViewGroup host,
@NonNull View child, @NonNull AccessibilityEvent event) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
return originalDelegate.onRequestSendAccessibilityEvent(host, child, event);
} else {
return super.onRequestSendAccessibilityEvent(host, child, event);
}
}
@Override
@Nullable
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(@NonNull View host) {
AccessibilityDelegateCompat originalDelegate = mOriginalItemDelegates.get(host);
if (originalDelegate != null) {
return originalDelegate.getAccessibilityNodeProvider(host);
} else {
return super.getAccessibilityNodeProvider(host);
}
}
}
}

@ -0,0 +1,101 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.view.View;
/**
* A helper class to do scroll offset calculations.
*/
class ScrollbarHelper {
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled, boolean reverseLayout) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|| endChild == null) {
return 0;
}
final int minPosition = Math.min(lm.getPosition(startChild),
lm.getPosition(endChild));
final int maxPosition = Math.max(lm.getPosition(startChild),
lm.getPosition(endChild));
final int itemsBefore = reverseLayout
? Math.max(0, state.getItemCount() - maxPosition - 1)
: Math.max(0, minPosition);
if (!smoothScrollbarEnabled) {
return itemsBefore;
}
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild));
final int itemRange = Math.abs(lm.getPosition(startChild)
- lm.getPosition(endChild)) + 1;
final float avgSizePerRow = (float) laidOutArea / itemRange;
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
- orientation.getDecoratedStart(startChild)));
}
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|| endChild == null) {
return 0;
}
if (!smoothScrollbarEnabled) {
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
}
final int extend = orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild);
return Math.min(orientation.getTotalSpace(), extend);
}
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|| endChild == null) {
return 0;
}
if (!smoothScrollbarEnabled) {
return state.getItemCount();
}
// smooth scrollbar enabled. try to estimate better.
final int laidOutArea = orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild);
final int laidOutRange = Math.abs(lm.getPosition(startChild)
- lm.getPosition(endChild))
+ 1;
// estimate a size for full list.
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
}
private ScrollbarHelper() {
}
}

@ -0,0 +1,479 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* A wrapper class for ItemAnimator that records View bounds and decides whether it should run
* move, change, add or remove animations. This class also replicates the original ItemAnimator
* API.
* <p>
* It uses {@link RecyclerView.ItemAnimator.ItemHolderInfo} to track the bounds information of
* the Views. If you would like to extend this class, you can override {@link #obtainHolderInfo()}
* method to provide your own info class that extends
* {@link RecyclerView.ItemAnimator.ItemHolderInfo}.
*/
public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
private static final boolean DEBUG = false;
private static final String TAG = "SimpleItemAnimator";
boolean mSupportsChangeAnimations = true;
/**
* Returns whether this ItemAnimator supports animations of change events.
*
* @return true if change animations are supported, false otherwise
*/
@SuppressWarnings("unused")
public boolean getSupportsChangeAnimations() {
return mSupportsChangeAnimations;
}
/**
* Sets whether this ItemAnimator supports animations of item change events.
* If you set this property to false, actions on the data set which change the
* contents of items will not be animated. What those animations do is left
* up to the discretion of the ItemAnimator subclass, in its
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} implementation.
* The value of this property is true by default.
*
* @param supportsChangeAnimations true if change animations are supported by
* this ItemAnimator, false otherwise. If the property is false,
* the ItemAnimator
* will not receive a call to
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int,
* int)} when changes occur.
* @see RecyclerView.Adapter#notifyItemChanged(int)
* @see RecyclerView.Adapter#notifyItemRangeChanged(int, int)
*/
public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {
mSupportsChangeAnimations = supportsChangeAnimations;
}
/**
* {@inheritDoc}
*
* @return True if change animations are not supported or the ViewHolder is invalid,
* false otherwise.
*
* @see #setSupportsChangeAnimations(boolean)
*/
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
return !mSupportsChangeAnimations || viewHolder.isInvalid();
}
@Override
public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
int oldLeft = preLayoutInfo.left;
int oldTop = preLayoutInfo.top;
View disappearingItemView = viewHolder.itemView;
int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
disappearingItemView.layout(newLeft, newTop,
newLeft + disappearingItemView.getWidth(),
newTop + disappearingItemView.getHeight());
if (DEBUG) {
Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView);
}
return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
} else {
if (DEBUG) {
Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView);
}
return animateRemove(viewHolder);
}
}
@Override
public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder,
@Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
|| preLayoutInfo.top != postLayoutInfo.top)) {
// slide items in if before/after locations differ
if (DEBUG) {
Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder);
}
return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
postLayoutInfo.left, postLayoutInfo.top);
} else {
if (DEBUG) {
Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder);
}
return animateAdd(viewHolder);
}
}
@Override
public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
if (preLayoutInfo.left != postLayoutInfo.left || preLayoutInfo.top != postLayoutInfo.top) {
if (DEBUG) {
Log.d(TAG, "PERSISTENT: " + viewHolder
+ " with view " + viewHolder.itemView);
}
return animateMove(viewHolder,
preLayoutInfo.left, preLayoutInfo.top, postLayoutInfo.left, postLayoutInfo.top);
}
dispatchMoveFinished(viewHolder);
return false;
}
@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
@NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preLayoutInfo,
@NonNull ItemHolderInfo postLayoutInfo) {
if (DEBUG) {
Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView);
}
final int fromLeft = preLayoutInfo.left;
final int fromTop = preLayoutInfo.top;
final int toLeft, toTop;
if (newHolder.shouldIgnore()) {
toLeft = preLayoutInfo.left;
toTop = preLayoutInfo.top;
} else {
toLeft = postLayoutInfo.left;
toTop = postLayoutInfo.top;
}
return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
}
/**
* Called when an item is removed from the RecyclerView. Implementors can choose
* whether and how to animate that change, but must always call
* {@link #dispatchRemoveFinished(RecyclerView.ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()},
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()},
* {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one,
* then start the animations together in the later call to {@link #runPendingAnimations()}.
*
* <p>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.</p>
*
* @param holder The item that is being removed.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract boolean animateRemove(RecyclerView.ViewHolder holder);
/**
* Called when an item is added to the RecyclerView. Implementors can choose
* whether and how to animate that change, but must always call
* {@link #dispatchAddFinished(RecyclerView.ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()},
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()},
* {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one,
* then start the animations together in the later call to {@link #runPendingAnimations()}.
*
* <p>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.</p>
*
* @param holder The item that is being added.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract boolean animateAdd(RecyclerView.ViewHolder holder);
/**
* Called when an item is moved in the RecyclerView. Implementors can choose
* whether and how to animate that change, but must always call
* {@link #dispatchMoveFinished(RecyclerView.ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()},
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()},
* {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one,
* then start the animations together in the later call to {@link #runPendingAnimations()}.
*
* @param holder The item that is being moved.
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY,
int toX, int toY);
/**
* Called when an item is changed in the RecyclerView, as indicated by a call to
* {@link RecyclerView.Adapter#notifyItemChanged(int)} or
* {@link RecyclerView.Adapter#notifyItemRangeChanged(int, int)}.
* <p>
* Implementers can choose whether and how to animate changes, but must always call
* {@link #dispatchChangeFinished(RecyclerView.ViewHolder, boolean)} for each non-null
* distinct ViewHolder,
* either immediately (if no animation will occur) or after the animation actually finishes.
* If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call
* {@link #dispatchChangeFinished(RecyclerView.ViewHolder, boolean)} once and only once. In
* that case, the
* second parameter of {@code dispatchChangeFinished} is ignored.
* <p>
* The return value indicates whether an animation has been set up and whether the
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(RecyclerView.ViewHolder) animateAdd()},
* {@link #animateMove(RecyclerView.ViewHolder, int, int, int, int) animateMove()},
* {@link #animateRemove(RecyclerView.ViewHolder) animateRemove()}, and
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)} come in one by one,
* then start the animations together in the later call to {@link #runPendingAnimations()}.
*
* @param oldHolder The original item that changed.
* @param newHolder The new item that was created with the changed content. Might be null
* @param fromLeft Left of the old view holder
* @param fromTop Top of the old view holder
* @param toLeft Left of the new view holder
* @param toTop Top of the new view holder
* @return true if a later call to {@link #runPendingAnimations()} is requested,
* false otherwise.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract boolean animateChange(RecyclerView.ViewHolder oldHolder,
RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);
/**
* Method to be called by subclasses when a remove animation is done.
*
* @param item The item which has been removed
* @see RecyclerView.ItemAnimator#animateDisappearance(RecyclerView.ViewHolder, ItemHolderInfo,
* ItemHolderInfo)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchRemoveFinished(RecyclerView.ViewHolder item) {
onRemoveFinished(item);
dispatchAnimationFinished(item);
}
/**
* Method to be called by subclasses when a move animation is done.
*
* @param item The item which has been moved
* @see RecyclerView.ItemAnimator#animateDisappearance(RecyclerView.ViewHolder, ItemHolderInfo,
* ItemHolderInfo)
* @see RecyclerView.ItemAnimator#animatePersistence(RecyclerView.ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see RecyclerView.ItemAnimator#animateAppearance(RecyclerView.ViewHolder, ItemHolderInfo, ItemHolderInfo)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchMoveFinished(RecyclerView.ViewHolder item) {
onMoveFinished(item);
dispatchAnimationFinished(item);
}
/**
* Method to be called by subclasses when an add animation is done.
*
* @param item The item which has been added
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchAddFinished(RecyclerView.ViewHolder item) {
onAddFinished(item);
dispatchAnimationFinished(item);
}
/**
* Method to be called by subclasses when a change animation is done.
*
* @param item The item which has been changed (this method must be called for
* each non-null ViewHolder passed into
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}).
* @param oldItem true if this is the old item that was changed, false if
* it is the new item that replaced the old item.
* @see #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) {
onChangeFinished(item, oldItem);
dispatchAnimationFinished(item);
}
/**
* Method to be called by subclasses when a remove animation is being started.
*
* @param item The item being removed
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchRemoveStarting(RecyclerView.ViewHolder item) {
onRemoveStarting(item);
}
/**
* Method to be called by subclasses when a move animation is being started.
*
* @param item The item being moved
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchMoveStarting(RecyclerView.ViewHolder item) {
onMoveStarting(item);
}
/**
* Method to be called by subclasses when an add animation is being started.
*
* @param item The item being added
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchAddStarting(RecyclerView.ViewHolder item) {
onAddStarting(item);
}
/**
* Method to be called by subclasses when a change animation is being started.
*
* @param item The item which has been changed (this method must be called for
* each non-null ViewHolder passed into
* {@link #animateChange(RecyclerView.ViewHolder, RecyclerView.ViewHolder, int, int, int, int)}).
* @param oldItem true if this is the old item that was changed, false if
* it is the new item that replaced the old item.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public final void dispatchChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
onChangeStarting(item, oldItem);
}
/**
* Called when a remove animation is being started on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
@SuppressWarnings("UnusedParameters")
public void onRemoveStarting(RecyclerView.ViewHolder item) {
}
/**
* Called when a remove animation has ended on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onRemoveFinished(RecyclerView.ViewHolder item) {
}
/**
* Called when an add animation is being started on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressWarnings("UnusedParameters")
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onAddStarting(RecyclerView.ViewHolder item) {
}
/**
* Called when an add animation has ended on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onAddFinished(RecyclerView.ViewHolder item) {
}
/**
* Called when a move animation is being started on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressWarnings("UnusedParameters")
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onMoveStarting(RecyclerView.ViewHolder item) {
}
/**
* Called when a move animation has ended on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onMoveFinished(RecyclerView.ViewHolder item) {
}
/**
* Called when a change animation is being started on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
* @param oldItem true if this is the old item that was changed, false if
* it is the new item that replaced the old item.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
@SuppressWarnings("UnusedParameters")
public void onChangeStarting(RecyclerView.ViewHolder item, boolean oldItem) {
}
/**
* Called when a change animation has ended on the given ViewHolder.
* The default implementation does nothing. Subclasses may wish to override
* this method to handle any ViewHolder-specific operations linked to animation
* lifecycles.
*
* @param item The ViewHolder being animated.
* @param oldItem true if this is the old item that was changed, false if
* it is the new item that replaced the old item.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) {
}
}

@ -0,0 +1,308 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Class intended to support snapping for a {@link RecyclerView}.
* <p>
* SnapHelper tries to handle fling as well but for this to work properly, the
* {@link RecyclerView.LayoutManager} must implement the {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface or
* you should override {@link #onFling(int, int)} and handle fling manually.
*/
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
static final float MILLISECONDS_PER_INCH = 100f;
RecyclerView mRecyclerView;
private Scroller mGravityScroller;
// Handles the snap on scroll case.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
@Override
public boolean onFling(int velocityX, int velocityY) {
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
/**
* Attaches the {@link SnapHelper} to the provided RecyclerView, by calling
* {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
* You can call this method with {@code null} to detach it from the current RecyclerView.
*
* @param recyclerView The RecyclerView instance to which you want to add this helper or
* {@code null} if you want to remove SnapHelper from the current
* RecyclerView.
*
* @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
* attached to the provided {@link RecyclerView}.
*
*/
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
/**
* Called when an instance of a {@link RecyclerView} is attached.
*/
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
/**
* Called when the instance of a {@link RecyclerView} is detached.
*/
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
/**
* Calculated the estimated scroll distance in each direction given velocities on both axes.
*
* @param velocityX Fling velocity on the horizontal axis.
* @param velocityY Fling velocity on the vertical axis.
*
* @return array holding the calculated distances in x and y directions
* respectively.
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public int[] calculateScrollDistance(int velocityX, int velocityY) {
int[] outDist = new int[2];
mGravityScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
outDist[0] = mGravityScroller.getFinalX();
outDist[1] = mGravityScroller.getFinalY();
return outDist;
}
/**
* Helper method to facilitate for snapping triggered by a fling.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
* @param velocityX Fling velocity on the horizontal axis.
* @param velocityY Fling velocity on the vertical axis.
*
* @return true if it is handled, false otherwise.
*/
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
/**
* Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
* method is used to snap the view when the {@link RecyclerView} is first attached; when
* snapping was triggered by a scroll and when the fling is at its final stages.
*/
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
/**
* Creates a scroller to be used in the snapping implementation.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
*
* @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
*/
@Nullable
protected RecyclerView.SmoothScroller createScroller(
@NonNull RecyclerView.LayoutManager layoutManager) {
return createSnapScroller(layoutManager);
}
/**
* Creates a scroller to be used in the snapping implementation.
*
* @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}.
*
* @return a {@link LinearSmoothScroller} which will handle the scrolling.
* @deprecated use {@link #createScroller(RecyclerView.LayoutManager)} instead.
*/
@Nullable
@Deprecated
protected LinearSmoothScroller createSnapScroller(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
if (mRecyclerView == null) {
// The associated RecyclerView has been removed so there is no action to take.
return;
}
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, mDecelerateInterpolator);
}
}
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
/**
* Override this method to snap to a particular point within the target view or the container
* view on any axis.
* <p>
* This method is called when the {@link SnapHelper} has intercepted a fling and it needs
* to know the exact distance required to scroll by in order to snap to the target view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param targetView the target view that is chosen as the view to snap
*
* @return the output coordinates the put the result into. out[0] is the distance
* on horizontal axis and out[1] is the distance on vertical axis.
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView);
/**
* Override this method to provide a particular target view for snapping.
* <p>
* This method is called when the {@link SnapHelper} is ready to start snapping and requires
* a target view to snap to. It will be explicitly called when the scroll state becomes idle
* after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
* after a fling and requires a reference view from the current set of child views.
* <p>
* If this method returns {@code null}, SnapHelper will not snap to any view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
*
* @return the target view to which to snap on fling or end of scroll
*/
@SuppressWarnings("WeakerAccess")
@Nullable
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract View findSnapView(RecyclerView.LayoutManager layoutManager);
/**
* Override to provide a particular adapter target position for snapping.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param velocityX fling velocity on the horizontal axis
* @param velocityY fling velocity on the vertical axis
*
* @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
* if no snapping should happen
*/
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public abstract int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
int velocityX, int velocityY);
}

@ -0,0 +1,67 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
/**
* A {@link SortedList.Callback} implementation that can bind a {@link SortedList} to a
* {@link RecyclerView.Adapter}.
*/
public abstract class SortedListAdapterCallback<T2> extends SortedList.Callback<T2> {
final RecyclerView.Adapter<?> mAdapter;
/**
* Creates a {@link SortedList.Callback} that will forward data change events to the provided
* Adapter.
*
* @param adapter The Adapter instance which should receive events from the SortedList.
*/
public SortedListAdapterCallback(
// b/240775049: Cannot annotate properly
@SuppressLint({"UnknownNullness", "MissingNullability"})
RecyclerView.Adapter<?> adapter) {
mAdapter = adapter;
}
@Override
public void onInserted(int position, int count) {
mAdapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
mAdapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
mAdapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count) {
mAdapter.notifyItemRangeChanged(position, count);
}
@Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public void onChanged(int position, int count, Object payload) {
mAdapter.notifyItemRangeChanged(position, count, payload);
}
}

@ -0,0 +1,106 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import androidx.annotation.NonNull;
import androidx.collection.LongSparseArray;
/**
* Used by {@link ConcatAdapter} to isolate item ids between nested adapters, if necessary.
*/
interface StableIdStorage {
@NonNull
StableIdLookup createStableIdLookup();
/**
* Interface that provides {@link NestedAdapterWrapper}s a way to map their local stable ids
* into global stable ids, based on the configuration of the {@link ConcatAdapter}.
*/
interface StableIdLookup {
long localToGlobal(long localId);
}
/**
* Returns {@link RecyclerView#NO_ID} for all positions. In other words, stable ids are not
* supported.
*/
class NoStableIdStorage implements StableIdStorage {
private final StableIdLookup mNoIdLookup = new StableIdLookup() {
@Override
public long localToGlobal(long localId) {
return RecyclerView.NO_ID;
}
};
@NonNull
@Override
public StableIdLookup createStableIdLookup() {
return mNoIdLookup;
}
}
/**
* A pass-through implementation that reports the stable id in sub adapters as is.
*/
class SharedPoolStableIdStorage implements StableIdStorage {
private final StableIdLookup mSameIdLookup = new StableIdLookup() {
@Override
public long localToGlobal(long localId) {
return localId;
}
};
@NonNull
@Override
public StableIdLookup createStableIdLookup() {
return mSameIdLookup;
}
}
/**
* An isolating implementation that ensures the stable ids among adapters do not conflict with
* each-other. It keeps a mapping for each adapter from its local stable ids to a global domain
* and always replaces the local id w/ a globally available ID to be consistent.
*/
class IsolatedStableIdStorage implements StableIdStorage {
long mNextStableId = 0;
long obtainId() {
return mNextStableId++;
}
@NonNull
@Override
public StableIdLookup createStableIdLookup() {
return new WrapperStableIdLookup();
}
class WrapperStableIdLookup implements StableIdLookup {
private final LongSparseArray<Long> mLocalToGlobalLookup = new LongSparseArray<>();
@Override
public long localToGlobal(long localId) {
Long globalId = mLocalToGlobalLookup.get(localId);
if (globalId == null) {
globalId = obtainId();
mLocalToGlobalLookup.put(localId, globalId);
}
return globalId;
}
}
}
}

@ -0,0 +1,49 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.annotation.SuppressLint;
interface ThreadUtil<T> {
interface MainThreadCallback<T> {
void updateItemCount(int generation, int itemCount);
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void addTile(int generation, TileList.Tile<T> tile);
void removeTile(int generation, int position);
}
interface BackgroundCallback<T> {
void refresh(int generation);
void updateRange(int rangeStart, int rangeEnd, int extRangeStart, int extRangeEnd,
int scrollHint);
void loadTile(int position, int scrollHint);
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
void recycleTile(TileList.Tile<T> tile);
}
MainThreadCallback<T> getMainThreadProxy(MainThreadCallback<T> callback);
BackgroundCallback<T> getBackgroundProxy(BackgroundCallback<T> callback);
}

@ -0,0 +1,115 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import java.lang.reflect.Array;
/**
* A sparse collection of tiles sorted for efficient access.
*/
class TileList<T> {
final int mTileSize;
// Keyed by start position.
private final SparseArray<Tile<T>> mTiles = new SparseArray<Tile<T>>(10);
Tile<T> mLastAccessedTile;
public TileList(int tileSize) {
mTileSize = tileSize;
}
public T getItemAt(int pos) {
if (mLastAccessedTile == null || !mLastAccessedTile.containsPosition(pos)) {
final int startPosition = pos - (pos % mTileSize);
final int index = mTiles.indexOfKey(startPosition);
if (index < 0) {
return null;
}
mLastAccessedTile = mTiles.valueAt(index);
}
return mLastAccessedTile.getByPosition(pos);
}
public int size() {
return mTiles.size();
}
public void clear() {
mTiles.clear();
}
/**
* Returns the {@link Tile} at the provided {@param index}, or {@code null} if the index
* provided is out of bounds.
*/
public Tile<T> getAtIndex(int index) {
if (index < 0 || index >= mTiles.size()) {
return null;
}
return mTiles.valueAt(index);
}
public Tile<T> addOrReplace(Tile<T> newTile) {
final int index = mTiles.indexOfKey(newTile.mStartPosition);
if (index < 0) {
mTiles.put(newTile.mStartPosition, newTile);
return null;
}
Tile<T> oldTile = mTiles.valueAt(index);
mTiles.setValueAt(index, newTile);
if (mLastAccessedTile == oldTile) {
mLastAccessedTile = newTile;
}
return oldTile;
}
public Tile<T> removeAtPos(int startPosition) {
Tile<T> tile = mTiles.get(startPosition);
if (mLastAccessedTile == tile) {
mLastAccessedTile = null;
}
mTiles.delete(startPosition);
return tile;
}
public static class Tile<T> {
public final T[] mItems;
public int mStartPosition;
public int mItemCount;
Tile<T> mNext; // Used only for pooling recycled tiles.
Tile(@NonNull Class<T> klass, int size) {
@SuppressWarnings("unchecked")
T[] items = (T[]) Array.newInstance(klass, size);
mItems = items;
}
boolean containsPosition(int pos) {
return mStartPosition <= pos && pos < mStartPosition + mItemCount;
}
T getByPosition(int pos) {
return mItems[pos - mStartPosition];
}
}
}

@ -0,0 +1,269 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.view.View;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A utility class used to check the boundaries of a given view within its parent view based on
* a set of boundary flags.
*/
class ViewBoundsCheck {
static final int GT = 1 << 0;
static final int EQ = 1 << 1;
static final int LT = 1 << 2;
static final int CVS_PVS_POS = 0;
/**
* The child view's start should be strictly greater than parent view's start.
*/
static final int FLAG_CVS_GT_PVS = GT << CVS_PVS_POS;
/**
* The child view's start can be equal to its parent view's start. This flag follows with GT
* or LT indicating greater (less) than or equal relation.
*/
static final int FLAG_CVS_EQ_PVS = EQ << CVS_PVS_POS;
/**
* The child view's start should be strictly less than parent view's start.
*/
static final int FLAG_CVS_LT_PVS = LT << CVS_PVS_POS;
static final int CVS_PVE_POS = 4;
/**
* The child view's start should be strictly greater than parent view's end.
*/
static final int FLAG_CVS_GT_PVE = GT << CVS_PVE_POS;
/**
* The child view's start can be equal to its parent view's end. This flag follows with GT
* or LT indicating greater (less) than or equal relation.
*/
static final int FLAG_CVS_EQ_PVE = EQ << CVS_PVE_POS;
/**
* The child view's start should be strictly less than parent view's end.
*/
static final int FLAG_CVS_LT_PVE = LT << CVS_PVE_POS;
static final int CVE_PVS_POS = 8;
/**
* The child view's end should be strictly greater than parent view's start.
*/
static final int FLAG_CVE_GT_PVS = GT << CVE_PVS_POS;
/**
* The child view's end can be equal to its parent view's start. This flag follows with GT
* or LT indicating greater (less) than or equal relation.
*/
static final int FLAG_CVE_EQ_PVS = EQ << CVE_PVS_POS;
/**
* The child view's end should be strictly less than parent view's start.
*/
static final int FLAG_CVE_LT_PVS = LT << CVE_PVS_POS;
static final int CVE_PVE_POS = 12;
/**
* The child view's end should be strictly greater than parent view's end.
*/
static final int FLAG_CVE_GT_PVE = GT << CVE_PVE_POS;
/**
* The child view's end can be equal to its parent view's end. This flag follows with GT
* or LT indicating greater (less) than or equal relation.
*/
static final int FLAG_CVE_EQ_PVE = EQ << CVE_PVE_POS;
/**
* The child view's end should be strictly less than parent view's end.
*/
static final int FLAG_CVE_LT_PVE = LT << CVE_PVE_POS;
static final int MASK = GT | EQ | LT;
final Callback mCallback;
BoundFlags mBoundFlags;
/**
* The set of flags that can be passed for checking the view boundary conditions.
* CVS in the flag name indicates the child view, and PV indicates the parent view.\
* The following S, E indicate a view's start and end points, respectively.
* GT and LT indicate a strictly greater and less than relationship.
* Greater than or equal (or less than or equal) can be specified by setting both GT and EQ (or
* LT and EQ) flags.
* For instance, setting both {@link #FLAG_CVS_GT_PVS} and {@link #FLAG_CVS_EQ_PVS} indicate the
* child view's start should be greater than or equal to its parent start.
*/
@IntDef(flag = true, value = {
FLAG_CVS_GT_PVS, FLAG_CVS_EQ_PVS, FLAG_CVS_LT_PVS,
FLAG_CVS_GT_PVE, FLAG_CVS_EQ_PVE, FLAG_CVS_LT_PVE,
FLAG_CVE_GT_PVS, FLAG_CVE_EQ_PVS, FLAG_CVE_LT_PVS,
FLAG_CVE_GT_PVE, FLAG_CVE_EQ_PVE, FLAG_CVE_LT_PVE
})
@Retention(RetentionPolicy.SOURCE)
public @interface ViewBounds {}
ViewBoundsCheck(Callback callback) {
mCallback = callback;
mBoundFlags = new BoundFlags();
}
static class BoundFlags {
int mBoundFlags = 0;
int mRvStart, mRvEnd, mChildStart, mChildEnd;
void setBounds(int rvStart, int rvEnd, int childStart, int childEnd) {
mRvStart = rvStart;
mRvEnd = rvEnd;
mChildStart = childStart;
mChildEnd = childEnd;
}
void addFlags(@ViewBounds int flags) {
mBoundFlags |= flags;
}
void resetFlags() {
mBoundFlags = 0;
}
int compare(int x, int y) {
if (x > y) {
return GT;
}
if (x == y) {
return EQ;
}
return LT;
}
boolean boundsMatch() {
if ((mBoundFlags & (MASK << CVS_PVS_POS)) != 0) {
if ((mBoundFlags & (compare(mChildStart, mRvStart) << CVS_PVS_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVS_PVE_POS)) != 0) {
if ((mBoundFlags & (compare(mChildStart, mRvEnd) << CVS_PVE_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVE_PVS_POS)) != 0) {
if ((mBoundFlags & (compare(mChildEnd, mRvStart) << CVE_PVS_POS)) == 0) {
return false;
}
}
if ((mBoundFlags & (MASK << CVE_PVE_POS)) != 0) {
if ((mBoundFlags & (compare(mChildEnd, mRvEnd) << CVE_PVE_POS)) == 0) {
return false;
}
}
return true;
}
};
/**
* Returns the first view starting from fromIndex to toIndex in views whose bounds lie within
* its parent bounds based on the provided preferredBoundFlags. If no match is found based on
* the preferred flags, and a nonzero acceptableBoundFlags is specified, the last view whose
* bounds lie within its parent view based on the acceptableBoundFlags is returned. If no such
* view is found based on either of these two flags, null is returned.
* @param fromIndex The view position index to start the search from.
* @param toIndex The view position index to end the search at.
* @param preferredBoundFlags The flags indicating the preferred match. Once a match is found
* based on this flag, that view is returned instantly.
* @param acceptableBoundFlags The flags indicating the acceptable match if no preferred match
* is found. If so, and if acceptableBoundFlags is non-zero, the
* last matching acceptable view is returned. Otherwise, null is
* returned.
* @return The first view that satisfies acceptableBoundFlags or the last view satisfying
* acceptableBoundFlags boundary conditions.
*/
View findOneViewWithinBoundFlags(int fromIndex, int toIndex,
@ViewBounds int preferredBoundFlags,
@ViewBounds int acceptableBoundFlags) {
final int start = mCallback.getParentStart();
final int end = mCallback.getParentEnd();
final int next = toIndex > fromIndex ? 1 : -1;
View acceptableMatch = null;
for (int i = fromIndex; i != toIndex; i += next) {
final View child = mCallback.getChildAt(i);
final int childStart = mCallback.getChildStart(child);
final int childEnd = mCallback.getChildEnd(child);
mBoundFlags.setBounds(start, end, childStart, childEnd);
if (preferredBoundFlags != 0) {
mBoundFlags.resetFlags();
mBoundFlags.addFlags(preferredBoundFlags);
if (mBoundFlags.boundsMatch()) {
// found a perfect match
return child;
}
}
if (acceptableBoundFlags != 0) {
mBoundFlags.resetFlags();
mBoundFlags.addFlags(acceptableBoundFlags);
if (mBoundFlags.boundsMatch()) {
acceptableMatch = child;
}
}
}
return acceptableMatch;
}
/**
* Returns whether the specified view lies within the boundary condition of its parent view.
* @param child The child view to be checked.
* @param boundsFlags The flag against which the child view and parent view are matched.
* @return True if the view meets the boundsFlag, false otherwise.
*/
boolean isViewWithinBoundFlags(View child, @ViewBounds int boundsFlags) {
mBoundFlags.setBounds(mCallback.getParentStart(), mCallback.getParentEnd(),
mCallback.getChildStart(child), mCallback.getChildEnd(child));
if (boundsFlags != 0) {
mBoundFlags.resetFlags();
mBoundFlags.addFlags(boundsFlags);
return mBoundFlags.boundsMatch();
}
return false;
}
/**
* Callback provided by the user of this class in order to retrieve information about child and
* parent boundaries.
*/
interface Callback {
View getChildAt(int index);
int getParentStart();
int getParentEnd();
int getChildStart(View view);
int getChildEnd(View view);
}
}

@ -0,0 +1,329 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_POST;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE;
import static androidx.recyclerview.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.LongSparseArray;
import androidx.collection.SimpleArrayMap;
import androidx.core.util.Pools;
/**
* This class abstracts all tracking for Views to run animations.
*/
class ViewInfoStore {
private static final boolean DEBUG = false;
/**
* View data records for pre-layout
*/
@VisibleForTesting
final SimpleArrayMap<RecyclerView.ViewHolder, InfoRecord> mLayoutHolderMap =
new SimpleArrayMap<>();
@VisibleForTesting
final LongSparseArray<RecyclerView.ViewHolder> mOldChangedHolders = new LongSparseArray<>();
/**
* Clears the state and all existing tracking data
*/
void clear() {
mLayoutHolderMap.clear();
mOldChangedHolders.clear();
}
/**
* Adds the item information to the prelayout tracking
* @param holder The ViewHolder whose information is being saved
* @param info The information to save
*/
void addToPreLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.preInfo = info;
record.flags |= FLAG_PRE;
}
boolean isDisappearing(RecyclerView.ViewHolder holder) {
final InfoRecord record = mLayoutHolderMap.get(holder);
return record != null && ((record.flags & FLAG_DISAPPEARED) != 0);
}
/**
* Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it.
*
* @param vh The ViewHolder whose information is being queried
* @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
*/
@Nullable
RecyclerView.ItemAnimator.ItemHolderInfo popFromPreLayout(RecyclerView.ViewHolder vh) {
return popFromLayoutStep(vh, FLAG_PRE);
}
/**
* Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it.
*
* @param vh The ViewHolder whose information is being queried
* @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
*/
@Nullable
RecyclerView.ItemAnimator.ItemHolderInfo popFromPostLayout(RecyclerView.ViewHolder vh) {
return popFromLayoutStep(vh, FLAG_POST);
}
private RecyclerView.ItemAnimator.ItemHolderInfo popFromLayoutStep(RecyclerView.ViewHolder vh, int flag) {
int index = mLayoutHolderMap.indexOfKey(vh);
if (index < 0) {
return null;
}
final InfoRecord record = mLayoutHolderMap.valueAt(index);
if (record != null && (record.flags & flag) != 0) {
record.flags &= ~flag;
final RecyclerView.ItemAnimator.ItemHolderInfo info;
if (flag == FLAG_PRE) {
info = record.preInfo;
} else if (flag == FLAG_POST) {
info = record.postInfo;
} else {
throw new IllegalArgumentException("Must provide flag PRE or POST");
}
// if not pre-post flag is left, clear.
if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
mLayoutHolderMap.removeAt(index);
InfoRecord.recycle(record);
}
return info;
}
return null;
}
/**
* Adds the given ViewHolder to the oldChangeHolders list
* @param key The key to identify the ViewHolder.
* @param holder The ViewHolder to store
*/
void addToOldChangeHolders(long key, RecyclerView.ViewHolder holder) {
mOldChangedHolders.put(key, holder);
}
/**
* Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the
* LayoutManager during a pre-layout pass. We distinguish them from other views that were
* already in the pre-layout so that ItemAnimator can choose to run a different animation for
* them.
*
* @param holder The ViewHolder to store
* @param info The information to save
*/
void addToAppearedInPreLayoutHolders(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.flags |= FLAG_APPEAR;
record.preInfo = info;
}
/**
* Checks whether the given ViewHolder is in preLayout list
* @param viewHolder The ViewHolder to query
*
* @return True if the ViewHolder is present in preLayout, false otherwise
*/
boolean isInPreLayout(RecyclerView.ViewHolder viewHolder) {
final InfoRecord record = mLayoutHolderMap.get(viewHolder);
return record != null && (record.flags & FLAG_PRE) != 0;
}
/**
* Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns
* null.
* @param key The key to be used to find the ViewHolder.
*
* @return A ViewHolder if exists or null if it does not exist.
*/
RecyclerView.ViewHolder getFromOldChangeHolders(long key) {
return mOldChangedHolders.get(key);
}
/**
* Adds the item information to the post layout list
* @param holder The ViewHolder whose information is being saved
* @param info The information to save
*/
void addToPostLayout(RecyclerView.ViewHolder holder, RecyclerView.ItemAnimator.ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.postInfo = info;
record.flags |= FLAG_POST;
}
/**
* A ViewHolder might be added by the LayoutManager just to animate its disappearance.
* This list holds such items so that we can animate / recycle these ViewHolders properly.
*
* @param holder The ViewHolder which disappeared during a layout.
*/
void addToDisappearedInLayout(RecyclerView.ViewHolder holder) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.flags |= FLAG_DISAPPEARED;
}
/**
* Removes a ViewHolder from disappearing list.
* @param holder The ViewHolder to be removed from the disappearing list.
*/
void removeFromDisappearedInLayout(RecyclerView.ViewHolder holder) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
return;
}
record.flags &= ~FLAG_DISAPPEARED;
}
void process(ProcessCallback callback) {
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
final InfoRecord record = mLayoutHolderMap.removeAt(index);
if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
// Appeared then disappeared. Not useful for animations.
callback.unused(viewHolder);
} else if ((record.flags & FLAG_DISAPPEARED) != 0) {
// Set as "disappeared" by the LayoutManager (addDisappearingView)
if (record.preInfo == null) {
// similar to appear disappear but happened between different layout passes.
// this can happen when the layout manager is using auto-measure
callback.unused(viewHolder);
} else {
callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
}
} else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
// Appeared in the layout but not in the adapter (e.g. entered the viewport)
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
// Persistent in both passes. Animate persistence
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_PRE) != 0) {
// Was in pre-layout, never been added to post layout
callback.processDisappeared(viewHolder, record.preInfo, null);
} else if ((record.flags & FLAG_POST) != 0) {
// Was not in pre-layout, been added to post layout
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
} else if ((record.flags & FLAG_APPEAR) != 0) {
// Scrap view. RecyclerView will handle removing/recycling this.
} else if (DEBUG) {
throw new IllegalStateException("record without any reasonable flag combination:/");
}
InfoRecord.recycle(record);
}
}
/**
* Removes the ViewHolder from all list
* @param holder The ViewHolder which we should stop tracking
*/
void removeViewHolder(RecyclerView.ViewHolder holder) {
for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) {
if (holder == mOldChangedHolders.valueAt(i)) {
mOldChangedHolders.removeAt(i);
break;
}
}
final InfoRecord info = mLayoutHolderMap.remove(holder);
if (info != null) {
InfoRecord.recycle(info);
}
}
void onDetach() {
InfoRecord.drainCache();
}
public void onViewDetached(RecyclerView.ViewHolder viewHolder) {
removeFromDisappearedInLayout(viewHolder);
}
interface ProcessCallback {
void processDisappeared(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo,
@Nullable RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
void processAppeared(RecyclerView.ViewHolder viewHolder, @Nullable RecyclerView.ItemAnimator.ItemHolderInfo preInfo,
RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
void processPersistent(RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ItemAnimator.ItemHolderInfo preInfo,
@NonNull RecyclerView.ItemAnimator.ItemHolderInfo postInfo);
void unused(RecyclerView.ViewHolder holder);
}
static class InfoRecord {
// disappearing list
static final int FLAG_DISAPPEARED = 1;
// appear in pre layout list
static final int FLAG_APPEAR = 1 << 1;
// pre layout, this is necessary to distinguish null item info
static final int FLAG_PRE = 1 << 2;
// post layout, this is necessary to distinguish null item info
static final int FLAG_POST = 1 << 3;
static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
int flags;
@Nullable
RecyclerView.ItemAnimator.ItemHolderInfo preInfo;
@Nullable
RecyclerView.ItemAnimator.ItemHolderInfo postInfo;
static Pools.Pool<InfoRecord> sPool = new Pools.SimplePool<>(20);
private InfoRecord() {
}
static InfoRecord obtain() {
InfoRecord record = sPool.acquire();
return record == null ? new InfoRecord() : record;
}
static void recycle(InfoRecord record) {
record.flags = 0;
record.preInfo = null;
record.postInfo = null;
sPool.release(record);
}
static void drainCache() {
//noinspection StatementWithEmptyBody
while (sPool.acquire() != null);
}
}
}

@ -0,0 +1,197 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.recyclerview.widget;
import android.util.SparseArray;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
/**
* Used by {@link ConcatAdapter} to isolate view types between nested adapters, if necessary.
*/
interface ViewTypeStorage {
@NonNull
NestedAdapterWrapper getWrapperForGlobalType(int globalViewType);
@NonNull
ViewTypeLookup createViewTypeWrapper(
@NonNull NestedAdapterWrapper wrapper
);
/**
* Api given to {@link NestedAdapterWrapper}s.
*/
interface ViewTypeLookup {
int localToGlobal(int localType);
int globalToLocal(int globalType);
void dispose();
}
class SharedIdRangeViewTypeStorage implements ViewTypeStorage {
// we keep a list of nested wrappers here even though we only need 1 to create because
// they might be removed.
SparseArray<List<NestedAdapterWrapper>> mGlobalTypeToWrapper = new SparseArray<>();
@NonNull
@Override
public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
List<NestedAdapterWrapper> nestedAdapterWrappers = mGlobalTypeToWrapper.get(
globalViewType);
if (nestedAdapterWrappers == null || nestedAdapterWrappers.isEmpty()) {
throw new IllegalArgumentException("Cannot find the wrapper for global view"
+ " type " + globalViewType);
}
// just return the first one since they are shared
return nestedAdapterWrappers.get(0);
}
@NonNull
@Override
public ViewTypeLookup createViewTypeWrapper(
@NonNull NestedAdapterWrapper wrapper) {
return new WrapperViewTypeLookup(wrapper);
}
void removeWrapper(@NonNull NestedAdapterWrapper wrapper) {
for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) {
List<NestedAdapterWrapper> wrappers = mGlobalTypeToWrapper.valueAt(i);
if (wrappers.remove(wrapper)) {
if (wrappers.isEmpty()) {
mGlobalTypeToWrapper.removeAt(i);
}
}
}
}
class WrapperViewTypeLookup implements ViewTypeLookup {
final NestedAdapterWrapper mWrapper;
WrapperViewTypeLookup(NestedAdapterWrapper wrapper) {
mWrapper = wrapper;
}
@Override
public int localToGlobal(int localType) {
// register it first
List<NestedAdapterWrapper> wrappers = mGlobalTypeToWrapper.get(
localType);
if (wrappers == null) {
wrappers = new ArrayList<>();
mGlobalTypeToWrapper.put(localType, wrappers);
}
if (!wrappers.contains(mWrapper)) {
wrappers.add(mWrapper);
}
return localType;
}
@Override
public int globalToLocal(int globalType) {
return globalType;
}
@Override
public void dispose() {
removeWrapper(mWrapper);
}
}
}
class IsolatedViewTypeStorage implements ViewTypeStorage {
SparseArray<NestedAdapterWrapper> mGlobalTypeToWrapper = new SparseArray<>();
int mNextViewType = 0;
int obtainViewType(NestedAdapterWrapper wrapper) {
int nextId = mNextViewType++;
mGlobalTypeToWrapper.put(nextId, wrapper);
return nextId;
}
@NonNull
@Override
public NestedAdapterWrapper getWrapperForGlobalType(int globalViewType) {
NestedAdapterWrapper wrapper = mGlobalTypeToWrapper.get(
globalViewType);
if (wrapper == null) {
throw new IllegalArgumentException("Cannot find the wrapper for global"
+ " view type " + globalViewType);
}
return wrapper;
}
@Override
@NonNull
public ViewTypeLookup createViewTypeWrapper(
@NonNull NestedAdapterWrapper wrapper) {
return new WrapperViewTypeLookup(wrapper);
}
void removeWrapper(@NonNull NestedAdapterWrapper wrapper) {
for (int i = mGlobalTypeToWrapper.size() - 1; i >= 0; i--) {
NestedAdapterWrapper existingWrapper = mGlobalTypeToWrapper.valueAt(i);
if (existingWrapper == wrapper) {
mGlobalTypeToWrapper.removeAt(i);
}
}
}
class WrapperViewTypeLookup implements ViewTypeLookup {
private SparseIntArray mLocalToGlobalMapping = new SparseIntArray(1);
private SparseIntArray mGlobalToLocalMapping = new SparseIntArray(1);
final NestedAdapterWrapper mWrapper;
WrapperViewTypeLookup(NestedAdapterWrapper wrapper) {
mWrapper = wrapper;
}
@Override
public int localToGlobal(int localType) {
int index = mLocalToGlobalMapping.indexOfKey(localType);
if (index > -1) {
return mLocalToGlobalMapping.valueAt(index);
}
// get a new key.
int globalType = obtainViewType(mWrapper);
mLocalToGlobalMapping.put(localType, globalType);
mGlobalToLocalMapping.put(globalType, localType);
return globalType;
}
@Override
public int globalToLocal(int globalType) {
int index = mGlobalToLocalMapping.indexOfKey(globalType);
if (index < 0) {
throw new IllegalStateException("requested global type " + globalType + " does"
+ " not belong to the adapter:" + mWrapper.adapter);
}
return mGlobalToLocalMapping.valueAt(index);
}
@Override
public void dispose() {
removeWrapper(mWrapper);
}
}
}
}
Loading…
Cancel
Save