Make RecyclerView smooth scroll speed customizable
This commit is contained in:
1 parent
224760abf6
commit
dae22d0284
17 files changed
+17367
-4
No files matched your search
@@ -93,7 +93,6 @@
|
||||
<orderEntry type="library" exported="" name="bolts-android-1.1.4" level="project" />
|
||||
<orderEntry type="library" exported="" name="drawee-0.4.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="fbcore-0.4.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="recyclerview-v7-22.1.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="library-2.4.0" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-v4-22.1.1" level="project" />
|
||||
<orderEntry type="library" exported="" name="support-annotations-22.1.1" level="project" />
|
||||
|
||||
@@ -26,7 +26,6 @@ android {
|
||||
dependencies {
|
||||
compile 'com.android.support:support-v4:22.1.1'
|
||||
compile 'com.android.support:appcompat-v7:22.1.1'
|
||||
compile 'com.android.support:recyclerview-v7:22.1.1'
|
||||
compile 'com.github.japgolly.android:svg-android:2.0.6'
|
||||
compile 'com.facebook.fresco:fresco:0.4.0'
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v4.util.Pools;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.RecyclerView.*;
|
||||
|
||||
/**
|
||||
* Helper class that can enqueue and process adapter update operations.
|
||||
* <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 {
|
||||
|
||||
final static int POSITION_TYPE_INVISIBLE = 0;
|
||||
|
||||
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String TAG = "AHT";
|
||||
|
||||
private Pools.Pool<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;
|
||||
|
||||
AdapterHelper(Callback callback) {
|
||||
this(callback, false);
|
||||
}
|
||||
|
||||
AdapterHelper(Callback callback, boolean disableRecycler) {
|
||||
mCallback = callback;
|
||||
mDisableRecycler = disableRecycler;
|
||||
mOpReorderer = new OpReorderer(this);
|
||||
}
|
||||
|
||||
AdapterHelper addUpdateOp(UpdateOp... ops) {
|
||||
Collections.addAll(mPendingUpdates, ops);
|
||||
return this;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
void preProcess() {
|
||||
mOpReorderer.reorderOps(mPendingUpdates);
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
applyAdd(op);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
applyRemove(op);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
applyUpdate(op);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
applyMove(op);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
mPendingUpdates.clear();
|
||||
}
|
||||
|
||||
void consumePostponedUpdates() {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
mCallback.onDispatchSecondPass(mPostponedList.get(i));
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPostponedList);
|
||||
}
|
||||
|
||||
private void applyMove(UpdateOp op) {
|
||||
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
|
||||
// otherwise, it would be converted into a REMOVE operation
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void applyRemove(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
boolean typeChanged = false;
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) {
|
||||
// If a ViewHolder exists or this is a newly added item, we can defer this update
|
||||
// to post layout stage.
|
||||
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
|
||||
// * For items that are added and removed in the same process cycle, they won't
|
||||
// have any effect in pre-layout since their add ops are already deferred to
|
||||
// post-layout pass.
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create an UpdateOp and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else {
|
||||
// This update cannot be recovered because we don't have a ViewHolder representing
|
||||
// this position. Instead, post it to LayoutManager immediately
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
// Looks like we have other updates that we cannot merge with this one.
|
||||
// Create UpdateOp op and dispatch it to LayoutManager.
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
typeChanged = true;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
if (typeChanged) {
|
||||
position -= tmpCount; // also equal to tmpStart
|
||||
tmpEnd -= tmpCount;
|
||||
tmpCount = 1;
|
||||
} else {
|
||||
tmpCount++;
|
||||
}
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyUpdate(UpdateOp op) {
|
||||
int tmpStart = op.positionStart;
|
||||
int tmpCount = 0;
|
||||
int tmpEnd = op.positionStart + op.itemCount;
|
||||
int type = -1;
|
||||
for (int position = op.positionStart; position < tmpEnd; position++) {
|
||||
ViewHolder vh = mCallback.findViewHolder(position);
|
||||
if (vh != null || canFindInPreLayout(position)) { // deferred
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
dispatchAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_NEW_OR_LAID_OUT;
|
||||
} else { // applied
|
||||
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
|
||||
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
postponeAndUpdateViewHolders(newOp);
|
||||
tmpCount = 0;
|
||||
tmpStart = position;
|
||||
}
|
||||
type = POSITION_TYPE_INVISIBLE;
|
||||
}
|
||||
tmpCount++;
|
||||
}
|
||||
if (tmpCount != op.itemCount) { // all 1 effect
|
||||
recycleUpdateOp(op);
|
||||
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
|
||||
}
|
||||
if (type == POSITION_TYPE_INVISIBLE) {
|
||||
dispatchAndUpdateViewHolders(op);
|
||||
} else {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchAndUpdateViewHolders(UpdateOp op) {
|
||||
// tricky part.
|
||||
// traverse all postpones and revert their changes on this op if necessary, apply updated
|
||||
// dispatch to them since now they are after this op.
|
||||
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
|
||||
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatch (pre)" + op);
|
||||
Log.d(TAG, "postponed state before:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
|
||||
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
|
||||
// TODO Since move ops are pushed to end, we should not need this anymore
|
||||
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
|
||||
}
|
||||
int tmpCnt = 1;
|
||||
int offsetPositionForPartial = op.positionStart;
|
||||
final int positionMultiplier;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
positionMultiplier = 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
positionMultiplier = 0;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("op should be remove or update." + op);
|
||||
}
|
||||
for (int p = 1; p < op.itemCount; p++) {
|
||||
final int pos = op.positionStart + (positionMultiplier * p);
|
||||
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
|
||||
}
|
||||
boolean continuous = false;
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.UPDATE:
|
||||
continuous = updatedPos == tmpStart + 1;
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
continuous = updatedPos == tmpStart;
|
||||
break;
|
||||
}
|
||||
if (continuous) {
|
||||
tmpCnt++;
|
||||
} else {
|
||||
// need to dispatch this separately
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "need to dispatch separately " + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
if (op.cmd == UpdateOp.UPDATE) {
|
||||
offsetPositionForPartial += tmpCnt;
|
||||
}
|
||||
tmpStart = updatedPos;// need to remove previously dispatched
|
||||
tmpCnt = 1;
|
||||
}
|
||||
}
|
||||
recycleUpdateOp(op);
|
||||
if (tmpCnt > 0) {
|
||||
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispatching:" + tmp);
|
||||
}
|
||||
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
|
||||
recycleUpdateOp(tmp);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "post dispatch");
|
||||
Log.d(TAG, "postponed state after:");
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
|
||||
mCallback.onDispatchFirstPass(op);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("only remove and update ops can be dispatched"
|
||||
+ " in first pass");
|
||||
}
|
||||
}
|
||||
|
||||
private int updatePositionWithPostponed(int pos, int cmd) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
UpdateOp postponed = mPostponedList.get(i);
|
||||
if (postponed.cmd == UpdateOp.MOVE) {
|
||||
int start, end;
|
||||
if (postponed.positionStart < postponed.itemCount) {
|
||||
start = postponed.positionStart;
|
||||
end = postponed.itemCount;
|
||||
} else {
|
||||
start = postponed.itemCount;
|
||||
end = postponed.positionStart;
|
||||
}
|
||||
if (pos >= start && pos <= end) {
|
||||
//i'm affected
|
||||
if (start == postponed.positionStart) {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.itemCount--;
|
||||
}
|
||||
// op moved to left, move it right to revert
|
||||
pos++;
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
// op was moved right, move left to revert
|
||||
pos--;
|
||||
}
|
||||
} else if (pos < postponed.positionStart) {
|
||||
// postponed MV is outside the dispatched OP. if it is before, offset
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
postponed.itemCount++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
postponed.itemCount--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (postponed.positionStart <= pos) {
|
||||
if (postponed.cmd == UpdateOp.ADD) {
|
||||
pos -= postponed.itemCount;
|
||||
} else if (postponed.cmd == UpdateOp.REMOVE) {
|
||||
pos += postponed.itemCount;
|
||||
}
|
||||
} else {
|
||||
if (cmd == UpdateOp.ADD) {
|
||||
postponed.positionStart++;
|
||||
} else if (cmd == UpdateOp.REMOVE) {
|
||||
postponed.positionStart--;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "dispath (step" + i + ")");
|
||||
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
|
||||
for (UpdateOp updateOp : mPostponedList) {
|
||||
Log.d(TAG, updateOp.toString());
|
||||
}
|
||||
Log.d(TAG, "----");
|
||||
}
|
||||
}
|
||||
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.itemCount == op.positionStart || op.itemCount < 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
} else if (op.itemCount <= 0) {
|
||||
mPostponedList.remove(i);
|
||||
recycleUpdateOp(op);
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private boolean canFindInPreLayout(int position) {
|
||||
final int count = mPostponedList.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (findPositionOffset(op.itemCount, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
// TODO optimize.
|
||||
final int end = op.positionStart + op.itemCount;
|
||||
for (int pos = op.positionStart; pos < end; pos++) {
|
||||
if (findPositionOffset(pos, i + 1) == position) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void applyAdd(UpdateOp op) {
|
||||
postponeAndUpdateViewHolders(op);
|
||||
}
|
||||
|
||||
private void postponeAndUpdateViewHolders(UpdateOp op) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "postponing " + op);
|
||||
}
|
||||
mPostponedList.add(op);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
|
||||
op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown update op type for " + op);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasPendingUpdates() {
|
||||
return mPendingUpdates.size() > 0;
|
||||
}
|
||||
|
||||
int findPositionOffset(int position) {
|
||||
return findPositionOffset(position, 0);
|
||||
}
|
||||
|
||||
int findPositionOffset(int position, int firstPostponedItem) {
|
||||
int count = mPostponedList.size();
|
||||
for (int i = firstPostponedItem; i < count; ++i) {
|
||||
UpdateOp op = mPostponedList.get(i);
|
||||
if (op.cmd == UpdateOp.MOVE) {
|
||||
if (op.positionStart == position) {
|
||||
position = op.itemCount;
|
||||
} else {
|
||||
if (op.positionStart < position) {
|
||||
position--; // like a remove
|
||||
}
|
||||
if (op.itemCount <= position) {
|
||||
position++; // like an add
|
||||
}
|
||||
}
|
||||
} else if (op.positionStart <= position) {
|
||||
if (op.cmd == UpdateOp.REMOVE) {
|
||||
if (position < op.positionStart + op.itemCount) {
|
||||
return -1;
|
||||
}
|
||||
position -= op.itemCount;
|
||||
} else if (op.cmd == UpdateOp.ADD) {
|
||||
position += op.itemCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeChanged(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeInserted(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if updates should be processed.
|
||||
*/
|
||||
boolean onItemRangeMoved(int from, int to, int itemCount) {
|
||||
if (from == to) {
|
||||
return false;//no-op
|
||||
}
|
||||
if (itemCount != 1) {
|
||||
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
|
||||
}
|
||||
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
|
||||
return mPendingUpdates.size() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips pre-processing and applies all updates in one pass.
|
||||
*/
|
||||
void consumeUpdatesInOnePass() {
|
||||
// we still consume postponed updates (if there is) in case there was a pre-process call
|
||||
// w/o a matching consumePostponedUpdates.
|
||||
consumePostponedUpdates();
|
||||
final int count = mPendingUpdates.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.UPDATE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
mCallback.onDispatchSecondPass(op);
|
||||
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
|
||||
break;
|
||||
}
|
||||
if (mOnItemProcessedCallback != null) {
|
||||
mOnItemProcessedCallback.run();
|
||||
}
|
||||
}
|
||||
recycleUpdateOpsAndClearList(mPendingUpdates);
|
||||
}
|
||||
|
||||
public int applyPendingUpdatesToPosition(int position) {
|
||||
final int size = mPendingUpdates.size();
|
||||
for (int i = 0; i < size; i ++) {
|
||||
UpdateOp op = mPendingUpdates.get(i);
|
||||
switch (op.cmd) {
|
||||
case UpdateOp.ADD:
|
||||
if (op.positionStart <= position) {
|
||||
position += op.itemCount;
|
||||
}
|
||||
break;
|
||||
case UpdateOp.REMOVE:
|
||||
if (op.positionStart <= position) {
|
||||
final int end = op.positionStart + op.itemCount;
|
||||
if (end > position) {
|
||||
return RecyclerView.NO_POSITION;
|
||||
}
|
||||
position -= op.itemCount;
|
||||
}
|
||||
break;
|
||||
case UpdateOp.MOVE:
|
||||
if (op.positionStart == position) {
|
||||
position = op.itemCount;//position end
|
||||
} else {
|
||||
if (op.positionStart < position) {
|
||||
position -= 1;
|
||||
}
|
||||
if (op.itemCount <= position) {
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queued operation to happen when child views are updated.
|
||||
*/
|
||||
static class UpdateOp {
|
||||
|
||||
static final int ADD = 0;
|
||||
|
||||
static final int REMOVE = 1;
|
||||
|
||||
static final int UPDATE = 2;
|
||||
|
||||
static final int MOVE = 3;
|
||||
|
||||
static final int POOL_SIZE = 30;
|
||||
|
||||
int cmd;
|
||||
|
||||
int positionStart;
|
||||
|
||||
// holds the target position if this is a MOVE
|
||||
int itemCount;
|
||||
|
||||
UpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
this.cmd = cmd;
|
||||
this.positionStart = positionStart;
|
||||
this.itemCount = itemCount;
|
||||
}
|
||||
|
||||
String cmdToString() {
|
||||
switch (cmd) {
|
||||
case ADD:
|
||||
return "add";
|
||||
case REMOVE:
|
||||
return "rm";
|
||||
case UPDATE:
|
||||
return "up";
|
||||
case MOVE:
|
||||
return "mv";
|
||||
}
|
||||
return "??";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateOp op = (UpdateOp) o;
|
||||
|
||||
if (cmd != op.cmd) {
|
||||
return false;
|
||||
}
|
||||
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
|
||||
// reverse of this is also true
|
||||
if (itemCount == op.positionStart && positionStart == op.itemCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (itemCount != op.itemCount) {
|
||||
return false;
|
||||
}
|
||||
if (positionStart != op.positionStart) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = cmd;
|
||||
result = 31 * result + positionStart;
|
||||
result = 31 * result + itemCount;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
|
||||
UpdateOp op = mUpdateOpPool.acquire();
|
||||
if (op == null) {
|
||||
op = new UpdateOp(cmd, positionStart, itemCount);
|
||||
} else {
|
||||
op.cmd = cmd;
|
||||
op.positionStart = positionStart;
|
||||
op.itemCount = itemCount;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recycleUpdateOp(UpdateOp op) {
|
||||
if (!mDisableRecycler) {
|
||||
mUpdateOpPool.release(op);
|
||||
}
|
||||
}
|
||||
|
||||
void recycleUpdateOpsAndClearList(List<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.
|
||||
*/
|
||||
static interface Callback {
|
||||
|
||||
ViewHolder findViewHolder(int position);
|
||||
|
||||
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
|
||||
|
||||
void markViewHoldersUpdated(int positionStart, int itemCount);
|
||||
|
||||
void onDispatchFirstPass(UpdateOp updateOp);
|
||||
|
||||
void onDispatchSecondPass(UpdateOp updateOp);
|
||||
|
||||
void offsetPositionsForAdd(int positionStart, int itemCount);
|
||||
|
||||
void offsetPositionsForMove(int from, int to);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class to manage children.
|
||||
* <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>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a view to the ViewGroup
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, boolean hidden) {
|
||||
addView(child, -1, hidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the ViewGroup at an index
|
||||
*
|
||||
* @param child View to add.
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
* @param hidden If set to true, this item will be invisible from regular methods.
|
||||
*/
|
||||
void addView(View child, int index, boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mBucket.insert(offset, hidden);
|
||||
if (hidden) {
|
||||
mHiddenViews.add(child);
|
||||
}
|
||||
mCallback.addView(child, offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
private int getOffset(int index) {
|
||||
if (index < 0) {
|
||||
return -1; //anything below 0 won't work as diff will be undefined.
|
||||
}
|
||||
final int limit = mCallback.getChildCount();
|
||||
int offset = index;
|
||||
while (offset < limit) {
|
||||
final int removedBefore = mBucket.countOnesBefore(offset);
|
||||
final int diff = index - (offset - removedBefore);
|
||||
if (diff == 0) {
|
||||
while (mBucket.get(offset)) { // ensure this offset is not hidden
|
||||
offset ++;
|
||||
}
|
||||
return offset;
|
||||
} else {
|
||||
offset += diff;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the provided View from underlying RecyclerView.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
*/
|
||||
void removeView(View view) {
|
||||
int index = mCallback.indexOfChild(view);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (mBucket.remove(index)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "remove View off:" + index + "," + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the view at the provided index from RecyclerView.
|
||||
*
|
||||
* @param index Index of the child from the regular perspective (excluding hidden views).
|
||||
* ChildHelper offsets this index to actual ViewGroup index.
|
||||
*/
|
||||
void removeViewAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
final View view = mCallback.getChildAt(offset);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
if (mBucket.remove(offset)) {
|
||||
mHiddenViews.remove(view);
|
||||
}
|
||||
mCallback.removeViewAt(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the child at provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
View getChildAt(int index) {
|
||||
final int offset = getOffset(index);
|
||||
return mCallback.getChildAt(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all views from the ViewGroup including the hidden ones.
|
||||
*/
|
||||
void removeAllViewsUnfiltered() {
|
||||
mBucket.reset();
|
||||
mHiddenViews.clear();
|
||||
mCallback.removeAllViews();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "removeAllViewsUnfiltered");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be used to find a disappearing view by position.
|
||||
*
|
||||
* @param position The adapter position of the item.
|
||||
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
|
||||
* @return A hidden view with a valid ViewHolder that matches the position and type.
|
||||
*/
|
||||
View findHiddenNonRemovedView(int position, int type) {
|
||||
final int count = mHiddenViews.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View view = mHiddenViews.get(i);
|
||||
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
|
||||
if (holder.getLayoutPosition() == position && !holder.isInvalid() &&
|
||||
(type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the provided view to the underlying ViewGroup.
|
||||
*
|
||||
* @param child Child to attach.
|
||||
* @param index Index of the child to attach in regular perspective.
|
||||
* @param layoutParams LayoutParams for the child.
|
||||
* @param hidden If set to true, this item will be invisible to the regular methods.
|
||||
*/
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
|
||||
boolean hidden) {
|
||||
final int offset;
|
||||
if (index < 0) {
|
||||
offset = mCallback.getChildCount();
|
||||
} else {
|
||||
offset = getOffset(index);
|
||||
}
|
||||
mBucket.insert(offset, hidden);
|
||||
if (hidden) {
|
||||
mHiddenViews.add(child);
|
||||
}
|
||||
mCallback.attachViewToParent(child, offset, layoutParams);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
|
||||
"h:" + hidden + ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of children that are not hidden.
|
||||
*
|
||||
* @return Number of children that are not hidden.
|
||||
* @see #getChildAt(int)
|
||||
*/
|
||||
int getChildCount() {
|
||||
return mCallback.getChildCount() - mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of children.
|
||||
*
|
||||
* @return The total number of children including the hidden views.
|
||||
* @see #getUnfilteredChildAt(int)
|
||||
*/
|
||||
int getUnfilteredChildCount() {
|
||||
return mCallback.getChildCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
|
||||
*
|
||||
* @param index ViewGroup index of the child to return.
|
||||
* @return The view in the provided index.
|
||||
*/
|
||||
View getUnfilteredChildAt(int index) {
|
||||
return mCallback.getChildAt(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the view at the provided index.
|
||||
*
|
||||
* @param index Index of the child to return in regular perspective.
|
||||
*/
|
||||
void detachViewFromParent(int index) {
|
||||
final int offset = getOffset(index);
|
||||
mBucket.remove(offset);
|
||||
mCallback.detachViewFromParent(offset);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the child in regular perspective.
|
||||
*
|
||||
* @param child The child whose index will be returned.
|
||||
* @return The regular perspective index of the child or -1 if it does not exists.
|
||||
*/
|
||||
int indexOfChild(View child) {
|
||||
final int index = mCallback.indexOfChild(child);
|
||||
if (index == -1) {
|
||||
return -1;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
if (DEBUG) {
|
||||
throw new IllegalArgumentException("cannot get index of a hidden child");
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// reverse the index
|
||||
return index - mBucket.countOnesBefore(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View is visible to LayoutManager or not.
|
||||
*
|
||||
* @param view The child view to check. Should be a child of the Callback.
|
||||
* @return True if the View is not visible to LayoutManager
|
||||
*/
|
||||
boolean isHidden(View view) {
|
||||
return mHiddenViews.contains(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a child view as hidden.
|
||||
*
|
||||
* @param view The view to hide.
|
||||
*/
|
||||
void hide(View view) {
|
||||
final int offset = mCallback.indexOfChild(view);
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
|
||||
}
|
||||
if (DEBUG && mBucket.get(offset)) {
|
||||
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
|
||||
}
|
||||
mBucket.set(offset);
|
||||
mHiddenViews.add(view);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mBucket.toString() + ", hidden list:" + mHiddenViews.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view from the ViewGroup if it is hidden.
|
||||
*
|
||||
* @param view The view to remove.
|
||||
* @return True if the View is found and it is hidden. False otherwise.
|
||||
*/
|
||||
boolean removeViewIfHidden(View view) {
|
||||
final int index = mCallback.indexOfChild(view);
|
||||
if (index == -1) {
|
||||
if (mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException("view is in hidden list but not in view group");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (mBucket.get(index)) {
|
||||
mBucket.remove(index);
|
||||
if (!mHiddenViews.remove(view) && DEBUG) {
|
||||
throw new IllegalStateException(
|
||||
"removed a hidden view but it is not in hidden views list");
|
||||
}
|
||||
mCallback.removeViewAt(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitset implementation that provides methods to offset indices.
|
||||
*/
|
||||
static class Bucket {
|
||||
|
||||
final static int BITS_PER_WORD = Long.SIZE;
|
||||
|
||||
final static long LAST_BIT = 1L << (Long.SIZE - 1);
|
||||
|
||||
long mData = 0;
|
||||
|
||||
Bucket next;
|
||||
|
||||
void set(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.set(index - BITS_PER_WORD);
|
||||
} else {
|
||||
mData |= 1L << index;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNext() {
|
||||
if (next == null) {
|
||||
next = new Bucket();
|
||||
}
|
||||
}
|
||||
|
||||
void clear(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
if (next != null) {
|
||||
next.clear(index - BITS_PER_WORD);
|
||||
}
|
||||
} else {
|
||||
mData &= ~(1L << index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
boolean get(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.get(index - BITS_PER_WORD);
|
||||
} else {
|
||||
return (mData & (1L << index)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
mData = 0;
|
||||
if (next != null) {
|
||||
next.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void insert(int index, boolean value) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
next.insert(index - BITS_PER_WORD, value);
|
||||
} else {
|
||||
final boolean lastBit = (mData & LAST_BIT) != 0;
|
||||
long mask = (1L << index) - 1;
|
||||
final long before = mData & mask;
|
||||
final long after = ((mData & ~mask)) << 1;
|
||||
mData = before | after;
|
||||
if (value) {
|
||||
set(index);
|
||||
} else {
|
||||
clear(index);
|
||||
}
|
||||
if (lastBit || next != null) {
|
||||
ensureNext();
|
||||
next.insert(0, lastBit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean remove(int index) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
ensureNext();
|
||||
return next.remove(index - BITS_PER_WORD);
|
||||
} else {
|
||||
long mask = (1L << index);
|
||||
final boolean value = (mData & mask) != 0;
|
||||
mData &= ~mask;
|
||||
mask = mask - 1;
|
||||
final long before = mData & mask;
|
||||
// cannot use >> because it adds one.
|
||||
final long after = Long.rotateRight(mData & ~mask, 1);
|
||||
mData = before | after;
|
||||
if (next != null) {
|
||||
if (next.get(0)) {
|
||||
set(BITS_PER_WORD - 1);
|
||||
}
|
||||
next.remove(0);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int countOnesBefore(int index) {
|
||||
if (next == null) {
|
||||
if (index >= BITS_PER_WORD) {
|
||||
return Long.bitCount(mData);
|
||||
}
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
}
|
||||
if (index < BITS_PER_WORD) {
|
||||
return Long.bitCount(mData & ((1L << index) - 1));
|
||||
} else {
|
||||
return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return next == null ? Long.toBinaryString(mData)
|
||||
: next.toString() + "xx" + Long.toBinaryString(mData);
|
||||
}
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
int getChildCount();
|
||||
|
||||
void addView(View child, int index);
|
||||
|
||||
int indexOfChild(View view);
|
||||
|
||||
void removeViewAt(int index);
|
||||
|
||||
View getChildAt(int offset);
|
||||
|
||||
void removeAllViews();
|
||||
|
||||
RecyclerView.ViewHolder getChildViewHolder(View view);
|
||||
|
||||
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
|
||||
|
||||
void detachViewFromParent(int offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
||||
import android.support.v4.view.ViewPropertyAnimatorListener;
|
||||
import android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
|
||||
* animations on remove, add, and move events that happen to the items in
|
||||
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
|
||||
*
|
||||
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
|
||||
*/
|
||||
public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
|
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
|
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
|
||||
|
||||
private ArrayList<ArrayList<ViewHolder>> mAdditionsList =
|
||||
new ArrayList<ArrayList<ViewHolder>>();
|
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
|
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
|
||||
|
||||
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
|
||||
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>();
|
||||
|
||||
private static class MoveInfo {
|
||||
public ViewHolder holder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
|
||||
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
this.holder = holder;
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ChangeInfo {
|
||||
public ViewHolder oldHolder, newHolder;
|
||||
public int fromX, fromY, toX, toY;
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
|
||||
this.oldHolder = oldHolder;
|
||||
this.newHolder = newHolder;
|
||||
}
|
||||
|
||||
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
this(oldHolder, newHolder);
|
||||
this.fromX = fromX;
|
||||
this.fromY = fromY;
|
||||
this.toX = toX;
|
||||
this.toY = toY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChangeInfo{" +
|
||||
"oldHolder=" + oldHolder +
|
||||
", newHolder=" + newHolder +
|
||||
", fromX=" + fromX +
|
||||
", fromY=" + fromY +
|
||||
", toX=" + toX +
|
||||
", toY=" + toY +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void runPendingAnimations() {
|
||||
boolean removalsPending = !mPendingRemovals.isEmpty();
|
||||
boolean movesPending = !mPendingMoves.isEmpty();
|
||||
boolean changesPending = !mPendingChanges.isEmpty();
|
||||
boolean additionsPending = !mPendingAdditions.isEmpty();
|
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
|
||||
// nothing to animate
|
||||
return;
|
||||
}
|
||||
// First, remove stuff
|
||||
for (ViewHolder holder : mPendingRemovals) {
|
||||
animateRemoveImpl(holder);
|
||||
}
|
||||
mPendingRemovals.clear();
|
||||
// Next, move stuff
|
||||
if (movesPending) {
|
||||
final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>();
|
||||
moves.addAll(mPendingMoves);
|
||||
mMovesList.add(moves);
|
||||
mPendingMoves.clear();
|
||||
Runnable mover = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (MoveInfo moveInfo : moves) {
|
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
|
||||
moveInfo.toX, moveInfo.toY);
|
||||
}
|
||||
moves.clear();
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
View view = moves.get(0).holder.itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
|
||||
} else {
|
||||
mover.run();
|
||||
}
|
||||
}
|
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) {
|
||||
final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>();
|
||||
changes.addAll(mPendingChanges);
|
||||
mChangesList.add(changes);
|
||||
mPendingChanges.clear();
|
||||
Runnable changer = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (ChangeInfo change : changes) {
|
||||
animateChangeImpl(change);
|
||||
}
|
||||
changes.clear();
|
||||
mChangesList.remove(changes);
|
||||
}
|
||||
};
|
||||
if (removalsPending) {
|
||||
ViewHolder holder = changes.get(0).oldHolder;
|
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
|
||||
} else {
|
||||
changer.run();
|
||||
}
|
||||
}
|
||||
// Next, add stuff
|
||||
if (additionsPending) {
|
||||
final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>();
|
||||
additions.addAll(mPendingAdditions);
|
||||
mAdditionsList.add(additions);
|
||||
mPendingAdditions.clear();
|
||||
Runnable adder = new Runnable() {
|
||||
public void run() {
|
||||
for (ViewHolder holder : additions) {
|
||||
animateAddImpl(holder);
|
||||
}
|
||||
additions.clear();
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
};
|
||||
if (removalsPending || movesPending || changesPending) {
|
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0;
|
||||
long moveDuration = movesPending ? getMoveDuration() : 0;
|
||||
long changeDuration = changesPending ? getChangeDuration() : 0;
|
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
|
||||
View view = additions.get(0).itemView;
|
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
|
||||
} else {
|
||||
adder.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateRemove(final ViewHolder holder) {
|
||||
endAnimation(holder);
|
||||
mPendingRemovals.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateRemoveImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mRemoveAnimations.add(holder);
|
||||
animation.setDuration(getRemoveDuration())
|
||||
.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchRemoveStarting(holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(holder);
|
||||
mRemoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(final ViewHolder holder) {
|
||||
endAnimation(holder);
|
||||
ViewCompat.setAlpha(holder.itemView, 0);
|
||||
mPendingAdditions.add(holder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateAddImpl(final ViewHolder holder) {
|
||||
final View view = holder.itemView;
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mAddAnimations.add(holder);
|
||||
animation.alpha(1).setDuration(getAddDuration()).
|
||||
setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchAddStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchAddFinished(holder);
|
||||
mAddAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
|
||||
int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
fromX += ViewCompat.getTranslationX(holder.itemView);
|
||||
fromY += ViewCompat.getTranslationY(holder.itemView);
|
||||
endAnimation(holder);
|
||||
int deltaX = toX - fromX;
|
||||
int deltaY = toY - fromY;
|
||||
if (deltaX == 0 && deltaY == 0) {
|
||||
dispatchMoveFinished(holder);
|
||||
return false;
|
||||
}
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, -deltaX);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, -deltaY);
|
||||
}
|
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
|
||||
final View view = holder.itemView;
|
||||
final int deltaX = toX - fromX;
|
||||
final int deltaY = toY - fromY;
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.animate(view).translationX(0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.animate(view).translationY(0);
|
||||
}
|
||||
// TODO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
|
||||
mMoveAnimations.add(holder);
|
||||
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchMoveStarting(holder);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {
|
||||
if (deltaX != 0) {
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
}
|
||||
if (deltaY != 0) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
animation.setListener(null);
|
||||
dispatchMoveFinished(holder);
|
||||
mMoveAnimations.remove(holder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
|
||||
int fromX, int fromY, int toX, int toY) {
|
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
|
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
|
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
|
||||
endAnimation(oldHolder);
|
||||
int deltaX = (int) (toX - fromX - prevTranslationX);
|
||||
int deltaY = (int) (toY - fromY - prevTranslationY);
|
||||
// recover prev translation state after ending animation
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
|
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
|
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
|
||||
if (newHolder != null && newHolder.itemView != null) {
|
||||
// carry over translation values
|
||||
endAnimation(newHolder);
|
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
|
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
|
||||
ViewCompat.setAlpha(newHolder.itemView, 0);
|
||||
}
|
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void animateChangeImpl(final ChangeInfo changeInfo) {
|
||||
final ViewHolder holder = changeInfo.oldHolder;
|
||||
final View view = holder == null ? null : holder.itemView;
|
||||
final ViewHolder newHolder = changeInfo.newHolder;
|
||||
final View newView = newHolder != null ? newHolder.itemView : null;
|
||||
if (view != null) {
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
|
||||
getChangeDuration());
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
oldViewAnim.setListener(null);
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
dispatchChangeFinished(changeInfo.oldHolder, true);
|
||||
mChangeAnimations.remove(changeInfo.oldHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
if (newView != null) {
|
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
|
||||
mChangeAnimations.add(changeInfo.newHolder);
|
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
|
||||
alpha(1).setListener(new VpaListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {
|
||||
dispatchChangeStarting(changeInfo.newHolder, false);
|
||||
}
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {
|
||||
newViewAnimation.setListener(null);
|
||||
ViewCompat.setAlpha(newView, 1);
|
||||
ViewCompat.setTranslationX(newView, 0);
|
||||
ViewCompat.setTranslationY(newView, 0);
|
||||
dispatchChangeFinished(changeInfo.newHolder, false);
|
||||
mChangeAnimations.remove(changeInfo.newHolder);
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
private void endChangeAnimation(List<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;
|
||||
}
|
||||
ViewCompat.setAlpha(item.itemView, 1);
|
||||
ViewCompat.setTranslationX(item.itemView, 0);
|
||||
ViewCompat.setTranslationY(item.itemView, 0);
|
||||
dispatchChangeFinished(item, oldItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimation(ViewHolder item) {
|
||||
final View view = item.itemView;
|
||||
// this will trigger end callback which should set properties to their target values.
|
||||
ViewCompat.animate(view).cancel();
|
||||
// TODO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
|
||||
MoveInfo moveInfo = mPendingMoves.get(i);
|
||||
if (moveInfo.holder == item) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
}
|
||||
endChangeAnimation(mPendingChanges, item);
|
||||
if (mPendingRemovals.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchRemoveFinished(item);
|
||||
}
|
||||
if (mPendingAdditions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
}
|
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<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) {
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
|
||||
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
|
||||
if (additions.remove(item)) {
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// animations should be ended by the cancel above.
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mRemoveAnimations list");
|
||||
}
|
||||
|
||||
if (mAddAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mAddAnimations list");
|
||||
}
|
||||
|
||||
if (mChangeAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mChangeAnimations list");
|
||||
}
|
||||
|
||||
if (mMoveAnimations.remove(item) && DEBUG) {
|
||||
throw new IllegalStateException("after animation is cancelled, item should not be in "
|
||||
+ "mMoveAnimations list");
|
||||
}
|
||||
dispatchFinishedWhenDone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return (!mPendingAdditions.isEmpty() ||
|
||||
!mPendingChanges.isEmpty() ||
|
||||
!mPendingMoves.isEmpty() ||
|
||||
!mPendingRemovals.isEmpty() ||
|
||||
!mMoveAnimations.isEmpty() ||
|
||||
!mRemoveAnimations.isEmpty() ||
|
||||
!mAddAnimations.isEmpty() ||
|
||||
!mChangeAnimations.isEmpty() ||
|
||||
!mMovesList.isEmpty() ||
|
||||
!mAdditionsList.isEmpty() ||
|
||||
!mChangesList.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the state of currently pending and running animations. If there are none
|
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
|
||||
* listeners.
|
||||
*/
|
||||
private void dispatchFinishedWhenDone() {
|
||||
if (!isRunning()) {
|
||||
dispatchAnimationsFinished();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endAnimations() {
|
||||
int count = mPendingMoves.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
MoveInfo item = mPendingMoves.get(i);
|
||||
View view = item.holder.itemView;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(item.holder);
|
||||
mPendingMoves.remove(i);
|
||||
}
|
||||
count = mPendingRemovals.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingRemovals.get(i);
|
||||
dispatchRemoveFinished(item);
|
||||
mPendingRemovals.remove(i);
|
||||
}
|
||||
count = mPendingAdditions.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
ViewHolder item = mPendingAdditions.get(i);
|
||||
View view = item.itemView;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
mPendingAdditions.remove(i);
|
||||
}
|
||||
count = mPendingChanges.size();
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i));
|
||||
}
|
||||
mPendingChanges.clear();
|
||||
if (!isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int listCount = mMovesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<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;
|
||||
ViewCompat.setTranslationY(view, 0);
|
||||
ViewCompat.setTranslationX(view, 0);
|
||||
dispatchMoveFinished(moveInfo.holder);
|
||||
moves.remove(j);
|
||||
if (moves.isEmpty()) {
|
||||
mMovesList.remove(moves);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mAdditionsList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<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;
|
||||
ViewCompat.setAlpha(view, 1);
|
||||
dispatchAddFinished(item);
|
||||
additions.remove(j);
|
||||
if (additions.isEmpty()) {
|
||||
mAdditionsList.remove(additions);
|
||||
}
|
||||
}
|
||||
}
|
||||
listCount = mChangesList.size();
|
||||
for (int i = listCount - 1; i >= 0; i--) {
|
||||
ArrayList<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--) {
|
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
|
||||
@Override
|
||||
public void onAnimationStart(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(View view) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(View view) {}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific languag`e governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
|
||||
* space.
|
||||
*/
|
||||
class LayoutState {
|
||||
|
||||
final static String TAG = "LayoutState";
|
||||
|
||||
final static int LAYOUT_START = -1;
|
||||
|
||||
final static int LAYOUT_END = 1;
|
||||
|
||||
final static int INVALID_LAYOUT = Integer.MIN_VALUE;
|
||||
|
||||
final static int ITEM_DIRECTION_HEAD = -1;
|
||||
|
||||
final static int ITEM_DIRECTION_TAIL = 1;
|
||||
|
||||
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
|
||||
|
||||
/**
|
||||
* Number of pixels that we should fill, in the layout direction.
|
||||
*/
|
||||
int mAvailable;
|
||||
|
||||
/**
|
||||
* Current position on the adapter to get the next item.
|
||||
*/
|
||||
int mCurrentPosition;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the data adapter is traversed.
|
||||
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
|
||||
*/
|
||||
int mItemDirection;
|
||||
|
||||
/**
|
||||
* Defines the direction in which the layout is filled.
|
||||
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
|
||||
*/
|
||||
int mLayoutDirection;
|
||||
|
||||
/**
|
||||
* Used if you want to pre-layout items that are not yet visible.
|
||||
* The difference with {@link #mAvailable} is that, when recycling, distance rendered for
|
||||
* {@link #mExtra} is not considered not to recycle visible children.
|
||||
*/
|
||||
int mExtra = 0;
|
||||
|
||||
/**
|
||||
* @return true if there are more items in the data adapter
|
||||
*/
|
||||
boolean hasMore(RecyclerView.State state) {
|
||||
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the view for the next element that we should render.
|
||||
* Also updates current item index to the next item, based on {@link #mItemDirection}
|
||||
*
|
||||
* @return The next element that we should render.
|
||||
*/
|
||||
View next(RecyclerView.Recycler recycler) {
|
||||
final View view = recycler.getViewForPosition(mCurrentPosition);
|
||||
mCurrentPosition += mItemDirection;
|
||||
return view;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
/**
|
||||
* {@link RecyclerView.SmoothScroller} implementation which uses
|
||||
* {@link android.view.animation.LinearInterpolator} until the target position becames a child of
|
||||
* the RecyclerView and then uses
|
||||
* {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
|
||||
*/
|
||||
abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
|
||||
|
||||
private static final String TAG = "LinearSmoothScroller";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final float MILLISECONDS_PER_INCH = 25f;
|
||||
|
||||
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
|
||||
|
||||
/**
|
||||
* Align child view's left or top with parent view's left or top
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(android.view.View, int)
|
||||
* @see #calculateDyToMakeVisible(android.view.View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_START = -1;
|
||||
|
||||
/**
|
||||
* Align child view's right or bottom with parent view's right or bottom
|
||||
*
|
||||
* @see #calculateDtToFit(int, int, int, int, int)
|
||||
* @see #calculateDxToMakeVisible(android.view.View, int)
|
||||
* @see #calculateDyToMakeVisible(android.view.View, int)
|
||||
*/
|
||||
public static final int SNAP_TO_END = 1;
|
||||
|
||||
/**
|
||||
* <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;
|
||||
}
|
||||
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)) {
|
||||
Log.e(TAG, "To support smooth scrolling, you should override \n"
|
||||
+ "LayoutManager#computeScrollVectorForPosition.\n"
|
||||
+ "Falling back to instant scroll");
|
||||
final int target = getTargetPosition();
|
||||
stop();
|
||||
instantScrollToPosition(target);
|
||||
return;
|
||||
}
|
||||
normalize(scrollVector);
|
||||
mTargetVector = scrollVector;
|
||||
|
||||
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
|
||||
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
|
||||
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
|
||||
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
|
||||
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
|
||||
// won't actually scroll more than what we need.
|
||||
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
|
||||
, (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
|
||||
}
|
||||
|
||||
private int clampApplyScroll(int tmpDt, int dt) {
|
||||
final int before = tmpDt;
|
||||
tmpDt -= dt;
|
||||
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
|
||||
return 0;
|
||||
}
|
||||
return tmpDt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
|
||||
* {@link #calculateDyToMakeVisible(android.view.View, int)}
|
||||
*/
|
||||
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
|
||||
snapPreference) {
|
||||
switch (snapPreference) {
|
||||
case SNAP_TO_START:
|
||||
return boxStart - viewStart;
|
||||
case SNAP_TO_END:
|
||||
return boxEnd - viewEnd;
|
||||
case SNAP_TO_ANY:
|
||||
final int dtStart = boxStart - viewStart;
|
||||
if (dtStart > 0) {
|
||||
return dtStart;
|
||||
}
|
||||
final int dtEnd = boxEnd - viewEnd;
|
||||
if (dtEnd < 0) {
|
||||
return dtEnd;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("snap preference should be one of the"
|
||||
+ " constants defined in SmoothScroller, starting with SNAP_");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the vertical scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}.
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDyToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollVertically()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
final int start = layoutManager.getPaddingTop();
|
||||
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
|
||||
return calculateDtToFit(top, bottom, start, end, snapPreference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the horizontal scroll amount necessary to make the given view fully visible
|
||||
* inside the RecyclerView.
|
||||
*
|
||||
* @param view The view which we want to make fully visible
|
||||
* @param snapPreference The edge which the view should snap to when entering the visible
|
||||
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
|
||||
* {@link #SNAP_TO_END}
|
||||
* @return The vertical scroll amount necessary to make the view visible with the given
|
||||
* snap preference.
|
||||
*/
|
||||
public int calculateDxToMakeVisible(View view, int snapPreference) {
|
||||
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
|
||||
if (!layoutManager.canScrollHorizontally()) {
|
||||
return 0;
|
||||
}
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
final int start = layoutManager.getPaddingLeft();
|
||||
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
|
||||
return calculateDtToFit(left, right, start, end, snapPreference);
|
||||
}
|
||||
|
||||
abstract public PointF computeScrollVectorForPosition(int targetPosition);
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.support.v7.widget.AdapterHelper.UpdateOp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE;
|
||||
import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE;
|
||||
|
||||
class OpReorderer {
|
||||
|
||||
final Callback mCallback;
|
||||
|
||||
public OpReorderer(Callback callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
void reorderOps(List<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);
|
||||
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);
|
||||
}
|
||||
// now affect of add is consumed. now apply effect of first remove
|
||||
if (moveOp.positionStart <= updateOp.positionStart) {
|
||||
updateOp.positionStart++;
|
||||
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
|
||||
final int remaining = updateOp.positionStart + updateOp.itemCount
|
||||
- moveOp.positionStart;
|
||||
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
|
||||
updateOp.itemCount -= remaining;
|
||||
}
|
||||
list.set(update, moveOp);
|
||||
if (updateOp.itemCount > 0) {
|
||||
list.set(move, updateOp);
|
||||
} else {
|
||||
list.remove(move);
|
||||
mCallback.recycleUpdateOp(updateOp);
|
||||
}
|
||||
if (extraUp1 != null) {
|
||||
list.add(move, extraUp1);
|
||||
}
|
||||
if (extraUp2 != null) {
|
||||
list.add(move, extraUp2);
|
||||
}
|
||||
}
|
||||
|
||||
private int getLastMoveOutOfOrder(List<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;
|
||||
}
|
||||
|
||||
static interface Callback {
|
||||
|
||||
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
|
||||
|
||||
void recycleUpdateOp(UpdateOp op);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
/**
|
||||
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
|
||||
* <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;
|
||||
|
||||
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 space occupied by this View in the current orientation including decorations and
|
||||
* margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view
|
||||
* @see #getDecoratedMeasurementInOther(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurement(View view);
|
||||
|
||||
/**
|
||||
* Returns the space occupied by this View in the perpendicular orientation including
|
||||
* decorations and margins.
|
||||
*
|
||||
* @param view The view element to check
|
||||
* @return Total space occupied by this view in the perpendicular orientation to current one
|
||||
* @see #getDecoratedMeasurement(View)
|
||||
*/
|
||||
public abstract int getDecoratedMeasurementInOther(View view);
|
||||
|
||||
/**
|
||||
* Returns the start position of the layout after the start padding is added.
|
||||
*
|
||||
* @return The very first pixel we can draw.
|
||||
*/
|
||||
public abstract int getStartAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout after the end padding is removed.
|
||||
*
|
||||
* @return The end boundary for this layout.
|
||||
*/
|
||||
public abstract int getEndAfterPadding();
|
||||
|
||||
/**
|
||||
* Returns the end position of the layout without taking padding into account.
|
||||
*
|
||||
* @return The end boundary for this layout without considering padding.
|
||||
*/
|
||||
public abstract int getEnd();
|
||||
|
||||
/**
|
||||
* Offsets all children's positions by the given amount.
|
||||
*
|
||||
* @param amount Value to add to each child's layout parameters
|
||||
*/
|
||||
public abstract void offsetChildren(int amount);
|
||||
|
||||
/**
|
||||
* Returns the total space to layout. This number is the difference between
|
||||
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
|
||||
*
|
||||
* @return Total space to layout children
|
||||
*/
|
||||
public abstract int getTotalSpace();
|
||||
|
||||
/**
|
||||
* Offsets the child in this orientation.
|
||||
*
|
||||
* @param view View to offset
|
||||
* @param offset offset amount
|
||||
*/
|
||||
public abstract void offsetChild(View view, int offset);
|
||||
|
||||
/**
|
||||
* Returns the padding at the end of the layout. For horizontal helper, this is the right
|
||||
* padding and for vertical helper, this is the bottom padding. This method does not check
|
||||
* whether the layout is RTL or not.
|
||||
*
|
||||
* @return The padding at the end of the layout.
|
||||
*/
|
||||
public abstract int getEndPadding();
|
||||
|
||||
/**
|
||||
* Creates an OrientationHelper for the given LayoutManager and orientation.
|
||||
*
|
||||
* @param layoutManager LayoutManager to attach to
|
||||
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createOrientationHelper(
|
||||
RecyclerView.LayoutManager layoutManager, int orientation) {
|
||||
switch (orientation) {
|
||||
case HORIZONTAL:
|
||||
return createHorizontalHelper(layoutManager);
|
||||
case VERTICAL:
|
||||
return createVerticalHelper(layoutManager);
|
||||
}
|
||||
throw new IllegalArgumentException("invalid orientation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontal OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createHorizontalHelper(
|
||||
RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenHorizontal(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingLeft();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
|
||||
- mLayoutManager.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetLeftAndRight(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingRight();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vertical OrientationHelper for the given LayoutManager.
|
||||
*
|
||||
* @param layoutManager The LayoutManager to attach to.
|
||||
* @return A new OrientationHelper
|
||||
*/
|
||||
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
|
||||
return new OrientationHelper(layoutManager) {
|
||||
@Override
|
||||
public int getEndAfterPadding() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnd() {
|
||||
return mLayoutManager.getHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChildren(int amount) {
|
||||
mLayoutManager.offsetChildrenVertical(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStartAfterPadding() {
|
||||
return mLayoutManager.getPaddingTop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurement(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
|
||||
+ params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedMeasurementInOther(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
|
||||
+ params.rightMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedEnd(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDecoratedStart(View view) {
|
||||
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
|
||||
view.getLayoutParams();
|
||||
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalSpace() {
|
||||
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
|
||||
- mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void offsetChild(View view, int offset) {
|
||||
view.offsetTopAndBottom(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEndPadding() {
|
||||
return mLayoutManager.getPaddingBottom();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.view.AccessibilityDelegateCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
/**
|
||||
* The AccessibilityDelegate used by RecyclerView.
|
||||
* <p>
|
||||
* This class handles basic accessibility actions and delegates them to LayoutManager.
|
||||
*/
|
||||
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
final RecyclerView mRecyclerView;
|
||||
|
||||
|
||||
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
}
|
||||
|
||||
private boolean shouldIgnore() {
|
||||
return mRecyclerView.hasPendingAdapterUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(RecyclerView.class.getName());
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(host, event);
|
||||
event.setClassName(RecyclerView.class.getName());
|
||||
if (host instanceof RecyclerView && !shouldIgnore()) {
|
||||
RecyclerView rv = (RecyclerView) host;
|
||||
if (rv.getLayoutManager() != null) {
|
||||
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccessibilityDelegateCompat getItemDelegate() {
|
||||
return mItemDelegate;
|
||||
}
|
||||
|
||||
final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
mRecyclerView.getLayoutManager().
|
||||
onInitializeAccessibilityNodeInfoForItem(host, info);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
||||
if (super.performAccessibilityAction(host, action, args)) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
|
||||
return mRecyclerView.getLayoutManager().
|
||||
performAccessibilityActionForItem(host, action, args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package android.support.v7.widget;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A helper class to do scroll offset calculations.
|
||||
*/
|
||||
class ScrollbarHelper {
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled, boolean reverseLayout) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
final int minPosition = Math.min(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int maxPosition = Math.max(lm.getPosition(startChild),
|
||||
lm.getPosition(endChild));
|
||||
final int itemsBefore = reverseLayout
|
||||
? Math.max(0, state.getItemCount() - maxPosition - 1)
|
||||
: Math.max(0, minPosition);
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return itemsBefore;
|
||||
}
|
||||
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild));
|
||||
final int itemRange = Math.abs(lm.getPosition(startChild) -
|
||||
lm.getPosition(endChild)) + 1;
|
||||
final float avgSizePerRow = (float) laidOutArea / itemRange;
|
||||
|
||||
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
|
||||
- orientation.getDecoratedStart(startChild)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
|
||||
}
|
||||
final int extend = orientation.getDecoratedEnd(endChild)
|
||||
- orientation.getDecoratedStart(startChild);
|
||||
return Math.min(orientation.getTotalSpace(), extend);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startChild View closest to start of the list. (top or left)
|
||||
* @param endChild View closest to end of the list (bottom or right)
|
||||
*/
|
||||
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
|
||||
View startChild, View endChild, RecyclerView.LayoutManager lm,
|
||||
boolean smoothScrollbarEnabled) {
|
||||
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
|
||||
endChild == null) {
|
||||
return 0;
|
||||
}
|
||||
if (!smoothScrollbarEnabled) {
|
||||
return state.getItemCount();
|
||||
}
|
||||
// smooth scrollbar enabled. try to estimate better.
|
||||
final int laidOutArea = orientation.getDecoratedEnd(endChild) -
|
||||
orientation.getDecoratedStart(startChild);
|
||||
final int laidOutRange = Math.abs(lm.getPosition(startChild) -
|
||||
lm.getPosition(endChild))
|
||||
+ 1;
|
||||
// estimate a size for full list.
|
||||
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -350,7 +350,9 @@ public class ContentLayout extends FrameLayout {
|
||||
if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {
|
||||
mFirstIndex += pageVolume;
|
||||
mLastIndex += pageVolume;
|
||||
// TODO
|
||||
|
||||
mRecyclerView.stopScroll();
|
||||
LayoutManagerUtils.smoothScrollToPosition(mLayoutManager, mContext, mFirstIndex - 1, 100);
|
||||
} else {
|
||||
mCurrentPage = mFirstPage;
|
||||
mFirstIndex = 0;
|
||||
@@ -464,7 +466,8 @@ public class ContentLayout extends FrameLayout {
|
||||
if (mViewTransition.getShownViewIndex() == 0) {
|
||||
// Go to top
|
||||
mRecyclerView.stopScroll();
|
||||
mLayoutManager.smoothScrollToPosition(mRecyclerView, null, 0);
|
||||
LayoutManagerUtils.scrollToPositionWithOffset(mLayoutManager, 0, 0);
|
||||
onScrollToPosition();
|
||||
// Show header refresh
|
||||
mRefreshLayout.setFooterRefreshing(false);
|
||||
mRefreshLayout.setHeaderRefreshing(true);
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
|
||||
package com.hippo.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.StaggeredGridLayoutManager;
|
||||
|
||||
import com.hippo.widget.recyclerview.SimpleSmoothScroller;
|
||||
|
||||
public final class LayoutManagerUtils {
|
||||
|
||||
public static void scrollToPositionWithOffset(
|
||||
@@ -33,6 +37,47 @@ public final class LayoutManagerUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void smoothScrollToPosition(
|
||||
RecyclerView.LayoutManager layoutManager, Context context, int position) {
|
||||
smoothScrollToPosition(layoutManager, context, position, -1);
|
||||
}
|
||||
|
||||
public static void smoothScrollToPosition(
|
||||
RecyclerView.LayoutManager layoutManager, Context context, int position,
|
||||
int millisecondsPerInch) {
|
||||
SimpleSmoothScroller smoothScroller;
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
final LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
|
||||
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
return linearLayoutManager.computeScrollVectorForPosition(targetPosition);
|
||||
}
|
||||
};
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
final StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager) layoutManager;
|
||||
smoothScroller = new SimpleSmoothScroller(context, millisecondsPerInch) {
|
||||
@Override
|
||||
public PointF computeScrollVectorForPosition(int targetPosition) {
|
||||
final int direction = staggeredGridLayoutManager.calculateScrollDirectionForPosition(targetPosition);
|
||||
if (direction == 0) {
|
||||
return null;
|
||||
}
|
||||
if (staggeredGridLayoutManager.getOrientation() == StaggeredGridLayoutManager.HORIZONTAL) {
|
||||
return new PointF(direction, 0);
|
||||
} else {
|
||||
return new PointF(0, direction);
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new IllegalStateException("Can't do smoothScrollToPosition for " +
|
||||
layoutManager.getClass().getName());
|
||||
}
|
||||
smoothScroller.setTargetPosition(position);
|
||||
layoutManager.startSmoothScroll(smoothScroller);
|
||||
}
|
||||
|
||||
public static int getFirstVisibleItemPostion(RecyclerView.LayoutManager layoutManager) {
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
return ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.hippo.widget.recyclerview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.LinearSmoothScroller;
|
||||
|
||||
public abstract class SimpleSmoothScroller extends LinearSmoothScroller {
|
||||
|
||||
private float mMillisecondsPerPx;
|
||||
|
||||
public SimpleSmoothScroller(Context context, float millisecondsPerInch) {
|
||||
super(context);
|
||||
mMillisecondsPerPx = millisecondsPerInch / context.getResources().getDisplayMetrics().densityDpi;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int calculateTimeForScrolling(int dx) {
|
||||
if (mMillisecondsPerPx <= 0) {
|
||||
return super.calculateTimeForScrolling(dx);
|
||||
} else {
|
||||
return (int) Math.ceil(Math.abs(dx) * mMillisecondsPerPx);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user