mirror of https://github.com/M66B/FairEmail.git
parent
be2bbed659
commit
445dc9f25f
@ -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<List<User>> usersByLastName();
|
||||
* }
|
||||
*
|
||||
* class MyViewModel extends ViewModel {
|
||||
* public final LiveData<List<User>> usersList;
|
||||
* public MyViewModel(UserDao userDao) {
|
||||
* usersList = userDao.usersByLastName();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyActivity extends AppCompatActivity {
|
||||
* {@literal @}Override
|
||||
* public void onCreate(Bundle savedState) {
|
||||
* super.onCreate(savedState);
|
||||
* MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
|
||||
* RecyclerView recyclerView = findViewById(R.id.user_list);
|
||||
* UserAdapter adapter = new UserAdapter();
|
||||
* viewModel.usersList.observe(this, list -> adapter.submitList(list));
|
||||
* recyclerView.setAdapter(adapter);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class UserAdapter extends RecyclerView.Adapter<UserViewHolder> {
|
||||
* private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK);
|
||||
* {@literal @}Override
|
||||
* public int getItemCount() {
|
||||
* return mDiffer.getCurrentList().size();
|
||||
* }
|
||||
* public void submitList(List<User> list) {
|
||||
* mDiffer.submitList(list);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public void onBindViewHolder(UserViewHolder holder, int position) {
|
||||
* User user = mDiffer.getCurrentList().get(position);
|
||||
* holder.bindTo(user);
|
||||
* }
|
||||
* public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK
|
||||
* = new DiffUtil.ItemCallback<User>() {
|
||||
* {@literal @}Override
|
||||
* public boolean areItemsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // User properties may have changed if reloaded from the DB, but ID is fixed
|
||||
* return oldUser.getId() == newUser.getId();
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean areContentsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // NOTE: if you use equals, your object must properly override Object#equals()
|
||||
* // Incorrectly returning false here will result in too many animations.
|
||||
* return oldUser.equals(newUser);
|
||||
* }
|
||||
* }
|
||||
* }</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();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
+ '}';
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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<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<List<User>> usersByLastName();
|
||||
* }
|
||||
*
|
||||
* class MyViewModel extends ViewModel {
|
||||
* public final LiveData<List<User>> usersList;
|
||||
* public MyViewModel(UserDao userDao) {
|
||||
* usersList = userDao.usersByLastName();
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class MyActivity extends AppCompatActivity {
|
||||
* {@literal @}Override
|
||||
* public void onCreate(Bundle savedState) {
|
||||
* super.onCreate(savedState);
|
||||
* MyViewModel viewModel = new ViewModelProvider(this).get(MyViewModel.class);
|
||||
* RecyclerView recyclerView = findViewById(R.id.user_list);
|
||||
* UserAdapter<User> adapter = new UserAdapter();
|
||||
* viewModel.usersList.observe(this, list -> adapter.submitList(list));
|
||||
* recyclerView.setAdapter(adapter);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* class UserAdapter extends ListAdapter<User, UserViewHolder> {
|
||||
* public UserAdapter() {
|
||||
* super(User.DIFF_CALLBACK);
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public void onBindViewHolder(UserViewHolder holder, int position) {
|
||||
* holder.bindTo(getItem(position));
|
||||
* }
|
||||
* public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK =
|
||||
* new DiffUtil.ItemCallback<User>() {
|
||||
* {@literal @}Override
|
||||
* public boolean areItemsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // User properties may have changed if reloaded from the DB, but ID is fixed
|
||||
* return oldUser.getId() == newUser.getId();
|
||||
* }
|
||||
* {@literal @}Override
|
||||
* public boolean areContentsTheSame(
|
||||
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
|
||||
* // NOTE: if you use equals, your object must properly override Object#equals()
|
||||
* // Incorrectly returning false here will result in too many animations.
|
||||
* return oldUser.equals(newUser);
|
||||
* }
|
||||
* }
|
||||
* }</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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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…
Reference in new issue