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