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 17package androidx.recyclerview.widget; 18 19import static org.junit.Assert.assertEquals; 20import static org.junit.Assert.assertFalse; 21import static org.junit.Assert.assertNotNull; 22import static org.junit.Assert.assertNotSame; 23import static org.junit.Assert.assertNull; 24import static org.junit.Assert.assertSame; 25import static org.junit.Assert.assertTrue; 26import static org.junit.Assert.fail; 27 28import android.graphics.Rect; 29import android.os.Build; 30import android.support.test.filters.LargeTest; 31import android.support.test.filters.SdkSuppress; 32import android.support.test.runner.AndroidJUnit4; 33import android.util.Log; 34import android.view.View; 35import android.view.ViewGroup; 36 37import androidx.annotation.NonNull; 38import androidx.core.view.ViewCompat; 39 40import org.hamcrest.CoreMatchers; 41import org.hamcrest.MatcherAssert; 42import org.junit.Test; 43import org.junit.runner.RunWith; 44 45import java.util.ArrayList; 46import java.util.HashMap; 47import java.util.HashSet; 48import java.util.List; 49import java.util.Map; 50import java.util.Set; 51import java.util.concurrent.atomic.AtomicBoolean; 52import java.util.concurrent.atomic.AtomicInteger; 53 54/** 55 * Tests for {@link SimpleItemAnimator} API. 56 */ 57@LargeTest 58@RunWith(AndroidJUnit4.class) 59public 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