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.assertNotSame;
     23 import static org.junit.Assert.assertNull;
     24 import static org.junit.Assert.assertSame;
     25 import static org.junit.Assert.assertTrue;
     26 import static org.junit.Assert.fail;
     27 
     28 import android.graphics.Rect;
     29 import android.os.Build;
     30 import android.support.test.filters.LargeTest;
     31 import android.support.test.filters.SdkSuppress;
     32 import android.support.test.runner.AndroidJUnit4;
     33 import android.util.Log;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 
     37 import androidx.annotation.NonNull;
     38 import androidx.core.view.ViewCompat;
     39 
     40 import org.hamcrest.CoreMatchers;
     41 import org.hamcrest.MatcherAssert;
     42 import org.junit.Test;
     43 import org.junit.runner.RunWith;
     44 
     45 import java.util.ArrayList;
     46 import java.util.HashMap;
     47 import java.util.HashSet;
     48 import java.util.List;
     49 import java.util.Map;
     50 import java.util.Set;
     51 import java.util.concurrent.atomic.AtomicBoolean;
     52 import java.util.concurrent.atomic.AtomicInteger;
     53 
     54 /**
     55  * Tests for {@link SimpleItemAnimator} API.
     56  */
     57 @LargeTest
     58 @RunWith(AndroidJUnit4.class)
     59 public class RecyclerViewAnimationsTest extends BaseRecyclerViewAnimationsTest {
     60 
     61     final List<TestViewHolder> recycledVHs = new ArrayList<>();
     62 
     63     @Test
     64     public void keepFocusAfterChangeAnimation() throws Throwable {
     65         setupBasic(10, 0, 5, new TestAdapter(10) {
     66             @Override
     67             public void onBindViewHolder(@NonNull TestViewHolder holder,
     68                     int position) {
     69                 super.onBindViewHolder(holder, position);
     70                 holder.itemView.setFocusableInTouchMode(true);
     71             }
     72         });
     73         ((SimpleItemAnimator)(mRecyclerView.getItemAnimator())).setSupportsChangeAnimations(true);
     74 
     75         final RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForAdapterPosition(3);
     76         assertNotNull("test sanity", oldVh);
     77         mActivityRule.runOnUiThread(new Runnable() {
     78             @Override
     79             public void run() {
     80                 oldVh.itemView.requestFocus();
     81             }
     82         });
     83         assertTrue("test sanity", oldVh.itemView.hasFocus());
     84         mLayoutManager.expectLayouts(2);
     85         mTestAdapter.changeAndNotify(3, 1);
     86         mLayoutManager.waitForLayout(2);
     87 
     88         RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3);
     89         assertNotNull("test sanity", newVh);
     90         assertNotSame(oldVh, newVh);
     91         assertFalse(oldVh.itemView.hasFocus());
     92         assertTrue(newVh.itemView.hasFocus());
     93     }
     94 
     95     @Test
     96     public void changeAndDisappearDontReUseViewHolder() throws Throwable {
     97         changeAndDisappearTest(false, false);
     98     }
     99 
    100     @Test
    101     public void changeAndDisappearReUseViewHolder() throws Throwable {
    102         changeAndDisappearTest(true, false);
    103     }
    104 
    105     @Test
    106     public void changeAndDisappearReUseWithScrapViewHolder() throws Throwable {
    107         changeAndDisappearTest(true, true);
    108     }
    109 
    110     public void changeAndDisappearTest(final boolean reUse, final boolean useScrap)
    111             throws Throwable {
    112         final List<RecyclerView.ViewHolder> mRecycled = new ArrayList<>();
    113         final TestAdapter adapter = new TestAdapter(1) {
    114             @Override
    115             public void onViewRecycled(@NonNull TestViewHolder holder) {
    116                 super.onViewRecycled(holder);
    117                 mRecycled.add(holder);
    118             }
    119         };
    120         setupBasic(1, 0, 1, adapter);
    121         RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(mRecyclerView.getChildAt(0));
    122         LoggingItemAnimator animator = new LoggingItemAnimator() {
    123             @Override
    124             public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
    125                                                      @NonNull List<Object> payloads) {
    126                 return reUse;
    127             }
    128         };
    129         mRecyclerView.setItemAnimator(animator);
    130         mLayoutManager.expectLayouts(2);
    131         final RecyclerView.ViewHolder[] updatedVH = new RecyclerView.ViewHolder[1];
    132         mActivityRule.runOnUiThread(new Runnable() {
    133             @Override
    134             public void run() {
    135                 adapter.notifyItemChanged(0);
    136                 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    137                     @Override
    138                     void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
    139                                   RecyclerView.State state) {
    140                         if (state.isPreLayout()) {
    141                             super.doLayout(recycler, lm, state);
    142                         } else {
    143                             lm.detachAndScrapAttachedViews(recycler);
    144                             final View view;
    145                             if (reUse && useScrap) {
    146                                 view = recycler.getScrapViewAt(0);
    147                             } else {
    148                                 view = recycler.getViewForPosition(0);
    149                             }
    150                             updatedVH[0] = RecyclerView.getChildViewHolderInt(view);
    151                             lm.addDisappearingView(view);
    152                         }
    153                     }
    154                 };
    155             }
    156         });
    157         mLayoutManager.waitForLayout(2);
    158 
    159         MatcherAssert.assertThat(animator.contains(vh, animator.mAnimateDisappearanceList),
    160                 CoreMatchers.is(reUse));
    161         MatcherAssert.assertThat(animator.contains(vh, animator.mAnimateChangeList),
    162                 CoreMatchers.is(!reUse));
    163         MatcherAssert.assertThat(animator.contains(updatedVH[0], animator.mAnimateChangeList),
    164                 CoreMatchers.is(!reUse));
    165         MatcherAssert.assertThat(animator.contains(updatedVH[0],
    166                 animator.mAnimateDisappearanceList), CoreMatchers.is(reUse));
    167         waitForAnimations(10);
    168         MatcherAssert.assertThat(mRecyclerView.getChildCount(), CoreMatchers.is(0));
    169         if (useScrap || !reUse) {
    170             MatcherAssert.assertThat(mRecycled.contains(vh), CoreMatchers.is(true));
    171         } else {
    172             MatcherAssert.assertThat(mRecyclerView.mRecycler.mCachedViews.contains(vh),
    173                     CoreMatchers.is(true));
    174         }
    175 
    176         if (!reUse) {
    177             MatcherAssert.assertThat(mRecycled.contains(updatedVH[0]), CoreMatchers.is(false));
    178             MatcherAssert.assertThat(mRecyclerView.mRecycler.mCachedViews.contains(updatedVH[0]),
    179                     CoreMatchers.is(true));
    180         }
    181     }
    182 
    183     @Test
    184     public void detectStableIdError() throws Throwable {
    185         setIgnoreMainThreadException(true);
    186         final AtomicBoolean useBadIds = new AtomicBoolean(false);
    187         TestAdapter adapter = new TestAdapter(10) {
    188             @Override
    189             public long getItemId(int position) {
    190                 if (useBadIds.get() && position == 5) {
    191                     return super.getItemId(position) - 1;
    192                 }
    193                 return super.getItemId(position);
    194             }
    195 
    196             @Override
    197             public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
    198                 // ignore validation
    199             }
    200         };
    201         adapter.setHasStableIds(true);
    202         setupBasic(10, 0, 10, adapter);
    203         mLayoutManager.expectLayouts(2);
    204         useBadIds.set(true);
    205         adapter.changeAndNotify(4, 2);
    206         mLayoutManager.waitForLayout(2);
    207         assertTrue(getMainThreadException() instanceof IllegalStateException);
    208         assertTrue(getMainThreadException().getMessage()
    209                 .contains("Two different ViewHolders have the same stable ID."));
    210         // TODO don't use this after moving this class to Junit 4
    211         try {
    212             removeRecyclerView();
    213         } catch (Throwable t){}
    214     }
    215 
    216 
    217     @Test
    218     public void dontLayoutReusedViewWithoutPredictive() throws Throwable {
    219         reuseHiddenViewTest(new ReuseTestCallback() {
    220             @Override
    221             public void postSetup(List<TestViewHolder> recycledList,
    222                     final TestViewHolder target) throws Throwable {
    223                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    224                         .getItemAnimator();
    225                 itemAnimator.reset();
    226                 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    227                     @Override
    228                     void beforePreLayout(RecyclerView.Recycler recycler,
    229                             AnimationLayoutManager lm, RecyclerView.State state) {
    230                         fail("pre layout is not expected");
    231                     }
    232 
    233                     @Override
    234                     void beforePostLayout(RecyclerView.Recycler recycler,
    235                             AnimationLayoutManager layoutManager,
    236                             RecyclerView.State state) {
    237                         mLayoutItemCount = 7;
    238                         View targetView = recycler
    239                                 .getViewForPosition(target.getAdapterPosition());
    240                         assertSame(targetView, target.itemView);
    241                         super.beforePostLayout(recycler, layoutManager, state);
    242                     }
    243 
    244                     @Override
    245                     void afterPostLayout(RecyclerView.Recycler recycler,
    246                             AnimationLayoutManager layoutManager,
    247                             RecyclerView.State state) {
    248                         super.afterPostLayout(recycler, layoutManager, state);
    249                         assertNull("test sanity. this view should not be re-laid out in post "
    250                                 + "layout", target.itemView.getParent());
    251                     }
    252                 };
    253                 mLayoutManager.expectLayouts(1);
    254                 mLayoutManager.requestSimpleAnimationsInNextLayout();
    255                 requestLayoutOnUIThread(mRecyclerView);
    256                 mLayoutManager.waitForLayout(2);
    257                 checkForMainThreadException();
    258                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    259                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    260                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    261                 // This is a LayoutManager problem if it asked for the view but didn't properly
    262                 // lay it out. It will move to disappearance
    263                 assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    264                 waitForAnimations(5);
    265                 assertTrue(recycledVHs.contains(target));
    266             }
    267         });
    268     }
    269 
    270     @Test
    271     public void dontLayoutReusedViewWithPredictive() throws Throwable {
    272         reuseHiddenViewTest(new ReuseTestCallback() {
    273             @Override
    274             public void postSetup(List<TestViewHolder> recycledList,
    275                     final TestViewHolder target) throws Throwable {
    276                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    277                         .getItemAnimator();
    278                 itemAnimator.reset();
    279                 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    280                     @Override
    281                     void beforePreLayout(RecyclerView.Recycler recycler,
    282                             AnimationLayoutManager lm, RecyclerView.State state) {
    283                         mLayoutItemCount = 9;
    284                         super.beforePreLayout(recycler, lm, state);
    285                     }
    286 
    287                     @Override
    288                     void beforePostLayout(RecyclerView.Recycler recycler,
    289                             AnimationLayoutManager layoutManager,
    290                             RecyclerView.State state) {
    291                         mLayoutItemCount = 7;
    292                         super.beforePostLayout(recycler, layoutManager, state);
    293                     }
    294 
    295                     @Override
    296                     void afterPostLayout(RecyclerView.Recycler recycler,
    297                             AnimationLayoutManager layoutManager,
    298                             RecyclerView.State state) {
    299                         super.afterPostLayout(recycler, layoutManager, state);
    300                         assertNull("test sanity. this view should not be re-laid out in post "
    301                                 + "layout", target.itemView.getParent());
    302                     }
    303                 };
    304                 mLayoutManager.expectLayouts(2);
    305                 mTestAdapter.deleteAndNotify(1, 1);
    306                 mLayoutManager.waitForLayout(2);
    307                 checkForMainThreadException();
    308                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    309                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    310                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    311                 // This is a LayoutManager problem if it asked for the view but didn't properly
    312                 // lay it out. It will move to disappearance.
    313                 assertTrue(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    314                 waitForAnimations(5);
    315                 assertTrue(recycledVHs.contains(target));
    316             }
    317         });
    318     }
    319 
    320     @Test
    321     public void reuseHiddenViewWithoutPredictive() throws Throwable {
    322         reuseHiddenViewTest(new ReuseTestCallback() {
    323             @Override
    324             public void postSetup(List<TestViewHolder> recycledList,
    325                     TestViewHolder target) throws Throwable {
    326                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    327                         .getItemAnimator();
    328                 itemAnimator.reset();
    329                 mLayoutManager.expectLayouts(1);
    330                 mLayoutManager.requestSimpleAnimationsInNextLayout();
    331                 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
    332                 requestLayoutOnUIThread(mRecyclerView);
    333                 mLayoutManager.waitForLayout(2);
    334                 waitForAnimations(5);
    335                 assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    336                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    337                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    338                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    339                 assertFalse(recycledVHs.contains(target));
    340             }
    341         });
    342     }
    343 
    344     @Test
    345     public void reuseHiddenViewWithoutAnimations() throws Throwable {
    346         reuseHiddenViewTest(new ReuseTestCallback() {
    347             @Override
    348             public void postSetup(List<TestViewHolder> recycledList,
    349                     TestViewHolder target) throws Throwable {
    350                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    351                         .getItemAnimator();
    352                 itemAnimator.reset();
    353                 mLayoutManager.expectLayouts(1);
    354                 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
    355                 requestLayoutOnUIThread(mRecyclerView);
    356                 mLayoutManager.waitForLayout(2);
    357                 waitForAnimations(5);
    358                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    359                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    360                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    361                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    362                 assertFalse(recycledVHs.contains(target));
    363             }
    364         });
    365     }
    366 
    367     @Test
    368     public void reuseHiddenViewWithPredictive() throws Throwable {
    369         reuseHiddenViewTest(new ReuseTestCallback() {
    370             @Override
    371             public void postSetup(List<TestViewHolder> recycledList,
    372                     TestViewHolder target) throws Throwable {
    373                 // it should move to change scrap and then show up from there
    374                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    375                         .getItemAnimator();
    376                 itemAnimator.reset();
    377                 mLayoutManager.expectLayouts(2);
    378                 mTestAdapter.deleteAndNotify(2, 1);
    379                 mLayoutManager.waitForLayout(2);
    380                 waitForAnimations(5);
    381                 // This LM does not layout the additional item so it does predictive wrong.
    382                 // We should still handle it and animate persistence for this item
    383                 assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    384                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    385                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    386                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    387                 assertTrue(itemAnimator.mMoveVHs.contains(target));
    388                 assertFalse(recycledVHs.contains(target));
    389             }
    390         });
    391     }
    392 
    393     @Test
    394     public void reuseHiddenViewWithProperPredictive() throws Throwable {
    395         reuseHiddenViewTest(new ReuseTestCallback() {
    396             @Override
    397             public void postSetup(List<TestViewHolder> recycledList,
    398                     TestViewHolder target) throws Throwable {
    399                 // it should move to change scrap and then show up from there
    400                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    401                         .getItemAnimator();
    402                 itemAnimator.reset();
    403                 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    404                     @Override
    405                     void beforePreLayout(RecyclerView.Recycler recycler,
    406                             AnimationLayoutManager lm, RecyclerView.State state) {
    407                         mLayoutItemCount = 9;
    408                         super.beforePreLayout(recycler, lm, state);
    409                     }
    410 
    411                     @Override
    412                     void afterPreLayout(RecyclerView.Recycler recycler,
    413                             AnimationLayoutManager layoutManager,
    414                             RecyclerView.State state) {
    415                         mLayoutItemCount = 8;
    416                         super.afterPreLayout(recycler, layoutManager, state);
    417                     }
    418                 };
    419 
    420                 mLayoutManager.expectLayouts(2);
    421                 mTestAdapter.deleteAndNotify(2, 1);
    422                 mLayoutManager.waitForLayout(2);
    423                 waitForAnimations(5);
    424                 // This LM implements predictive animations properly by requesting target view
    425                 // in pre-layout.
    426                 assertTrue(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    427                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    428                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    429                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    430                 assertTrue(itemAnimator.mMoveVHs.contains(target));
    431                 assertFalse(recycledVHs.contains(target));
    432             }
    433         });
    434     }
    435 
    436     // Disable this test on ICS because it causes testing devices to freeze.
    437     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
    438     @Test
    439     public void dontReuseHiddenViewOnInvalidate() throws Throwable {
    440         reuseHiddenViewTest(new ReuseTestCallback() {
    441             @Override
    442             public void postSetup(List<TestViewHolder> recycledList,
    443                     TestViewHolder target) throws Throwable {
    444                 // it should move to change scrap and then show up from there
    445                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    446                         .getItemAnimator();
    447                 itemAnimator.reset();
    448                 mLayoutManager.expectLayouts(1);
    449                 mTestAdapter.dispatchDataSetChanged();
    450                 mLayoutManager.waitForLayout(2);
    451                 waitForAnimations(5);
    452                 assertFalse(mRecyclerView.getItemAnimator().isRunning());
    453                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    454                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateChangeList));
    455                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    456                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    457                 assertTrue(recycledVHs.contains(target));
    458             }
    459         });
    460     }
    461 
    462     @Test
    463     public void dontReuseOnTypeChange() throws Throwable {
    464         reuseHiddenViewTest(new ReuseTestCallback() {
    465             @Override
    466             public void postSetup(List<TestViewHolder> recycledList,
    467                     TestViewHolder target) throws Throwable {
    468                 // it should move to change scrap and then show up from there
    469                 LoggingItemAnimator itemAnimator = (LoggingItemAnimator) mRecyclerView
    470                         .getItemAnimator();
    471                 itemAnimator.reset();
    472                 mLayoutManager.expectLayouts(1);
    473                 target.mBoundItem.mType += 2;
    474                 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 9;
    475                 mTestAdapter.changeAndNotify(target.getAdapterPosition(), 1);
    476                 requestLayoutOnUIThread(mRecyclerView);
    477                 mLayoutManager.waitForLayout(2);
    478 
    479                 assertTrue(itemAnimator.mChangeOldVHs.contains(target));
    480                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimatePersistenceList));
    481                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateAppearanceList));
    482                 assertFalse(itemAnimator.contains(target, itemAnimator.mAnimateDisappearanceList));
    483                 assertTrue(mRecyclerView.mChildHelper.isHidden(target.itemView));
    484                 assertFalse(recycledVHs.contains(target));
    485                 waitForAnimations(5);
    486                 assertTrue(recycledVHs.contains(target));
    487             }
    488         });
    489     }
    490 
    491     interface ReuseTestCallback {
    492 
    493         void postSetup(List<TestViewHolder> recycledList, TestViewHolder target) throws Throwable;
    494     }
    495 
    496     @Override
    497     protected RecyclerView.ItemAnimator createItemAnimator() {
    498         return new LoggingItemAnimator();
    499     }
    500 
    501     public void reuseHiddenViewTest(ReuseTestCallback callback) throws Throwable {
    502         TestAdapter adapter = new TestAdapter(10) {
    503             @Override
    504             public void onViewRecycled(@NonNull TestViewHolder holder) {
    505                 super.onViewRecycled(holder);
    506                 recycledVHs.add(holder);
    507             }
    508         };
    509         setupBasic(10, 0, 10, adapter);
    510         mRecyclerView.setItemViewCacheSize(0);
    511         TestViewHolder target = (TestViewHolder) mRecyclerView.findViewHolderForAdapterPosition(9);
    512         mRecyclerView.getItemAnimator().setAddDuration(1000);
    513         mRecyclerView.getItemAnimator().setRemoveDuration(1000);
    514         mRecyclerView.getItemAnimator().setChangeDuration(1000);
    515         mRecyclerView.getItemAnimator().setMoveDuration(1000);
    516         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
    517         mLayoutManager.expectLayouts(2);
    518         adapter.deleteAndNotify(2, 1);
    519         mLayoutManager.waitForLayout(2);
    520         // test sanity, make sure target is hidden now
    521         assertTrue("test sanity", mRecyclerView.mChildHelper.isHidden(target.itemView));
    522         callback.postSetup(recycledVHs, target);
    523         // TODO TEST ITEM INVALIDATION OR TYPE CHANGE IN BETWEEN
    524         // TODO TEST ITEM IS RECEIVED FROM RECYCLER BUT NOT RE-ADDED
    525         // TODO TEST ITEM ANIMATOR IS CALLED TO GET NEW INFORMATION ABOUT LOCATION
    526 
    527     }
    528 
    529     @Test
    530     public void detachBeforeAnimations() throws Throwable {
    531         setupBasic(10, 0, 5);
    532         final RecyclerView rv = mRecyclerView;
    533         waitForAnimations(2);
    534         final DefaultItemAnimator animator = new DefaultItemAnimator() {
    535             @Override
    536             public void runPendingAnimations() {
    537                 super.runPendingAnimations();
    538             }
    539         };
    540         rv.setItemAnimator(animator);
    541         mLayoutManager.expectLayouts(2);
    542         mTestAdapter.deleteAndNotify(3, 4);
    543         mLayoutManager.waitForLayout(2);
    544         removeRecyclerView();
    545         assertNull("test sanity check RV should be removed", rv.getParent());
    546         assertEquals("no views should be hidden", 0, rv.mChildHelper.mHiddenViews.size());
    547         assertFalse("there should not be any animations running", animator.isRunning());
    548     }
    549 
    550     @Test
    551     public void moveDeleted() throws Throwable {
    552         setupBasic(4, 0, 3);
    553         waitForAnimations(2);
    554         final View[] targetChild = new View[1];
    555         final LoggingItemAnimator animator = new LoggingItemAnimator();
    556         mActivityRule.runOnUiThread(new Runnable() {
    557             @Override
    558             public void run() {
    559                 mRecyclerView.setItemAnimator(animator);
    560                 targetChild[0] = mRecyclerView.getChildAt(1);
    561             }
    562         });
    563 
    564         assertNotNull("test sanity", targetChild);
    565         mLayoutManager.expectLayouts(1);
    566         mActivityRule.runOnUiThread(new Runnable() {
    567             @Override
    568             public void run() {
    569                 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
    570                     @Override
    571                     public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
    572                             RecyclerView.State state) {
    573                         if (view == targetChild[0]) {
    574                             outRect.set(10, 20, 30, 40);
    575                         } else {
    576                             outRect.set(0, 0, 0, 0);
    577                         }
    578                     }
    579                 });
    580             }
    581         });
    582         mLayoutManager.waitForLayout(1);
    583 
    584         // now delete that item.
    585         mLayoutManager.expectLayouts(2);
    586         RecyclerView.ViewHolder targetVH = mRecyclerView.getChildViewHolder(targetChild[0]);
    587         targetChild[0] = null;
    588         mTestAdapter.deleteAndNotify(1, 1);
    589         mLayoutManager.waitForLayout(2);
    590         assertFalse("if deleted view moves, it should not be in move animations",
    591                 animator.mMoveVHs.contains(targetVH));
    592         assertEquals("only 1 item is deleted", 1, animator.mRemoveVHs.size());
    593         assertTrue("the target view is removed", animator.mRemoveVHs.contains(targetVH
    594         ));
    595     }
    596 
    597     private void runTestImportantForAccessibilityWhileDeteling(
    598             final int boundImportantForAccessibility,
    599             final int expectedImportantForAccessibility) throws Throwable {
    600         // Adapter binding the item to the initial accessibility option.
    601         // RecyclerView is expected to change it to 'expectedImportantForAccessibility'.
    602         TestAdapter adapter = new TestAdapter(1) {
    603             @Override
    604             public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
    605                 super.onBindViewHolder(holder, position);
    606                 ViewCompat.setImportantForAccessibility(
    607                         holder.itemView, boundImportantForAccessibility);
    608             }
    609         };
    610 
    611         // Set up with 1 item.
    612         setupBasic(1, 0, 1, adapter);
    613         waitForAnimations(2);
    614         final View[] targetChild = new View[1];
    615         final LoggingItemAnimator animator = new LoggingItemAnimator();
    616         animator.setRemoveDuration(500);
    617         mActivityRule.runOnUiThread(new Runnable() {
    618             @Override
    619             public void run() {
    620                 mRecyclerView.setItemAnimator(animator);
    621                 targetChild[0] = mRecyclerView.getChildAt(0);
    622                 assertEquals(
    623                         expectedImportantForAccessibility,
    624                         ViewCompat.getImportantForAccessibility(targetChild[0]));
    625             }
    626         });
    627 
    628         assertNotNull("test sanity", targetChild[0]);
    629 
    630         // now delete that item.
    631         mLayoutManager.expectLayouts(2);
    632         mTestAdapter.deleteAndNotify(0, 1);
    633 
    634         mLayoutManager.waitForLayout(2);
    635 
    636         mActivityRule.runOnUiThread(new Runnable() {
    637             @Override
    638             public void run() {
    639                 // The view is still a child of mRecyclerView, and is invisible for accessibility.
    640                 assertTrue(targetChild[0].getParent() == mRecyclerView);
    641                 assertEquals(
    642                         ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
    643                         ViewCompat.getImportantForAccessibility(targetChild[0]));
    644             }
    645         });
    646 
    647         waitForAnimations(2);
    648 
    649         // Delete animation is now complete.
    650         mActivityRule.runOnUiThread(new Runnable() {
    651             @Override
    652             public void run() {
    653                 // The view is in recycled state, and back to the expected accessibility.
    654                 assertTrue(targetChild[0].getParent() == null);
    655                 assertEquals(
    656                         expectedImportantForAccessibility,
    657                         ViewCompat.getImportantForAccessibility(targetChild[0]));
    658             }
    659         });
    660 
    661         // Add 1 element, which should use same view.
    662         mLayoutManager.expectLayouts(2);
    663         mTestAdapter.addAndNotify(1);
    664         mLayoutManager.waitForLayout(2);
    665 
    666         mActivityRule.runOnUiThread(new Runnable() {
    667             @Override
    668             public void run() {
    669                 // The view should be reused, and have the expected accessibility.
    670                 assertTrue(
    671                         "the item must be reused", targetChild[0] == mRecyclerView.getChildAt(0));
    672                 assertEquals(
    673                         expectedImportantForAccessibility,
    674                         ViewCompat.getImportantForAccessibility(targetChild[0]));
    675             }
    676         });
    677     }
    678 
    679     @Test
    680     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    681     public void importantForAccessibilityWhileDetelingAuto() throws Throwable {
    682         runTestImportantForAccessibilityWhileDeteling(
    683                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO,
    684                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    685     }
    686 
    687     @Test
    688     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    689     public void importantForAccessibilityWhileDetelingNo() throws Throwable {
    690         runTestImportantForAccessibilityWhileDeteling(
    691                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO,
    692                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
    693     }
    694 
    695     @Test
    696     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    697     public void importantForAccessibilityWhileDetelingNoHideDescandants() throws Throwable {
    698         runTestImportantForAccessibilityWhileDeteling(
    699                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS,
    700                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    701     }
    702 
    703     @Test
    704     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    705     public void importantForAccessibilityWhileDetelingYes() throws Throwable {
    706         runTestImportantForAccessibilityWhileDeteling(
    707                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
    708                 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
    709     }
    710 
    711     @Test
    712     public void preLayoutPositionCleanup() throws Throwable {
    713         setupBasic(4, 0, 4);
    714         mLayoutManager.expectLayouts(2);
    715         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    716             @Override
    717             void beforePreLayout(RecyclerView.Recycler recycler,
    718                     AnimationLayoutManager lm, RecyclerView.State state) {
    719                 mLayoutMin = 0;
    720                 mLayoutItemCount = 3;
    721             }
    722 
    723             @Override
    724             void beforePostLayout(RecyclerView.Recycler recycler,
    725                     AnimationLayoutManager layoutManager,
    726                     RecyclerView.State state) {
    727                 mLayoutMin = 0;
    728                 mLayoutItemCount = 4;
    729             }
    730         };
    731         mTestAdapter.addAndNotify(0, 1);
    732         mLayoutManager.waitForLayout(2);
    733 
    734 
    735     }
    736 
    737     @Test
    738     public void addRemoveSamePass() throws Throwable {
    739         final List<RecyclerView.ViewHolder> mRecycledViews
    740                 = new ArrayList<RecyclerView.ViewHolder>();
    741         TestAdapter adapter = new TestAdapter(50) {
    742             @Override
    743             public void onViewRecycled(@NonNull TestViewHolder holder) {
    744                 super.onViewRecycled(holder);
    745                 mRecycledViews.add(holder);
    746             }
    747         };
    748         adapter.setHasStableIds(true);
    749         setupBasic(50, 3, 5, adapter);
    750         mRecyclerView.setItemViewCacheSize(0);
    751         final ArrayList<RecyclerView.ViewHolder> addVH
    752                 = new ArrayList<RecyclerView.ViewHolder>();
    753         final ArrayList<RecyclerView.ViewHolder> removeVH
    754                 = new ArrayList<RecyclerView.ViewHolder>();
    755 
    756         final ArrayList<RecyclerView.ViewHolder> moveVH
    757                 = new ArrayList<RecyclerView.ViewHolder>();
    758 
    759         final View[] testView = new View[1];
    760         mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
    761             @Override
    762             public boolean animateAdd(RecyclerView.ViewHolder holder) {
    763                 addVH.add(holder);
    764                 return true;
    765             }
    766 
    767             @Override
    768             public boolean animateRemove(RecyclerView.ViewHolder holder) {
    769                 removeVH.add(holder);
    770                 return true;
    771             }
    772 
    773             @Override
    774             public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY,
    775                     int toX, int toY) {
    776                 moveVH.add(holder);
    777                 return true;
    778             }
    779         });
    780         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    781             @Override
    782             void afterPreLayout(RecyclerView.Recycler recycler,
    783                     AnimationLayoutManager layoutManager,
    784                     RecyclerView.State state) {
    785                 super.afterPreLayout(recycler, layoutManager, state);
    786                 testView[0] = recycler.getViewForPosition(45);
    787                 testView[0].measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST),
    788                         View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST));
    789                 testView[0].layout(10, 10, 10 + testView[0].getMeasuredWidth(),
    790                         10 + testView[0].getMeasuredHeight());
    791                 layoutManager.addView(testView[0], 4);
    792             }
    793 
    794             @Override
    795             void afterPostLayout(RecyclerView.Recycler recycler,
    796                     AnimationLayoutManager layoutManager,
    797                     RecyclerView.State state) {
    798                 super.afterPostLayout(recycler, layoutManager, state);
    799                 testView[0].layout(50, 50, 50 + testView[0].getMeasuredWidth(),
    800                         50 + testView[0].getMeasuredHeight());
    801                 layoutManager.addDisappearingView(testView[0], 4);
    802             }
    803         };
    804         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 3;
    805         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 5;
    806         mRecycledViews.clear();
    807         mLayoutManager.expectLayouts(2);
    808         mTestAdapter.deleteAndNotify(3, 1);
    809         mLayoutManager.waitForLayout(2);
    810 
    811         for (RecyclerView.ViewHolder vh : addVH) {
    812             assertNotSame("add-remove item should not animate add", testView[0], vh.itemView);
    813         }
    814         for (RecyclerView.ViewHolder vh : moveVH) {
    815             assertNotSame("add-remove item should not animate move", testView[0], vh.itemView);
    816         }
    817         for (RecyclerView.ViewHolder vh : removeVH) {
    818             assertNotSame("add-remove item should not animate remove", testView[0], vh.itemView);
    819         }
    820         boolean found = false;
    821         for (RecyclerView.ViewHolder vh : mRecycledViews) {
    822             found |= vh.itemView == testView[0];
    823         }
    824         assertTrue("added-removed view should be recycled", found);
    825     }
    826 
    827     @Test
    828     public void tmpRemoveMe() throws Throwable {
    829         changeAnimTest(false, false, true, false);
    830     }
    831 
    832     @Test
    833     public void changeAnimations() throws Throwable {
    834         final boolean[] booleans = {true, false};
    835         for (boolean supportsChange : booleans) {
    836             for (boolean changeType : booleans) {
    837                 for (boolean hasStableIds : booleans) {
    838                     for (boolean deleteSomeItems : booleans) {
    839                         changeAnimTest(supportsChange, changeType, hasStableIds, deleteSomeItems);
    840                     }
    841                     removeRecyclerView();
    842                 }
    843             }
    844         }
    845     }
    846 
    847     public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType,
    848             final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable {
    849         final int changedIndex = 3;
    850         final int defaultType = 1;
    851         final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType);
    852         final String logPrefix = "supportsChangeAnim:" + supportsChangeAnim +
    853                 ", change view type:" + changeType +
    854                 ", has stable ids:" + hasStableIds +
    855                 ", delete some items:" + deleteSomeItems;
    856         TestAdapter testAdapter = new TestAdapter(10) {
    857             @Override
    858             public int getItemViewType(int position) {
    859                 return position == changedIndex ? changedIndexNewType.get() : defaultType;
    860             }
    861 
    862             @Override
    863             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    864                     int viewType) {
    865                 TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
    866                 if (DEBUG) {
    867                     Log.d(TAG, logPrefix + " onCreateVH" + vh.toString());
    868                 }
    869                 return vh;
    870             }
    871 
    872             @Override
    873             public void onBindViewHolder(@NonNull TestViewHolder holder,
    874                     int position) {
    875                 super.onBindViewHolder(holder, position);
    876                 if (DEBUG) {
    877                     Log.d(TAG, logPrefix + " onBind to " + position + "" + holder.toString());
    878                 }
    879             }
    880         };
    881         testAdapter.setHasStableIds(hasStableIds);
    882         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
    883         ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
    884                 supportsChangeAnim);
    885 
    886         final RecyclerView.ViewHolder toBeChangedVH =
    887                 mRecyclerView.findViewHolderForLayoutPosition(changedIndex);
    888         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
    889             @Override
    890             void afterPreLayout(RecyclerView.Recycler recycler,
    891                     AnimationLayoutManager layoutManager,
    892                     RecyclerView.State state) {
    893                 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
    894                         changedIndex);
    895                 assertTrue(logPrefix + " changed view holder should have correct flag"
    896                         , vh.isUpdated());
    897             }
    898 
    899             @Override
    900             void afterPostLayout(RecyclerView.Recycler recycler,
    901                     AnimationLayoutManager layoutManager, RecyclerView.State state) {
    902                 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(
    903                         changedIndex);
    904                 if (supportsChangeAnim) {
    905                     assertNotSame(logPrefix + "a new VH should be given if change is supported",
    906                             toBeChangedVH, vh);
    907                 } else if (!changeType && hasStableIds) {
    908                     assertSame(logPrefix + "if change animations are not supported but we have "
    909                             + "stable ids, same view holder should be returned", toBeChangedVH, vh);
    910                 }
    911                 super.beforePostLayout(recycler, layoutManager, state);
    912             }
    913         };
    914         mLayoutManager.expectLayouts(1);
    915         if (changeType) {
    916             changedIndexNewType.set(defaultType + 1);
    917         }
    918         if (deleteSomeItems) {
    919             mActivityRule.runOnUiThread(new Runnable() {
    920                 @Override
    921                 public void run() {
    922                     try {
    923                         mTestAdapter.deleteAndNotify(changedIndex + 2, 1);
    924                         mTestAdapter.notifyItemChanged(3);
    925                     } catch (Throwable throwable) {
    926                         throwable.printStackTrace();
    927                     }
    928 
    929                 }
    930             });
    931         } else {
    932             mTestAdapter.changeAndNotify(3, 1);
    933         }
    934 
    935         mLayoutManager.waitForLayout(2);
    936     }
    937 
    938     private void testChangeWithPayload(final boolean supportsChangeAnim,
    939             final boolean canReUse, Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
    940             throws Throwable {
    941         final List<Object> expectedPayloads = new ArrayList<Object>();
    942         final int changedIndex = 3;
    943         TestAdapter testAdapter = new TestAdapter(10) {
    944             @Override
    945             public int getItemViewType(int position) {
    946                 return 1;
    947             }
    948 
    949             @Override
    950             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    951                     int viewType) {
    952                 TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
    953                 if (DEBUG) {
    954                     Log.d(TAG, " onCreateVH" + vh.toString());
    955                 }
    956                 return vh;
    957             }
    958 
    959             @Override
    960             public void onBindViewHolder(TestViewHolder holder,
    961                     int position, List<Object> payloads) {
    962                 super.onBindViewHolder(holder, position);
    963                 if (DEBUG) {
    964                     Log.d(TAG, " onBind to " + position + "" + holder.toString());
    965                 }
    966                 assertEquals(expectedPayloads, payloads);
    967             }
    968         };
    969         testAdapter.setHasStableIds(false);
    970         setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter);
    971         mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
    972             @Override
    973             public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder,
    974                     @NonNull List<Object> payloads) {
    975                 return canReUse && super.canReuseUpdatedViewHolder(viewHolder, payloads);
    976             }
    977         });
    978         ((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(
    979                 supportsChangeAnim);
    980 
    981         int numTests = notifyPayloads.length;
    982         for (int i = 0; i < numTests; i++) {
    983             mLayoutManager.expectLayouts(1);
    984             expectedPayloads.clear();
    985             for (int j = 0; j < expectedPayloadsInOnBind[i].length; j++) {
    986                 expectedPayloads.add(expectedPayloadsInOnBind[i][j]);
    987             }
    988             final Object[] payloadsToSend = notifyPayloads[i];
    989             mActivityRule.runOnUiThread(new Runnable() {
    990                 @Override
    991                 public void run() {
    992                     for (int j = 0; j < payloadsToSend.length; j++) {
    993                         mTestAdapter.notifyItemChanged(changedIndex, payloadsToSend[j]);
    994                     }
    995                 }
    996             });
    997             mLayoutManager.waitForLayout(2);
    998             checkForMainThreadException();
    999         }
   1000     }
   1001 
   1002     @Test
   1003     public void crossFadingChangeAnimationWithPayload() throws Throwable {
   1004         // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
   1005         testChangeWithPayload(true, true,
   1006                 new Object[][]{
   1007                         new Object[]{"abc"},
   1008                         new Object[]{"abc", null, "cdf"},
   1009                         new Object[]{"abc", null},
   1010                         new Object[]{null, "abc"},
   1011                         new Object[]{"abc", "cdf"}
   1012                 },
   1013                 new Object[][]{
   1014                         new Object[]{"abc"},
   1015                         new Object[0],
   1016                         new Object[0],
   1017                         new Object[0],
   1018                         new Object[]{"abc", "cdf"}
   1019                 });
   1020     }
   1021 
   1022     @Test
   1023     public void crossFadingChangeAnimationWithPayloadWithoutReuse() throws Throwable {
   1024         // for crossfading change animation,  will receive EMPTY payload in onBindViewHolder
   1025         testChangeWithPayload(true, false,
   1026                 new Object[][]{
   1027                         new Object[]{"abc"},
   1028                         new Object[]{"abc", null, "cdf"},
   1029                         new Object[]{"abc", null},
   1030                         new Object[]{null, "abc"},
   1031                         new Object[]{"abc", "cdf"}
   1032                 },
   1033                 new Object[][]{
   1034                         new Object[0],
   1035                         new Object[0],
   1036                         new Object[0],
   1037                         new Object[0],
   1038                         new Object[0]
   1039                 });
   1040     }
   1041 
   1042     @Test
   1043     public void noChangeAnimationWithPayload() throws Throwable {
   1044         // for Change Animation disabled, payload should match the payloads unless
   1045         // null payload is fired.
   1046         testChangeWithPayload(false, true,
   1047                 new Object[][]{
   1048                         new Object[]{"abc"},
   1049                         new Object[]{"abc", null, "cdf"},
   1050                         new Object[]{"abc", null},
   1051                         new Object[]{null, "abc"},
   1052                         new Object[]{"abc", "cdf"}
   1053                 },
   1054                 new Object[][]{
   1055                         new Object[]{"abc"},
   1056                         new Object[0],
   1057                         new Object[0],
   1058                         new Object[0],
   1059                         new Object[]{"abc", "cdf"}
   1060                 });
   1061     }
   1062 
   1063     @Test
   1064     public void recycleDuringAnimations() throws Throwable {
   1065         final AtomicInteger childCount = new AtomicInteger(0);
   1066         final TestAdapter adapter = new TestAdapter(1000) {
   1067             @Override
   1068             public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
   1069                     int viewType) {
   1070                 childCount.incrementAndGet();
   1071                 return super.onCreateViewHolder(parent, viewType);
   1072             }
   1073         };
   1074         setupBasic(1000, 10, 20, adapter);
   1075         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 10;
   1076         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 20;
   1077 
   1078         mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
   1079             @Override
   1080             public void putRecycledView(RecyclerView.ViewHolder scrap) {
   1081                 super.putRecycledView(scrap);
   1082                 childCount.decrementAndGet();
   1083             }
   1084 
   1085             @Override
   1086             public RecyclerView.ViewHolder getRecycledView(int viewType) {
   1087                 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
   1088                 if (recycledView != null) {
   1089                     childCount.incrementAndGet();
   1090                 }
   1091                 return recycledView;
   1092             }
   1093         });
   1094 
   1095         // now keep adding children to trigger more children being created etc.
   1096         for (int i = 0; i < 100; i++) {
   1097             adapter.addAndNotify(15, 1);
   1098             Thread.sleep(50);
   1099         }
   1100         getInstrumentation().waitForIdleSync();
   1101         waitForAnimations(2);
   1102         assertEquals("Children count should add up", childCount.get(),
   1103                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
   1104     }
   1105 
   1106     @Test
   1107     public void notifyDataSetChanged() throws Throwable {
   1108         setupBasic(10, 3, 4);
   1109         int layoutCount = mLayoutManager.mTotalLayoutCount;
   1110         mLayoutManager.expectLayouts(1);
   1111         mActivityRule.runOnUiThread(new Runnable() {
   1112             @Override
   1113             public void run() {
   1114                 try {
   1115                     mTestAdapter.deleteAndNotify(4, 1);
   1116                     mTestAdapter.dispatchDataSetChanged();
   1117                 } catch (Throwable throwable) {
   1118                     throwable.printStackTrace();
   1119                 }
   1120 
   1121             }
   1122         });
   1123         mLayoutManager.waitForLayout(2);
   1124         getInstrumentation().waitForIdleSync();
   1125         assertEquals("on notify data set changed, predictive animations should not run",
   1126                 layoutCount + 1, mLayoutManager.mTotalLayoutCount);
   1127         mLayoutManager.expectLayouts(2);
   1128         mTestAdapter.addAndNotify(4, 2);
   1129         // make sure animations recover
   1130         mLayoutManager.waitForLayout(2);
   1131     }
   1132 
   1133     @Test
   1134     public void stableIdNotifyDataSetChanged() throws Throwable {
   1135         final int itemCount = 20;
   1136         List<Item> initialSet = new ArrayList<Item>();
   1137         final TestAdapter adapter = new TestAdapter(itemCount) {
   1138             @Override
   1139             public long getItemId(int position) {
   1140                 return mItems.get(position).mId;
   1141             }
   1142         };
   1143         adapter.setHasStableIds(true);
   1144         initialSet.addAll(adapter.mItems);
   1145         positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
   1146                     @Override
   1147                     void onRun(TestAdapter testAdapter) throws Throwable {
   1148                         Item item5 = adapter.mItems.get(5);
   1149                         Item item6 = adapter.mItems.get(6);
   1150                         item5.mAdapterIndex = 6;
   1151                         item6.mAdapterIndex = 5;
   1152                         adapter.mItems.remove(5);
   1153                         adapter.mItems.add(6, item5);
   1154                         adapter.dispatchDataSetChanged();
   1155                         //hacky, we support only 1 layout pass
   1156                         mLayoutManager.layoutLatch.countDown();
   1157                     }
   1158                 }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
   1159                 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
   1160                 PositionConstraint.scrap(9, -1, 9));
   1161         // now mix items.
   1162     }
   1163 
   1164 
   1165     @Test
   1166     public void getItemForDeletedView() throws Throwable {
   1167         getItemForDeletedViewTest(false);
   1168         getItemForDeletedViewTest(true);
   1169     }
   1170 
   1171     public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
   1172         final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
   1173         final Set<Integer> itemIdQueries = new HashSet<Integer>();
   1174         TestAdapter adapter = new TestAdapter(10) {
   1175             @Override
   1176             public int getItemViewType(int position) {
   1177                 itemViewTypeQueries.add(position);
   1178                 return super.getItemViewType(position);
   1179             }
   1180 
   1181             @Override
   1182             public long getItemId(int position) {
   1183                 itemIdQueries.add(position);
   1184                 return mItems.get(position).mId;
   1185             }
   1186         };
   1187         adapter.setHasStableIds(stableIds);
   1188         setupBasic(10, 0, 10, adapter);
   1189         assertEquals("getItemViewType for all items should be called", 10,
   1190                 itemViewTypeQueries.size());
   1191         if (adapter.hasStableIds()) {
   1192             assertEquals("getItemId should be called when adapter has stable ids", 10,
   1193                     itemIdQueries.size());
   1194         } else {
   1195             assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
   1196                     itemIdQueries.size());
   1197         }
   1198         itemViewTypeQueries.clear();
   1199         itemIdQueries.clear();
   1200         mLayoutManager.expectLayouts(2);
   1201         // delete last two
   1202         final int deleteStart = 8;
   1203         final int deleteCount = adapter.getItemCount() - deleteStart;
   1204         adapter.deleteAndNotify(deleteStart, deleteCount);
   1205         mLayoutManager.waitForLayout(2);
   1206         for (int i = 0; i < deleteStart; i++) {
   1207             assertTrue("getItemViewType for existing item " + i + " should be called",
   1208                     itemViewTypeQueries.contains(i));
   1209             if (adapter.hasStableIds()) {
   1210                 assertTrue("getItemId for existing item " + i
   1211                                 + " should be called when adapter has stable ids",
   1212                         itemIdQueries.contains(i));
   1213             }
   1214         }
   1215         for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
   1216             assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
   1217                     itemViewTypeQueries.contains(i));
   1218             if (adapter.hasStableIds()) {
   1219                 assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
   1220                         itemIdQueries.contains(i));
   1221             }
   1222         }
   1223     }
   1224 
   1225     @Test
   1226     public void deleteInvisibleMultiStep() throws Throwable {
   1227         setupBasic(1000, 1, 7);
   1228         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
   1229         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
   1230         mLayoutManager.expectLayouts(1);
   1231         // try to trigger race conditions
   1232         int targetItemCount = mTestAdapter.getItemCount();
   1233         for (int i = 0; i < 100; i++) {
   1234             mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
   1235             checkForMainThreadException();
   1236             targetItemCount -= 2;
   1237         }
   1238         // wait until main thread runnables are consumed
   1239         while (targetItemCount != mTestAdapter.getItemCount()) {
   1240             Thread.sleep(100);
   1241         }
   1242         mLayoutManager.waitForLayout(2);
   1243     }
   1244 
   1245     @Test
   1246     public void addManyMultiStep() throws Throwable {
   1247         setupBasic(10, 1, 7);
   1248         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
   1249         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
   1250         mLayoutManager.expectLayouts(1);
   1251         // try to trigger race conditions
   1252         int targetItemCount = mTestAdapter.getItemCount();
   1253         for (int i = 0; i < 100; i++) {
   1254             checkForMainThreadException();
   1255             mTestAdapter.addAndNotify(0, 1);
   1256             checkForMainThreadException();
   1257             mTestAdapter.addAndNotify(7, 1);
   1258             targetItemCount += 2;
   1259         }
   1260         checkForMainThreadException();
   1261         // wait until main thread runnables are consumed
   1262         while (targetItemCount != mTestAdapter.getItemCount()) {
   1263             Thread.sleep(100);
   1264             checkForMainThreadException();
   1265         }
   1266         mLayoutManager.waitForLayout(2);
   1267     }
   1268 
   1269     @Test
   1270     public void basicDelete() throws Throwable {
   1271         setupBasic(10);
   1272         final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() {
   1273             @Override
   1274             public void postDispatchLayout() {
   1275                 // verify this only in first layout
   1276                 assertEquals("deleted views should still be children of RV",
   1277                         mLayoutManager.getChildCount() + mDeletedViewCount
   1278                         , mRecyclerView.getChildCount());
   1279             }
   1280 
   1281             @Override
   1282             void afterPreLayout(RecyclerView.Recycler recycler,
   1283                     AnimationLayoutManager layoutManager,
   1284                     RecyclerView.State state) {
   1285                 super.afterPreLayout(recycler, layoutManager, state);
   1286                 mLayoutItemCount = 3;
   1287                 mLayoutMin = 0;
   1288             }
   1289         };
   1290         callbacks.mLayoutItemCount = 10;
   1291         callbacks.setExpectedItemCounts(10, 3);
   1292         mLayoutManager.setOnLayoutCallbacks(callbacks);
   1293 
   1294         mLayoutManager.expectLayouts(2);
   1295         mTestAdapter.deleteAndNotify(0, 7);
   1296         mLayoutManager.waitForLayout(2);
   1297         callbacks.reset();// when animations end another layout will happen
   1298     }
   1299 
   1300 
   1301     @Test
   1302     public void adapterChangeDuringScrolling() throws Throwable {
   1303         setupBasic(10);
   1304         final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
   1305         final AtomicInteger onScrollItemCount = new AtomicInteger(0);
   1306 
   1307         mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
   1308             @Override
   1309             void onLayoutChildren(RecyclerView.Recycler recycler,
   1310                     AnimationLayoutManager lm, RecyclerView.State state) {
   1311                 onLayoutItemCount.set(state.getItemCount());
   1312                 super.onLayoutChildren(recycler, lm, state);
   1313             }
   1314 
   1315             @Override
   1316             public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
   1317                 onScrollItemCount.set(state.getItemCount());
   1318                 super.onScroll(dx, recycler, state);
   1319             }
   1320         });
   1321         mActivityRule.runOnUiThread(new Runnable() {
   1322             @Override
   1323             public void run() {
   1324                 mTestAdapter.mItems.remove(5);
   1325                 mTestAdapter.notifyItemRangeRemoved(5, 1);
   1326                 mRecyclerView.scrollBy(0, 100);
   1327                 assertTrue("scrolling while there are pending adapter updates should "
   1328                         + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
   1329                 assertEquals("scroll by should be called w/ updated adapter count",
   1330                         mTestAdapter.mItems.size(), onScrollItemCount.get());
   1331 
   1332             }
   1333         });
   1334     }
   1335 
   1336     @Test
   1337     public void notifyDataSetChangedDuringScroll() throws Throwable {
   1338         setupBasic(10);
   1339         final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
   1340         final AtomicInteger onScrollItemCount = new AtomicInteger(0);
   1341 
   1342         mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
   1343             @Override
   1344             void onLayoutChildren(RecyclerView.Recycler recycler,
   1345                     AnimationLayoutManager lm, RecyclerView.State state) {
   1346                 onLayoutItemCount.set(state.getItemCount());
   1347                 super.onLayoutChildren(recycler, lm, state);
   1348             }
   1349 
   1350             @Override
   1351             public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
   1352                 onScrollItemCount.set(state.getItemCount());
   1353                 super.onScroll(dx, recycler, state);
   1354             }
   1355         });
   1356         mActivityRule.runOnUiThread(new Runnable() {
   1357             @Override
   1358             public void run() {
   1359                 mTestAdapter.mItems.remove(5);
   1360                 mTestAdapter.notifyDataSetChanged();
   1361                 mRecyclerView.scrollBy(0, 100);
   1362                 assertTrue("scrolling while there are pending adapter updates should "
   1363                         + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
   1364                 assertEquals("scroll by should be called w/ updated adapter count",
   1365                         mTestAdapter.mItems.size(), onScrollItemCount.get());
   1366 
   1367             }
   1368         });
   1369     }
   1370 
   1371     @Test
   1372     public void addInvisibleAndVisible() throws Throwable {
   1373         setupBasic(10, 1, 7);
   1374         mLayoutManager.expectLayouts(2);
   1375         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
   1376         mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
   1377         mLayoutManager.waitForLayout(2);
   1378     }
   1379 
   1380     @Test
   1381     public void addInvisible() throws Throwable {
   1382         setupBasic(10, 1, 7);
   1383         mLayoutManager.expectLayouts(1);
   1384         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
   1385         mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
   1386         mLayoutManager.waitForLayout(2);
   1387     }
   1388 
   1389     @Test
   1390     public void basicAdd() throws Throwable {
   1391         setupBasic(10);
   1392         mLayoutManager.expectLayouts(2);
   1393         setExpectedItemCounts(10, 13);
   1394         mTestAdapter.addAndNotify(2, 3);
   1395         mLayoutManager.waitForLayout(2);
   1396     }
   1397 
   1398     // Run this test on Jelly Bean and newer because hasTransientState was introduced in API 16.
   1399     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
   1400     @Test
   1401     public void appCancelAnimationInDetach() throws Throwable {
   1402         final View[] addedView = new View[2];
   1403         TestAdapter adapter = new TestAdapter(1) {
   1404             @Override
   1405             public void onViewDetachedFromWindow(TestViewHolder holder) {
   1406                 if ((addedView[0] == holder.itemView || addedView[1] == holder.itemView)
   1407                         && ViewCompat.hasTransientState(holder.itemView)) {
   1408                     holder.itemView.animate().cancel();
   1409                 }
   1410                 super.onViewDetachedFromWindow(holder);
   1411             }
   1412         };
   1413         // original 1 item
   1414         setupBasic(1, 0, 1, adapter);
   1415         mRecyclerView.getItemAnimator().setAddDuration(10000);
   1416         mLayoutManager.expectLayouts(2);
   1417         // add 2 items
   1418         setExpectedItemCounts(1, 3);
   1419         mTestAdapter.addAndNotify(0, 2);
   1420         mLayoutManager.waitForLayout(2);
   1421         checkForMainThreadException();
   1422         // wait till "add animation" starts
   1423         int limit = 200;
   1424         while (addedView[0] == null || addedView[1] == null) {
   1425             Thread.sleep(100);
   1426             mActivityRule.runOnUiThread(new Runnable() {
   1427                 @Override
   1428                 public void run() {
   1429                     if (mRecyclerView.getChildCount() == 3) {
   1430                         View view = mRecyclerView.getChildAt(0);
   1431                         if (ViewCompat.hasTransientState(view)) {
   1432                             addedView[0] = view;
   1433                         }
   1434                         view = mRecyclerView.getChildAt(1);
   1435                         if (ViewCompat.hasTransientState(view)) {
   1436                             addedView[1] = view;
   1437                         }
   1438                     }
   1439                 }
   1440             });
   1441             assertTrue("add should start on time", --limit > 0);
   1442         }
   1443 
   1444         // Layout from item2, exclude the current adding items
   1445         mLayoutManager.expectLayouts(1);
   1446         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
   1447             @Override
   1448             void beforePostLayout(RecyclerView.Recycler recycler,
   1449                     AnimationLayoutManager layoutManager,
   1450                     RecyclerView.State state) {
   1451                 mLayoutMin = 2;
   1452                 mLayoutItemCount = 1;
   1453             }
   1454         };
   1455         requestLayoutOnUIThread(mRecyclerView);
   1456         mLayoutManager.waitForLayout(2);
   1457     }
   1458 
   1459     @Test
   1460     public void adapterChangeFrozen() throws Throwable {
   1461         setupBasic(10, 1, 7);
   1462         assertTrue(mRecyclerView.getChildCount() == 7);
   1463 
   1464         mLayoutManager.expectLayouts(2);
   1465         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
   1466         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 8;
   1467         freezeLayout(true);
   1468         mTestAdapter.addAndNotify(0, 1);
   1469 
   1470         mLayoutManager.assertNoLayout("RV should keep old child during frozen", 2);
   1471         assertEquals(7, mRecyclerView.getChildCount());
   1472 
   1473         freezeLayout(false);
   1474         mLayoutManager.waitForLayout(2);
   1475         assertEquals("RV should get updated after waken from frozen",
   1476                 8, mRecyclerView.getChildCount());
   1477     }
   1478 
   1479     @Test
   1480     public void removeScrapInvalidate() throws Throwable {
   1481         setupBasic(10);
   1482         TestRecyclerView testRecyclerView = getTestRecyclerView();
   1483         mLayoutManager.expectLayouts(1);
   1484         testRecyclerView.expectDraw(1);
   1485         mActivityRule.runOnUiThread(new Runnable() {
   1486             @Override
   1487             public void run() {
   1488                 mTestAdapter.mItems.clear();
   1489                 mTestAdapter.notifyDataSetChanged();
   1490             }
   1491         });
   1492         mLayoutManager.waitForLayout(2);
   1493         testRecyclerView.waitForDraw(2);
   1494     }
   1495 
   1496     @Test
   1497     public void deleteVisibleAndInvisible() throws Throwable {
   1498         setupBasic(11, 3, 5); //layout items  3 4 5 6 7
   1499         mLayoutManager.expectLayouts(2);
   1500         setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
   1501         setExpectedItemCounts(9, 8);
   1502         mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
   1503         mLayoutManager.waitForLayout(2);
   1504     }
   1505 
   1506     @Test
   1507     public void findPositionOffset() throws Throwable {
   1508         setupBasic(10);
   1509         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
   1510             @Override
   1511             void beforePreLayout(RecyclerView.Recycler recycler,
   1512                     AnimationLayoutManager lm, RecyclerView.State state) {
   1513                 super.beforePreLayout(recycler, lm, state);
   1514                 // [0,2,4]
   1515                 assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
   1516                 assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
   1517                 assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
   1518             }
   1519         };
   1520         mActivityRule.runOnUiThread(new Runnable() {
   1521             @Override
   1522             public void run() {
   1523                 try {
   1524                     // delete 1
   1525                     mTestAdapter.deleteAndNotify(1, 1);
   1526                     // delete 3
   1527                     mTestAdapter.deleteAndNotify(2, 1);
   1528                 } catch (Throwable throwable) {
   1529                     throwable.printStackTrace();
   1530                 }
   1531             }
   1532         });
   1533         mLayoutManager.waitForLayout(2);
   1534     }
   1535 
   1536     private void setLayoutRange(int start, int count) {
   1537         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
   1538         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
   1539     }
   1540 
   1541     private void setExpectedItemCounts(int preLayout, int postLayout) {
   1542         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
   1543     }
   1544 
   1545     @Test
   1546     public void deleteInvisible() throws Throwable {
   1547         setupBasic(10, 1, 7);
   1548         mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
   1549         mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
   1550         mLayoutManager.expectLayouts(1);
   1551         mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
   1552         mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
   1553         mLayoutManager.waitForLayout(2);
   1554     }
   1555 
   1556     private CollectPositionResult findByPos(RecyclerView recyclerView,
   1557             RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
   1558         View view = recycler.getViewForPosition(position, true);
   1559         RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
   1560         if (vh.wasReturnedFromScrap()) {
   1561             vh.clearReturnedFromScrapFlag(); //keep data consistent.
   1562             return CollectPositionResult.fromScrap(vh);
   1563         } else {
   1564             return CollectPositionResult.fromAdapter(vh);
   1565         }
   1566     }
   1567 
   1568     public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
   1569             RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
   1570         Map<Integer, CollectPositionResult> positionToAdapterMapping
   1571                 = new HashMap<Integer, CollectPositionResult>();
   1572         for (int position : positions) {
   1573             if (position < 0) {
   1574                 continue;
   1575             }
   1576             positionToAdapterMapping.put(position,
   1577                     findByPos(recyclerView, recycler, state, position));
   1578         }
   1579         return positionToAdapterMapping;
   1580     }
   1581 
   1582     @Test
   1583     public void addDelete2() throws Throwable {
   1584         positionStatesTest(5, 0, 5, new AdapterOps() {
   1585                     // 0 1 2 3 4
   1586                     // 0 1 2 a b 3 4
   1587                     // 0 1 b 3 4
   1588                     // pre: 0 1 2 3 4
   1589                     // pre w/ adap: 0 1 2 b 3 4
   1590                     @Override
   1591                     void onRun(TestAdapter adapter) throws Throwable {
   1592                         adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
   1593                     }
   1594                 }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
   1595                 PositionConstraint.scrap(3, 3, 3)
   1596         );
   1597     }
   1598 
   1599     @Test
   1600     public void addDelete1() throws Throwable {
   1601         positionStatesTest(5, 0, 5, new AdapterOps() {
   1602                     // 0 1 2 3 4
   1603                     // 0 1 2 a b 3 4
   1604                     // 0 2 a b 3 4
   1605                     // 0 c d 2 a b 3 4
   1606                     // 0 c d 2 a 4
   1607                     // c d 2 a 4
   1608                     // pre: 0 1 2 3 4
   1609                     @Override
   1610                     void onRun(TestAdapter adapter) throws Throwable {
   1611                         adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
   1612                                 new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
   1613                     }
   1614                 }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
   1615                 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
   1616                 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
   1617                 PositionConstraint.adapter(1), PositionConstraint.adapter(3)
   1618         );
   1619     }
   1620 
   1621     @Test
   1622     public void addSameIndexTwice() throws Throwable {
   1623         positionStatesTest(12, 2, 7, new AdapterOps() {
   1624                     @Override
   1625                     void onRun(TestAdapter adapter) throws Throwable {
   1626                         adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
   1627                                 new int[]{11, 1});
   1628                     }
   1629                 }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
   1630                 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
   1631                 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
   1632                 PositionConstraint.scrap(8, 8, 13)
   1633         );
   1634     }
   1635 
   1636     @Test
   1637     public void deleteTwice() throws Throwable {
   1638         positionStatesTest(12, 2, 7, new AdapterOps() {
   1639                     @Override
   1640                     void onRun(TestAdapter adapter) throws Throwable {
   1641                         adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
   1642                                 new int[]{0, 1});// delete item ids 0,2,9,1
   1643                     }
   1644                 }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
   1645                 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
   1646                 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
   1647                 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
   1648         );
   1649     }
   1650 
   1651 
   1652     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
   1653             int firstLayoutItemCount, AdapterOps adapterChanges,
   1654             final PositionConstraint... constraints) throws Throwable {
   1655         positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
   1656                 adapterChanges, constraints);
   1657     }
   1658 
   1659     public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
   1660             int firstLayoutItemCount, TestAdapter adapter, AdapterOps adapterChanges,
   1661             final PositionConstraint... constraints) throws Throwable {
   1662         setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
   1663         mLayoutManager.expectLayouts(2);
   1664         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
   1665             @Override
   1666             void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
   1667                     RecyclerView.State state) {
   1668                 super.beforePreLayout(recycler, lm, state);
   1669                 //harmless
   1670                 lm.detachAndScrapAttachedViews(recycler);
   1671                 final int[] ids = new int[constraints.length];
   1672                 for (int i = 0; i < constraints.length; i++) {
   1673                     ids[i] = constraints[i].mPreLayoutPos;
   1674                 }
   1675                 Map<Integer, CollectPositionResult> positions
   1676                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
   1677                 StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
   1678                 for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
   1679                     positionLog.append(entry.getKey()).append(":").append(entry.getValue())
   1680                             .append("\n");
   1681                 }
   1682                 for (PositionConstraint constraint : constraints) {
   1683                     if (constraint.mPreLayoutPos != -1) {
   1684                         constraint.validate(state, positions.get(constraint.mPreLayoutPos),
   1685                                 lm.getLog() + positionLog);
   1686                     }
   1687                 }
   1688             }
   1689 
   1690             @Override
   1691             void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
   1692                     RecyclerView.State state) {
   1693                 super.beforePostLayout(recycler, lm, state);
   1694                 lm.detachAndScrapAttachedViews(recycler);
   1695                 final int[] ids = new int[constraints.length];
   1696                 for (int i = 0; i < constraints.length; i++) {
   1697                     ids[i] = constraints[i].mPostLayoutPos;
   1698                 }
   1699                 Map<Integer, CollectPositionResult> positions
   1700                         = collectPositions(lm.mRecyclerView, recycler, state, ids);
   1701                 StringBuilder positionLog = new StringBuilder("\nPosition logs:\n");
   1702                 for (Map.Entry<Integer, CollectPositionResult> entry : positions.entrySet()) {
   1703                     positionLog.append(entry.getKey()).append(":")
   1704                             .append(entry.getValue()).append("\n");
   1705                 }
   1706                 for (PositionConstraint constraint : constraints) {
   1707                     if (constraint.mPostLayoutPos >= 0) {
   1708                         constraint.validate(state, positions.get(constraint.mPostLayoutPos),
   1709                                 lm.getLog() + positionLog);
   1710                     }
   1711                 }
   1712             }
   1713         };
   1714         adapterChanges.run(mTestAdapter);
   1715         mLayoutManager.waitForLayout(2);
   1716         checkForMainThreadException();
   1717         for (PositionConstraint constraint : constraints) {
   1718             constraint.assertValidate();
   1719         }
   1720     }
   1721 
   1722     @Test
   1723     public void addThenRecycleRemovedView() throws Throwable {
   1724         setupBasic(10);
   1725         final AtomicInteger step = new AtomicInteger(0);
   1726         final List<RecyclerView.ViewHolder> animateRemoveList
   1727                 = new ArrayList<RecyclerView.ViewHolder>();
   1728         DefaultItemAnimator animator = new DefaultItemAnimator() {
   1729             @Override
   1730             public boolean animateRemove(RecyclerView.ViewHolder holder) {
   1731                 animateRemoveList.add(holder);
   1732                 return super.animateRemove(holder);
   1733             }
   1734         };
   1735         mRecyclerView.setItemAnimator(animator);
   1736         final List<RecyclerView.ViewHolder> pooledViews = new ArrayList<RecyclerView.ViewHolder>();
   1737         mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() {
   1738             @Override
   1739             public void putRecycledView(RecyclerView.ViewHolder scrap) {
   1740                 pooledViews.add(scrap);
   1741                 super.putRecycledView(scrap);
   1742             }
   1743         });
   1744         final RecyclerView.ViewHolder[] targetVh = new RecyclerView.ViewHolder[1];
   1745         mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
   1746             @Override
   1747             void doLayout(RecyclerView.Recycler recycler,
   1748                     AnimationLayoutManager lm, RecyclerView.State state) {
   1749                 switch (step.get()) {
   1750                     case 1:
   1751                         super.doLayout(recycler, lm, state);
   1752                         if (state.isPreLayout()) {
   1753                             View view = mLayoutManager.getChildAt(1);
   1754                             RecyclerView.ViewHolder holder =
   1755                                     mRecyclerView.getChildViewHolderInt(view);
   1756                             targetVh[0] = holder;
   1757                             assertTrue("test sanity", holder.isRemoved());
   1758                             mLayoutManager.removeAndRecycleView(view, recycler);
   1759                         }
   1760                         break;
   1761                 }
   1762             }
   1763         };
   1764         step.set(1);
   1765         animateRemoveList.clear();
   1766         mLayoutManager.expectLayouts(2);
   1767         mTestAdapter.deleteAndNotify(1, 1);
   1768         mLayoutManager.waitForLayout(2);
   1769         assertTrue("test sanity, view should be recycled", pooledViews.contains(targetVh[0]));
   1770         assertTrue("since LM force recycled a view, animate disappearance should not be called",
   1771                 animateRemoveList.isEmpty());
   1772     }
   1773 }
   1774