Merge "Port RecyclerView to com.android.internal.widget.RecyclerView"
This commit is contained in:
committed by
Android (Google) Code Review
commit
4ced22d644
775
core/java/com/android/internal/widget/AdapterHelper.java
Normal file
775
core/java/com/android/internal/widget/AdapterHelper.java
Normal file
@@ -0,0 +1,775 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Pools;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class that can enqueue and process adapter update operations.
|
||||
* <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.
|
||||
*/
|
||||
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 class UpdateOp {
|
||||
|
||||
static final int ADD = 1;
|
||||
|
||||
static final int REMOVE = 1 << 1;
|
||||
|
||||
static final int UPDATE = 1 << 2;
|
||||
|
||||
static final int MOVE = 1 << 3;
|
||||
|
||||
static final int POOL_SIZE = 30;
|
||||
|
||||
int cmd;
|
||||
|
||||
int positionStart;
|
||||
|
||||
Object payload;
|
||||
|
||||
// holds the target position if this is a MOVE
|
||||
int itemCount;
|
||||
|
||||
UpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
|
||||
this.cmd = cmd;
|
||||
this.positionStart = positionStart;
|
||||
this.itemCount = itemCount;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
String cmdToString() {
|
||||
switch (cmd) {
|
||||
case ADD:
|
||||
return "add";
|
||||
case REMOVE:
|
||||
return "rm";
|
||||
case UPDATE:
|
||||
return "up";
|
||||
case MOVE:
|
||||
return "mv";
|
||||
}
|
||||
return "??";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Integer.toHexString(System.identityHashCode(this))
|
||||
+ "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount
|
||||
+ ",p:" + payload + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateOp op = (UpdateOp) o;
|
||||
|
||||
if (cmd != op.cmd) {
|
||||
return false;
|
||||
}
|
||||
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
|
||||
// reverse of this is also true
|
||||
if (itemCount == op.positionStart && positionStart == op.itemCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (itemCount != op.itemCount) {
|
||||
return false;
|
||||
}
|
||||
if (positionStart != op.positionStart) {
|
||||
return false;
|
||||
}
|
||||
if (payload != null) {
|
||||
if (!payload.equals(op.payload)) {
|
||||
return false;
|
||||
}
|
||||
} else if (op.payload != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = cmd;
|
||||
result = 31 * result + positionStart;
|
||||
result = 31 * result + itemCount;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount, Object payload) {
|
||||
UpdateOp op = mUpdateOpPool.acquire();
|
||||
if (op == null) {
|
||||
op = new UpdateOp(cmd, positionStart, itemCount, payload);
|
||||
} else {
|
||||
op.cmd = cmd;
|
||||
op.positionStart = positionStart;
|
||||
op.itemCount = itemCount;
|
||||
op.payload = payload;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycleUpdateOp(UpdateOp op) {
|
||||
if (!mDisableRecycler) {
|
||||
op.payload = null;
|
||||
mUpdateOpPool.release(op);
|
||||
}
|
||||
}
|
||||
|
||||
void recycleUpdateOpsAndClearList(List<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);
|
||||
}
|
||||
}
|
||||
538
core/java/com/android/internal/widget/ChildHelper.java
Normal file
538
core/java/com/android/internal/widget/ChildHelper.java
Normal file
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to manage children.
|
||||
* <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";
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
final Bucket mBucket;
|
||||
|
||||
final List<View> mHiddenViews;
|
||||
|
||||
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) {
|
||||
int index = mCallback.indexOfChild(view);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (mBucket.remove(index)) {
|
||||
unhideViewInternal(view);
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "remove View off:" + index + "," + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the view at the provided index from RecyclerView.
|
||||
*
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
*/
|
||||
void removeViewAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
final View view = mCallback.getChildAt(offset);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
if (mBucket.remove(offset)) {
|
||||
unhideViewInternal(view);
|
||||
}
|
||||
mCallback.removeViewAt(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child at provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
View getChildAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
return mCallback.getChildAt(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all views from the ViewGroup including the hidden ones.
|
||||
*/
|
||||
void removeAllViewsUnfiltered() {
|
||||
mBucket.reset();
|
||||
for (int i = mHiddenViews.size() - 1; i >= 0; i--) {
|
||||
mCallback.onLeftHiddenState(mHiddenViews.get(i));
|
||||
mHiddenViews.remove(i);
|
||||
}
|
||||
mCallback.removeAllViews();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeAllViewsUnfiltered");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be used to find a disappearing view by position.
|
||||
*
|
||||
* @param position The adapter position of the item.
|
||||
* @return A hidden view with a valid ViewHolder that matches the position.
|
||||
*/
|
||||
View findHiddenNonRemovedView(int position) {
|
||||
final int count = mHiddenViews.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View view = mHiddenViews.get(i);
|
||||
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
|
||||
if (holder.getLayoutPosition() == position
|
||||
&& !holder.isInvalid()
|
||||
&& !holder.isRemoved()) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the provided view to the underlying ViewGroup.
|
||||
*
|
||||
* @param child Child to attach.
|
||||
* @param index Index of the child to attach in regular perspective.
|
||||
* @param layoutParams LayoutParams for the child.
|
||||
* @param hidden If set to true, this item will be invisible to the regular methods.
|
||||
*/
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
|
||||
boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mBucket.insert(offset, hidden);
|
||||
if (hidden) {
|
||||
hideViewInternal(child);
|
||||
}
|
||||
mCallback.attachViewToParent(child, offset, layoutParams);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + ","
|
||||
+ "h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of children that are not hidden.
|
||||
*
|
||||
* @return Number of children that are not hidden.
|
||||
* @see #getChildAt(int)
|
||||
*/
|
||||
int getChildCount() {
|
||||
return mCallback.getChildCount() - mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of children.
|
||||
*
|
||||
* @return The total number of children including the hidden views.
|
||||
* @see #getUnfilteredChildAt(int)
|
||||
*/
|
||||
int getUnfilteredChildCount() {
|
||||
return mCallback.getChildCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
|
||||
*
|
||||
* @param index ViewGroup index of the child to return.
|
||||
* @return The view in the provided index.
|
||||
*/
|
||||
View getUnfilteredChildAt(int index) {
|
||||
return mCallback.getChildAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the view at the provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
void detachViewFromParent(int index) {
|
||||
final int offset = getOffset(index);
|
||||
mBucket.remove(offset);
|
||||
mCallback.detachViewFromParent(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the child in regular perspective.
|
||||
*
|
||||
* @param child The child whose index will be returned.
|
||||
* @return The regular perspective index of the child or -1 if it does not exists.
|
||||
*/
|
||||
int indexOfChild(View child) {
|
||||
final int index = mCallback.indexOfChild(child);
|
||||
if (index == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
if (DEBUG) {
|
||||
throw new IllegalArgumentException("cannot get index of a hidden child");
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// reverse the index
|
||||
return index - mBucket.countOnesBefore(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View is visible to LayoutManager or not.
|
||||
*
|
||||
* @param view The child view to check. Should be a child of the Callback.
|
||||
* @return True if the View is not visible to LayoutManager
|
||||
*/
|
||||
boolean isHidden(View view) {
|
||||
return mHiddenViews.contains(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a child view as hidden.
|
||||
*
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
void hide(View view) {
|
||||
final int offset = mCallback.indexOfChild(view);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
|
||||
}
|
||||
if (DEBUG && mBucket.get(offset)) {
|
||||
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
|
||||
}
|
||||
mBucket.set(offset);
|
||||
hideViewInternal(view);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hiding child " + view + " at offset " + offset + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a child view from hidden list to regular list.
|
||||
* Calling this method should probably be followed by a detach, otherwise, it will suddenly
|
||||
* show up in LayoutManager's children list.
|
||||
*
|
||||
* @param view The hidden View to unhide
|
||||
*/
|
||||
void unhide(View view) {
|
||||
final int offset = mCallback.indexOfChild(view);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
|
||||
}
|
||||
if (!mBucket.get(offset)) {
|
||||
throw new RuntimeException("trying to unhide a view that was not hidden" + view);
|
||||
}
|
||||
mBucket.clear(offset);
|
||||
unhideViewInternal(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view from the ViewGroup if it is hidden.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
* @return True if the View is found and it is hidden. False otherwise.
|
||||
*/
|
||||
boolean removeViewIfHidden(View view) {
|
||||
final int index = mCallback.indexOfChild(view);
|
||||
if (index == -1) {
|
||||
if (unhideViewInternal(view) && DEBUG) {
|
||||
throw new IllegalStateException("view is in hidden list but not in view group");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
mBucket.remove(index);
|
||||
if (!unhideViewInternal(view) && DEBUG) {
|
||||
throw new IllegalStateException(
|
||||
"removed a hidden view but it is not in hidden views list");
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitset implementation that provides methods to offset indices.
|
||||
*/
|
||||
static class Bucket {
|
||||
|
||||
static final int BITS_PER_WORD = Long.SIZE;
|
||||
|
||||
static final long LAST_BIT = 1L << (Long.SIZE - 1);
|
||||
|
||||
long mData = 0;
|
||||
|
||||
Bucket mNext;
|
||||
|
||||
void set(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
mNext.set(index - BITS_PER_WORD);
|
||||
} else {
|
||||
mData |= 1L << index;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNext() {
|
||||
if (mNext == null) {
|
||||
mNext = new Bucket();
|
||||
}
|
||||
}
|
||||
|
||||
void clear(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
if (mNext != null) {
|
||||
mNext.clear(index - BITS_PER_WORD);
|
||||
}
|
||||
} else {
|
||||
mData &= ~(1L << index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean get(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return mNext.get(index - BITS_PER_WORD);
|
||||
} else {
|
||||
return (mData & (1L << index)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mData = 0;
|
||||
if (mNext != null) {
|
||||
mNext.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void insert(int index, boolean value) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
mNext.insert(index - BITS_PER_WORD, value);
|
||||
} else {
|
||||
final boolean lastBit = (mData & LAST_BIT) != 0;
|
||||
long mask = (1L << index) - 1;
|
||||
final long before = mData & mask;
|
||||
final long after = ((mData & ~mask)) << 1;
|
||||
mData = before | after;
|
||||
if (value) {
|
||||
set(index);
|
||||
} else {
|
||||
clear(index);
|
||||
}
|
||||
if (lastBit || mNext != null) {
|
||||
ensureNext();
|
||||
mNext.insert(0, lastBit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return mNext.remove(index - BITS_PER_WORD);
|
||||
} else {
|
||||
long mask = (1L << index);
|
||||
final boolean value = (mData & mask) != 0;
|
||||
mData &= ~mask;
|
||||
mask = mask - 1;
|
||||
final long before = mData & mask;
|
||||
// cannot use >> because it adds one.
|
||||
final long after = Long.rotateRight(mData & ~mask, 1);
|
||||
mData = before | after;
|
||||
if (mNext != null) {
|
||||
if (mNext.get(0)) {
|
||||
set(BITS_PER_WORD - 1);
|
||||
}
|
||||
mNext.remove(0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int countOnesBefore(int index) {
|
||||
if (mNext == null) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
return Long.bitCount(mData);
|
||||
}
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
}
|
||||
if (index < BITS_PER_WORD) {
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
} else {
|
||||
return mNext.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mNext == null ? Long.toBinaryString(mData)
|
||||
: mNext.toString() + "xx" + Long.toBinaryString(mData);
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
int getChildCount();
|
||||
|
||||
void addView(View child, int index);
|
||||
|
||||
int indexOfChild(View view);
|
||||
|
||||
void removeViewAt(int index);
|
||||
|
||||
View getChildAt(int offset);
|
||||
|
||||
void removeAllViews();
|
||||
|
||||
RecyclerView.ViewHolder getChildViewHolder(View view);
|
||||
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
|
||||
|
||||
void detachViewFromParent(int offset);
|
||||
|
||||
void onEnteredHiddenState(View child);
|
||||
|
||||
void onLeftHiddenState(View child);
|
||||
}
|
||||
}
|
||||
|
||||
668
core/java/com/android/internal/widget/DefaultItemAnimator.java
Normal file
668
core/java/com/android/internal/widget/DefaultItemAnimator.java
Normal file
@@ -0,0 +1,668 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
|
||||
import com.android.internal.widget.RecyclerView.ViewHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class DefaultItemAnimator extends SimpleItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static TimeInterpolator sDefaultInterpolator;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
|
||||
|
||||
ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
|
||||
ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
|
||||
ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
|
||||
|
||||
ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
|
||||
ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
|
||||
ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
|
||||
ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{"
|
||||
+ "oldHolder=" + oldHolder
|
||||
+ ", newHolder=" + newHolder
|
||||
+ ", fromX=" + fromX
|
||||
+ ", fromY=" + fromY
|
||||
+ ", toX=" + toX
|
||||
+ ", toY=" + toY
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<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;
|
||||
view.postOnAnimationDelayed(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) {
|
||||
ViewHolder holder = changes.get(0).oldHolder;
|
||||
holder.itemView.postOnAnimationDelayed(changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<ViewHolder> additions = new ArrayList<>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
view.postOnAnimationDelayed(adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mRemoveAnimations.add(holder);
|
||||
animation.setDuration(getRemoveDuration()).alpha(0).setListener(
|
||||
new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
view.setAlpha(1);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
resetAnimation(holder);
|
||||
holder.itemView.setAlpha(0);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mAddAnimations.add(holder);
|
||||
animation.alpha(1).setDuration(getAddDuration())
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {
|
||||
view.setAlpha(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += holder.itemView.getTranslationX();
|
||||
fromY += holder.itemView.getTranslationY();
|
||||
resetAnimation(holder);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
if (deltaX != 0) {
|
||||
view.setTranslationX(-deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.setTranslationY(-deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
view.animate().translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.animate().translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimator animation = view.animate();
|
||||
mMoveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animator) {
|
||||
if (deltaX != 0) {
|
||||
view.setTranslationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
view.setTranslationY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
if (oldHolder == newHolder) {
|
||||
// Don't know how to run change animations when the same view holder is re-used.
|
||||
// run a move animation to handle position changes.
|
||||
return animateMove(oldHolder, fromX, fromY, toX, toY);
|
||||
}
|
||||
final float prevTranslationX = oldHolder.itemView.getTranslationX();
|
||||
final float prevTranslationY = oldHolder.itemView.getTranslationY();
|
||||
final float prevAlpha = oldHolder.itemView.getAlpha();
|
||||
resetAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
oldHolder.itemView.setTranslationX(prevTranslationX);
|
||||
oldHolder.itemView.setTranslationY(prevTranslationY);
|
||||
oldHolder.itemView.setAlpha(prevAlpha);
|
||||
if (newHolder != null) {
|
||||
// carry over translation values
|
||||
resetAnimation(newHolder);
|
||||
newHolder.itemView.setTranslationX(-deltaX);
|
||||
newHolder.itemView.setTranslationY(-deltaY);
|
||||
newHolder.itemView.setAlpha(0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimator oldViewAnim = view.animate().setDuration(
|
||||
getChangeDuration());
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
oldViewAnim.setListener(null);
|
||||
view.setAlpha(1);
|
||||
view.setTranslationX(0);
|
||||
view.setTranslationY(0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimator newViewAnimation = newView.animate();
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration())
|
||||
.alpha(1).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
newViewAnimation.setListener(null);
|
||||
newView.setAlpha(1);
|
||||
newView.setTranslationX(0);
|
||||
newView.setTranslationY(0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
|
||||
for (int i = infoList.size() - 1; i >= 0; i--) {
|
||||
ChangeInfo changeInfo = infoList.get(i);
|
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) {
|
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
|
||||
infoList.remove(changeInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
|
||||
if (changeInfo.oldHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
|
||||
}
|
||||
if (changeInfo.newHolder != null) {
|
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
|
||||
}
|
||||
}
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
|
||||
boolean oldItem = false;
|
||||
if (changeInfo.newHolder == item) {
|
||||
changeInfo.newHolder = null;
|
||||
} else if (changeInfo.oldHolder == item) {
|
||||
changeInfo.oldHolder = null;
|
||||
oldItem = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
item.itemView.setAlpha(1);
|
||||
item.itemView.setTranslationX(0);
|
||||
item.itemView.setTranslationY(0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
view.animate().cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
view.setAlpha(1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
view.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<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<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(ViewHolder holder) {
|
||||
if (sDefaultInterpolator == null) {
|
||||
sDefaultInterpolator = new ValueAnimator().getInterpolator();
|
||||
}
|
||||
holder.itemView.animate().setInterpolator(sDefaultInterpolator);
|
||||
endAnimation(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty()
|
||||
|| !mPendingChanges.isEmpty()
|
||||
|| !mPendingMoves.isEmpty()
|
||||
|| !mPendingRemovals.isEmpty()
|
||||
|| !mMoveAnimations.isEmpty()
|
||||
|| !mRemoveAnimations.isEmpty()
|
||||
|| !mAddAnimations.isEmpty()
|
||||
|| !mChangeAnimations.isEmpty()
|
||||
|| !mMovesList.isEmpty()
|
||||
|| !mAdditionsList.isEmpty()
|
||||
|| !mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingAdditions.get(i);
|
||||
item.itemView.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<MoveInfo> moves = mMovesList.get(i);
|
||||
count = moves.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
MoveInfo moveInfo = moves.get(j);
|
||||
ViewHolder item = moveInfo.holder;
|
||||
View view = item.itemView;
|
||||
view.setTranslationY(0);
|
||||
view.setTranslationX(0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
count = additions.size();
|
||||
for (int j = count - 1; j >= 0; j--) {
|
||||
ViewHolder item = additions.get(j);
|
||||
View view = item.itemView;
|
||||
view.setAlpha(1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<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<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(ViewHolder, ViewHolder, int, int, int, int)}, both
|
||||
* ViewHolder arguments will be the same instance.
|
||||
* </li>
|
||||
* <li>
|
||||
* If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
|
||||
* then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
|
||||
* run a move animation instead.
|
||||
* </li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
|
||||
@NonNull List<Object> payloads) {
|
||||
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
|
||||
}
|
||||
}
|
||||
379
core/java/com/android/internal/widget/GapWorker.java
Normal file
379
core/java/com/android/internal/widget/GapWorker.java
Normal file
@@ -0,0 +1,379 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.os.Trace;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
final class GapWorker implements Runnable {
|
||||
|
||||
static final ThreadLocal<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.
|
||||
*/
|
||||
static class LayoutPrefetchRegistryImpl
|
||||
implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
|
||||
int mPrefetchDx;
|
||||
int mPrefetchDy;
|
||||
int[] mPrefetchArray;
|
||||
|
||||
int mCount;
|
||||
|
||||
void setPrefetchVector(int dx, int dy) {
|
||||
mPrefetchDx = dx;
|
||||
mPrefetchDy = dy;
|
||||
}
|
||||
|
||||
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
|
||||
mCount = 0;
|
||||
if (mPrefetchArray != null) {
|
||||
Arrays.fill(mPrefetchArray, -1);
|
||||
}
|
||||
|
||||
final RecyclerView.LayoutManager layout = view.mLayout;
|
||||
if (view.mAdapter != null
|
||||
&& layout != null
|
||||
&& layout.isItemPrefetchEnabled()) {
|
||||
if (nested) {
|
||||
// nested prefetch, only if no adapter updates pending. Note: we don't query
|
||||
// view.hasPendingAdapterUpdates(), as first layout may not have occurred
|
||||
if (!view.mAdapterHelper.hasPendingUpdates()) {
|
||||
layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
|
||||
}
|
||||
} else {
|
||||
// momentum based prefetch, only if we trust current child/adapter state
|
||||
if (!view.hasPendingAdapterUpdates()) {
|
||||
layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
|
||||
view.mState, this);
|
||||
}
|
||||
}
|
||||
|
||||
if (mCount > layout.mPrefetchMaxCountObserved) {
|
||||
layout.mPrefetchMaxCountObserved = mCount;
|
||||
layout.mPrefetchMaxObservedInInitialPrefetch = nested;
|
||||
view.mRecycler.updateViewCacheSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPosition(int layoutPosition, int pixelDistance) {
|
||||
if (pixelDistance < 0) {
|
||||
throw new IllegalArgumentException("Pixel distance must be non-negative");
|
||||
}
|
||||
|
||||
// allocate or expand array as needed, doubling when needed
|
||||
final int storagePosition = mCount * 2;
|
||||
if (mPrefetchArray == null) {
|
||||
mPrefetchArray = new int[4];
|
||||
Arrays.fill(mPrefetchArray, -1);
|
||||
} else if (storagePosition >= mPrefetchArray.length) {
|
||||
final int[] oldArray = mPrefetchArray;
|
||||
mPrefetchArray = new int[storagePosition * 2];
|
||||
System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
|
||||
}
|
||||
|
||||
// add position
|
||||
mPrefetchArray[storagePosition] = layoutPosition;
|
||||
mPrefetchArray[storagePosition + 1] = pixelDistance;
|
||||
|
||||
mCount++;
|
||||
}
|
||||
|
||||
boolean lastPrefetchIncludedPosition(int position) {
|
||||
if (mPrefetchArray != null) {
|
||||
final int count = mCount * 2;
|
||||
for (int i = 0; i < count; i += 2) {
|
||||
if (mPrefetchArray[i] == position) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when prefetch indices are no longer valid for cache prioritization.
|
||||
*/
|
||||
void clearPrefetchPositions() {
|
||||
if (mPrefetchArray != null) {
|
||||
Arrays.fill(mPrefetchArray, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void add(RecyclerView recyclerView) {
|
||||
if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
|
||||
throw new IllegalStateException("RecyclerView already present in worker list!");
|
||||
}
|
||||
mRecyclerViews.add(recyclerView);
|
||||
}
|
||||
|
||||
public void remove(RecyclerView recyclerView) {
|
||||
boolean removeSuccess = mRecyclerViews.remove(recyclerView);
|
||||
if (RecyclerView.DEBUG && !removeSuccess) {
|
||||
throw new IllegalStateException("RecyclerView removal failed!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a prefetch immediately after the current traversal.
|
||||
*/
|
||||
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
|
||||
if (recyclerView.isAttachedToWindow()) {
|
||||
if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
|
||||
throw new IllegalStateException("attempting to post unregistered view!");
|
||||
}
|
||||
if (mPostTimeNs == 0) {
|
||||
mPostTimeNs = recyclerView.getNanoTime();
|
||||
recyclerView.post(this);
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
|
||||
}
|
||||
|
||||
static Comparator<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);
|
||||
view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
|
||||
totalTaskCount += view.mPrefetchRegistry.mCount;
|
||||
}
|
||||
|
||||
// Populate task list from prefetch data...
|
||||
mTasks.ensureCapacity(totalTaskCount);
|
||||
int totalTaskIndex = 0;
|
||||
for (int i = 0; i < viewCount; i++) {
|
||||
RecyclerView view = mRecyclerViews.get(i);
|
||||
LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
|
||||
final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
|
||||
+ Math.abs(prefetchRegistry.mPrefetchDy);
|
||||
for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
|
||||
final Task task;
|
||||
if (totalTaskIndex >= mTasks.size()) {
|
||||
task = new Task();
|
||||
mTasks.add(task);
|
||||
} else {
|
||||
task = mTasks.get(totalTaskIndex);
|
||||
}
|
||||
final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
|
||||
|
||||
task.immediate = distanceToItem <= viewVelocity;
|
||||
task.viewVelocity = viewVelocity;
|
||||
task.distanceToItem = distanceToItem;
|
||||
task.view = view;
|
||||
task.position = prefetchRegistry.mPrefetchArray[j];
|
||||
|
||||
totalTaskIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// ... and priority sort
|
||||
Collections.sort(mTasks, sTaskComparator);
|
||||
}
|
||||
|
||||
static boolean isPrefetchPositionAttached(RecyclerView view, int position) {
|
||||
final int childCount = view.mChildHelper.getUnfilteredChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
View attachedView = view.mChildHelper.getUnfilteredChildAt(i);
|
||||
RecyclerView.ViewHolder holder = RecyclerView.getChildViewHolderInt(attachedView);
|
||||
// Note: can use mPosition here because adapter doesn't have pending updates
|
||||
if (holder.mPosition == position && !holder.isInvalid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
|
||||
int position, long deadlineNs) {
|
||||
if (isPrefetchPositionAttached(view, position)) {
|
||||
// don't attempt to prefetch attached views
|
||||
return null;
|
||||
}
|
||||
|
||||
RecyclerView.Recycler recycler = view.mRecycler;
|
||||
RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
|
||||
position, false, deadlineNs);
|
||||
|
||||
if (holder != null) {
|
||||
if (holder.isBound()) {
|
||||
// Only give the view a chance to go into the cache if binding succeeded
|
||||
// Note that we must use public method, since item may need cleanup
|
||||
recycler.recycleView(holder.itemView);
|
||||
} else {
|
||||
// Didn't bind, so we can't cache the view, but it will stay in the pool until
|
||||
// next prefetch/traversal. If a View fails to bind, it means we didn't have
|
||||
// enough time prior to the deadline (and won't for other instances of this
|
||||
// type, during this GapWorker prefetch pass).
|
||||
recycler.addViewHolderToRecycledViewPool(holder, false);
|
||||
}
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private void prefetchInnerRecyclerViewWithDeadline(@Nullable RecyclerView innerView,
|
||||
long deadlineNs) {
|
||||
if (innerView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (innerView.mDataSetHasChangedAfterLayout
|
||||
&& innerView.mChildHelper.getUnfilteredChildCount() != 0) {
|
||||
// RecyclerView has new data, but old attached views. Clear everything, so that
|
||||
// we can prefetch without partially stale data.
|
||||
innerView.removeAndRecycleViews();
|
||||
}
|
||||
|
||||
// do nested prefetch!
|
||||
final LayoutPrefetchRegistryImpl innerPrefetchRegistry = innerView.mPrefetchRegistry;
|
||||
innerPrefetchRegistry.collectPrefetchPositionsFromView(innerView, true);
|
||||
|
||||
if (innerPrefetchRegistry.mCount != 0) {
|
||||
try {
|
||||
Trace.beginSection(RecyclerView.TRACE_NESTED_PREFETCH_TAG);
|
||||
innerView.mState.prepareForNestedPrefetch(innerView.mAdapter);
|
||||
for (int i = 0; i < innerPrefetchRegistry.mCount * 2; i += 2) {
|
||||
// Note that we ignore immediate flag for inner items because
|
||||
// we have lower confidence they're needed next frame.
|
||||
final int innerPosition = innerPrefetchRegistry.mPrefetchArray[i];
|
||||
prefetchPositionWithDeadline(innerView, innerPosition, deadlineNs);
|
||||
}
|
||||
} finally {
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void flushTaskWithDeadline(Task task, long deadlineNs) {
|
||||
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
|
||||
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
|
||||
task.position, taskDeadlineNs);
|
||||
if (holder != null && holder.mNestedRecyclerView != null) {
|
||||
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
|
||||
}
|
||||
}
|
||||
|
||||
private void flushTasksWithDeadline(long deadlineNs) {
|
||||
for (int i = 0; i < mTasks.size(); i++) {
|
||||
final Task task = mTasks.get(i);
|
||||
if (task.view == null) {
|
||||
break; // done with populated tasks
|
||||
}
|
||||
flushTaskWithDeadline(task, deadlineNs);
|
||||
task.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void prefetch(long deadlineNs) {
|
||||
buildTaskList();
|
||||
flushTasksWithDeadline(deadlineNs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);
|
||||
|
||||
if (mRecyclerViews.isEmpty()) {
|
||||
// abort - no work to do
|
||||
return;
|
||||
}
|
||||
|
||||
// Query last vsync so we can predict next one. Note that drawing time not yet
|
||||
// valid in animation/input callbacks, so query it here to be safe.
|
||||
long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
|
||||
mRecyclerViews.get(0).getDrawingTime());
|
||||
if (lastFrameVsyncNs == 0) {
|
||||
// abort - couldn't get last vsync for estimating next
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: consider rebasing deadline if frame was already dropped due to long UI work.
|
||||
// Next frame will still wait for VSYNC, so we can still use the gap if it exists.
|
||||
long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;
|
||||
|
||||
prefetch(nextFrameNs);
|
||||
|
||||
// TODO: consider rescheduling self, if there's more work to do
|
||||
} finally {
|
||||
mPostTimeNs = 0;
|
||||
Trace.endSection();
|
||||
}
|
||||
}
|
||||
}
|
||||
2398
core/java/com/android/internal/widget/LinearLayoutManager.java
Normal file
2398
core/java/com/android/internal/widget/LinearLayoutManager.java
Normal file
File diff suppressed because it is too large
Load Diff
361
core/java/com/android/internal/widget/LinearSmoothScroller.java
Normal file
361
core/java/com/android/internal/widget/LinearSmoothScroller.java
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
/**
|
||||
* {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
|
||||
* the target position becomes a child of the RecyclerView and then uses a
|
||||
* {@link DecelerateInterpolator} to slowly approach to target position.
|
||||
* <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 String TAG = "LinearSmoothScroller";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final float MILLISECONDS_PER_INCH = 25f;
|
||||
|
||||
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
|
||||
|
||||
/**
|
||||
* Align child view's left or top with parent view's left or top
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(android.view.View, int)
|
||||
* @see #calculateDyToMakeVisible(android.view.View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_START = -1;
|
||||
|
||||
/**
|
||||
* Align child view's right or bottom with parent view's right or bottom
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(android.view.View, int)
|
||||
* @see #calculateDyToMakeVisible(android.view.View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_END = 1;
|
||||
|
||||
/**
|
||||
* <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();
|
||||
|
||||
protected PointF mTargetVector;
|
||||
|
||||
private final float MILLISECONDS_PER_PX;
|
||||
|
||||
// Temporary variables to keep track of the interim scroll target. These values do not
|
||||
// point to a real item position, rather point to an estimated location pixels.
|
||||
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
|
||||
|
||||
public LinearSmoothScroller(Context context) {
|
||||
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStart() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
|
||||
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
|
||||
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
|
||||
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
|
||||
final int time = calculateTimeForDeceleration(distance);
|
||||
if (time > 0) {
|
||||
action.update(-dx, -dy, time, mDecelerateInterpolator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
|
||||
if (getChildCount() == 0) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && mTargetVector != null
|
||||
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
|
||||
throw new IllegalStateException("Scroll happened in the opposite direction"
|
||||
+ " of the target. Some calculations are wrong");
|
||||
}
|
||||
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
|
||||
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
|
||||
|
||||
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
|
||||
updateActionForInterimTarget(action);
|
||||
} // everything is valid, keep going
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
mInterimTargetDx = mInterimTargetDy = 0;
|
||||
mTargetVector = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scroll speed.
|
||||
*
|
||||
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
|
||||
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
|
||||
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
|
||||
*/
|
||||
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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) * MILLISECONDS_PER_PX);
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the left
|
||||
* or the right edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getHorizontalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling towards a child view, this method defines whether we should align the top
|
||||
* or the bottom edge of the child with the parent RecyclerView.
|
||||
*
|
||||
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
|
||||
* @see #SNAP_TO_START
|
||||
* @see #SNAP_TO_END
|
||||
* @see #SNAP_TO_ANY
|
||||
*/
|
||||
protected int getVerticalSnapPreference() {
|
||||
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
|
||||
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the target scroll position is not a child of the RecyclerView, this method calculates
|
||||
* a direction vector towards that child and triggers a smooth scroll.
|
||||
*
|
||||
* @see #computeScrollVectorForPosition(int)
|
||||
*/
|
||||
protected void updateActionForInterimTarget(Action action) {
|
||||
// find an interim target position
|
||||
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
|
||||
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
|
||||
final int target = getTargetPosition();
|
||||
action.jumpTo(target);
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
normalize(scrollVector);
|
||||
mTargetVector = scrollVector;
|
||||
|
||||
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
|
||||
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
|
||||
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
|
||||
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
|
||||
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
|
||||
// won't actually scroll more than what we need.
|
||||
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO),
|
||||
(int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO),
|
||||
(int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
|
||||
}
|
||||
|
||||
private int clampApplyScroll(int tmpDt, int dt) {
|
||||
final int before = tmpDt;
|
||||
tmpDt -= dt;
|
||||
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
|
||||
return 0;
|
||||
}
|
||||
return tmpDt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
|
||||
* {@link #calculateDyToMakeVisible(android.view.View, int)}
|
||||
*/
|
||||
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
|
||||
snapPreference) {
|
||||
switch (snapPreference) {
|
||||
case SNAP_TO_START:
|
||||
return boxStart - viewStart;
|
||||
case SNAP_TO_END:
|
||||
return boxEnd - viewEnd;
|
||||
case SNAP_TO_ANY:
|
||||
final int dtStart = boxStart - viewStart;
|
||||
if (dtStart > 0) {
|
||||
return dtStart;
|
||||
}
|
||||
final int dtEnd = boxEnd - viewEnd;
|
||||
if (dtEnd < 0) {
|
||||
return dtEnd;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("snap preference should be one of the"
|
||||
+ " constants defined in SmoothScroller, starting with SNAP_");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the vertical scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_ANY}.
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDyToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (layoutManager == null || !layoutManager.canScrollVertically()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
final int start = layoutManager.getPaddingTop();
|
||||
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
|
||||
return calculateDtToFit(top, bottom, start, end, snapPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the horizontal scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDxToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (layoutManager == null || !layoutManager.canScrollHorizontally()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
final int start = layoutManager.getPaddingLeft();
|
||||
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
|
||||
return calculateDtToFit(left, right, start, end, snapPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the scroll vector for a given target position.
|
||||
* <p>
|
||||
* This method can return null if the layout manager cannot calculate a scroll vector
|
||||
* for the given position (e.g. it has no current scroll position).
|
||||
*
|
||||
* @param targetPosition the position to which the scroller is scrolling
|
||||
*
|
||||
* @return the scroll vector for a given target position
|
||||
*/
|
||||
@Nullable
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (layoutManager instanceof ScrollVectorProvider) {
|
||||
return ((ScrollVectorProvider) layoutManager)
|
||||
.computeScrollVectorForPosition(targetPosition);
|
||||
}
|
||||
Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
|
||||
+ " does not implement " + ScrollVectorProvider.class.getCanonicalName());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
226
core/java/com/android/internal/widget/NestedScrollingChild.java
Normal file
226
core/java/com/android/internal/widget/NestedScrollingChild.java
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewParent;
|
||||
|
||||
/**
|
||||
* This interface should be implemented by {@link android.view.View View} subclasses that wish
|
||||
* to support dispatching nested scrolling operations to a cooperating parent
|
||||
* {@link android.view.ViewGroup ViewGroup}.
|
||||
*
|
||||
* <p>Classes implementing this interface should create a final instance of a
|
||||
* {@link NestedScrollingChildHelper} as a field and delegate any View methods to the
|
||||
* <code>NestedScrollingChildHelper</code> methods of the same signature.</p>
|
||||
*
|
||||
* <p>Views invoking nested scrolling functionality should always do so from the relevant
|
||||
* {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility
|
||||
* shim static methods. This ensures interoperability with nested scrolling views on Android
|
||||
* 5.0 Lollipop and newer.</p>
|
||||
*/
|
||||
public interface NestedScrollingChild {
|
||||
/**
|
||||
* Enable or disable nested scrolling for this view.
|
||||
*
|
||||
* <p>If this property is set to true the view will be permitted to initiate nested
|
||||
* scrolling operations with a compatible parent view in the current hierarchy. If this
|
||||
* view does not implement nested scrolling this will have no effect. Disabling nested scrolling
|
||||
* while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping}
|
||||
* the nested scroll.</p>
|
||||
*
|
||||
* @param enabled true to enable nested scrolling, false to disable
|
||||
*
|
||||
* @see #isNestedScrollingEnabled()
|
||||
*/
|
||||
void setNestedScrollingEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Returns true if nested scrolling is enabled for this view.
|
||||
*
|
||||
* <p>If nested scrolling is enabled and this View class implementation supports it,
|
||||
* this view will act as a nested scrolling child view when applicable, forwarding data
|
||||
* about the scroll operation in progress to a compatible and cooperating nested scrolling
|
||||
* parent.</p>
|
||||
*
|
||||
* @return true if nested scrolling is enabled
|
||||
*
|
||||
* @see #setNestedScrollingEnabled(boolean)
|
||||
*/
|
||||
boolean isNestedScrollingEnabled();
|
||||
|
||||
/**
|
||||
* Begin a nestable scroll operation along the given axes.
|
||||
*
|
||||
* <p>A view starting a nested scroll promises to abide by the following contract:</p>
|
||||
*
|
||||
* <p>The view will call startNestedScroll upon initiating a scroll operation. In the case
|
||||
* of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}.
|
||||
* In the case of touch scrolling the nested scroll will be terminated automatically in
|
||||
* the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}.
|
||||
* In the event of programmatic scrolling the caller must explicitly call
|
||||
* {@link #stopNestedScroll()} to indicate the end of the nested scroll.</p>
|
||||
*
|
||||
* <p>If <code>startNestedScroll</code> returns true, a cooperative parent was found.
|
||||
* If it returns false the caller may ignore the rest of this contract until the next scroll.
|
||||
* Calling startNestedScroll while a nested scroll is already in progress will return true.</p>
|
||||
*
|
||||
* <p>At each incremental step of the scroll the caller should invoke
|
||||
* {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll}
|
||||
* once it has calculated the requested scrolling delta. If it returns true the nested scrolling
|
||||
* parent at least partially consumed the scroll and the caller should adjust the amount it
|
||||
* scrolls by.</p>
|
||||
*
|
||||
* <p>After applying the remainder of the scroll delta the caller should invoke
|
||||
* {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing
|
||||
* both the delta consumed and the delta unconsumed. A nested scrolling parent may treat
|
||||
* these values differently. See
|
||||
* {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}.
|
||||
* </p>
|
||||
*
|
||||
* @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}
|
||||
* and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}.
|
||||
* @return true if a cooperative parent was found and nested scrolling has been enabled for
|
||||
* the current gesture.
|
||||
*
|
||||
* @see #stopNestedScroll()
|
||||
* @see #dispatchNestedPreScroll(int, int, int[], int[])
|
||||
* @see #dispatchNestedScroll(int, int, int, int, int[])
|
||||
*/
|
||||
boolean startNestedScroll(int axes);
|
||||
|
||||
/**
|
||||
* Stop a nested scroll in progress.
|
||||
*
|
||||
* <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
|
||||
*
|
||||
* @see #startNestedScroll(int)
|
||||
*/
|
||||
void stopNestedScroll();
|
||||
|
||||
/**
|
||||
* Returns true if this view has a nested scrolling parent.
|
||||
*
|
||||
* <p>The presence of a nested scrolling parent indicates that this view has initiated
|
||||
* a nested scroll and it was accepted by an ancestor view further up the view hierarchy.</p>
|
||||
*
|
||||
* @return whether this view has a nested scrolling parent
|
||||
*/
|
||||
boolean hasNestedScrollingParent();
|
||||
|
||||
/**
|
||||
* Dispatch one step of a nested scroll in progress.
|
||||
*
|
||||
* <p>Implementations of views that support nested scrolling should call this to report
|
||||
* info about a scroll in progress to the current nested scrolling parent. If a nested scroll
|
||||
* is not currently in progress or nested scrolling is not
|
||||
* {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.</p>
|
||||
*
|
||||
* <p>Compatible View implementations should also call
|
||||
* {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
|
||||
* consuming a component of the scroll event themselves.</p>
|
||||
*
|
||||
* @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
|
||||
* @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
|
||||
* @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
|
||||
* @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
|
||||
* @param offsetInWindow Optional. If not null, on return this will contain the offset
|
||||
* in local view coordinates of this view from before this operation
|
||||
* to after it completes. View implementations may use this to adjust
|
||||
* expected input coordinate tracking.
|
||||
* @return true if the event was dispatched, false if it could not be dispatched.
|
||||
* @see #dispatchNestedPreScroll(int, int, int[], int[])
|
||||
*/
|
||||
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
|
||||
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
|
||||
|
||||
/**
|
||||
* Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
|
||||
*
|
||||
* <p>Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
|
||||
* <code>dispatchNestedPreScroll</code> offers an opportunity for the parent view in a nested
|
||||
* scrolling operation to consume some or all of the scroll operation before the child view
|
||||
* consumes it.</p>
|
||||
*
|
||||
* @param dx Horizontal scroll distance in pixels
|
||||
* @param dy Vertical scroll distance in pixels
|
||||
* @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
|
||||
* and consumed[1] the consumed dy.
|
||||
* @param offsetInWindow Optional. If not null, on return this will contain the offset
|
||||
* in local view coordinates of this view from before this operation
|
||||
* to after it completes. View implementations may use this to adjust
|
||||
* expected input coordinate tracking.
|
||||
* @return true if the parent consumed some or all of the scroll delta
|
||||
* @see #dispatchNestedScroll(int, int, int, int, int[])
|
||||
*/
|
||||
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
|
||||
|
||||
/**
|
||||
* Dispatch a fling to a nested scrolling parent.
|
||||
*
|
||||
* <p>This method should be used to indicate that a nested scrolling child has detected
|
||||
* suitable conditions for a fling. Generally this means that a touch scroll has ended with a
|
||||
* {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
|
||||
* the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
|
||||
* along a scrollable axis.</p>
|
||||
*
|
||||
* <p>If a nested scrolling child view would normally fling but it is at the edge of
|
||||
* its own content, it can use this method to delegate the fling to its nested scrolling
|
||||
* parent instead. The parent may optionally consume the fling or observe a child fling.</p>
|
||||
*
|
||||
* @param velocityX Horizontal fling velocity in pixels per second
|
||||
* @param velocityY Vertical fling velocity in pixels per second
|
||||
* @param consumed true if the child consumed the fling, false otherwise
|
||||
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
|
||||
*/
|
||||
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
|
||||
|
||||
/**
|
||||
* Dispatch a fling to a nested scrolling parent before it is processed by this view.
|
||||
*
|
||||
* <p>Nested pre-fling events are to nested fling events what touch intercept is to touch
|
||||
* and what nested pre-scroll is to nested scroll. <code>dispatchNestedPreFling</code>
|
||||
* offsets an opportunity for the parent view in a nested fling to fully consume the fling
|
||||
* before the child view consumes it. If this method returns <code>true</code>, a nested
|
||||
* parent view consumed the fling and this view should not scroll as a result.</p>
|
||||
*
|
||||
* <p>For a better user experience, only one view in a nested scrolling chain should consume
|
||||
* the fling at a time. If a parent view consumed the fling this method will return false.
|
||||
* Custom view implementations should account for this in two ways:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>If a custom view is paged and needs to settle to a fixed page-point, do not
|
||||
* call <code>dispatchNestedPreFling</code>; consume the fling and settle to a valid
|
||||
* position regardless.</li>
|
||||
* <li>If a nested parent does consume the fling, this view should not scroll at all,
|
||||
* even to settle back to a valid idle position.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Views should also not offer fling velocities to nested parent views along an axis
|
||||
* where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView}
|
||||
* should not offer a horizontal fling velocity to its parents since scrolling along that
|
||||
* axis is not permitted and carrying velocity along that motion does not make sense.</p>
|
||||
*
|
||||
* @param velocityX Horizontal fling velocity in pixels per second
|
||||
* @param velocityY Vertical fling velocity in pixels per second
|
||||
* @return true if a nested scrolling parent consumed the fling
|
||||
*/
|
||||
boolean dispatchNestedPreFling(float velocityX, float velocityY);
|
||||
}
|
||||
239
core/java/com/android/internal/widget/OpReorderer.java
Normal file
239
core/java/com/android/internal/widget/OpReorderer.java
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import static com.android.internal.widget.AdapterHelper.UpdateOp.ADD;
|
||||
import static com.android.internal.widget.AdapterHelper.UpdateOp.MOVE;
|
||||
import static com.android.internal.widget.AdapterHelper.UpdateOp.REMOVE;
|
||||
import static com.android.internal.widget.AdapterHelper.UpdateOp.UPDATE;
|
||||
|
||||
import com.android.internal.widget.AdapterHelper.UpdateOp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class OpReorderer {
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
OpReorderer(Callback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void reorderOps(List<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<UpdateOp> list, int badMove, int next) {
|
||||
final UpdateOp moveOp = list.get(badMove);
|
||||
final UpdateOp nextOp = list.get(next);
|
||||
switch (nextOp.cmd) {
|
||||
case REMOVE:
|
||||
swapMoveRemove(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case ADD:
|
||||
swapMoveAdd(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
case UPDATE:
|
||||
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
|
||||
int removePos, UpdateOp removeOp) {
|
||||
UpdateOp extraRm = null;
|
||||
// check if move is nulled out by remove
|
||||
boolean revertedMove = false;
|
||||
final boolean moveIsBackwards;
|
||||
|
||||
if (moveOp.positionStart < moveOp.itemCount) {
|
||||
moveIsBackwards = false;
|
||||
if (removeOp.positionStart == moveOp.positionStart
|
||||
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
|
||||
revertedMove = true;
|
||||
}
|
||||
} else {
|
||||
moveIsBackwards = true;
|
||||
if (removeOp.positionStart == moveOp.itemCount + 1
|
||||
&& removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
|
||||
revertedMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < removeOp.positionStart) {
|
||||
removeOp.positionStart--;
|
||||
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
|
||||
// move is removed.
|
||||
removeOp.itemCount--;
|
||||
moveOp.cmd = REMOVE;
|
||||
moveOp.itemCount = 1;
|
||||
if (removeOp.itemCount == 0) {
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(removeOp);
|
||||
}
|
||||
// no need to swap, it is already a remove
|
||||
return;
|
||||
}
|
||||
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= removeOp.positionStart) {
|
||||
removeOp.positionStart++;
|
||||
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
|
||||
final int remaining = removeOp.positionStart + removeOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining, null);
|
||||
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
|
||||
}
|
||||
|
||||
// if effects of move is reverted by remove, we are done.
|
||||
if (revertedMove) {
|
||||
list.set(movePos, removeOp);
|
||||
list.remove(removePos);
|
||||
mCallback.recycleUpdateOp(moveOp);
|
||||
return;
|
||||
}
|
||||
|
||||
// now find out the new locations for move actions
|
||||
if (moveIsBackwards) {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart > extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart > removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount > removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (extraRm != null) {
|
||||
if (moveOp.positionStart >= extraRm.positionStart) {
|
||||
moveOp.positionStart -= extraRm.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= extraRm.positionStart) {
|
||||
moveOp.itemCount -= extraRm.itemCount;
|
||||
}
|
||||
}
|
||||
if (moveOp.positionStart >= removeOp.positionStart) {
|
||||
moveOp.positionStart -= removeOp.itemCount;
|
||||
}
|
||||
if (moveOp.itemCount >= removeOp.positionStart) {
|
||||
moveOp.itemCount -= removeOp.itemCount;
|
||||
}
|
||||
}
|
||||
|
||||
list.set(movePos, removeOp);
|
||||
if (moveOp.positionStart != moveOp.itemCount) {
|
||||
list.set(removePos, moveOp);
|
||||
} else {
|
||||
list.remove(removePos);
|
||||
}
|
||||
if (extraRm != null) {
|
||||
list.add(movePos, extraRm);
|
||||
}
|
||||
}
|
||||
|
||||
private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
|
||||
UpdateOp addOp) {
|
||||
int offset = 0;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < addOp.positionStart) {
|
||||
offset--;
|
||||
}
|
||||
if (moveOp.positionStart < addOp.positionStart) {
|
||||
offset++;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.positionStart) {
|
||||
moveOp.positionStart += addOp.itemCount;
|
||||
}
|
||||
if (addOp.positionStart <= moveOp.itemCount) {
|
||||
moveOp.itemCount += addOp.itemCount;
|
||||
}
|
||||
addOp.positionStart += offset;
|
||||
list.set(move, addOp);
|
||||
list.set(add, moveOp);
|
||||
}
|
||||
|
||||
void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
|
||||
UpdateOp updateOp) {
|
||||
UpdateOp extraUp1 = null;
|
||||
UpdateOp extraUp2 = null;
|
||||
// going in reverse, first revert the effect of add
|
||||
if (moveOp.itemCount < updateOp.positionStart) {
|
||||
updateOp.positionStart--;
|
||||
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
|
||||
// moved item is updated. add an update for it
|
||||
updateOp.itemCount--;
|
||||
extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1, updateOp.payload);
|
||||
}
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= updateOp.positionStart) {
|
||||
updateOp.positionStart++;
|
||||
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
|
||||
final int remaining = updateOp.positionStart + updateOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining,
|
||||
updateOp.payload);
|
||||
updateOp.itemCount -= remaining;
|
||||
}
|
||||
list.set(update, moveOp);
|
||||
if (updateOp.itemCount > 0) {
|
||||
list.set(move, updateOp);
|
||||
} else {
|
||||
list.remove(move);
|
||||
mCallback.recycleUpdateOp(updateOp);
|
||||
}
|
||||
if (extraUp1 != null) {
|
||||
list.add(move, extraUp1);
|
||||
}
|
||||
if (extraUp2 != null) {
|
||||
list.add(move, extraUp2);
|
||||
}
|
||||
}
|
||||
|
||||
private int getLastMoveOutOfOrder(List<UpdateOp> list) {
|
||||
boolean foundNonMove = false;
|
||||
for (int i = list.size() - 1; i >= 0; i--) {
|
||||
final UpdateOp op1 = list.get(i);
|
||||
if (op1.cmd == MOVE) {
|
||||
if (foundNonMove) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
foundNonMove = true;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
|
||||
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount, Object payload);
|
||||
|
||||
void recycleUpdateOp(UpdateOp op);
|
||||
}
|
||||
}
|
||||
439
core/java/com/android/internal/widget/OrientationHelper.java
Normal file
439
core/java/com/android/internal/widget/OrientationHelper.java
Normal file
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
|
||||
* <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 = LinearLayout.HORIZONTAL;
|
||||
|
||||
public static final int VERTICAL = LinearLayout.VERTICAL;
|
||||
|
||||
private int mLastTotalSpace = INVALID_SIZE;
|
||||
|
||||
final Rect mTmpRect = new Rect();
|
||||
|
||||
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
mLayoutManager = layoutManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method after onLayout method is complete if state is NOT pre-layout.
|
||||
* This method records information like layout bounds that might be useful in the next layout
|
||||
* calculations.
|
||||
*/
|
||||
public void onLayoutComplete() {
|
||||
mLastTotalSpace = getTotalSpace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the layout space change between the previous layout pass and current layout pass.
|
||||
* <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, 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
12255
core/java/com/android/internal/widget/RecyclerView.java
Normal file
12255
core/java/com/android/internal/widget/RecyclerView.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.AccessibilityDelegate;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
|
||||
/**
|
||||
* The AccessibilityDelegate used by RecyclerView.
|
||||
* <p>
|
||||
* This class handles basic accessibility actions and delegates them to LayoutManager.
|
||||
*/
|
||||
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegate {
|
||||
final RecyclerView mRecyclerView;
|
||||
|
||||
|
||||
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
boolean shouldIgnore() {
|
||||
return mRecyclerView.hasPendingAdapterUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(RecyclerView.class.getName());
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
event.setClassName(RecyclerView.class.getName());
|
||||
if (host instanceof RecyclerView && !shouldIgnore()) {
|
||||
RecyclerView rv = (RecyclerView) host;
|
||||
if (rv.getLayoutManager() != null) {
|
||||
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AccessibilityDelegate for an individual item in the RecyclerView.
|
||||
* A basic item delegate is provided by default, but you can override this
|
||||
* method to provide a custom per-item delegate.
|
||||
*/
|
||||
public AccessibilityDelegate getItemDelegate() {
|
||||
return mItemDelegate;
|
||||
}
|
||||
|
||||
final AccessibilityDelegate mItemDelegate = new AccessibilityDelegate() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager()
|
||||
.onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager()
|
||||
.performAccessibilityActionForItem(host, action, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
99
core/java/com/android/internal/widget/ScrollbarHelper.java
Normal file
99
core/java/com/android/internal/widget/ScrollbarHelper.java
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A helper class to do scroll offset calculations.
|
||||
*/
|
||||
class ScrollbarHelper {
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled, boolean reverseLayout) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
final int minPosition = Math.min(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int maxPosition = Math.max(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int itemsBefore = reverseLayout
|
||||
? Math.max(0, state.getItemCount() - maxPosition - 1)
|
||||
: Math.max(0, minPosition);
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return itemsBefore;
|
||||
}
|
||||
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild));
|
||||
final int itemRange = Math.abs(lm.getPosition(startChild)
|
||||
- lm.getPosition(endChild)) + 1;
|
||||
final float avgSizePerRow = (float) laidOutArea / itemRange;
|
||||
|
||||
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
|
||||
- orientation.getDecoratedStart(startChild)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
}
|
||||
final int extend = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
return Math.min(orientation.getTotalSpace(), extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null
|
||||
|| endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return state.getItemCount();
|
||||
}
|
||||
// smooth scrollbar enabled. try to estimate better.
|
||||
final int laidOutArea = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
final int laidOutRange = Math.abs(lm.getPosition(startChild)
|
||||
- lm.getPosition(endChild))
|
||||
+ 1;
|
||||
// estimate a size for full list.
|
||||
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
|
||||
}
|
||||
}
|
||||
134
core/java/com/android/internal/widget/ScrollingView.java
Normal file
134
core/java/com/android/internal/widget/ScrollingView.java
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
/**
|
||||
* An interface that can be implemented by Views to provide scroll related APIs.
|
||||
*/
|
||||
public interface ScrollingView {
|
||||
/**
|
||||
* <p>Compute the horizontal range that the horizontal scrollbar
|
||||
* represents.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeHorizontalScrollExtent()} and
|
||||
* {@link #computeHorizontalScrollOffset()}.</p>
|
||||
*
|
||||
* <p>The default range is the drawing width of this view.</p>
|
||||
*
|
||||
* @return the total horizontal range represented by the horizontal
|
||||
* scrollbar
|
||||
*
|
||||
* @see #computeHorizontalScrollExtent()
|
||||
* @see #computeHorizontalScrollOffset()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeHorizontalScrollRange();
|
||||
|
||||
/**
|
||||
* <p>Compute the horizontal offset of the horizontal scrollbar's thumb
|
||||
* within the horizontal range. This value is used to compute the position
|
||||
* of the thumb within the scrollbar's track.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeHorizontalScrollRange()} and
|
||||
* {@link #computeHorizontalScrollExtent()}.</p>
|
||||
*
|
||||
* <p>The default offset is the scroll offset of this view.</p>
|
||||
*
|
||||
* @return the horizontal offset of the scrollbar's thumb
|
||||
*
|
||||
* @see #computeHorizontalScrollRange()
|
||||
* @see #computeHorizontalScrollExtent()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeHorizontalScrollOffset();
|
||||
|
||||
/**
|
||||
* <p>Compute the horizontal extent of the horizontal scrollbar's thumb
|
||||
* within the horizontal range. This value is used to compute the length
|
||||
* of the thumb within the scrollbar's track.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeHorizontalScrollRange()} and
|
||||
* {@link #computeHorizontalScrollOffset()}.</p>
|
||||
*
|
||||
* <p>The default extent is the drawing width of this view.</p>
|
||||
*
|
||||
* @return the horizontal extent of the scrollbar's thumb
|
||||
*
|
||||
* @see #computeHorizontalScrollRange()
|
||||
* @see #computeHorizontalScrollOffset()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeHorizontalScrollExtent();
|
||||
|
||||
/**
|
||||
* <p>Compute the vertical range that the vertical scrollbar represents.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeVerticalScrollExtent()} and
|
||||
* {@link #computeVerticalScrollOffset()}.</p>
|
||||
*
|
||||
* @return the total vertical range represented by the vertical scrollbar
|
||||
*
|
||||
* <p>The default range is the drawing height of this view.</p>
|
||||
*
|
||||
* @see #computeVerticalScrollExtent()
|
||||
* @see #computeVerticalScrollOffset()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeVerticalScrollRange();
|
||||
|
||||
/**
|
||||
* <p>Compute the vertical offset of the vertical scrollbar's thumb
|
||||
* within the horizontal range. This value is used to compute the position
|
||||
* of the thumb within the scrollbar's track.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeVerticalScrollRange()} and
|
||||
* {@link #computeVerticalScrollExtent()}.</p>
|
||||
*
|
||||
* <p>The default offset is the scroll offset of this view.</p>
|
||||
*
|
||||
* @return the vertical offset of the scrollbar's thumb
|
||||
*
|
||||
* @see #computeVerticalScrollRange()
|
||||
* @see #computeVerticalScrollExtent()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeVerticalScrollOffset();
|
||||
|
||||
/**
|
||||
* <p>Compute the vertical extent of the vertical scrollbar's thumb
|
||||
* within the vertical range. This value is used to compute the length
|
||||
* of the thumb within the scrollbar's track.</p>
|
||||
*
|
||||
* <p>The range is expressed in arbitrary units that must be the same as the
|
||||
* units used by {@link #computeVerticalScrollRange()} and
|
||||
* {@link #computeVerticalScrollOffset()}.</p>
|
||||
*
|
||||
* <p>The default extent is the drawing height of this view.</p>
|
||||
*
|
||||
* @return the vertical extent of the scrollbar's thumb
|
||||
*
|
||||
* @see #computeVerticalScrollRange()
|
||||
* @see #computeVerticalScrollOffset()
|
||||
* @see android.widget.ScrollBarDrawable
|
||||
*/
|
||||
int computeVerticalScrollExtent();
|
||||
}
|
||||
457
core/java/com/android/internal/widget/SimpleItemAnimator.java
Normal file
457
core/java/com/android/internal/widget/SimpleItemAnimator.java
Normal file
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.widget.RecyclerView.Adapter;
|
||||
import com.android.internal.widget.RecyclerView.ViewHolder;
|
||||
|
||||
/**
|
||||
* A wrapper class for ItemAnimator that records View bounds and decides whether it should run
|
||||
* move, change, add or remove animations. This class also replicates the original ItemAnimator
|
||||
* API.
|
||||
* <p>
|
||||
* It uses {@link ItemHolderInfo} to track the bounds information of the Views. If you would like
|
||||
* to
|
||||
* extend this class, you can override {@link #obtainHolderInfo()} method to provide your own info
|
||||
* class that extends {@link ItemHolderInfo}.
|
||||
*/
|
||||
public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "SimpleItemAnimator";
|
||||
|
||||
boolean mSupportsChangeAnimations = true;
|
||||
|
||||
/**
|
||||
* Returns whether this ItemAnimator supports animations of change events.
|
||||
*
|
||||
* @return true if change animations are supported, false otherwise
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public boolean getSupportsChangeAnimations() {
|
||||
return mSupportsChangeAnimations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this ItemAnimator supports animations of item change events.
|
||||
* If you set this property to false, actions on the data set which change the
|
||||
* contents of items will not be animated. What those animations do is left
|
||||
* up to the discretion of the ItemAnimator subclass, in its
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} implementation.
|
||||
* The value of this property is true by default.
|
||||
*
|
||||
* @param supportsChangeAnimations true if change animations are supported by
|
||||
* this ItemAnimator, false otherwise. If the property is false,
|
||||
* the ItemAnimator
|
||||
* will not receive a call to
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int,
|
||||
* int)} when changes occur.
|
||||
* @see Adapter#notifyItemChanged(int)
|
||||
* @see Adapter#notifyItemRangeChanged(int, int)
|
||||
*/
|
||||
public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {
|
||||
mSupportsChangeAnimations = supportsChangeAnimations;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return True if change animations are not supported or the ViewHolder is invalid,
|
||||
* false otherwise.
|
||||
*
|
||||
* @see #setSupportsChangeAnimations(boolean)
|
||||
*/
|
||||
@Override
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return !mSupportsChangeAnimations || viewHolder.isInvalid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateDisappearance(@NonNull ViewHolder viewHolder,
|
||||
@NonNull ItemHolderInfo preLayoutInfo, @Nullable ItemHolderInfo postLayoutInfo) {
|
||||
int oldLeft = preLayoutInfo.left;
|
||||
int oldTop = preLayoutInfo.top;
|
||||
View disappearingItemView = viewHolder.itemView;
|
||||
int newLeft = postLayoutInfo == null ? disappearingItemView.getLeft() : postLayoutInfo.left;
|
||||
int newTop = postLayoutInfo == null ? disappearingItemView.getTop() : postLayoutInfo.top;
|
||||
if (!viewHolder.isRemoved() && (oldLeft != newLeft || oldTop != newTop)) {
|
||||
disappearingItemView.layout(newLeft, newTop,
|
||||
newLeft + disappearingItemView.getWidth(),
|
||||
newTop + disappearingItemView.getHeight());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "DISAPPEARING: " + viewHolder + " with view " + disappearingItemView);
|
||||
}
|
||||
return animateMove(viewHolder, oldLeft, oldTop, newLeft, newTop);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "REMOVED: " + viewHolder + " with view " + disappearingItemView);
|
||||
}
|
||||
return animateRemove(viewHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAppearance(@NonNull ViewHolder viewHolder,
|
||||
@Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
|
||||
if (preLayoutInfo != null && (preLayoutInfo.left != postLayoutInfo.left
|
||||
|| preLayoutInfo.top != postLayoutInfo.top)) {
|
||||
// slide items in if before/after locations differ
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "APPEARING: " + viewHolder + " with view " + viewHolder);
|
||||
}
|
||||
return animateMove(viewHolder, preLayoutInfo.left, preLayoutInfo.top,
|
||||
postLayoutInfo.left, postLayoutInfo.top);
|
||||
} else {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "ADDED: " + viewHolder + " with view " + viewHolder);
|
||||
}
|
||||
return animateAdd(viewHolder);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animatePersistence(@NonNull ViewHolder viewHolder,
|
||||
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
|
||||
if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "PERSISTENT: " + viewHolder
|
||||
+ " with view " + viewHolder.itemView);
|
||||
}
|
||||
return animateMove(viewHolder,
|
||||
preInfo.left, preInfo.top, postInfo.left, postInfo.top);
|
||||
}
|
||||
dispatchMoveFinished(viewHolder);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(@NonNull ViewHolder oldHolder, @NonNull ViewHolder newHolder,
|
||||
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldHolder.itemView);
|
||||
}
|
||||
final int fromLeft = preInfo.left;
|
||||
final int fromTop = preInfo.top;
|
||||
final int toLeft, toTop;
|
||||
if (newHolder.shouldIgnore()) {
|
||||
toLeft = preInfo.left;
|
||||
toTop = preInfo.top;
|
||||
} else {
|
||||
toLeft = postInfo.left;
|
||||
toTop = postInfo.top;
|
||||
}
|
||||
return animateChange(oldHolder, newHolder, fromLeft, fromTop, toLeft, toTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item is removed from the RecyclerView. Implementors can choose
|
||||
* whether and how to animate that change, but must always call
|
||||
* {@link #dispatchRemoveFinished(ViewHolder)} when done, either
|
||||
* immediately (if no animation will occur) or after the animation actually finishes.
|
||||
* The return value indicates whether an animation has been set up and whether the
|
||||
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
|
||||
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
|
||||
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
|
||||
* {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
|
||||
* {@link #animateRemove(ViewHolder) animateRemove()}, and
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
|
||||
* then start the animations together in the later call to {@link #runPendingAnimations()}.
|
||||
*
|
||||
* <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.
|
||||
*/
|
||||
public abstract boolean animateRemove(ViewHolder holder);
|
||||
|
||||
/**
|
||||
* Called when an item is added to the RecyclerView. Implementors can choose
|
||||
* whether and how to animate that change, but must always call
|
||||
* {@link #dispatchAddFinished(ViewHolder)} when done, either
|
||||
* immediately (if no animation will occur) or after the animation actually finishes.
|
||||
* The return value indicates whether an animation has been set up and whether the
|
||||
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
|
||||
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
|
||||
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
|
||||
* {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
|
||||
* {@link #animateRemove(ViewHolder) animateRemove()}, and
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
|
||||
* then start the animations together in the later call to {@link #runPendingAnimations()}.
|
||||
*
|
||||
* <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.
|
||||
*/
|
||||
public abstract boolean animateAdd(ViewHolder holder);
|
||||
|
||||
/**
|
||||
* Called when an item is moved in the RecyclerView. Implementors can choose
|
||||
* whether and how to animate that change, but must always call
|
||||
* {@link #dispatchMoveFinished(ViewHolder)} when done, either
|
||||
* immediately (if no animation will occur) or after the animation actually finishes.
|
||||
* The return value indicates whether an animation has been set up and whether the
|
||||
* ItemAnimator's {@link #runPendingAnimations()} method should be called at the
|
||||
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
|
||||
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
|
||||
* {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
|
||||
* {@link #animateRemove(ViewHolder) animateRemove()}, and
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
|
||||
* then start the animations together in the later call to {@link #runPendingAnimations()}.
|
||||
*
|
||||
* @param holder The item that is being moved.
|
||||
* @return true if a later call to {@link #runPendingAnimations()} is requested,
|
||||
* false otherwise.
|
||||
*/
|
||||
public abstract boolean animateMove(ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY);
|
||||
|
||||
/**
|
||||
* Called when an item is changed in the RecyclerView, as indicated by a call to
|
||||
* {@link Adapter#notifyItemChanged(int)} or
|
||||
* {@link Adapter#notifyItemRangeChanged(int, int)}.
|
||||
* <p>
|
||||
* Implementers can choose whether and how to animate changes, but must always call
|
||||
* {@link #dispatchChangeFinished(ViewHolder, boolean)} for each non-null distinct ViewHolder,
|
||||
* either immediately (if no animation will occur) or after the animation actually finishes.
|
||||
* If the {@code oldHolder} is the same ViewHolder as the {@code newHolder}, you must call
|
||||
* {@link #dispatchChangeFinished(ViewHolder, boolean)} once and only once. In that case, the
|
||||
* second parameter of {@code dispatchChangeFinished} is ignored.
|
||||
* <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(ViewHolder) animateAdd()},
|
||||
* {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
|
||||
* {@link #animateRemove(ViewHolder) animateRemove()}, and
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)} come in one by one,
|
||||
* then start the animations together in the later call to {@link #runPendingAnimations()}.
|
||||
*
|
||||
* @param oldHolder The original item that changed.
|
||||
* @param newHolder The new item that was created with the changed content. Might be null
|
||||
* @param fromLeft Left of the old view holder
|
||||
* @param fromTop Top of the old view holder
|
||||
* @param toLeft Left of the new view holder
|
||||
* @param toTop Top of the new view holder
|
||||
* @return true if a later call to {@link #runPendingAnimations()} is requested,
|
||||
* false otherwise.
|
||||
*/
|
||||
public abstract boolean animateChange(ViewHolder oldHolder,
|
||||
ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop);
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a remove animation is done.
|
||||
*
|
||||
* @param item The item which has been removed
|
||||
* @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
|
||||
* ItemHolderInfo)
|
||||
*/
|
||||
public final void dispatchRemoveFinished(ViewHolder item) {
|
||||
onRemoveFinished(item);
|
||||
dispatchAnimationFinished(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a move animation is done.
|
||||
*
|
||||
* @param item The item which has been moved
|
||||
* @see RecyclerView.ItemAnimator#animateDisappearance(ViewHolder, ItemHolderInfo,
|
||||
* ItemHolderInfo)
|
||||
* @see RecyclerView.ItemAnimator#animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
|
||||
* @see RecyclerView.ItemAnimator#animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
|
||||
*/
|
||||
public final void dispatchMoveFinished(ViewHolder item) {
|
||||
onMoveFinished(item);
|
||||
dispatchAnimationFinished(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when an add animation is done.
|
||||
*
|
||||
* @param item The item which has been added
|
||||
*/
|
||||
public final void dispatchAddFinished(ViewHolder item) {
|
||||
onAddFinished(item);
|
||||
dispatchAnimationFinished(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a change animation is done.
|
||||
*
|
||||
* @param item The item which has been changed (this method must be called for
|
||||
* each non-null ViewHolder passed into
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
|
||||
* @param oldItem true if this is the old item that was changed, false if
|
||||
* it is the new item that replaced the old item.
|
||||
* @see #animateChange(ViewHolder, ViewHolder, int, int, int, int)
|
||||
*/
|
||||
public final void dispatchChangeFinished(ViewHolder item, boolean oldItem) {
|
||||
onChangeFinished(item, oldItem);
|
||||
dispatchAnimationFinished(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a remove animation is being started.
|
||||
*
|
||||
* @param item The item being removed
|
||||
*/
|
||||
public final void dispatchRemoveStarting(ViewHolder item) {
|
||||
onRemoveStarting(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a move animation is being started.
|
||||
*
|
||||
* @param item The item being moved
|
||||
*/
|
||||
public final void dispatchMoveStarting(ViewHolder item) {
|
||||
onMoveStarting(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when an add animation is being started.
|
||||
*
|
||||
* @param item The item being added
|
||||
*/
|
||||
public final void dispatchAddStarting(ViewHolder item) {
|
||||
onAddStarting(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be called by subclasses when a change animation is being started.
|
||||
*
|
||||
* @param item The item which has been changed (this method must be called for
|
||||
* each non-null ViewHolder passed into
|
||||
* {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}).
|
||||
* @param oldItem true if this is the old item that was changed, false if
|
||||
* it is the new item that replaced the old item.
|
||||
*/
|
||||
public final void dispatchChangeStarting(ViewHolder item, boolean oldItem) {
|
||||
onChangeStarting(item, oldItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a remove animation is being started on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
public void onRemoveStarting(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a remove animation has ended on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
public void onRemoveFinished(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an add animation is being started on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
public void onAddStarting(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an add animation has ended on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
public void onAddFinished(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a move animation is being started on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
public void onMoveStarting(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a move animation has ended on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
*/
|
||||
public void onMoveFinished(ViewHolder item) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a change animation is being started on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
* @param oldItem true if this is the old item that was changed, false if
|
||||
* it is the new item that replaced the old item.
|
||||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
public void onChangeStarting(ViewHolder item, boolean oldItem) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a change animation has ended on the given ViewHolder.
|
||||
* The default implementation does nothing. Subclasses may wish to override
|
||||
* this method to handle any ViewHolder-specific operations linked to animation
|
||||
* lifecycles.
|
||||
*
|
||||
* @param item The ViewHolder being animated.
|
||||
* @param oldItem true if this is the old item that was changed, false if
|
||||
* it is the new item that replaced the old item.
|
||||
*/
|
||||
public void onChangeFinished(ViewHolder item, boolean oldItem) {
|
||||
}
|
||||
}
|
||||
|
||||
330
core/java/com/android/internal/widget/ViewInfoStore.java
Normal file
330
core/java/com/android/internal/widget/ViewInfoStore.java
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.LongSparseArray;
|
||||
import android.util.Pools;
|
||||
|
||||
import static com.android.internal.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
|
||||
import static com.android.internal.widget.RecyclerView.ViewHolder;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_AND_DISAPPEAR;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_APPEAR_PRE_AND_POST;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_DISAPPEARED;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_POST;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE;
|
||||
import static com.android.internal.widget.ViewInfoStore.InfoRecord.FLAG_PRE_AND_POST;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* This class abstracts all tracking for Views to run animations.
|
||||
*/
|
||||
class ViewInfoStore {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* View data records for pre-layout
|
||||
*/
|
||||
@VisibleForTesting
|
||||
final ArrayMap<ViewHolder, InfoRecord> mLayoutHolderMap = new ArrayMap<>();
|
||||
|
||||
@VisibleForTesting
|
||||
final LongSparseArray<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(ViewHolder holder, ItemHolderInfo info) {
|
||||
InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
if (record == null) {
|
||||
record = InfoRecord.obtain();
|
||||
mLayoutHolderMap.put(holder, record);
|
||||
}
|
||||
record.preInfo = info;
|
||||
record.flags |= FLAG_PRE;
|
||||
}
|
||||
|
||||
boolean isDisappearing(ViewHolder holder) {
|
||||
final InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
return record != null && ((record.flags & FLAG_DISAPPEARED) != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ItemHolderInfo for the given ViewHolder in preLayout list and removes it.
|
||||
*
|
||||
* @param vh The ViewHolder whose information is being queried
|
||||
* @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
|
||||
*/
|
||||
@Nullable
|
||||
ItemHolderInfo popFromPreLayout(ViewHolder vh) {
|
||||
return popFromLayoutStep(vh, FLAG_PRE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the ItemHolderInfo for the given ViewHolder in postLayout list and removes it.
|
||||
*
|
||||
* @param vh The ViewHolder whose information is being queried
|
||||
* @return The ItemHolderInfo for the given ViewHolder or null if it does not exist
|
||||
*/
|
||||
@Nullable
|
||||
ItemHolderInfo popFromPostLayout(ViewHolder vh) {
|
||||
return popFromLayoutStep(vh, FLAG_POST);
|
||||
}
|
||||
|
||||
private ItemHolderInfo popFromLayoutStep(ViewHolder vh, int flag) {
|
||||
int index = mLayoutHolderMap.indexOfKey(vh);
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
final InfoRecord record = mLayoutHolderMap.valueAt(index);
|
||||
if (record != null && (record.flags & flag) != 0) {
|
||||
record.flags &= ~flag;
|
||||
final ItemHolderInfo info;
|
||||
if (flag == FLAG_PRE) {
|
||||
info = record.preInfo;
|
||||
} else if (flag == FLAG_POST) {
|
||||
info = record.postInfo;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Must provide flag PRE or POST");
|
||||
}
|
||||
// if not pre-post flag is left, clear.
|
||||
if ((record.flags & (FLAG_PRE | FLAG_POST)) == 0) {
|
||||
mLayoutHolderMap.removeAt(index);
|
||||
InfoRecord.recycle(record);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ViewHolder to the oldChangeHolders list
|
||||
* @param key The key to identify the ViewHolder.
|
||||
* @param holder The ViewHolder to store
|
||||
*/
|
||||
void addToOldChangeHolders(long key, ViewHolder holder) {
|
||||
mOldChangedHolders.put(key, holder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ViewHolder to the appeared in pre layout list. These are Views added by the
|
||||
* LayoutManager during a pre-layout pass. We distinguish them from other views that were
|
||||
* already in the pre-layout so that ItemAnimator can choose to run a different animation for
|
||||
* them.
|
||||
*
|
||||
* @param holder The ViewHolder to store
|
||||
* @param info The information to save
|
||||
*/
|
||||
void addToAppearedInPreLayoutHolders(ViewHolder holder, ItemHolderInfo info) {
|
||||
InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
if (record == null) {
|
||||
record = InfoRecord.obtain();
|
||||
mLayoutHolderMap.put(holder, record);
|
||||
}
|
||||
record.flags |= FLAG_APPEAR;
|
||||
record.preInfo = info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given ViewHolder is in preLayout list
|
||||
* @param viewHolder The ViewHolder to query
|
||||
*
|
||||
* @return True if the ViewHolder is present in preLayout, false otherwise
|
||||
*/
|
||||
boolean isInPreLayout(ViewHolder viewHolder) {
|
||||
final InfoRecord record = mLayoutHolderMap.get(viewHolder);
|
||||
return record != null && (record.flags & FLAG_PRE) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the oldChangeHolder list for the given key. If they are not tracked, simply returns
|
||||
* null.
|
||||
* @param key The key to be used to find the ViewHolder.
|
||||
*
|
||||
* @return A ViewHolder if exists or null if it does not exist.
|
||||
*/
|
||||
ViewHolder getFromOldChangeHolders(long key) {
|
||||
return mOldChangedHolders.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the item information to the post layout list
|
||||
* @param holder The ViewHolder whose information is being saved
|
||||
* @param info The information to save
|
||||
*/
|
||||
void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
|
||||
InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
if (record == null) {
|
||||
record = InfoRecord.obtain();
|
||||
mLayoutHolderMap.put(holder, record);
|
||||
}
|
||||
record.postInfo = info;
|
||||
record.flags |= FLAG_POST;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ViewHolder might be added by the LayoutManager just to animate its disappearance.
|
||||
* This list holds such items so that we can animate / recycle these ViewHolders properly.
|
||||
*
|
||||
* @param holder The ViewHolder which disappeared during a layout.
|
||||
*/
|
||||
void addToDisappearedInLayout(ViewHolder holder) {
|
||||
InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
if (record == null) {
|
||||
record = InfoRecord.obtain();
|
||||
mLayoutHolderMap.put(holder, record);
|
||||
}
|
||||
record.flags |= FLAG_DISAPPEARED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a ViewHolder from disappearing list.
|
||||
* @param holder The ViewHolder to be removed from the disappearing list.
|
||||
*/
|
||||
void removeFromDisappearedInLayout(ViewHolder holder) {
|
||||
InfoRecord record = mLayoutHolderMap.get(holder);
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
record.flags &= ~FLAG_DISAPPEARED;
|
||||
}
|
||||
|
||||
void process(ProcessCallback callback) {
|
||||
for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
|
||||
final ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
|
||||
final InfoRecord record = mLayoutHolderMap.removeAt(index);
|
||||
if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
|
||||
// Appeared then disappeared. Not useful for animations.
|
||||
callback.unused(viewHolder);
|
||||
} else if ((record.flags & FLAG_DISAPPEARED) != 0) {
|
||||
// Set as "disappeared" by the LayoutManager (addDisappearingView)
|
||||
if (record.preInfo == null) {
|
||||
// similar to appear disappear but happened between different layout passes.
|
||||
// this can happen when the layout manager is using auto-measure
|
||||
callback.unused(viewHolder);
|
||||
} else {
|
||||
callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
|
||||
}
|
||||
} else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
|
||||
// Appeared in the layout but not in the adapter (e.g. entered the viewport)
|
||||
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
|
||||
} else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
|
||||
// Persistent in both passes. Animate persistence
|
||||
callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
|
||||
} else if ((record.flags & FLAG_PRE) != 0) {
|
||||
// Was in pre-layout, never been added to post layout
|
||||
callback.processDisappeared(viewHolder, record.preInfo, null);
|
||||
} else if ((record.flags & FLAG_POST) != 0) {
|
||||
// Was not in pre-layout, been added to post layout
|
||||
callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
|
||||
} else if ((record.flags & FLAG_APPEAR) != 0) {
|
||||
// Scrap view. RecyclerView will handle removing/recycling this.
|
||||
} else if (DEBUG) {
|
||||
throw new IllegalStateException("record without any reasonable flag combination:/");
|
||||
}
|
||||
InfoRecord.recycle(record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the ViewHolder from all list
|
||||
* @param holder The ViewHolder which we should stop tracking
|
||||
*/
|
||||
void removeViewHolder(ViewHolder holder) {
|
||||
for (int i = mOldChangedHolders.size() - 1; i >= 0; i--) {
|
||||
if (holder == mOldChangedHolders.valueAt(i)) {
|
||||
mOldChangedHolders.removeAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
final InfoRecord info = mLayoutHolderMap.remove(holder);
|
||||
if (info != null) {
|
||||
InfoRecord.recycle(info);
|
||||
}
|
||||
}
|
||||
|
||||
void onDetach() {
|
||||
InfoRecord.drainCache();
|
||||
}
|
||||
|
||||
public void onViewDetached(ViewHolder viewHolder) {
|
||||
removeFromDisappearedInLayout(viewHolder);
|
||||
}
|
||||
|
||||
interface ProcessCallback {
|
||||
void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
|
||||
@Nullable ItemHolderInfo postInfo);
|
||||
void processAppeared(ViewHolder viewHolder, @Nullable ItemHolderInfo preInfo,
|
||||
ItemHolderInfo postInfo);
|
||||
void processPersistent(ViewHolder viewHolder, @NonNull ItemHolderInfo preInfo,
|
||||
@NonNull ItemHolderInfo postInfo);
|
||||
void unused(ViewHolder holder);
|
||||
}
|
||||
|
||||
static class InfoRecord {
|
||||
// disappearing list
|
||||
static final int FLAG_DISAPPEARED = 1;
|
||||
// appear in pre layout list
|
||||
static final int FLAG_APPEAR = 1 << 1;
|
||||
// pre layout, this is necessary to distinguish null item info
|
||||
static final int FLAG_PRE = 1 << 2;
|
||||
// post layout, this is necessary to distinguish null item info
|
||||
static final int FLAG_POST = 1 << 3;
|
||||
static final int FLAG_APPEAR_AND_DISAPPEAR = FLAG_APPEAR | FLAG_DISAPPEARED;
|
||||
static final int FLAG_PRE_AND_POST = FLAG_PRE | FLAG_POST;
|
||||
static final int FLAG_APPEAR_PRE_AND_POST = FLAG_APPEAR | FLAG_PRE | FLAG_POST;
|
||||
int flags;
|
||||
@Nullable ItemHolderInfo preInfo;
|
||||
@Nullable ItemHolderInfo postInfo;
|
||||
static Pools.Pool<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2391
core/java/com/android/internal/widget/helper/ItemTouchHelper.java
Normal file
2391
core/java/com/android/internal/widget/helper/ItemTouchHelper.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget.helper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Utility class for {@link ItemTouchHelper} which handles item transformations for different
|
||||
* API versions.
|
||||
* <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)}
|
||||
*/
|
||||
void onDraw(Canvas c, RecyclerView recyclerView, View view,
|
||||
float dX, float dY, int actionState, boolean isCurrentlyActive);
|
||||
|
||||
/**
|
||||
* The default implementation for {@link ItemTouchHelper.Callback#onChildDrawOver(Canvas,
|
||||
* RecyclerView, RecyclerView.ViewHolder, float, float, int, boolean)}
|
||||
*/
|
||||
void onDrawOver(Canvas c, RecyclerView recyclerView, View view,
|
||||
float dX, float dY, int actionState, boolean isCurrentlyActive);
|
||||
|
||||
/**
|
||||
* The default implementation for {@link ItemTouchHelper.Callback#clearView(RecyclerView,
|
||||
* RecyclerView.ViewHolder)}
|
||||
*/
|
||||
void clearView(View view);
|
||||
|
||||
/**
|
||||
* The default implementation for {@link ItemTouchHelper.Callback#onSelectedChanged(
|
||||
* RecyclerView.ViewHolder, int)}
|
||||
*/
|
||||
void onSelected(View view);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.internal.widget.helper;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Package private class to keep implementations. Putting them inside ItemTouchUIUtil makes them
|
||||
* public API, which is not desired in this case.
|
||||
*/
|
||||
class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
|
||||
@Override
|
||||
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
|
||||
float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
if (isCurrentlyActive) {
|
||||
Object originalElevation = view.getTag(
|
||||
R.id.item_touch_helper_previous_elevation);
|
||||
if (originalElevation == null) {
|
||||
originalElevation = view.getElevation();
|
||||
float newElevation = 1f + findMaxElevation(recyclerView, view);
|
||||
view.setElevation(newElevation);
|
||||
view.setTag(R.id.item_touch_helper_previous_elevation,
|
||||
originalElevation);
|
||||
}
|
||||
}
|
||||
view.setTranslationX(dX);
|
||||
view.setTranslationY(dY);
|
||||
}
|
||||
|
||||
private float findMaxElevation(RecyclerView recyclerView, View itemView) {
|
||||
final int childCount = recyclerView.getChildCount();
|
||||
float max = 0;
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = recyclerView.getChildAt(i);
|
||||
if (child == itemView) {
|
||||
continue;
|
||||
}
|
||||
final float elevation = child.getElevation();
|
||||
if (elevation > max) {
|
||||
max = elevation;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(View view) {
|
||||
final Object tag = view.getTag(
|
||||
R.id.item_touch_helper_previous_elevation);
|
||||
if (tag != null && tag instanceof Float) {
|
||||
view.setElevation((Float) tag);
|
||||
}
|
||||
view.setTag(R.id.item_touch_helper_previous_elevation, null);
|
||||
view.setTranslationX(0f);
|
||||
view.setTranslationY(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelected(View view) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(Canvas c, RecyclerView recyclerView,
|
||||
View view, float dX, float dY, int actionState, boolean isCurrentlyActive) {
|
||||
}
|
||||
}
|
||||
@@ -8477,4 +8477,14 @@
|
||||
<attr name="font" format="reference" />
|
||||
<attr name="fontWeight" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
<!-- @hide -->
|
||||
<declare-styleable name="RecyclerView">
|
||||
<attr name="layoutManager" format="string" />
|
||||
<attr name="orientation" />
|
||||
<attr name="descendantFocusability" />
|
||||
<attr name="spanCount" format="integer"/>
|
||||
<attr name="reverseLayout" format="boolean" />
|
||||
<attr name="stackFromEnd" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
||||
@@ -509,4 +509,10 @@
|
||||
<dimen name="tooltip_precise_anchor_threshold">96dp</dimen>
|
||||
<!-- Extra tooltip offset used when anchoring to the mouse/touch position -->
|
||||
<dimen name="tooltip_precise_anchor_extra_offset">8dp</dimen>
|
||||
|
||||
<!-- The max amount of scroll ItemTouchHelper will trigger if dragged view is out of
|
||||
RecyclerView's bounds.-->
|
||||
<dimen name="item_touch_helper_max_drag_scroll_per_frame">20dp</dimen>
|
||||
<dimen name="item_touch_helper_swipe_escape_velocity">120dp</dimen>
|
||||
<dimen name="item_touch_helper_swipe_escape_max_velocity">800dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -131,4 +131,7 @@
|
||||
<item type="id" name="cross_task_transition" />
|
||||
|
||||
<item type="id" name="accessibilityActionClickOnClickableSpan" />
|
||||
|
||||
<!-- ItemTouchHelper uses this id to save a View's original elevation. -->
|
||||
<item type="id" name="item_touch_helper_previous_elevation"/>
|
||||
</resources>
|
||||
|
||||
@@ -2806,4 +2806,10 @@
|
||||
<java-symbol type="string" name="disable_accessibility_shortcut" />
|
||||
<java-symbol type="string" name="leave_accessibility_shortcut_on" />
|
||||
<java-symbol type="string" name="config_defaultAccessibilityService" />
|
||||
|
||||
<!-- com.android.internal.widget.RecyclerView -->
|
||||
<java-symbol type="id" name="item_touch_helper_previous_elevation"/>
|
||||
<java-symbol type="dimen" name="item_touch_helper_max_drag_scroll_per_frame"/>
|
||||
<java-symbol type="dimen" name="item_touch_helper_swipe_escape_velocity"/>
|
||||
<java-symbol type="dimen" name="item_touch_helper_swipe_escape_max_velocity"/>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user