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