Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.recyclerview.widget;
     18 
     19 import static org.junit.Assert.assertEquals;
     20 import static org.junit.Assert.assertFalse;
     21 import static org.junit.Assert.assertNotNull;
     22 import static org.junit.Assert.assertTrue;
     23 
     24 import android.support.test.filters.LargeTest;
     25 import android.support.test.runner.AndroidJUnit4;
     26 import android.util.Log;
     27 import android.view.View;
     28 import android.view.ViewGroup;
     29 import android.widget.TextView;
     30 
     31 import androidx.annotation.NonNull;
     32 
     33 import org.junit.Before;
     34 import org.junit.Test;
     35 import org.junit.runner.RunWith;
     36 
     37 import java.util.ArrayList;
     38 import java.util.Arrays;
     39 import java.util.HashSet;
     40 import java.util.List;
     41 import java.util.Set;
     42 import java.util.concurrent.Semaphore;
     43 import java.util.concurrent.TimeUnit;
     44 
     45 @LargeTest
     46 @RunWith(AndroidJUnit4.class)
     47 public class DefaultItemAnimatorTest extends BaseRecyclerViewInstrumentationTest {
     48 
     49     private static final String TAG = "DefaultItemAnimatorTest";
     50     Throwable mainThreadException;
     51 
     52     DefaultItemAnimator mAnimator;
     53     Adapter mAdapter;
     54     ViewGroup mDummyParent;
     55     List<RecyclerView.ViewHolder> mExpectedItems = new ArrayList<RecyclerView.ViewHolder>();
     56 
     57     Set<RecyclerView.ViewHolder> mRemoveFinished = new HashSet<RecyclerView.ViewHolder>();
     58     Set<RecyclerView.ViewHolder> mAddFinished = new HashSet<RecyclerView.ViewHolder>();
     59     Set<RecyclerView.ViewHolder> mMoveFinished = new HashSet<RecyclerView.ViewHolder>();
     60     Set<RecyclerView.ViewHolder> mChangeFinished = new HashSet<RecyclerView.ViewHolder>();
     61 
     62     Semaphore mExpectedItemCount = new Semaphore(0);
     63 
     64     @Before
     65     public void setUp() throws Exception {
     66         mAnimator = new DefaultItemAnimator() {
     67             @Override
     68             public void onRemoveFinished(RecyclerView.ViewHolder item) {
     69                 try {
     70                     assertTrue(mRemoveFinished.add(item));
     71                     onFinished(item);
     72                 } catch (Throwable t) {
     73                     postExceptionToInstrumentation(t);
     74                 }
     75             }
     76 
     77             @Override
     78             public void onAddFinished(RecyclerView.ViewHolder item) {
     79                 try {
     80                     assertTrue(mAddFinished.add(item));
     81                     onFinished(item);
     82                 } catch (Throwable t) {
     83                     postExceptionToInstrumentation(t);
     84                 }
     85             }
     86 
     87             @Override
     88             public void onMoveFinished(RecyclerView.ViewHolder item) {
     89                 try {
     90                     assertTrue(mMoveFinished.add(item));
     91                     onFinished(item);
     92                 } catch (Throwable t) {
     93                     postExceptionToInstrumentation(t);
     94                 }
     95             }
     96 
     97             @Override
     98             public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) {
     99                 try {
    100                     assertTrue(mChangeFinished.add(item));
    101                     onFinished(item);
    102                 } catch (Throwable t) {
    103                     postExceptionToInstrumentation(t);
    104                 }
    105             }
    106 
    107             private void onFinished(RecyclerView.ViewHolder item) {
    108                 assertNotNull(mExpectedItems.remove(item));
    109                 mExpectedItemCount.release(1);
    110             }
    111         };
    112         mAdapter = new Adapter(20);
    113         mDummyParent = getActivity().getContainer();
    114     }
    115 
    116     @Override
    117     void checkForMainThreadException() throws Throwable {
    118         if (mainThreadException != null) {
    119             throw mainThreadException;
    120         }
    121     }
    122 
    123     @Test
    124     public void reUseWithPayload() {
    125         RecyclerView.ViewHolder vh = new ViewHolder(new TextView(getActivity()));
    126         assertFalse(mAnimator.canReuseUpdatedViewHolder(vh, new ArrayList<>()));
    127         assertTrue(mAnimator.canReuseUpdatedViewHolder(vh, Arrays.asList((Object) "a")));
    128     }
    129 
    130     void expectItems(RecyclerView.ViewHolder... viewHolders) {
    131         mExpectedItems.addAll(Arrays.asList(viewHolders));
    132     }
    133 
    134     void runAndWait(int itemCount, int seconds) throws Throwable {
    135         runAndWait(itemCount, seconds, null);
    136     }
    137 
    138     void runAndWait(int itemCount, int seconds, final ThrowingRunnable postRun) throws Throwable {
    139         mActivityRule.runOnUiThread(new Runnable() {
    140             @Override
    141             public void run() {
    142                 mAnimator.runPendingAnimations();
    143                 if (postRun != null) {
    144                     try {
    145                         postRun.run();
    146                     } catch (Throwable e) {
    147                         throw new RuntimeException(e);
    148                     }
    149                 }
    150             }
    151         });
    152         waitForItems(itemCount, seconds);
    153         checkForMainThreadException();
    154     }
    155 
    156     void waitForItems(int itemCount, int seconds) throws InterruptedException {
    157         assertTrue("all vh animations should end",
    158                 mExpectedItemCount.tryAcquire(itemCount, seconds, TimeUnit.SECONDS));
    159         assertEquals("all expected finish events should happen", 0, mExpectedItems.size());
    160         // wait one more second for unwanted
    161         assertFalse("should not receive any more permits",
    162                 mExpectedItemCount.tryAcquire(1, 2, TimeUnit.SECONDS));
    163     }
    164 
    165     @Test
    166     public void animateAdd() throws Throwable {
    167         ViewHolder vh = createViewHolder(1);
    168         expectItems(vh);
    169         assertTrue(animateAdd(vh));
    170         assertTrue(mAnimator.isRunning());
    171         runAndWait(1, 1);
    172     }
    173 
    174     @Test
    175     public void animateRemove() throws Throwable {
    176         ViewHolder vh = createViewHolder(1);
    177         expectItems(vh);
    178         assertTrue(animateRemove(vh));
    179         assertTrue(mAnimator.isRunning());
    180         runAndWait(1, 1);
    181     }
    182 
    183     @Test
    184     public void animateMove() throws Throwable {
    185         ViewHolder vh = createViewHolder(1);
    186         expectItems(vh);
    187         assertTrue(animateMove(vh, 0, 0, 100, 100));
    188         assertTrue(mAnimator.isRunning());
    189         runAndWait(1, 1);
    190     }
    191 
    192     @Test
    193     public void animateChange() throws Throwable {
    194         ViewHolder vh = createViewHolder(1);
    195         ViewHolder vh2 = createViewHolder(2);
    196         expectItems(vh, vh2);
    197         assertTrue(animateChange(vh, vh2, 0, 0, 100, 100));
    198         assertTrue(mAnimator.isRunning());
    199         runAndWait(2, 1);
    200     }
    201 
    202     public void cancelBefore(int count, final RecyclerView.ViewHolder... toCancel)
    203             throws Throwable {
    204         cancelTest(true, count, toCancel);
    205     }
    206 
    207     public void cancelAfter(int count, final RecyclerView.ViewHolder... toCancel)
    208             throws Throwable {
    209         cancelTest(false, count, toCancel);
    210     }
    211 
    212     public void cancelTest(boolean before, int count, final RecyclerView.ViewHolder... toCancel) throws Throwable {
    213         if (before) {
    214             endAnimations(toCancel);
    215             runAndWait(count, 1);
    216         } else {
    217             runAndWait(count, 1, new ThrowingRunnable() {
    218                 @Override
    219                 public void run() throws Throwable {
    220                     endAnimations(toCancel);
    221                 }
    222             });
    223         }
    224     }
    225 
    226     @Test
    227     public void cancelAddBefore() throws Throwable {
    228         final ViewHolder vh = createViewHolder(1);
    229         expectItems(vh);
    230         assertTrue(animateAdd(vh));
    231         cancelBefore(1, vh);
    232     }
    233 
    234     @Test
    235     public void cancelAddAfter() throws Throwable {
    236         final ViewHolder vh = createViewHolder(1);
    237         expectItems(vh);
    238         assertTrue(animateAdd(vh));
    239         cancelAfter(1, vh);
    240     }
    241 
    242     @Test
    243     public void cancelMoveBefore() throws Throwable {
    244         ViewHolder vh = createViewHolder(1);
    245         expectItems(vh);
    246         assertTrue(animateMove(vh, 10, 10, 100, 100));
    247         cancelBefore(1, vh);
    248     }
    249 
    250     @Test
    251     public void cancelMoveAfter() throws Throwable {
    252         ViewHolder vh = createViewHolder(1);
    253         expectItems(vh);
    254         assertTrue(animateMove(vh, 10, 10, 100, 100));
    255         cancelAfter(1, vh);
    256     }
    257 
    258     @Test
    259     public void cancelRemove() throws Throwable {
    260         ViewHolder vh = createViewHolder(1);
    261         expectItems(vh);
    262         assertTrue(animateRemove(vh));
    263         endAnimations(vh);
    264         runAndWait(1, 1);
    265     }
    266 
    267     @Test
    268     public void cancelChangeOldBefore() throws Throwable {
    269         cancelChangeOldTest(true);
    270     }
    271     @Test
    272     public void cancelChangeOldAfter() throws Throwable {
    273         cancelChangeOldTest(false);
    274     }
    275 
    276     public void cancelChangeOldTest(boolean before) throws Throwable {
    277         ViewHolder vh = createViewHolder(1);
    278         ViewHolder vh2 = createViewHolder(1);
    279         expectItems(vh, vh2);
    280         assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
    281         cancelTest(before, 2, vh);
    282     }
    283 
    284     @Test
    285     public void cancelChangeNewBefore() throws Throwable {
    286         cancelChangeNewTest(true);
    287     }
    288 
    289     @Test
    290     public void cancelChangeNewAfter() throws Throwable {
    291         cancelChangeNewTest(false);
    292     }
    293 
    294     public void cancelChangeNewTest(boolean before) throws Throwable {
    295         ViewHolder vh = createViewHolder(1);
    296         ViewHolder vh2 = createViewHolder(1);
    297         expectItems(vh, vh2);
    298         assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
    299         cancelTest(before, 2, vh2);
    300     }
    301 
    302     @Test
    303     public void cancelChangeBothBefore() throws Throwable {
    304         cancelChangeBothTest(true);
    305     }
    306 
    307     @Test
    308     public void cancelChangeBothAfter() throws Throwable {
    309         cancelChangeBothTest(false);
    310     }
    311 
    312     public void cancelChangeBothTest(boolean before) throws Throwable {
    313         ViewHolder vh = createViewHolder(1);
    314         ViewHolder vh2 = createViewHolder(1);
    315         expectItems(vh, vh2);
    316         assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
    317         cancelTest(before, 2, vh, vh2);
    318     }
    319 
    320     void endAnimations(final RecyclerView.ViewHolder... vhs) throws Throwable {
    321         mActivityRule.runOnUiThread(new Runnable() {
    322             @Override
    323             public void run() {
    324                 for (RecyclerView.ViewHolder vh : vhs) {
    325                     mAnimator.endAnimation(vh);
    326                 }
    327             }
    328         });
    329     }
    330 
    331     boolean animateAdd(final RecyclerView.ViewHolder vh) throws Throwable {
    332         final boolean[] result = new boolean[1];
    333         mActivityRule.runOnUiThread(new Runnable() {
    334             @Override
    335             public void run() {
    336                 result[0] = mAnimator.animateAdd(vh);
    337             }
    338         });
    339         return result[0];
    340     }
    341 
    342     boolean animateRemove(final RecyclerView.ViewHolder vh) throws Throwable {
    343         final boolean[] result = new boolean[1];
    344         mActivityRule.runOnUiThread(new Runnable() {
    345             @Override
    346             public void run() {
    347                 result[0] = mAnimator.animateRemove(vh);
    348             }
    349         });
    350         return result[0];
    351     }
    352 
    353     boolean animateMove(final RecyclerView.ViewHolder vh, final int fromX, final int fromY,
    354             final int toX, final int toY) throws Throwable {
    355         final boolean[] result = new boolean[1];
    356         mActivityRule.runOnUiThread(new Runnable() {
    357             @Override
    358             public void run() {
    359                 result[0] = mAnimator.animateMove(vh, fromX, fromY, toX, toY);
    360             }
    361         });
    362         return result[0];
    363     }
    364 
    365     boolean animateChange(final RecyclerView.ViewHolder oldHolder,
    366             final RecyclerView.ViewHolder newHolder,
    367             final int fromX, final int fromY, final int toX, final int toY) throws Throwable {
    368         final boolean[] result = new boolean[1];
    369         mActivityRule.runOnUiThread(new Runnable() {
    370             @Override
    371             public void run() {
    372                 result[0] = mAnimator.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
    373             }
    374         });
    375         return result[0];
    376     }
    377 
    378     private ViewHolder createViewHolder(final int pos) throws Throwable {
    379         final ViewHolder vh = mAdapter.createViewHolder(mDummyParent, 1);
    380         mActivityRule.runOnUiThread(new Runnable() {
    381             @Override
    382             public void run() {
    383                 mAdapter.bindViewHolder(vh, pos);
    384                 mDummyParent.addView(vh.itemView);
    385             }
    386         });
    387 
    388         return vh;
    389     }
    390 
    391     @Override
    392     void postExceptionToInstrumentation(Throwable t) {
    393         if (mainThreadException == null) {
    394             mainThreadException = t;
    395         } else {
    396             Log.e(TAG, "skipping secondary main thread exception", t);
    397         }
    398     }
    399 
    400 
    401     private class Adapter extends RecyclerView.Adapter<ViewHolder> {
    402 
    403         List<String> mItems;
    404 
    405         private Adapter(int count) {
    406             mItems = new ArrayList<>();
    407             for (int i = 0; i < count; i++) {
    408                 mItems.add("item-" + i);
    409             }
    410         }
    411 
    412         @Override
    413         public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    414             return new ViewHolder(new TextView(parent.getContext()));
    415         }
    416 
    417         @Override
    418         public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    419             holder.bind(mItems.get(position));
    420         }
    421 
    422         @Override
    423         public int getItemCount() {
    424             return mItems.size();
    425         }
    426     }
    427 
    428     private class ViewHolder extends RecyclerView.ViewHolder {
    429 
    430         String mBindedText;
    431 
    432         public ViewHolder(View itemView) {
    433             super(itemView);
    434         }
    435 
    436         public void bind(String text) {
    437             mBindedText = text;
    438             ((TextView) itemView).setText(text);
    439         }
    440     }
    441 
    442     private interface ThrowingRunnable {
    443         void run() throws Throwable;
    444     }
    445 }
    446