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