1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.util.AttributeSet; 22import android.util.Log; 23import android.view.View; 24import android.view.ViewGroup; 25 26import java.util.ArrayList; 27import java.util.HashMap; 28import java.util.HashSet; 29import java.util.List; 30import java.util.Map; 31import java.util.Set; 32import java.util.concurrent.CountDownLatch; 33import java.util.concurrent.TimeUnit; 34import java.util.concurrent.atomic.AtomicInteger; 35 36public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest { 37 38 private static final boolean DEBUG = false; 39 40 private static final String TAG = "RecyclerViewAnimationsTest"; 41 42 AnimationLayoutManager mLayoutManager; 43 44 TestAdapter mTestAdapter; 45 46 public RecyclerViewAnimationsTest() { 47 super(DEBUG); 48 } 49 50 @Override 51 protected void setUp() throws Exception { 52 super.setUp(); 53 } 54 55 RecyclerView setupBasic(int itemCount) throws Throwable { 56 return setupBasic(itemCount, 0, itemCount); 57 } 58 59 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount) 60 throws Throwable { 61 return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null); 62 } 63 64 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, 65 TestAdapter testAdapter) 66 throws Throwable { 67 final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); 68 recyclerView.setHasFixedSize(true); 69 if (testAdapter == null) { 70 mTestAdapter = new TestAdapter(itemCount); 71 } else { 72 mTestAdapter = testAdapter; 73 } 74 recyclerView.setAdapter(mTestAdapter); 75 mLayoutManager = new AnimationLayoutManager(); 76 recyclerView.setLayoutManager(mLayoutManager); 77 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex; 78 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount; 79 80 mLayoutManager.expectLayouts(1); 81 recyclerView.expectDraw(1); 82 setRecyclerView(recyclerView); 83 mLayoutManager.waitForLayout(2); 84 recyclerView.waitForDraw(1); 85 mLayoutManager.mOnLayoutCallbacks.reset(); 86 getInstrumentation().waitForIdleSync(); 87 assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount()); 88 assertEquals("all expected children should be laid out", firstLayoutItemCount, 89 mLayoutManager.getChildCount()); 90 return recyclerView; 91 } 92 93 public void testDetachBeforeAnimations() throws Throwable { 94 setupBasic(10, 0, 5); 95 final RecyclerView rv = mRecyclerView; 96 waitForAnimations(2); 97 final DefaultItemAnimator animator = new DefaultItemAnimator() { 98 @Override 99 public void runPendingAnimations() { 100 super.runPendingAnimations(); 101 } 102 }; 103 rv.setItemAnimator(animator); 104 mLayoutManager.expectLayouts(2); 105 mTestAdapter.deleteAndNotify(3, 4); 106 mLayoutManager.waitForLayout(2); 107 removeRecyclerView(); 108 assertNull("test sanity check RV should be removed", rv.getParent()); 109 assertEquals("no views should be hidden", 0, rv.mChildHelper.mHiddenViews.size()); 110 assertFalse("there should not be any animations running", animator.isRunning()); 111 } 112 113 public void testPreLayoutPositionCleanup() throws Throwable { 114 setupBasic(4, 0, 4); 115 mLayoutManager.expectLayouts(2); 116 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 117 @Override 118 void beforePreLayout(RecyclerView.Recycler recycler, 119 AnimationLayoutManager lm, RecyclerView.State state) { 120 mLayoutMin = 0; 121 mLayoutItemCount = 3; 122 } 123 124 @Override 125 void beforePostLayout(RecyclerView.Recycler recycler, 126 AnimationLayoutManager layoutManager, 127 RecyclerView.State state) { 128 mLayoutMin = 0; 129 mLayoutItemCount = 4; 130 } 131 }; 132 mTestAdapter.addAndNotify(0, 1); 133 mLayoutManager.waitForLayout(2); 134 135 136 137 } 138 139 public void testAddRemoveSamePass() throws Throwable { 140 final List<RecyclerView.ViewHolder> mRecycledViews 141 = new ArrayList<RecyclerView.ViewHolder>(); 142 TestAdapter adapter = new TestAdapter(50) { 143 @Override 144 public void onViewRecycled(TestViewHolder holder) { 145 super.onViewRecycled(holder); 146 mRecycledViews.add(holder); 147 } 148 }; 149 adapter.setHasStableIds(true); 150 setupBasic(50, 3, 5, adapter); 151 mRecyclerView.setItemViewCacheSize(0); 152 final ArrayList<RecyclerView.ViewHolder> addVH 153 = new ArrayList<RecyclerView.ViewHolder>(); 154 final ArrayList<RecyclerView.ViewHolder> removeVH 155 = new ArrayList<RecyclerView.ViewHolder>(); 156 157 final ArrayList<RecyclerView.ViewHolder> moveVH 158 = new ArrayList<RecyclerView.ViewHolder>(); 159 160 final View[] testView = new View[1]; 161 mRecyclerView.setItemAnimator(new DefaultItemAnimator() { 162 @Override 163 public boolean animateAdd(RecyclerView.ViewHolder holder) { 164 addVH.add(holder); 165 return true; 166 } 167 168 @Override 169 public boolean animateRemove(RecyclerView.ViewHolder holder) { 170 removeVH.add(holder); 171 return true; 172 } 173 174 @Override 175 public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, 176 int toX, int toY) { 177 moveVH.add(holder); 178 return true; 179 } 180 }); 181 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 182 @Override 183 void afterPreLayout(RecyclerView.Recycler recycler, 184 AnimationLayoutManager layoutManager, 185 RecyclerView.State state) { 186 super.afterPreLayout(recycler, layoutManager, state); 187 testView[0] = recycler.getViewForPosition(45); 188 testView[0].measure(View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST), 189 View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST)); 190 testView[0].layout(10, 10, 10 + testView[0].getMeasuredWidth(), 191 10 + testView[0].getMeasuredHeight()); 192 layoutManager.addView(testView[0], 4); 193 } 194 195 @Override 196 void afterPostLayout(RecyclerView.Recycler recycler, 197 AnimationLayoutManager layoutManager, 198 RecyclerView.State state) { 199 super.afterPostLayout(recycler, layoutManager, state); 200 testView[0].layout(50, 50, 50 + testView[0].getMeasuredWidth(), 201 50 + testView[0].getMeasuredHeight()); 202 layoutManager.addDisappearingView(testView[0], 4); 203 } 204 }; 205 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 3; 206 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 5; 207 mRecycledViews.clear(); 208 mLayoutManager.expectLayouts(2); 209 mTestAdapter.deleteAndNotify(3, 1); 210 mLayoutManager.waitForLayout(2); 211 212 for (RecyclerView.ViewHolder vh : addVH) { 213 assertNotSame("add-remove item should not animate add", testView[0], vh.itemView); 214 } 215 for (RecyclerView.ViewHolder vh : moveVH) { 216 assertNotSame("add-remove item should not animate move", testView[0], vh.itemView); 217 } 218 for (RecyclerView.ViewHolder vh : removeVH) { 219 assertNotSame("add-remove item should not animate remove", testView[0], vh.itemView); 220 } 221 boolean found = false; 222 for (RecyclerView.ViewHolder vh : mRecycledViews) { 223 found |= vh.itemView == testView[0]; 224 } 225 assertTrue("added-removed view should be recycled", found); 226 } 227 228 public void testChangeAnimations() throws Throwable { 229 final boolean[] booleans = {true, false}; 230 for (boolean supportsChange : booleans) { 231 for (boolean changeType : booleans) { 232 for (boolean hasStableIds : booleans) { 233 for (boolean deleteSomeItems : booleans) { 234 changeAnimTest(supportsChange, changeType, hasStableIds, deleteSomeItems); 235 } 236 removeRecyclerView(); 237 } 238 } 239 } 240 } 241 public void changeAnimTest(final boolean supportsChangeAnim, final boolean changeType, 242 final boolean hasStableIds, final boolean deleteSomeItems) throws Throwable { 243 final int changedIndex = 3; 244 final int defaultType = 1; 245 final AtomicInteger changedIndexNewType = new AtomicInteger(defaultType); 246 final String logPrefix = "supportsChangeAnim:" + supportsChangeAnim + 247 ", change view type:" + changeType + 248 ", has stable ids:" + hasStableIds + 249 ", force predictive:" + deleteSomeItems; 250 TestAdapter testAdapter = new TestAdapter(10) { 251 @Override 252 public int getItemViewType(int position) { 253 return position == changedIndex ? changedIndexNewType.get() : defaultType; 254 } 255 256 @Override 257 public TestViewHolder onCreateViewHolder(ViewGroup parent, 258 int viewType) { 259 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 260 if (DEBUG) { 261 Log.d(TAG, logPrefix + " onCreateVH" + vh.toString()); 262 } 263 return vh; 264 } 265 266 @Override 267 public void onBindViewHolder(TestViewHolder holder, 268 int position) { 269 super.onBindViewHolder(holder, position); 270 if (DEBUG) { 271 Log.d(TAG, logPrefix + " onBind to " + position + "" + holder.toString()); 272 } 273 } 274 }; 275 testAdapter.setHasStableIds(hasStableIds); 276 setupBasic(testAdapter.getItemCount(), 0, 10, testAdapter); 277 mRecyclerView.getItemAnimator().setSupportsChangeAnimations(supportsChangeAnim); 278 279 final RecyclerView.ViewHolder toBeChangedVH = 280 mRecyclerView.findViewHolderForLayoutPosition(changedIndex); 281 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 282 @Override 283 void afterPreLayout(RecyclerView.Recycler recycler, 284 AnimationLayoutManager layoutManager, 285 RecyclerView.State state) { 286 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition( 287 changedIndex); 288 if (supportsChangeAnim) { 289 assertTrue(logPrefix + " changed view holder should have correct flag" 290 , vh.isChanged()); 291 } else { 292 assertFalse(logPrefix + " changed view holder should have correct flag" 293 , vh.isChanged()); 294 } 295 } 296 297 @Override 298 void afterPostLayout(RecyclerView.Recycler recycler, 299 AnimationLayoutManager layoutManager, RecyclerView.State state) { 300 RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition( 301 changedIndex); 302 assertFalse(logPrefix + "VH should not be marked as changed", vh.isChanged()); 303 if (supportsChangeAnim) { 304 assertNotSame(logPrefix + "a new VH should be given if change is supported", 305 toBeChangedVH, vh); 306 } else if (!changeType && hasStableIds) { 307 assertSame(logPrefix + "if change animations are not supported but we have " 308 + "stable ids, same view holder should be returned", toBeChangedVH, vh); 309 } 310 super.beforePostLayout(recycler, layoutManager, state); 311 } 312 }; 313 mLayoutManager.expectLayouts(1); 314 if (changeType) { 315 changedIndexNewType.set(defaultType + 1); 316 } 317 if (deleteSomeItems) { 318 runTestOnUiThread(new Runnable() { 319 @Override 320 public void run() { 321 try { 322 mTestAdapter.deleteAndNotify(changedIndex + 2, 1); 323 mTestAdapter.notifyItemChanged(3); 324 } catch (Throwable throwable) { 325 throwable.printStackTrace(); 326 } 327 328 } 329 }); 330 } else { 331 mTestAdapter.notifyItemChanged(3); 332 } 333 334 mLayoutManager.waitForLayout(2); 335 } 336 337 public void testRecycleDuringAnimations() throws Throwable { 338 final AtomicInteger childCount = new AtomicInteger(0); 339 final TestAdapter adapter = new TestAdapter(1000) { 340 @Override 341 public TestViewHolder onCreateViewHolder(ViewGroup parent, 342 int viewType) { 343 childCount.incrementAndGet(); 344 return super.onCreateViewHolder(parent, viewType); 345 } 346 }; 347 setupBasic(1000, 10, 20, adapter); 348 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 10; 349 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 20; 350 351 mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool() { 352 @Override 353 public void putRecycledView(RecyclerView.ViewHolder scrap) { 354 super.putRecycledView(scrap); 355 childCount.decrementAndGet(); 356 } 357 358 @Override 359 public RecyclerView.ViewHolder getRecycledView(int viewType) { 360 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType); 361 if (recycledView != null) { 362 childCount.incrementAndGet(); 363 } 364 return recycledView; 365 } 366 }); 367 368 // now keep adding children to trigger more children being created etc. 369 for (int i = 0; i < 100; i ++) { 370 adapter.addAndNotify(15, 1); 371 Thread.sleep(50); 372 } 373 getInstrumentation().waitForIdleSync(); 374 waitForAnimations(2); 375 assertEquals("Children count should add up", childCount.get(), 376 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 377 } 378 379 public void testNotifyDataSetChanged() throws Throwable { 380 setupBasic(10, 3, 4); 381 int layoutCount = mLayoutManager.mTotalLayoutCount; 382 mLayoutManager.expectLayouts(1); 383 runTestOnUiThread(new Runnable() { 384 @Override 385 public void run() { 386 try { 387 mTestAdapter.deleteAndNotify(4, 1); 388 mTestAdapter.dispatchDataSetChanged(); 389 } catch (Throwable throwable) { 390 throwable.printStackTrace(); 391 } 392 393 } 394 }); 395 mLayoutManager.waitForLayout(2); 396 getInstrumentation().waitForIdleSync(); 397 assertEquals("on notify data set changed, predictive animations should not run", 398 layoutCount + 1, mLayoutManager.mTotalLayoutCount); 399 mLayoutManager.expectLayouts(2); 400 mTestAdapter.addAndNotify(4, 2); 401 // make sure animations recover 402 mLayoutManager.waitForLayout(2); 403 } 404 405 public void testStableIdNotifyDataSetChanged() throws Throwable { 406 final int itemCount = 20; 407 List<Item> initialSet = new ArrayList<Item>(); 408 final TestAdapter adapter = new TestAdapter(itemCount) { 409 @Override 410 public long getItemId(int position) { 411 return mItems.get(position).mId; 412 } 413 }; 414 adapter.setHasStableIds(true); 415 initialSet.addAll(adapter.mItems); 416 positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() { 417 @Override 418 void onRun(TestAdapter testAdapter) throws Throwable { 419 Item item5 = adapter.mItems.get(5); 420 Item item6 = adapter.mItems.get(6); 421 item5.mAdapterIndex = 6; 422 item6.mAdapterIndex = 5; 423 adapter.mItems.remove(5); 424 adapter.mItems.add(6, item5); 425 adapter.dispatchDataSetChanged(); 426 //hacky, we support only 1 layout pass 427 mLayoutManager.layoutLatch.countDown(); 428 } 429 }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6), 430 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8), 431 PositionConstraint.scrap(9, -1, 9)); 432 // now mix items. 433 } 434 435 436 public void testGetItemForDeletedView() throws Throwable { 437 getItemForDeletedViewTest(false); 438 getItemForDeletedViewTest(true); 439 } 440 441 public void getItemForDeletedViewTest(boolean stableIds) throws Throwable { 442 final Set<Integer> itemViewTypeQueries = new HashSet<Integer>(); 443 final Set<Integer> itemIdQueries = new HashSet<Integer>(); 444 TestAdapter adapter = new TestAdapter(10) { 445 @Override 446 public int getItemViewType(int position) { 447 itemViewTypeQueries.add(position); 448 return super.getItemViewType(position); 449 } 450 451 @Override 452 public long getItemId(int position) { 453 itemIdQueries.add(position); 454 return mItems.get(position).mId; 455 } 456 }; 457 adapter.setHasStableIds(stableIds); 458 setupBasic(10, 0, 10, adapter); 459 assertEquals("getItemViewType for all items should be called", 10, 460 itemViewTypeQueries.size()); 461 if (adapter.hasStableIds()) { 462 assertEquals("getItemId should be called when adapter has stable ids", 10, 463 itemIdQueries.size()); 464 } else { 465 assertEquals("getItemId should not be called when adapter does not have stable ids", 0, 466 itemIdQueries.size()); 467 } 468 itemViewTypeQueries.clear(); 469 itemIdQueries.clear(); 470 mLayoutManager.expectLayouts(2); 471 // delete last two 472 final int deleteStart = 8; 473 final int deleteCount = adapter.getItemCount() - deleteStart; 474 adapter.deleteAndNotify(deleteStart, deleteCount); 475 mLayoutManager.waitForLayout(2); 476 for (int i = 0; i < deleteStart; i++) { 477 assertTrue("getItemViewType for existing item " + i + " should be called", 478 itemViewTypeQueries.contains(i)); 479 if (adapter.hasStableIds()) { 480 assertTrue("getItemId for existing item " + i 481 + " should be called when adapter has stable ids", 482 itemIdQueries.contains(i)); 483 } 484 } 485 for (int i = deleteStart; i < deleteStart + deleteCount; i++) { 486 assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called", 487 itemViewTypeQueries.contains(i)); 488 if (adapter.hasStableIds()) { 489 assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called", 490 itemIdQueries.contains(i)); 491 } 492 } 493 } 494 495 public void testDeleteInvisibleMultiStep() throws Throwable { 496 setupBasic(1000, 1, 7); 497 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 498 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 499 mLayoutManager.expectLayouts(1); 500 // try to trigger race conditions 501 int targetItemCount = mTestAdapter.getItemCount(); 502 for (int i = 0; i < 100; i++) { 503 mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1}); 504 checkForMainThreadException(); 505 targetItemCount -= 2; 506 } 507 // wait until main thread runnables are consumed 508 while (targetItemCount != mTestAdapter.getItemCount()) { 509 Thread.sleep(100); 510 } 511 mLayoutManager.waitForLayout(2); 512 } 513 514 public void testAddManyMultiStep() throws Throwable { 515 setupBasic(10, 1, 7); 516 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 517 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 518 mLayoutManager.expectLayouts(1); 519 // try to trigger race conditions 520 int targetItemCount = mTestAdapter.getItemCount(); 521 for (int i = 0; i < 100; i++) { 522 mTestAdapter.addAndNotify(0, 1); 523 mTestAdapter.addAndNotify(7, 1); 524 targetItemCount += 2; 525 } 526 // wait until main thread runnables are consumed 527 while (targetItemCount != mTestAdapter.getItemCount()) { 528 Thread.sleep(100); 529 } 530 mLayoutManager.waitForLayout(2); 531 } 532 533 public void testBasicDelete() throws Throwable { 534 setupBasic(10); 535 final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() { 536 @Override 537 public void postDispatchLayout() { 538 // verify this only in first layout 539 assertEquals("deleted views should still be children of RV", 540 mLayoutManager.getChildCount() + mDeletedViewCount 541 , mRecyclerView.getChildCount()); 542 } 543 544 @Override 545 void afterPreLayout(RecyclerView.Recycler recycler, 546 AnimationLayoutManager layoutManager, 547 RecyclerView.State state) { 548 super.afterPreLayout(recycler, layoutManager, state); 549 mLayoutItemCount = 3; 550 mLayoutMin = 0; 551 } 552 }; 553 callbacks.mLayoutItemCount = 10; 554 callbacks.setExpectedItemCounts(10, 3); 555 mLayoutManager.setOnLayoutCallbacks(callbacks); 556 557 mLayoutManager.expectLayouts(2); 558 mTestAdapter.deleteAndNotify(0, 7); 559 mLayoutManager.waitForLayout(2); 560 callbacks.reset();// when animations end another layout will happen 561 } 562 563 564 public void testAdapterChangeDuringScrolling() throws Throwable { 565 setupBasic(10); 566 final AtomicInteger onLayoutItemCount = new AtomicInteger(0); 567 final AtomicInteger onScrollItemCount = new AtomicInteger(0); 568 569 mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() { 570 @Override 571 void onLayoutChildren(RecyclerView.Recycler recycler, 572 AnimationLayoutManager lm, RecyclerView.State state) { 573 onLayoutItemCount.set(state.getItemCount()); 574 super.onLayoutChildren(recycler, lm, state); 575 } 576 577 @Override 578 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 579 onScrollItemCount.set(state.getItemCount()); 580 super.onScroll(dx, recycler, state); 581 } 582 }); 583 runTestOnUiThread(new Runnable() { 584 @Override 585 public void run() { 586 mTestAdapter.mItems.remove(5); 587 mTestAdapter.notifyItemRangeRemoved(5, 1); 588 mRecyclerView.scrollBy(0, 100); 589 assertTrue("scrolling while there are pending adapter updates should " 590 + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0); 591 assertEquals("scroll by should be called w/ updated adapter count", 592 mTestAdapter.mItems.size(), onScrollItemCount.get()); 593 594 } 595 }); 596 } 597 598 public void testNotifyDataSetChangedDuringScroll() throws Throwable { 599 setupBasic(10); 600 final AtomicInteger onLayoutItemCount = new AtomicInteger(0); 601 final AtomicInteger onScrollItemCount = new AtomicInteger(0); 602 603 mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() { 604 @Override 605 void onLayoutChildren(RecyclerView.Recycler recycler, 606 AnimationLayoutManager lm, RecyclerView.State state) { 607 onLayoutItemCount.set(state.getItemCount()); 608 super.onLayoutChildren(recycler, lm, state); 609 } 610 611 @Override 612 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 613 onScrollItemCount.set(state.getItemCount()); 614 super.onScroll(dx, recycler, state); 615 } 616 }); 617 runTestOnUiThread(new Runnable() { 618 @Override 619 public void run() { 620 mTestAdapter.mItems.remove(5); 621 mTestAdapter.notifyDataSetChanged(); 622 mRecyclerView.scrollBy(0, 100); 623 assertTrue("scrolling while there are pending adapter updates should " 624 + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0); 625 assertEquals("scroll by should be called w/ updated adapter count", 626 mTestAdapter.mItems.size(), onScrollItemCount.get()); 627 628 } 629 }); 630 } 631 632 public void testAddInvisibleAndVisible() throws Throwable { 633 setupBasic(10, 1, 7); 634 mLayoutManager.expectLayouts(2); 635 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 636 mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible 637 mLayoutManager.waitForLayout(2); 638 } 639 640 public void testAddInvisible() throws Throwable { 641 setupBasic(10, 1, 7); 642 mLayoutManager.expectLayouts(1); 643 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 644 mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0 645 mLayoutManager.waitForLayout(2); 646 } 647 648 public void testBasicAdd() throws Throwable { 649 setupBasic(10); 650 mLayoutManager.expectLayouts(2); 651 setExpectedItemCounts(10, 13); 652 mTestAdapter.addAndNotify(2, 3); 653 mLayoutManager.waitForLayout(2); 654 } 655 656 public TestRecyclerView getTestRecyclerView() { 657 return (TestRecyclerView) mRecyclerView; 658 } 659 660 public void testRemoveScrapInvalidate() throws Throwable { 661 setupBasic(10); 662 TestRecyclerView testRecyclerView = getTestRecyclerView(); 663 mLayoutManager.expectLayouts(1); 664 testRecyclerView.expectDraw(1); 665 runTestOnUiThread(new Runnable() { 666 @Override 667 public void run() { 668 mTestAdapter.mItems.clear(); 669 mTestAdapter.notifyDataSetChanged(); 670 } 671 }); 672 mLayoutManager.waitForLayout(2); 673 testRecyclerView.waitForDraw(2); 674 } 675 676 public void testDeleteVisibleAndInvisible() throws Throwable { 677 setupBasic(11, 3, 5); //layout items 3 4 5 6 7 678 mLayoutManager.expectLayouts(2); 679 setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list 680 setExpectedItemCounts(9, 8); 681 mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9 682 mLayoutManager.waitForLayout(2); 683 } 684 685 public void testFindPositionOffset() throws Throwable { 686 setupBasic(10); 687 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 688 @Override 689 void beforePreLayout(RecyclerView.Recycler recycler, 690 AnimationLayoutManager lm, RecyclerView.State state) { 691 super.beforePreLayout(recycler, lm, state); 692 // [0,2,4] 693 assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0)); 694 assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2)); 695 assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4)); 696 } 697 }; 698 runTestOnUiThread(new Runnable() { 699 @Override 700 public void run() { 701 // [0,1,2,3,4] 702 // delete 1 703 mTestAdapter.notifyItemRangeRemoved(1, 1); 704 // delete 3 705 mTestAdapter.notifyItemRangeRemoved(2, 1); 706 } 707 }); 708 mLayoutManager.waitForLayout(2); 709 } 710 711 private void setLayoutRange(int start, int count) { 712 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start; 713 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count; 714 } 715 716 private void setExpectedItemCounts(int preLayout, int postLayout) { 717 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout); 718 } 719 720 public void testDeleteInvisible() throws Throwable { 721 setupBasic(10, 1, 7); 722 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 723 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 724 mLayoutManager.expectLayouts(1); 725 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8); 726 mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8 727 mLayoutManager.waitForLayout(2); 728 } 729 730 private CollectPositionResult findByPos(RecyclerView recyclerView, 731 RecyclerView.Recycler recycler, RecyclerView.State state, int position) { 732 View view = recycler.getViewForPosition(position, true); 733 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 734 if (vh.wasReturnedFromScrap()) { 735 vh.clearReturnedFromScrapFlag(); //keep data consistent. 736 return CollectPositionResult.fromScrap(vh); 737 } else { 738 return CollectPositionResult.fromAdapter(vh); 739 } 740 } 741 742 public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView, 743 RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) { 744 Map<Integer, CollectPositionResult> positionToAdapterMapping 745 = new HashMap<Integer, CollectPositionResult>(); 746 for (int position : positions) { 747 if (position < 0) { 748 continue; 749 } 750 positionToAdapterMapping.put(position, 751 findByPos(recyclerView, recycler, state, position)); 752 } 753 return positionToAdapterMapping; 754 } 755 756 public void testAddDelete2() throws Throwable { 757 positionStatesTest(5, 0, 5, new AdapterOps() { 758 // 0 1 2 3 4 759 // 0 1 2 a b 3 4 760 // 0 1 b 3 4 761 // pre: 0 1 2 3 4 762 // pre w/ adap: 0 1 2 b 3 4 763 @Override 764 void onRun(TestAdapter adapter) throws Throwable { 765 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2}); 766 } 767 }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1), 768 PositionConstraint.scrap(3, 3, 3) 769 ); 770 } 771 772 public void testAddDelete1() throws Throwable { 773 positionStatesTest(5, 0, 5, new AdapterOps() { 774 // 0 1 2 3 4 775 // 0 1 2 a b 3 4 776 // 0 2 a b 3 4 777 // 0 c d 2 a b 3 4 778 // 0 c d 2 a 4 779 // c d 2 a 4 780 // pre: 0 1 2 3 4 781 @Override 782 void onRun(TestAdapter adapter) throws Throwable { 783 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1}, 784 new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1}); 785 } 786 }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1), 787 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1), 788 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0), 789 PositionConstraint.adapter(1), PositionConstraint.adapter(3) 790 ); 791 } 792 793 public void testAddSameIndexTwice() throws Throwable { 794 positionStatesTest(12, 2, 7, new AdapterOps() { 795 @Override 796 void onRun(TestAdapter adapter) throws Throwable { 797 adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1}, 798 new int[]{11, 1}); 799 } 800 }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3), 801 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7), 802 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12), 803 PositionConstraint.scrap(8, 8, 13) 804 ); 805 } 806 807 public void testDeleteTwice() throws Throwable { 808 positionStatesTest(12, 2, 7, new AdapterOps() { 809 @Override 810 void onRun(TestAdapter adapter) throws Throwable { 811 adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1}, 812 new int[]{0, 1});// delete item ids 0,2,9,1 813 } 814 }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0), 815 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2), 816 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5), 817 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7) 818 ); 819 } 820 821 822 public void positionStatesTest(int itemCount, int firstLayoutStartIndex, 823 int firstLayoutItemCount, AdapterOps adapterChanges, 824 final PositionConstraint... constraints) throws Throwable { 825 positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null, 826 adapterChanges, constraints); 827 } 828 public void positionStatesTest(int itemCount, int firstLayoutStartIndex, 829 int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges, 830 final PositionConstraint... constraints) throws Throwable { 831 setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter); 832 mLayoutManager.expectLayouts(2); 833 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 834 @Override 835 void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 836 RecyclerView.State state) { 837 super.beforePreLayout(recycler, lm, state); 838 //harmless 839 lm.detachAndScrapAttachedViews(recycler); 840 final int[] ids = new int[constraints.length]; 841 for (int i = 0; i < constraints.length; i++) { 842 ids[i] = constraints[i].mPreLayoutPos; 843 } 844 Map<Integer, CollectPositionResult> positions 845 = collectPositions(lm.mRecyclerView, recycler, state, ids); 846 for (PositionConstraint constraint : constraints) { 847 if (constraint.mPreLayoutPos != -1) { 848 constraint.validate(state, positions.get(constraint.mPreLayoutPos), 849 lm.getLog()); 850 } 851 } 852 } 853 854 @Override 855 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 856 RecyclerView.State state) { 857 super.beforePostLayout(recycler, lm, state); 858 lm.detachAndScrapAttachedViews(recycler); 859 final int[] ids = new int[constraints.length]; 860 for (int i = 0; i < constraints.length; i++) { 861 ids[i] = constraints[i].mPostLayoutPos; 862 } 863 Map<Integer, CollectPositionResult> positions 864 = collectPositions(lm.mRecyclerView, recycler, state, ids); 865 for (PositionConstraint constraint : constraints) { 866 if (constraint.mPostLayoutPos >= 0) { 867 constraint.validate(state, positions.get(constraint.mPostLayoutPos), 868 lm.getLog()); 869 } 870 } 871 } 872 }; 873 adapterChanges.run(mTestAdapter); 874 mLayoutManager.waitForLayout(2); 875 checkForMainThreadException(); 876 for (PositionConstraint constraint : constraints) { 877 constraint.assertValidate(); 878 } 879 } 880 881 class AnimationLayoutManager extends TestLayoutManager { 882 883 private int mTotalLayoutCount = 0; 884 private String log; 885 886 OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() { 887 }; 888 889 890 891 @Override 892 public boolean supportsPredictiveItemAnimations() { 893 return true; 894 } 895 896 public String getLog() { 897 return log; 898 } 899 900 private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) { 901 StringBuilder builder = new StringBuilder(); 902 builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done); 903 builder.append("\nViewHolders:\n"); 904 for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) { 905 builder.append(vh).append("\n"); 906 } 907 builder.append("scrap:\n"); 908 for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { 909 builder.append(vh).append("\n"); 910 } 911 912 if (state.isPreLayout() && !done) { 913 log = "\n" + builder.toString(); 914 } else { 915 log += "\n" + builder.toString(); 916 } 917 return log; 918 } 919 920 @Override 921 public void expectLayouts(int count) { 922 super.expectLayouts(count); 923 mOnLayoutCallbacks.mLayoutCount = 0; 924 } 925 926 public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) { 927 mOnLayoutCallbacks = onLayoutCallbacks; 928 } 929 930 @Override 931 public final void onLayoutChildren(RecyclerView.Recycler recycler, 932 RecyclerView.State state) { 933 try { 934 mTotalLayoutCount++; 935 prepareLog(recycler, state, false); 936 if (state.isPreLayout()) { 937 validateOldPositions(recycler, state); 938 } else { 939 validateClearedOldPositions(recycler, state); 940 } 941 mOnLayoutCallbacks.onLayoutChildren(recycler, this, state); 942 prepareLog(recycler, state, true); 943 } finally { 944 layoutLatch.countDown(); 945 } 946 } 947 948 private void validateClearedOldPositions(RecyclerView.Recycler recycler, 949 RecyclerView.State state) { 950 if (getTestRecyclerView() == null) { 951 return; 952 } 953 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 954 assertEquals("there should NOT be an old position in post layout", 955 RecyclerView.NO_POSITION, viewHolder.mOldPosition); 956 assertEquals("there should NOT be a pre layout position in post layout", 957 RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition); 958 } 959 } 960 961 private void validateOldPositions(RecyclerView.Recycler recycler, 962 RecyclerView.State state) { 963 if (getTestRecyclerView() == null) { 964 return; 965 } 966 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 967 if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) { 968 assertTrue("there should be an old position in pre-layout", 969 viewHolder.mOldPosition != RecyclerView.NO_POSITION); 970 } 971 } 972 } 973 974 public int getTotalLayoutCount() { 975 return mTotalLayoutCount; 976 } 977 978 @Override 979 public boolean canScrollVertically() { 980 return true; 981 } 982 983 @Override 984 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 985 RecyclerView.State state) { 986 mOnLayoutCallbacks.onScroll(dy, recycler, state); 987 return super.scrollVerticallyBy(dy, recycler, state); 988 } 989 990 public void onPostDispatchLayout() { 991 mOnLayoutCallbacks.postDispatchLayout(); 992 } 993 994 @Override 995 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 996 super.waitForLayout(timeout, timeUnit); 997 checkForMainThreadException(); 998 } 999 } 1000 1001 abstract class OnLayoutCallbacks { 1002 1003 int mLayoutMin = Integer.MIN_VALUE; 1004 1005 int mLayoutItemCount = Integer.MAX_VALUE; 1006 1007 int expectedPreLayoutItemCount = -1; 1008 1009 int expectedPostLayoutItemCount = -1; 1010 1011 int mDeletedViewCount; 1012 1013 int mLayoutCount = 0; 1014 1015 void setExpectedItemCounts(int preLayout, int postLayout) { 1016 expectedPreLayoutItemCount = preLayout; 1017 expectedPostLayoutItemCount = postLayout; 1018 } 1019 1020 void reset() { 1021 mLayoutMin = Integer.MIN_VALUE; 1022 mLayoutItemCount = Integer.MAX_VALUE; 1023 expectedPreLayoutItemCount = -1; 1024 expectedPostLayoutItemCount = -1; 1025 mLayoutCount = 0; 1026 } 1027 1028 void beforePreLayout(RecyclerView.Recycler recycler, 1029 AnimationLayoutManager lm, RecyclerView.State state) { 1030 mDeletedViewCount = 0; 1031 for (int i = 0; i < lm.getChildCount(); i++) { 1032 View v = lm.getChildAt(i); 1033 if (lm.getLp(v).isItemRemoved()) { 1034 mDeletedViewCount++; 1035 } 1036 } 1037 } 1038 1039 void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 1040 RecyclerView.State state) { 1041 if (DEBUG) { 1042 Log.d(TAG, "item count " + state.getItemCount()); 1043 } 1044 lm.detachAndScrapAttachedViews(recycler); 1045 final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin; 1046 final int count = mLayoutItemCount 1047 == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount; 1048 lm.layoutRange(recycler, start, start + count); 1049 assertEquals("correct # of children should be laid out", 1050 count, lm.getChildCount()); 1051 lm.assertVisibleItemPositions(); 1052 } 1053 1054 private void assertNoPreLayoutPosition(RecyclerView.Recycler recycler) { 1055 for (RecyclerView.ViewHolder vh : recycler.mAttachedScrap) { 1056 assertPreLayoutPosition(vh); 1057 } 1058 } 1059 1060 private void assertNoPreLayoutPosition(RecyclerView.LayoutManager lm) { 1061 for (int i = 0; i < lm.getChildCount(); i ++) { 1062 final RecyclerView.ViewHolder vh = mRecyclerView 1063 .getChildViewHolder(lm.getChildAt(i)); 1064 assertPreLayoutPosition(vh); 1065 } 1066 } 1067 1068 private void assertPreLayoutPosition(RecyclerView.ViewHolder vh) { 1069 assertEquals("in post layout, there should not be a view holder w/ a pre " 1070 + "layout position", RecyclerView.NO_POSITION, vh.mPreLayoutPosition); 1071 assertEquals("in post layout, there should not be a view holder w/ an old " 1072 + "layout position", RecyclerView.NO_POSITION, vh.mOldPosition); 1073 } 1074 1075 void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 1076 RecyclerView.State state) { 1077 1078 if (state.isPreLayout()) { 1079 if (expectedPreLayoutItemCount != -1) { 1080 assertEquals("on pre layout, state should return abstracted adapter size", 1081 expectedPreLayoutItemCount, state.getItemCount()); 1082 } 1083 beforePreLayout(recycler, lm, state); 1084 } else { 1085 if (expectedPostLayoutItemCount != -1) { 1086 assertEquals("on post layout, state should return real adapter size", 1087 expectedPostLayoutItemCount, state.getItemCount()); 1088 } 1089 beforePostLayout(recycler, lm, state); 1090 } 1091 if (!state.isPreLayout()) { 1092 assertNoPreLayoutPosition(recycler); 1093 } 1094 doLayout(recycler, lm, state); 1095 if (state.isPreLayout()) { 1096 afterPreLayout(recycler, lm, state); 1097 } else { 1098 afterPostLayout(recycler, lm, state); 1099 assertNoPreLayoutPosition(lm); 1100 } 1101 mLayoutCount++; 1102 } 1103 1104 void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 1105 RecyclerView.State state) { 1106 } 1107 1108 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 1109 RecyclerView.State state) { 1110 } 1111 1112 void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 1113 RecyclerView.State state) { 1114 } 1115 1116 void postDispatchLayout() { 1117 } 1118 1119 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 1120 1121 } 1122 } 1123 1124 class TestRecyclerView extends RecyclerView { 1125 1126 CountDownLatch drawLatch; 1127 1128 public TestRecyclerView(Context context) { 1129 super(context); 1130 } 1131 1132 public TestRecyclerView(Context context, AttributeSet attrs) { 1133 super(context, attrs); 1134 } 1135 1136 public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) { 1137 super(context, attrs, defStyle); 1138 } 1139 1140 @Override 1141 void initAdapterManager() { 1142 super.initAdapterManager(); 1143 mAdapterHelper.mOnItemProcessedCallback = new Runnable() { 1144 @Override 1145 public void run() { 1146 validatePostUpdateOp(); 1147 } 1148 }; 1149 } 1150 1151 public void expectDraw(int count) { 1152 drawLatch = new CountDownLatch(count); 1153 } 1154 1155 public void waitForDraw(long timeout) throws Throwable { 1156 drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS); 1157 assertEquals("all expected draws should happen at the expected time frame", 1158 0, drawLatch.getCount()); 1159 } 1160 1161 List<ViewHolder> collectViewHolders() { 1162 List<ViewHolder> holders = new ArrayList<ViewHolder>(); 1163 final int childCount = getChildCount(); 1164 for (int i = 0; i < childCount; i++) { 1165 ViewHolder holder = getChildViewHolderInt(getChildAt(i)); 1166 if (holder != null) { 1167 holders.add(holder); 1168 } 1169 } 1170 return holders; 1171 } 1172 1173 1174 private void validateViewHolderPositions() { 1175 final Set<Integer> existingOffsets = new HashSet<Integer>(); 1176 int childCount = getChildCount(); 1177 StringBuilder log = new StringBuilder(); 1178 for (int i = 0; i < childCount; i++) { 1179 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 1180 TestViewHolder tvh = (TestViewHolder) vh; 1181 log.append(tvh.mBoundItem).append(vh) 1182 .append(" hidden:") 1183 .append(mChildHelper.mHiddenViews.contains(vh.itemView)) 1184 .append("\n"); 1185 } 1186 for (int i = 0; i < childCount; i++) { 1187 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 1188 if (vh.isInvalid()) { 1189 continue; 1190 } 1191 if (vh.getLayoutPosition() < 0) { 1192 LayoutManager lm = getLayoutManager(); 1193 for (int j = 0; j < lm.getChildCount(); j ++) { 1194 assertNotSame("removed view holder should not be in LM's child list", 1195 vh.itemView, lm.getChildAt(j)); 1196 } 1197 } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) { 1198 if (!existingOffsets.add(vh.getLayoutPosition())) { 1199 throw new IllegalStateException("view holder position conflict for " 1200 + "existing views " + vh + "\n" + log); 1201 } 1202 } 1203 } 1204 } 1205 1206 void validatePostUpdateOp() { 1207 try { 1208 validateViewHolderPositions(); 1209 if (super.mState.isPreLayout()) { 1210 validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager()); 1211 } 1212 validateAdapterPosition((AnimationLayoutManager) getLayoutManager()); 1213 } catch (Throwable t) { 1214 postExceptionToInstrumentation(t); 1215 } 1216 } 1217 1218 1219 1220 private void validateAdapterPosition(AnimationLayoutManager lm) { 1221 for (ViewHolder vh : collectViewHolders()) { 1222 if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) { 1223 assertEquals("adapter position calculations should match view holder " 1224 + "pre layout:" + mState.isPreLayout() 1225 + " positions\n" + vh + "\n" + lm.getLog(), 1226 mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition); 1227 } 1228 } 1229 } 1230 1231 // ensures pre layout positions are continuous block. This is not necessarily a case 1232 // but valid in test RV 1233 private void validatePreLayoutSequence(AnimationLayoutManager lm) { 1234 Set<Integer> preLayoutPositions = new HashSet<Integer>(); 1235 for (ViewHolder vh : collectViewHolders()) { 1236 assertTrue("pre layout positions should be distinct " + lm.getLog(), 1237 preLayoutPositions.add(vh.mPreLayoutPosition)); 1238 } 1239 int minPos = Integer.MAX_VALUE; 1240 for (Integer pos : preLayoutPositions) { 1241 if (pos < minPos) { 1242 minPos = pos; 1243 } 1244 } 1245 for (int i = 1; i < preLayoutPositions.size(); i++) { 1246 assertNotNull("next position should exist " + lm.getLog(), 1247 preLayoutPositions.contains(minPos + i)); 1248 } 1249 } 1250 1251 @Override 1252 protected void dispatchDraw(Canvas canvas) { 1253 super.dispatchDraw(canvas); 1254 if (drawLatch != null) { 1255 drawLatch.countDown(); 1256 } 1257 } 1258 1259 @Override 1260 void dispatchLayout() { 1261 try { 1262 super.dispatchLayout(); 1263 if (getLayoutManager() instanceof AnimationLayoutManager) { 1264 ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout(); 1265 } 1266 } catch (Throwable t) { 1267 postExceptionToInstrumentation(t); 1268 } 1269 1270 } 1271 1272 1273 } 1274 1275 abstract class AdapterOps { 1276 1277 final public void run(TestAdapter adapter) throws Throwable { 1278 onRun(adapter); 1279 } 1280 1281 abstract void onRun(TestAdapter testAdapter) throws Throwable; 1282 } 1283 1284 static class CollectPositionResult { 1285 1286 // true if found in scrap 1287 public RecyclerView.ViewHolder scrapResult; 1288 1289 public RecyclerView.ViewHolder adapterResult; 1290 1291 static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) { 1292 CollectPositionResult cpr = new CollectPositionResult(); 1293 cpr.scrapResult = viewHolder; 1294 return cpr; 1295 } 1296 1297 static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) { 1298 CollectPositionResult cpr = new CollectPositionResult(); 1299 cpr.adapterResult = viewHolder; 1300 return cpr; 1301 } 1302 } 1303 1304 static class PositionConstraint { 1305 1306 public static enum Type { 1307 scrap, 1308 adapter, 1309 adapterScrap /*first pass adapter, second pass scrap*/ 1310 } 1311 1312 Type mType; 1313 1314 int mOldPos; // if VH 1315 1316 int mPreLayoutPos; 1317 1318 int mPostLayoutPos; 1319 1320 int mValidateCount = 0; 1321 1322 public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) { 1323 PositionConstraint constraint = new PositionConstraint(); 1324 constraint.mType = Type.scrap; 1325 constraint.mOldPos = oldPos; 1326 constraint.mPreLayoutPos = preLayoutPos; 1327 constraint.mPostLayoutPos = postLayoutPos; 1328 return constraint; 1329 } 1330 1331 public static PositionConstraint adapterScrap(int preLayoutPos, int position) { 1332 PositionConstraint constraint = new PositionConstraint(); 1333 constraint.mType = Type.adapterScrap; 1334 constraint.mOldPos = RecyclerView.NO_POSITION; 1335 constraint.mPreLayoutPos = preLayoutPos; 1336 constraint.mPostLayoutPos = position;// adapter pos does not change 1337 return constraint; 1338 } 1339 1340 public static PositionConstraint adapter(int position) { 1341 PositionConstraint constraint = new PositionConstraint(); 1342 constraint.mType = Type.adapter; 1343 constraint.mPreLayoutPos = RecyclerView.NO_POSITION; 1344 constraint.mOldPos = RecyclerView.NO_POSITION; 1345 constraint.mPostLayoutPos = position;// adapter pos does not change 1346 return constraint; 1347 } 1348 1349 public void assertValidate() { 1350 int expectedValidate = 0; 1351 if (mPreLayoutPos >= 0) { 1352 expectedValidate ++; 1353 } 1354 if (mPostLayoutPos >= 0) { 1355 expectedValidate ++; 1356 } 1357 assertEquals("should run all validates", expectedValidate, mValidateCount); 1358 } 1359 1360 @Override 1361 public String toString() { 1362 return "Cons{" + 1363 "t=" + mType.name() + 1364 ", old=" + mOldPos + 1365 ", pre=" + mPreLayoutPos + 1366 ", post=" + mPostLayoutPos + 1367 '}'; 1368 } 1369 1370 public void validate(RecyclerView.State state, CollectPositionResult result, String log) { 1371 mValidateCount ++; 1372 assertNotNull(this + ": result should not be null\n" + log, result); 1373 RecyclerView.ViewHolder viewHolder; 1374 if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) { 1375 assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult); 1376 viewHolder = result.scrapResult; 1377 } else { 1378 assertNotNull(this + ": result should come from adapter\n" + log, 1379 result.adapterResult); 1380 assertEquals(this + ": old position should be none when it came from adapter\n" + log, 1381 RecyclerView.NO_POSITION, result.adapterResult.getOldPosition()); 1382 viewHolder = result.adapterResult; 1383 } 1384 if (state.isPreLayout()) { 1385 assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos, 1386 viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition : 1387 viewHolder.mPreLayoutPosition); 1388 assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos, 1389 viewHolder.getLayoutPosition()); 1390 if (mType == Type.scrap) { 1391 assertEquals(this + ": old position should match\n" + log, mOldPos, 1392 result.scrapResult.getOldPosition()); 1393 } 1394 } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult 1395 .isRemoved()) { 1396 assertEquals(this + ": post-layout position should match\n" + log + "\n\n" 1397 + viewHolder, mPostLayoutPos, viewHolder.getLayoutPosition()); 1398 } 1399 } 1400 } 1401} 1402