blob: 3ef0b3f04f6a141792bc3bc882441daac6b516d3 [file] [log] [blame]
/*
* Copyright (C) 2015 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.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_CHANGED;
import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_MOVED;
import static android.support.v7.widget.RecyclerView.ItemAnimator.FLAG_REMOVED;
/**
* Includes tests for the new RecyclerView animations API (v2).
*/
public class ItemAnimatorV2ApiTest extends BaseRecyclerViewAnimationsTest {
@Override
protected RecyclerView.ItemAnimator createItemAnimator() {
return mAnimator;
}
public void testChangeMovedOutside() throws Throwable {
setupBasic(10);
final RecyclerView.ViewHolder target = mRecyclerView.findViewHolderForAdapterPosition(9);
mLayoutManager.expectLayouts(2);
mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
mTestAdapter.changeAndNotify(9, 1);
mLayoutManager.waitForLayout(2);
// changed item shold not be laid out and should just receive disappear
LoggingInfo pre = mAnimator.preLayoutInfoMap.get(target);
assertNotNull("test sanity", pre);
assertNull("test sanity", mAnimator.postLayoutInfoMap.get(target));
assertTrue(mAnimator.animateChangeList.isEmpty());
assertEquals(1, mAnimator.animateDisappearanceList.size());
assertEquals(new AnimateDisappearance(target, pre, null),
mAnimator.animateDisappearanceList.get(0));
// This is kind of problematic because layout manager will never layout the updated
// version of this view since it went out of bounds and it won't show up in scrap.
// I don't think we can do much better since other option is to bind a fresh view
}
public void testSimpleAdd() throws Throwable {
setupBasic(10);
mLayoutManager.expectLayouts(2);
mTestAdapter.addAndNotify(2, 1);
mLayoutManager.waitForLayout(2);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
assertEquals(1, mAnimator.animateAppearanceList.size());
AnimateAppearance log = mAnimator.animateAppearanceList.get(0);
assertSame(vh, log.viewHolder);
assertNull(log.preInfo);
assertEquals(0, log.postInfo.changeFlags);
// the first two should not receive anything
for (int i = 0; i < 2; i++) {
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
for (int i = 3; i < mTestAdapter.getItemCount(); i++) {
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(FLAG_MOVED, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testSimpleRemove() throws Throwable {
setupBasic(10);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
mLayoutManager.expectLayouts(2);
mTestAdapter.deleteAndNotify(2, 1);
mLayoutManager.waitForLayout(2);
checkForMainThreadException();
assertEquals(1, mAnimator.animateDisappearanceList.size());
AnimateDisappearance log = mAnimator.animateDisappearanceList.get(0);
assertSame(vh, log.viewHolder);
assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
assertEquals(FLAG_REMOVED, log.preInfo.changeFlags);
// the first two should not receive anything
for (int i = 0; i < 2; i++) {
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
for (int i = 3; i < mTestAdapter.getItemCount(); i++) {
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(FLAG_MOVED, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testSimpleUpdate() throws Throwable {
setupBasic(10);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
mLayoutManager.expectLayouts(2);
mTestAdapter.changeAndNotify(2, 1);
mLayoutManager.waitForLayout(2);
assertEquals(1, mAnimator.animateChangeList.size());
AnimateChange log = mAnimator.animateChangeList.get(0);
assertSame(vh, log.viewHolder);
assertSame(vh, log.newHolder);
assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
assertTrue(mAnimator.postLayoutInfoMap.containsKey(vh));
assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
assertEquals(0, log.postInfo.changeFlags);
//others should not receive anything
for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
if (i == 2) {
continue;
}
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testUpdateWithDuplicateViewHolder() throws Throwable {
setupBasic(10);
final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
mAnimator.canReUseCallback = new CanReUseCallback() {
@Override
public boolean canReUse(RecyclerView.ViewHolder viewHolder) {
assertSame(viewHolder, vh);
return false;
}
};
mLayoutManager.expectLayouts(2);
mTestAdapter.changeAndNotify(2, 1);
mLayoutManager.waitForLayout(2);
final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
assertNotSame(vh, newVh);
assertEquals(1, mAnimator.animateChangeList.size());
AnimateChange log = mAnimator.animateChangeList.get(0);
assertSame(vh, log.viewHolder);
assertSame(newVh, log.newHolder);
assertNull(vh.itemView.getParent());
assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
assertTrue(mAnimator.postLayoutInfoMap.containsKey(newVh));
assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
assertEquals(0, log.postInfo.changeFlags);
//others should not receive anything
for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
if (i == 2) {
continue;
}
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testUpdateWithOneDuplicateAndOneInPlace() throws Throwable {
setupBasic(10);
final RecyclerView.ViewHolder replaced = mRecyclerView.findViewHolderForAdapterPosition(2);
final RecyclerView.ViewHolder reused = mRecyclerView.findViewHolderForAdapterPosition(3);
mAnimator.canReUseCallback = new CanReUseCallback() {
@Override
public boolean canReUse(RecyclerView.ViewHolder viewHolder) {
if (viewHolder == replaced) {
return false;
} else if (viewHolder == reused) {
return true;
}
fail("unpexpected view");
return false;
}
};
mLayoutManager.expectLayouts(2);
mTestAdapter.changeAndNotify(2, 2);
mLayoutManager.waitForLayout(2);
final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
assertNotSame(replaced, newVh);
assertSame(reused, mRecyclerView.findViewHolderForAdapterPosition(3));
assertEquals(2, mAnimator.animateChangeList.size());
AnimateChange logReplaced = null, logReused = null;
for (AnimateChange change : mAnimator.animateChangeList) {
if (change.newHolder == change.viewHolder) {
logReused = change;
} else {
logReplaced = change;
}
}
assertNotNull(logReplaced);
assertNotNull(logReused);
assertSame(replaced, logReplaced.viewHolder);
assertSame(newVh, logReplaced.newHolder);
assertSame(reused, logReused.viewHolder);
assertSame(reused, logReused.newHolder);
assertTrue(mAnimator.preLayoutInfoMap.containsKey(replaced));
assertTrue(mAnimator.preLayoutInfoMap.containsKey(reused));
assertTrue(mAnimator.postLayoutInfoMap.containsKey(newVh));
assertTrue(mAnimator.postLayoutInfoMap.containsKey(reused));
assertFalse(mAnimator.postLayoutInfoMap.containsKey(replaced));
assertEquals(FLAG_CHANGED, logReplaced.preInfo.changeFlags);
assertEquals(FLAG_CHANGED, logReused.preInfo.changeFlags);
assertEquals(0, logReplaced.postInfo.changeFlags);
assertEquals(0, logReused.postInfo.changeFlags);
//others should not receive anything
for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
if (i == 2 || i == 3) {
continue;
}
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testChangeToDisappear() throws Throwable {
setupBasic(10);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(9);
mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
mLayoutManager.expectLayouts(2);
mTestAdapter.changeAndNotify(9, 1);
mLayoutManager.waitForLayout(2);
assertEquals(1, mAnimator.animateDisappearanceList.size());
AnimateDisappearance log = mAnimator.animateDisappearanceList.get(0);
assertSame(vh, log.viewHolder);
assertFalse(mAnimator.postLayoutInfoMap.containsKey(vh));
assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
assertEquals(0, mAnimator.animateChangeList.size());
assertEquals(0, mAnimator.animateAppearanceList.size());
assertEquals(9, mAnimator.animatePersistenceList.size());
checkForMainThreadException();
}
public void testUpdatePayload() throws Throwable {
setupBasic(10);
RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
mLayoutManager.expectLayouts(2);
Object payload = new Object();
mTestAdapter.changeAndNotifyWithPayload(2, 1, payload);
mLayoutManager.waitForLayout(2);
assertEquals(1, mAnimator.animateChangeList.size());
AnimateChange log = mAnimator.animateChangeList.get(0);
assertSame(vh, log.viewHolder);
assertSame(vh, log.newHolder);
assertTrue(mAnimator.preLayoutInfoMap.containsKey(vh));
assertTrue(mAnimator.postLayoutInfoMap.containsKey(vh));
assertEquals(FLAG_CHANGED, log.preInfo.changeFlags);
assertEquals(0, log.postInfo.changeFlags);
assertNotNull(log.preInfo.payloads);
assertTrue(log.preInfo.payloads.contains(payload));
//others should not receive anything
for (int i = 0; i < mTestAdapter.getItemCount(); i++) {
if (i == 2) {
continue;
}
RecyclerView.ViewHolder other = mRecyclerView.findViewHolderForAdapterPosition(i);
assertEquals(0, mAnimator.preLayoutInfoMap.get(other).changeFlags);
}
checkForMainThreadException();
}
public void testNotifyDataSetChanged() throws Throwable {
TestAdapter adapter = new TestAdapter(10);
adapter.setHasStableIds(true);
setupBasic(10, 0, 10, adapter);
mLayoutManager.expectLayouts(1);
mTestAdapter.dispatchDataSetChanged();
mLayoutManager.waitForLayout(2);
assertEquals(10, mAnimator.animateChangeList.size());
for (AnimateChange change : mAnimator.animateChangeList) {
assertNotNull(change.preInfo);
assertNotNull(change.postInfo);
assertSame(change.preInfo.viewHolder, change.postInfo.viewHolder);
}
assertEquals(0, mAnimator.animatePersistenceList.size());
assertEquals(0, mAnimator.animateAppearanceList.size());
assertEquals(0, mAnimator.animateDisappearanceList.size());
}
public void testNotifyDataSetChangedWithoutStableIds() throws Throwable {
TestAdapter adapter = new TestAdapter(10);
adapter.setHasStableIds(false);
setupBasic(10, 0, 10, adapter);
mLayoutManager.expectLayouts(1);
mTestAdapter.dispatchDataSetChanged();
mLayoutManager.waitForLayout(2);
assertEquals(0, mAnimator.animateChangeList.size());
assertEquals(0, mAnimator.animatePersistenceList.size());
assertEquals(0, mAnimator.animateAppearanceList.size());
assertEquals(0, mAnimator.animateDisappearanceList.size());
}
public void testNotifyDataSetChangedWithAppearing() throws Throwable {
notifyDataSetChangedWithAppearing(false);
}
public void testNotifyDataSetChangedWithAppearingNotifyBoth() throws Throwable {
notifyDataSetChangedWithAppearing(true);
}
public void notifyDataSetChangedWithAppearing(final boolean notifyBoth) throws Throwable {
final TestAdapter adapter = new TestAdapter(10);
adapter.setHasStableIds(true);
setupBasic(10, 0, 10, adapter);
mLayoutManager.expectLayouts(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
try {
if (notifyBoth) {
adapter.addAndNotify(2, 2);
} else {
adapter.mItems.add(2, new Item(2, "custom 1"));
adapter.mItems.add(3, new Item(3, "custom 2"));
}
adapter.notifyDataSetChanged();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
});
mLayoutManager.waitForLayout(2);
assertEquals(10, mAnimator.animateChangeList.size());
assertEquals(0, mAnimator.animatePersistenceList.size());
assertEquals(2, mAnimator.animateAppearanceList.size());
assertEquals(0, mAnimator.animateDisappearanceList.size());
}
public void testNotifyDataSetChangedWithDispappearing() throws Throwable {
notifyDataSetChangedWithDispappearing(false);
}
public void testNotifyDataSetChangedWithDispappearingNotifyBoth() throws Throwable {
notifyDataSetChangedWithDispappearing(true);
}
public void notifyDataSetChangedWithDispappearing(final boolean notifyBoth) throws Throwable {
final TestAdapter adapter = new TestAdapter(10);
adapter.setHasStableIds(true);
setupBasic(10, 0, 10, adapter);
mLayoutManager.expectLayouts(1);
runTestOnUiThread(new Runnable() {
@Override
public void run() {
try {
if (notifyBoth) {
adapter.deleteAndNotify(2, 2);
} else {
adapter.mItems.remove(2);
adapter.mItems.remove(2);
}
adapter.notifyDataSetChanged();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
});
mLayoutManager.waitForLayout(2);
assertEquals(8, mAnimator.animateChangeList.size());
assertEquals(0, mAnimator.animatePersistenceList.size());
assertEquals(0, mAnimator.animateAppearanceList.size());
assertEquals(2, mAnimator.animateDisappearanceList.size());
}
public void testNotifyUpdateWithChangedAdapterType() throws Throwable {
final AtomicInteger itemType = new AtomicInteger(1);
final TestAdapter adapter = new TestAdapter(10) {
@Override
public int getItemViewType(int position) {
return position == 2 ? itemType.get() : 20;
}
};
adapter.setHasStableIds(true);
setupBasic(10, 0, 10, adapter);
final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(2);
mAnimator.canReUseCallback = new CanReUseCallback() {
@Override
public boolean canReUse(RecyclerView.ViewHolder viewHolder) {
return viewHolder != vh;
}
};
mLayoutManager.expectLayouts(1);
itemType.set(3);
adapter.dispatchDataSetChanged();
mLayoutManager.waitForLayout(2);
final RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(2);
// TODO we should be able to map old type to the new one but doing that change has some
// recycling side effects.
assertEquals(9, mAnimator.animateChangeList.size());
assertEquals(0, mAnimator.animatePersistenceList.size());
assertEquals(1, mAnimator.animateAppearanceList.size());
assertEquals(0, mAnimator.animateDisappearanceList.size());
assertNotSame(vh, newVh);
for (AnimateChange change : mAnimator.animateChangeList) {
if (change.viewHolder == vh) {
assertSame(change.newHolder, newVh);
assertSame(change.viewHolder, vh);
} else {
assertSame(change.newHolder, change.viewHolder);
}
}
}
LoggingV2Animator mAnimator = new LoggingV2Animator();
class LoggingV2Animator extends RecyclerView.ItemAnimator {
CanReUseCallback canReUseCallback = new CanReUseCallback() {
@Override
public boolean canReUse(RecyclerView.ViewHolder viewHolder) {
return true;
}
};
Map<RecyclerView.ViewHolder, LoggingInfo> preLayoutInfoMap = new HashMap<>();
Map<RecyclerView.ViewHolder, LoggingInfo> postLayoutInfoMap = new HashMap<>();
List<AnimateAppearance> animateAppearanceList = new ArrayList<>();
List<AnimateDisappearance> animateDisappearanceList = new ArrayList<>();
List<AnimatePersistence> animatePersistenceList = new ArrayList<>();
List<AnimateChange> animateChangeList = new ArrayList<>();
@Override
public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
return canReUseCallback.canReUse(viewHolder);
}
@NonNull
@Override
public ItemHolderInfo recordPreLayoutInformation(@NonNull RecyclerView.State state,
@NonNull RecyclerView.ViewHolder viewHolder,
@AdapterChanges int changeFlags, @NonNull List<Object> payloads) {
LoggingInfo loggingInfo = new LoggingInfo(viewHolder, changeFlags, payloads);
preLayoutInfoMap.put(viewHolder, loggingInfo);
return loggingInfo;
}
@NonNull
@Override
public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
@NonNull RecyclerView.ViewHolder viewHolder) {
LoggingInfo loggingInfo = new LoggingInfo(viewHolder, 0, null);
postLayoutInfoMap.put(viewHolder, loggingInfo);
return loggingInfo;
}
@Override
public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo,
@Nullable ItemHolderInfo postLayoutInfo) {
animateDisappearanceList.add(new AnimateDisappearance(viewHolder,
(LoggingInfo) preLayoutInfo, (LoggingInfo) postLayoutInfo));
assertSame(preLayoutInfoMap.get(viewHolder), preLayoutInfo);
assertSame(postLayoutInfoMap.get(viewHolder), postLayoutInfo);
dispatchAnimationFinished(viewHolder);
return false;
}
@Override
public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder,
ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
animateAppearanceList.add(
new AnimateAppearance(viewHolder, (LoggingInfo) preInfo, (LoggingInfo) postInfo));
assertSame(preLayoutInfoMap.get(viewHolder), preInfo);
assertSame(postLayoutInfoMap.get(viewHolder), postInfo);
dispatchAnimationFinished(viewHolder);
return false;
}
@Override
public boolean animatePersistence(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
animatePersistenceList.add(new AnimatePersistence(viewHolder, (LoggingInfo) preInfo,
(LoggingInfo) postInfo));
dispatchAnimationFinished(viewHolder);
assertSame(preLayoutInfoMap.get(viewHolder), preInfo);
assertSame(postLayoutInfoMap.get(viewHolder), postInfo);
return false;
}
@Override
public boolean animateChange(@NonNull RecyclerView.ViewHolder oldHolder,
@NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
@NonNull ItemHolderInfo postInfo) {
animateChangeList.add(new AnimateChange(oldHolder, newHolder, (LoggingInfo) preInfo,
(LoggingInfo) postInfo));
if (oldHolder != null) {
dispatchAnimationFinished(oldHolder);
assertSame(preLayoutInfoMap.get(oldHolder), preInfo);
}
if (newHolder != null) {
dispatchAnimationFinished(newHolder);
assertSame(postLayoutInfoMap.get(newHolder), postInfo);
}
return false;
}
@Override
public void runPendingAnimations() {
}
@Override
public void endAnimation(RecyclerView.ViewHolder item) {
}
@Override
public void endAnimations() {
}
@Override
public boolean isRunning() {
return false;
}
}
interface CanReUseCallback {
boolean canReUse(RecyclerView.ViewHolder viewHolder);
}
}