| /* |
| * 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 org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import android.os.Looper; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.test.ActivityInstrumentationTestCase2; |
| import android.test.suitebuilder.annotation.MediumTest; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.TextView; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.Semaphore; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| import static org.junit.Assert.*; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class DefaultItemAnimatorTest extends BaseRecyclerViewInstrumentationTest { |
| |
| private static final String TAG = "DefaultItemAnimatorTest"; |
| Throwable mainThreadException; |
| |
| DefaultItemAnimator mAnimator; |
| Adapter mAdapter; |
| ViewGroup mDummyParent; |
| List<RecyclerView.ViewHolder> mExpectedItems = new ArrayList<RecyclerView.ViewHolder>(); |
| |
| Set<RecyclerView.ViewHolder> mRemoveFinished = new HashSet<RecyclerView.ViewHolder>(); |
| Set<RecyclerView.ViewHolder> mAddFinished = new HashSet<RecyclerView.ViewHolder>(); |
| Set<RecyclerView.ViewHolder> mMoveFinished = new HashSet<RecyclerView.ViewHolder>(); |
| Set<RecyclerView.ViewHolder> mChangeFinished = new HashSet<RecyclerView.ViewHolder>(); |
| |
| Semaphore mExpectedItemCount = new Semaphore(0); |
| |
| @Before |
| public void setUp() throws Exception { |
| mAnimator = new DefaultItemAnimator() { |
| @Override |
| public void onRemoveFinished(RecyclerView.ViewHolder item) { |
| try { |
| assertTrue(mRemoveFinished.add(item)); |
| onFinished(item); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| |
| @Override |
| public void onAddFinished(RecyclerView.ViewHolder item) { |
| try { |
| assertTrue(mAddFinished.add(item)); |
| onFinished(item); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| |
| @Override |
| public void onMoveFinished(RecyclerView.ViewHolder item) { |
| try { |
| assertTrue(mMoveFinished.add(item)); |
| onFinished(item); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| |
| @Override |
| public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) { |
| try { |
| assertTrue(mChangeFinished.add(item)); |
| onFinished(item); |
| } catch (Throwable t) { |
| postExceptionToInstrumentation(t); |
| } |
| } |
| |
| private void onFinished(RecyclerView.ViewHolder item) { |
| assertNotNull(mExpectedItems.remove(item)); |
| mExpectedItemCount.release(1); |
| } |
| }; |
| mAdapter = new Adapter(20); |
| mDummyParent = getActivity().getContainer(); |
| } |
| |
| void checkForMainThreadException() throws Throwable { |
| if (mainThreadException != null) { |
| throw mainThreadException; |
| } |
| } |
| |
| @Test |
| public void reUseWithPayload() { |
| RecyclerView.ViewHolder vh = new ViewHolder(new TextView(getActivity())); |
| assertFalse(mAnimator.canReuseUpdatedViewHolder(vh, new ArrayList<>())); |
| assertTrue(mAnimator.canReuseUpdatedViewHolder(vh, Arrays.asList((Object) "a"))); |
| } |
| |
| void expectItems(RecyclerView.ViewHolder... viewHolders) { |
| mExpectedItems.addAll(Arrays.asList(viewHolders)); |
| } |
| |
| void runAndWait(int itemCount, int seconds) throws Throwable { |
| runAndWait(itemCount, seconds, null); |
| } |
| |
| void runAndWait(int itemCount, int seconds, final ThrowingRunnable postRun) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mAnimator.runPendingAnimations(); |
| if (postRun != null) { |
| try { |
| postRun.run(); |
| } catch (Throwable e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| }); |
| waitForItems(itemCount, seconds); |
| checkForMainThreadException(); |
| } |
| |
| void waitForItems(int itemCount, int seconds) throws InterruptedException { |
| assertTrue("all vh animations should end", |
| mExpectedItemCount.tryAcquire(itemCount, seconds, TimeUnit.SECONDS)); |
| assertEquals("all expected finish events should happen", 0, mExpectedItems.size()); |
| // wait one more second for unwanted |
| assertFalse("should not receive any more permits", |
| mExpectedItemCount.tryAcquire(1, 2, TimeUnit.SECONDS)); |
| } |
| |
| @Test |
| public void animateAdd() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateAdd(vh)); |
| assertTrue(mAnimator.isRunning()); |
| runAndWait(1, 1); |
| } |
| |
| @Test |
| public void animateRemove() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateRemove(vh)); |
| assertTrue(mAnimator.isRunning()); |
| runAndWait(1, 1); |
| } |
| |
| @Test |
| public void animateMove() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateMove(vh, 0, 0, 100, 100)); |
| assertTrue(mAnimator.isRunning()); |
| runAndWait(1, 1); |
| } |
| |
| @Test |
| public void animateChange() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| ViewHolder vh2 = createViewHolder(2); |
| expectItems(vh, vh2); |
| assertTrue(animateChange(vh, vh2, 0, 0, 100, 100)); |
| assertTrue(mAnimator.isRunning()); |
| runAndWait(2, 1); |
| } |
| |
| public void cancelBefore(int count, final RecyclerView.ViewHolder... toCancel) |
| throws Throwable { |
| cancelTest(true, count, toCancel); |
| } |
| |
| public void cancelAfter(int count, final RecyclerView.ViewHolder... toCancel) |
| throws Throwable { |
| cancelTest(false, count, toCancel); |
| } |
| |
| public void cancelTest(boolean before, int count, final RecyclerView.ViewHolder... toCancel) throws Throwable { |
| if (before) { |
| endAnimations(toCancel); |
| runAndWait(count, 1); |
| } else { |
| runAndWait(count, 1, new ThrowingRunnable() { |
| @Override |
| public void run() throws Throwable { |
| endAnimations(toCancel); |
| } |
| }); |
| } |
| } |
| |
| @Test |
| public void cancelAddBefore() throws Throwable { |
| final ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateAdd(vh)); |
| cancelBefore(1, vh); |
| } |
| |
| @Test |
| public void cancelAddAfter() throws Throwable { |
| final ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateAdd(vh)); |
| cancelAfter(1, vh); |
| } |
| |
| @Test |
| public void cancelMoveBefore() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateMove(vh, 10, 10, 100, 100)); |
| cancelBefore(1, vh); |
| } |
| |
| @Test |
| public void cancelMoveAfter() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateMove(vh, 10, 10, 100, 100)); |
| cancelAfter(1, vh); |
| } |
| |
| @Test |
| public void cancelRemove() throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| expectItems(vh); |
| assertTrue(animateRemove(vh)); |
| endAnimations(vh); |
| runAndWait(1, 1); |
| } |
| |
| @Test |
| public void cancelChangeOldBefore() throws Throwable { |
| cancelChangeOldTest(true); |
| } |
| @Test |
| public void cancelChangeOldAfter() throws Throwable { |
| cancelChangeOldTest(false); |
| } |
| |
| public void cancelChangeOldTest(boolean before) throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| ViewHolder vh2 = createViewHolder(1); |
| expectItems(vh, vh2); |
| assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); |
| cancelTest(before, 2, vh); |
| } |
| |
| @Test |
| public void cancelChangeNewBefore() throws Throwable { |
| cancelChangeNewTest(true); |
| } |
| |
| @Test |
| public void cancelChangeNewAfter() throws Throwable { |
| cancelChangeNewTest(false); |
| } |
| |
| public void cancelChangeNewTest(boolean before) throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| ViewHolder vh2 = createViewHolder(1); |
| expectItems(vh, vh2); |
| assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); |
| cancelTest(before, 2, vh2); |
| } |
| |
| @Test |
| public void cancelChangeBothBefore() throws Throwable { |
| cancelChangeBothTest(true); |
| } |
| |
| @Test |
| public void cancelChangeBothAfter() throws Throwable { |
| cancelChangeBothTest(false); |
| } |
| |
| public void cancelChangeBothTest(boolean before) throws Throwable { |
| ViewHolder vh = createViewHolder(1); |
| ViewHolder vh2 = createViewHolder(1); |
| expectItems(vh, vh2); |
| assertTrue(animateChange(vh, vh2, 20, 20, 100, 100)); |
| cancelTest(before, 2, vh, vh2); |
| } |
| |
| void endAnimations(final RecyclerView.ViewHolder... vhs) throws Throwable { |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| for (RecyclerView.ViewHolder vh : vhs) { |
| mAnimator.endAnimation(vh); |
| } |
| } |
| }); |
| } |
| |
| boolean animateAdd(final RecyclerView.ViewHolder vh) throws Throwable { |
| final boolean[] result = new boolean[1]; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| result[0] = mAnimator.animateAdd(vh); |
| } |
| }); |
| return result[0]; |
| } |
| |
| boolean animateRemove(final RecyclerView.ViewHolder vh) throws Throwable { |
| final boolean[] result = new boolean[1]; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| result[0] = mAnimator.animateRemove(vh); |
| } |
| }); |
| return result[0]; |
| } |
| |
| boolean animateMove(final RecyclerView.ViewHolder vh, final int fromX, final int fromY, |
| final int toX, final int toY) throws Throwable { |
| final boolean[] result = new boolean[1]; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| result[0] = mAnimator.animateMove(vh, fromX, fromY, toX, toY); |
| } |
| }); |
| return result[0]; |
| } |
| |
| boolean animateChange(final RecyclerView.ViewHolder oldHolder, |
| final RecyclerView.ViewHolder newHolder, |
| final int fromX, final int fromY, final int toX, final int toY) throws Throwable { |
| final boolean[] result = new boolean[1]; |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| result[0] = mAnimator.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY); |
| } |
| }); |
| return result[0]; |
| } |
| |
| private ViewHolder createViewHolder(final int pos) throws Throwable { |
| final ViewHolder vh = mAdapter.createViewHolder(mDummyParent, 1); |
| runTestOnUiThread(new Runnable() { |
| @Override |
| public void run() { |
| mAdapter.bindViewHolder(vh, pos); |
| mDummyParent.addView(vh.itemView); |
| } |
| }); |
| |
| return vh; |
| } |
| |
| void postExceptionToInstrumentation(Throwable t) { |
| if (mainThreadException == null) { |
| mainThreadException = t; |
| } else { |
| Log.e(TAG, "skipping secondary main thread exception", t); |
| } |
| } |
| |
| |
| private class Adapter extends RecyclerView.Adapter<ViewHolder> { |
| |
| List<String> mItems; |
| |
| private Adapter(int count) { |
| mItems = new ArrayList<>(); |
| for (int i = 0; i < count; i++) { |
| mItems.add("item-" + i); |
| } |
| } |
| |
| @Override |
| public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| return new ViewHolder(new TextView(parent.getContext())); |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder holder, int position) { |
| holder.bind(mItems.get(position)); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mItems.size(); |
| } |
| } |
| |
| private class ViewHolder extends RecyclerView.ViewHolder { |
| |
| String mBindedText; |
| |
| public ViewHolder(View itemView) { |
| super(itemView); |
| } |
| |
| public void bind(String text) { |
| mBindedText = text; |
| ((TextView) itemView).setText(text); |
| } |
| } |
| |
| private interface ThrowingRunnable { |
| void run() throws Throwable; |
| } |
| |
| @Override |
| public void runTestOnUiThread(Runnable r) throws Throwable { |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| r.run(); |
| } else { |
| super.runTestOnUiThread(r); |
| } |
| } |
| } |