RecyclerViewAnimationsTest.java revision e0c347f627f8a78d3e5e3e5eaac9c3ae26208689
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; 24 25import java.util.ArrayList; 26import java.util.HashMap; 27import java.util.HashSet; 28import java.util.List; 29import java.util.Map; 30import java.util.Set; 31import java.util.concurrent.CountDownLatch; 32import java.util.concurrent.TimeUnit; 33import java.util.concurrent.atomic.AtomicInteger; 34 35public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest { 36 37 private static final boolean DEBUG = false; 38 39 private static final String TAG = "RecyclerViewAnimationsTest"; 40 41 AnimationLayoutManager mLayoutManager; 42 43 TestAdapter mTestAdapter; 44 45 public RecyclerViewAnimationsTest() { 46 super(DEBUG); 47 } 48 49 @Override 50 protected void setUp() throws Exception { 51 super.setUp(); 52 } 53 54 RecyclerView setupBasic(int itemCount) throws Throwable { 55 return setupBasic(itemCount, 0, itemCount); 56 } 57 58 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount) 59 throws Throwable { 60 return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null); 61 } 62 63 RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount, 64 TestAdapter testAdapter) 65 throws Throwable { 66 final TestRecyclerView recyclerView = new TestRecyclerView(getActivity()); 67 recyclerView.setHasFixedSize(true); 68 if (testAdapter == null) { 69 mTestAdapter = new TestAdapter(itemCount); 70 } else { 71 mTestAdapter = testAdapter; 72 } 73 recyclerView.setAdapter(mTestAdapter); 74 mLayoutManager = new AnimationLayoutManager(); 75 recyclerView.setLayoutManager(mLayoutManager); 76 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = firstLayoutStartIndex; 77 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount; 78 79 mLayoutManager.expectLayouts(1); 80 recyclerView.expectDraw(1); 81 setRecyclerView(recyclerView); 82 mLayoutManager.waitForLayout(2); 83 recyclerView.waitForDraw(1); 84 mLayoutManager.mOnLayoutCallbacks.reset(); 85 getInstrumentation().waitForIdleSync(); 86 assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount()); 87 assertEquals("all expected children should be laid out", firstLayoutItemCount, 88 mLayoutManager.getChildCount()); 89 return recyclerView; 90 } 91 92 public void testNotifyDataSetChanged() throws Throwable { 93 setupBasic(10, 3, 4); 94 int layoutCount = mLayoutManager.mTotalLayoutCount; 95 mLayoutManager.expectLayouts(1); 96 runTestOnUiThread(new Runnable() { 97 @Override 98 public void run() { 99 try { 100 mTestAdapter.deleteAndNotify(4, 1); 101 mTestAdapter.notifyChange(); 102 } catch (Throwable throwable) { 103 throwable.printStackTrace(); 104 } 105 106 } 107 }); 108 mLayoutManager.waitForLayout(2); 109 getInstrumentation().waitForIdleSync(); 110 assertEquals("on notify data set changed, predictive animations should not run", 111 layoutCount + 1, mLayoutManager.mTotalLayoutCount); 112 mLayoutManager.expectLayouts(2); 113 mTestAdapter.addAndNotify(4, 2); 114 // make sure animations recover 115 mLayoutManager.waitForLayout(2); 116 } 117 118 public void testStableIdNotifyDataSetChanged() throws Throwable { 119 final int itemCount = 20; 120 List<Item> initialSet = new ArrayList<Item>(); 121 final TestAdapter adapter = new TestAdapter(itemCount) { 122 @Override 123 public long getItemId(int position) { 124 return mItems.get(position).mId; 125 } 126 }; 127 adapter.setHasStableIds(true); 128 initialSet.addAll(adapter.mItems); 129 positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() { 130 @Override 131 void onRun(TestAdapter testAdapter) throws Throwable { 132 Item item5 = adapter.mItems.get(5); 133 Item item6 = adapter.mItems.get(6); 134 item5.mAdapterIndex = 6; 135 item6.mAdapterIndex = 5; 136 adapter.mItems.remove(5); 137 adapter.mItems.add(6, item5); 138 adapter.notifyChange(); 139 //hacky, we support only 1 layout pass 140 mLayoutManager.layoutLatch.countDown(); 141 } 142 }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6), 143 PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8), 144 PositionConstraint.scrap(9, -1, 9)); 145 // now mix items. 146 } 147 148 149 public void testGetItemForDeletedView() throws Throwable { 150 getItemForDeletedViewTest(false); 151 getItemForDeletedViewTest(true); 152 } 153 154 public void getItemForDeletedViewTest(boolean stableIds) throws Throwable { 155 final Set<Integer> itemViewTypeQueries = new HashSet<Integer>(); 156 final Set<Integer> itemIdQueries = new HashSet<Integer>(); 157 TestAdapter adapter = new TestAdapter(10) { 158 @Override 159 public int getItemViewType(int position) { 160 itemViewTypeQueries.add(position); 161 return super.getItemViewType(position); 162 } 163 164 @Override 165 public long getItemId(int position) { 166 itemIdQueries.add(position); 167 return mItems.get(position).mId; 168 } 169 }; 170 adapter.setHasStableIds(stableIds); 171 setupBasic(10, 0, 10, adapter); 172 assertEquals("getItemViewType for all items should be called", 10, 173 itemViewTypeQueries.size()); 174 if (adapter.hasStableIds()) { 175 assertEquals("getItemId should be called when adapter has stable ids", 10, 176 itemIdQueries.size()); 177 } else { 178 assertEquals("getItemId should not be called when adapter does not have stable ids", 0, 179 itemIdQueries.size()); 180 } 181 itemViewTypeQueries.clear(); 182 itemIdQueries.clear(); 183 mLayoutManager.expectLayouts(2); 184 // delete last two 185 final int deleteStart = 8; 186 final int deleteCount = adapter.getItemCount() - deleteStart; 187 adapter.deleteAndNotify(deleteStart, deleteCount); 188 mLayoutManager.waitForLayout(2); 189 for (int i = 0; i < deleteStart; i++) { 190 assertTrue("getItemViewType for existing item " + i + " should be called", 191 itemViewTypeQueries.contains(i)); 192 if (adapter.hasStableIds()) { 193 assertTrue("getItemId for existing item " + i 194 + " should be called when adapter has stable ids", 195 itemIdQueries.contains(i)); 196 } 197 } 198 for (int i = deleteStart; i < deleteStart + deleteCount; i++) { 199 assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called", 200 itemViewTypeQueries.contains(i)); 201 if (adapter.hasStableIds()) { 202 assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called", 203 itemIdQueries.contains(i)); 204 } 205 } 206 } 207 208 public void testDeleteInvisibleMultiStep() throws Throwable { 209 setupBasic(1000, 1, 7); 210 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 211 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 212 mLayoutManager.expectLayouts(1); 213 // try to trigger race conditions 214 int targetItemCount = mTestAdapter.getItemCount(); 215 for (int i = 0; i < 100; i++) { 216 mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1}); 217 targetItemCount -= 2; 218 } 219 // wait until main thread runnables are consumed 220 while (targetItemCount != mTestAdapter.getItemCount()) { 221 Thread.sleep(100); 222 } 223 mLayoutManager.waitForLayout(2); 224 } 225 226 public void testAddManyMultiStep() throws Throwable { 227 setupBasic(10, 1, 7); 228 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 229 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 230 mLayoutManager.expectLayouts(1); 231 // try to trigger race conditions 232 int targetItemCount = mTestAdapter.getItemCount(); 233 for (int i = 0; i < 100; i++) { 234 mTestAdapter.addAndNotify(0, 1); 235 mTestAdapter.addAndNotify(7, 1); 236 targetItemCount += 2; 237 } 238 // wait until main thread runnables are consumed 239 while (targetItemCount != mTestAdapter.getItemCount()) { 240 Thread.sleep(100); 241 } 242 mLayoutManager.waitForLayout(2); 243 } 244 245 public void testBasicDelete() throws Throwable { 246 setupBasic(10); 247 final OnLayoutCallbacks callbacks = new OnLayoutCallbacks() { 248 @Override 249 public void postDispatchLayout() { 250 // verify this only in first layout 251 assertEquals("deleted views should still be children of RV", 252 mLayoutManager.getChildCount() + mDeletedViewCount 253 , mRecyclerView.getChildCount()); 254 } 255 256 @Override 257 void afterPreLayout(RecyclerView.Recycler recycler, 258 AnimationLayoutManager layoutManager, 259 RecyclerView.State state) { 260 super.afterPreLayout(recycler, layoutManager, state); 261 mLayoutItemCount = 3; 262 mLayoutMin = 0; 263 } 264 }; 265 callbacks.mLayoutItemCount = 10; 266 callbacks.setExpectedItemCounts(10, 3); 267 mLayoutManager.setOnLayoutCallbacks(callbacks); 268 269 mLayoutManager.expectLayouts(2); 270 mTestAdapter.deleteAndNotify(0, 7); 271 mLayoutManager.waitForLayout(2); 272 callbacks.reset();// when animations end another layout will happen 273 } 274 275 276 public void testAdapterChangeDuringScrolling() throws Throwable { 277 setupBasic(10); 278 final AtomicInteger onLayoutItemCount = new AtomicInteger(0); 279 final AtomicInteger onScrollItemCount = new AtomicInteger(0); 280 281 mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() { 282 @Override 283 void onLayoutChildren(RecyclerView.Recycler recycler, 284 AnimationLayoutManager lm, RecyclerView.State state) { 285 onLayoutItemCount.set(state.getItemCount()); 286 super.onLayoutChildren(recycler, lm, state); 287 } 288 289 @Override 290 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 291 onScrollItemCount.set(state.getItemCount()); 292 super.onScroll(dx, recycler, state); 293 } 294 }); 295 runTestOnUiThread(new Runnable() { 296 @Override 297 public void run() { 298 mTestAdapter.mItems.remove(5); 299 mTestAdapter.notifyItemRangeRemoved(5, 1); 300 mRecyclerView.scrollBy(0, 100); 301 assertTrue("scrolling while there are pending adapter updates should " 302 + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0); 303 assertEquals("scroll by should be called w/ updated adapter count", 304 mTestAdapter.mItems.size(), onScrollItemCount.get()); 305 306 } 307 }); 308 } 309 310 public void testAddInvisibleAndVisible() throws Throwable { 311 setupBasic(10, 1, 7); 312 mLayoutManager.expectLayouts(2); 313 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 314 mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible 315 mLayoutManager.waitForLayout(2); 316 } 317 318 public void testAddInvisible() throws Throwable { 319 setupBasic(10, 1, 7); 320 mLayoutManager.expectLayouts(1); 321 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12); 322 mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0 323 mLayoutManager.waitForLayout(2); 324 } 325 326 public void testBasicAdd() throws Throwable { 327 setupBasic(10); 328 mLayoutManager.expectLayouts(2); 329 setExpectedItemCounts(10, 13); 330 mTestAdapter.addAndNotify(2, 3); 331 mLayoutManager.waitForLayout(2); 332 } 333 334 public TestRecyclerView getTestRecyclerView() { 335 return (TestRecyclerView) mRecyclerView; 336 } 337 338 public void testRemoveScrapInvalidate() throws Throwable { 339 setupBasic(10); 340 TestRecyclerView testRecyclerView = getTestRecyclerView(); 341 mLayoutManager.expectLayouts(1); 342 testRecyclerView.expectDraw(1); 343 runTestOnUiThread(new Runnable() { 344 @Override 345 public void run() { 346 mTestAdapter.mItems.clear(); 347 mTestAdapter.notifyDataSetChanged(); 348 } 349 }); 350 mLayoutManager.waitForLayout(2); 351 testRecyclerView.waitForDraw(2); 352 } 353 354 public void testDeleteVisibleAndInvisible() throws Throwable { 355 setupBasic(11, 3, 5); //layout items 3 4 5 6 7 356 mLayoutManager.expectLayouts(2); 357 setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list 358 setExpectedItemCounts(9, 8); 359 mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9 360 mLayoutManager.waitForLayout(2); 361 } 362 363 public void testFindPositionOffset() throws Throwable { 364 setupBasic(10); 365 runTestOnUiThread(new Runnable() { 366 @Override 367 public void run() { 368 // [0,1,2,3,4] 369 // delete 1 370 mTestAdapter.notifyItemRangeRemoved(1, 1); 371 // delete 3 372 mTestAdapter.notifyItemRangeRemoved(2, 1); 373 mAdapterHelper.preProcess(); 374 // [0,2,4] 375 assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0)); 376 assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2)); 377 assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4)); 378 379 } 380 }); 381 } 382 383 private void setLayoutRange(int start, int count) { 384 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start; 385 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count; 386 } 387 388 private void setExpectedItemCounts(int preLayout, int postLayout) { 389 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout); 390 } 391 392 public void testDeleteInvisible() throws Throwable { 393 setupBasic(10, 1, 7); 394 mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1; 395 mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7; 396 mLayoutManager.expectLayouts(1); 397 mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8); 398 mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8 399 mLayoutManager.waitForLayout(2); 400 } 401 402 private CollectPositionResult findByPos(RecyclerView recyclerView, 403 RecyclerView.Recycler recycler, RecyclerView.State state, int position) { 404 View view = recycler.getViewForPosition(position, true); 405 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 406 if (vh.wasReturnedFromScrap()) { 407 vh.clearReturnedFromScrapFlag(); //keep data consistent. 408 return CollectPositionResult.fromScrap(vh); 409 } else { 410 return CollectPositionResult.fromAdapter(vh); 411 } 412 } 413 414 public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView, 415 RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) { 416 Map<Integer, CollectPositionResult> positionToAdapterMapping 417 = new HashMap<Integer, CollectPositionResult>(); 418 for (int position : positions) { 419 if (position < 0) { 420 continue; 421 } 422 positionToAdapterMapping.put(position, 423 findByPos(recyclerView, recycler, state, position)); 424 } 425 return positionToAdapterMapping; 426 } 427 428 public void testAddDelete2() throws Throwable { 429 positionStatesTest(5, 0, 5, new AdapterOps() { 430 // 0 1 2 3 4 431 // 0 1 2 a b 3 4 432 // 0 1 b 3 4 433 // pre: 0 1 2 3 4 434 // pre w/ adap: 0 1 2 b 3 4 435 @Override 436 void onRun(TestAdapter adapter) throws Throwable { 437 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2}); 438 } 439 }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1), 440 PositionConstraint.scrap(3, 3, 3) 441 ); 442 } 443 444 public void testAddDelete1() throws Throwable { 445 positionStatesTest(5, 0, 5, new AdapterOps() { 446 // 0 1 2 3 4 447 // 0 1 2 a b 3 4 448 // 0 2 a b 3 4 449 // 0 c d 2 a b 3 4 450 // 0 c d 2 a 4 451 // c d 2 a 4 452 // pre: 0 1 2 3 4 453 @Override 454 void onRun(TestAdapter adapter) throws Throwable { 455 adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1}, 456 new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1}); 457 } 458 }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1), 459 PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1), 460 PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0), 461 PositionConstraint.adapter(1), PositionConstraint.adapter(3) 462 ); 463 } 464 465 public void testAddSameIndexTwice() throws Throwable { 466 positionStatesTest(12, 2, 7, new AdapterOps() { 467 @Override 468 void onRun(TestAdapter adapter) throws Throwable { 469 adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1}, 470 new int[]{11, 1}); 471 } 472 }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3), 473 PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7), 474 PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12), 475 PositionConstraint.scrap(8, 8, 13) 476 ); 477 } 478 479 public void testDeleteTwice() throws Throwable { 480 positionStatesTest(12, 2, 7, new AdapterOps() { 481 @Override 482 void onRun(TestAdapter adapter) throws Throwable { 483 adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1}, 484 new int[]{0, 1});// delete item ids 0,2,9,1 485 } 486 }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0), 487 PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2), 488 PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5), 489 PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7) 490 ); 491 } 492 493 494 public void positionStatesTest(int itemCount, int firstLayoutStartIndex, 495 int firstLayoutItemCount, AdapterOps adapterChanges, 496 final PositionConstraint... constraints) throws Throwable { 497 positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null, 498 adapterChanges, constraints); 499 } 500 public void positionStatesTest(int itemCount, int firstLayoutStartIndex, 501 int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges, 502 final PositionConstraint... constraints) throws Throwable { 503 setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter); 504 mLayoutManager.expectLayouts(2); 505 mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() { 506 @Override 507 void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 508 RecyclerView.State state) { 509 super.beforePreLayout(recycler, lm, state); 510 //harmless 511 lm.detachAndScrapAttachedViews(recycler); 512 final int[] ids = new int[constraints.length]; 513 for (int i = 0; i < constraints.length; i++) { 514 ids[i] = constraints[i].mPreLayoutPos; 515 } 516 Map<Integer, CollectPositionResult> positions 517 = collectPositions(lm.mRecyclerView, recycler, state, ids); 518 for (PositionConstraint constraint : constraints) { 519 if (constraint.mPreLayoutPos != -1) { 520 constraint.validate(state, positions.get(constraint.mPreLayoutPos), 521 lm.getLog()); 522 } 523 } 524 } 525 526 @Override 527 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 528 RecyclerView.State state) { 529 super.beforePostLayout(recycler, lm, state); 530 lm.detachAndScrapAttachedViews(recycler); 531 final int[] ids = new int[constraints.length]; 532 for (int i = 0; i < constraints.length; i++) { 533 ids[i] = constraints[i].mPostLayoutPos; 534 } 535 Map<Integer, CollectPositionResult> positions 536 = collectPositions(lm.mRecyclerView, recycler, state, ids); 537 for (PositionConstraint constraint : constraints) { 538 if (constraint.mPostLayoutPos >= 0) { 539 constraint.validate(state, positions.get(constraint.mPostLayoutPos), 540 lm.getLog()); 541 } 542 } 543 } 544 }; 545 adapterChanges.run(mTestAdapter); 546 mLayoutManager.waitForLayout(2); 547 checkForMainThreadException(); 548 for (PositionConstraint constraint : constraints) { 549 constraint.assertValidate(); 550 } 551 } 552 553 class AnimationLayoutManager extends TestLayoutManager { 554 555 private int mTotalLayoutCount = 0; 556 private String log; 557 558 OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() { 559 }; 560 561 562 563 @Override 564 public boolean supportsPredictiveItemAnimations() { 565 return true; 566 } 567 568 public String getLog() { 569 return log; 570 } 571 572 private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) { 573 StringBuilder builder = new StringBuilder(); 574 builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done); 575 builder.append("\nViewHolders:\n"); 576 for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) { 577 builder.append(vh).append("\n"); 578 } 579 builder.append("scrap:\n"); 580 for (RecyclerView.ViewHolder vh : recycler.getScrapList()) { 581 builder.append(vh).append("\n"); 582 } 583 584 if (state.isPreLayout() && !done) { 585 log = "\n" + builder.toString(); 586 } else { 587 log += "\n" + builder.toString(); 588 } 589 return log; 590 } 591 592 @Override 593 public void expectLayouts(int count) { 594 super.expectLayouts(count); 595 mOnLayoutCallbacks.mLayoutCount = 0; 596 } 597 598 public void setOnLayoutCallbacks(OnLayoutCallbacks onLayoutCallbacks) { 599 mOnLayoutCallbacks = onLayoutCallbacks; 600 } 601 602 @Override 603 public final void onLayoutChildren(RecyclerView.Recycler recycler, 604 RecyclerView.State state) { 605 try { 606 mTotalLayoutCount++; 607 prepareLog(recycler, state, false); 608 if (state.isPreLayout()) { 609 validateOldPositions(recycler, state); 610 } else { 611 validateClearedOldPositions(recycler, state); 612 } 613 mOnLayoutCallbacks.onLayoutChildren(recycler, this, state); 614 prepareLog(recycler, state, true); 615 } finally { 616 layoutLatch.countDown(); 617 } 618 } 619 620 private void validateClearedOldPositions(RecyclerView.Recycler recycler, 621 RecyclerView.State state) { 622 if (getTestRecyclerView() == null) { 623 return; 624 } 625 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 626 assertEquals("there should NOT be an old position in post layout", 627 RecyclerView.NO_POSITION, viewHolder.mOldPosition); 628 assertEquals("there should NOT be a pre layout position in post layout", 629 RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition); 630 } 631 } 632 633 private void validateOldPositions(RecyclerView.Recycler recycler, 634 RecyclerView.State state) { 635 if (getTestRecyclerView() == null) { 636 return; 637 } 638 for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) { 639 if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) { 640 assertTrue("there should be an old position in pre-layout", 641 viewHolder.mOldPosition != RecyclerView.NO_POSITION); 642 } 643 } 644 } 645 646 public int getTotalLayoutCount() { 647 return mTotalLayoutCount; 648 } 649 650 @Override 651 public boolean canScrollVertically() { 652 return true; 653 } 654 655 @Override 656 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 657 RecyclerView.State state) { 658 mOnLayoutCallbacks.onScroll(dy, recycler, state); 659 return super.scrollVerticallyBy(dy, recycler, state); 660 } 661 662 public void onPostDispatchLayout() { 663 mOnLayoutCallbacks.postDispatchLayout(); 664 } 665 666 @Override 667 public void waitForLayout(long timeout, TimeUnit timeUnit) throws Throwable { 668 super.waitForLayout(timeout, timeUnit); 669 checkForMainThreadException(); 670 } 671 } 672 673 abstract class OnLayoutCallbacks { 674 675 int mLayoutMin = Integer.MIN_VALUE; 676 677 int mLayoutItemCount = Integer.MAX_VALUE; 678 679 int expectedPreLayoutItemCount = -1; 680 681 int expectedPostLayoutItemCount = -1; 682 683 int mDeletedViewCount; 684 685 int mLayoutCount = 0; 686 687 void setExpectedItemCounts(int preLayout, int postLayout) { 688 expectedPreLayoutItemCount = preLayout; 689 expectedPostLayoutItemCount = postLayout; 690 } 691 692 void reset() { 693 mLayoutMin = Integer.MIN_VALUE; 694 mLayoutItemCount = Integer.MAX_VALUE; 695 expectedPreLayoutItemCount = -1; 696 expectedPostLayoutItemCount = -1; 697 mLayoutCount = 0; 698 } 699 700 void beforePreLayout(RecyclerView.Recycler recycler, 701 AnimationLayoutManager lm, RecyclerView.State state) { 702 mDeletedViewCount = 0; 703 for (int i = 0; i < lm.getChildCount(); i++) { 704 View v = lm.getChildAt(i); 705 if (lm.getLp(v).isItemRemoved()) { 706 mDeletedViewCount++; 707 } 708 } 709 } 710 711 void doLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 712 RecyclerView.State state) { 713 if (DEBUG) { 714 Log.d(TAG, "item count " + state.getItemCount()); 715 } 716 lm.detachAndScrapAttachedViews(recycler); 717 final int start = mLayoutMin == Integer.MIN_VALUE ? 0 : mLayoutMin; 718 final int count = mLayoutItemCount 719 == Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount; 720 int skippedAdd = lm.layoutRange(recycler, start, start + count); 721 assertEquals("correct # of children should be laid out", 722 count - skippedAdd, lm.getChildCount()); 723 lm.assertVisibleItemPositions(); 724 } 725 726 void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm, 727 RecyclerView.State state) { 728 729 if (state.isPreLayout()) { 730 if (expectedPreLayoutItemCount != -1) { 731 assertEquals("on pre layout, state should return abstracted adapter size", 732 expectedPreLayoutItemCount, state.getItemCount()); 733 } 734 beforePreLayout(recycler, lm, state); 735 } else { 736 if (expectedPostLayoutItemCount != -1) { 737 assertEquals("on post layout, state should return real adapter size", 738 expectedPostLayoutItemCount, state.getItemCount()); 739 } 740 beforePostLayout(recycler, lm, state); 741 } 742 doLayout(recycler, lm, state); 743 if (state.isPreLayout()) { 744 afterPreLayout(recycler, lm, state); 745 } else { 746 afterPostLayout(recycler, lm, state); 747 } 748 mLayoutCount++; 749 } 750 751 void afterPreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 752 RecyclerView.State state) { 753 } 754 755 void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 756 RecyclerView.State state) { 757 } 758 759 void afterPostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager layoutManager, 760 RecyclerView.State state) { 761 } 762 763 void postDispatchLayout() { 764 } 765 766 public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { 767 768 } 769 } 770 771 class TestRecyclerView extends RecyclerView { 772 773 CountDownLatch drawLatch; 774 775 public TestRecyclerView(Context context) { 776 super(context); 777 } 778 779 public TestRecyclerView(Context context, AttributeSet attrs) { 780 super(context, attrs); 781 } 782 783 public TestRecyclerView(Context context, AttributeSet attrs, int defStyle) { 784 super(context, attrs, defStyle); 785 } 786 787 @Override 788 void initAdapterManager() { 789 super.initAdapterManager(); 790 mAdapterHelper.mOnItemProcessedCallback = new Runnable() { 791 @Override 792 public void run() { 793 validatePostUpdateOp(); 794 } 795 }; 796 } 797 798 public void expectDraw(int count) { 799 drawLatch = new CountDownLatch(count); 800 } 801 802 public void waitForDraw(long timeout) throws Throwable { 803 drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS); 804 assertEquals("all expected draws should happen at the expected time frame", 805 0, drawLatch.getCount()); 806 } 807 808 List<ViewHolder> collectViewHolders() { 809 List<ViewHolder> holders = new ArrayList<ViewHolder>(); 810 final int childCount = getChildCount(); 811 for (int i = 0; i < childCount; i++) { 812 ViewHolder holder = getChildViewHolderInt(getChildAt(i)); 813 if (holder != null) { 814 holders.add(holder); 815 } 816 } 817 return holders; 818 } 819 820 821 private void validateViewHolderPositions() { 822 final Set<Integer> existingOffsets = new HashSet<Integer>(); 823 int childCount = getChildCount(); 824 StringBuilder log = new StringBuilder(); 825 for (int i = 0; i < childCount; i++) { 826 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 827 TestViewHolder tvh = (TestViewHolder) vh; 828 log.append(tvh.mBindedItem).append(vh) 829 .append(" hidden:") 830 .append(mChildHelper.mHiddenViews.contains(vh.itemView)) 831 .append("\n"); 832 } 833 for (int i = 0; i < childCount; i++) { 834 ViewHolder vh = getChildViewHolderInt(getChildAt(i)); 835 if (vh.isInvalid()) { 836 continue; 837 } 838 if (vh.getPosition() < 0) { 839 LayoutManager lm = getLayoutManager(); 840 for (int j = 0; j < lm.getChildCount(); j ++) { 841 assertNotSame("removed view holder should not be in LM's child list", 842 vh.itemView, lm.getChildAt(j)); 843 } 844 } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) { 845 if (!existingOffsets.add(vh.getPosition())) { 846 throw new IllegalStateException("view holder position conflict for " 847 + "existing views " + vh + "\n" + log); 848 } 849 } 850 } 851 } 852 853 void validatePostUpdateOp() { 854 try { 855 validateViewHolderPositions(); 856 if (super.mState.isPreLayout()) { 857 validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager()); 858 } 859 validateAdapterPosition((AnimationLayoutManager) getLayoutManager()); 860 } catch (Throwable t) { 861 postExceptionToInstrumentation(t); 862 } 863 } 864 865 866 867 private void validateAdapterPosition(AnimationLayoutManager lm) { 868 for (ViewHolder vh : collectViewHolders()) { 869 if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) { 870 assertEquals("adapter position calculations should match view holder " 871 + "pre layout:" + mState.isPreLayout() 872 + " positions\n" + vh + "\n" + lm.getLog(), 873 mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition); 874 } 875 } 876 } 877 878 // ensures pre layout positions are continuous block. This is not necessarily a case 879 // but valid in test RV 880 private void validatePreLayoutSequence(AnimationLayoutManager lm) { 881 Set<Integer> preLayoutPositions = new HashSet<Integer>(); 882 for (ViewHolder vh : collectViewHolders()) { 883 assertTrue("pre layout positions should be distinct " + lm.getLog(), 884 preLayoutPositions.add(vh.mPreLayoutPosition)); 885 } 886 int minPos = Integer.MAX_VALUE; 887 for (Integer pos : preLayoutPositions) { 888 if (pos < minPos) { 889 minPos = pos; 890 } 891 } 892 for (int i = 1; i < preLayoutPositions.size(); i++) { 893 assertNotNull("next position should exist " + lm.getLog(), 894 preLayoutPositions.contains(minPos + i)); 895 } 896 } 897 898 @Override 899 protected void dispatchDraw(Canvas canvas) { 900 super.dispatchDraw(canvas); 901 if (drawLatch != null) { 902 drawLatch.countDown(); 903 } 904 } 905 906 @Override 907 void dispatchLayout() { 908 try { 909 super.dispatchLayout(); 910 if (getLayoutManager() instanceof AnimationLayoutManager) { 911 ((AnimationLayoutManager) getLayoutManager()).onPostDispatchLayout(); 912 } 913 } catch (Throwable t) { 914 postExceptionToInstrumentation(t); 915 } 916 917 } 918 919 920 } 921 922 abstract class AdapterOps { 923 924 final public void run(TestAdapter adapter) throws Throwable { 925 onRun(adapter); 926 } 927 928 abstract void onRun(TestAdapter testAdapter) throws Throwable; 929 } 930 931 static class CollectPositionResult { 932 933 // true if found in scrap 934 public RecyclerView.ViewHolder scrapResult; 935 936 public RecyclerView.ViewHolder adapterResult; 937 938 static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) { 939 CollectPositionResult cpr = new CollectPositionResult(); 940 cpr.scrapResult = viewHolder; 941 return cpr; 942 } 943 944 static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) { 945 CollectPositionResult cpr = new CollectPositionResult(); 946 cpr.adapterResult = viewHolder; 947 return cpr; 948 } 949 } 950 951 static class PositionConstraint { 952 953 public static enum Type { 954 scrap, 955 adapter, 956 adapterScrap /*first pass adapter, second pass scrap*/ 957 } 958 959 Type mType; 960 961 int mOldPos; // if VH 962 963 int mPreLayoutPos; 964 965 int mPostLayoutPos; 966 967 int mValidateCount = 0; 968 969 public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) { 970 PositionConstraint constraint = new PositionConstraint(); 971 constraint.mType = Type.scrap; 972 constraint.mOldPos = oldPos; 973 constraint.mPreLayoutPos = preLayoutPos; 974 constraint.mPostLayoutPos = postLayoutPos; 975 return constraint; 976 } 977 978 public static PositionConstraint adapterScrap(int preLayoutPos, int position) { 979 PositionConstraint constraint = new PositionConstraint(); 980 constraint.mType = Type.adapterScrap; 981 constraint.mOldPos = RecyclerView.NO_POSITION; 982 constraint.mPreLayoutPos = preLayoutPos; 983 constraint.mPostLayoutPos = position;// adapter pos does not change 984 return constraint; 985 } 986 987 public static PositionConstraint adapter(int position) { 988 PositionConstraint constraint = new PositionConstraint(); 989 constraint.mType = Type.adapter; 990 constraint.mPreLayoutPos = RecyclerView.NO_POSITION; 991 constraint.mOldPos = RecyclerView.NO_POSITION; 992 constraint.mPostLayoutPos = position;// adapter pos does not change 993 return constraint; 994 } 995 996 public void assertValidate() { 997 int expectedValidate = 0; 998 if (mPreLayoutPos >= 0) { 999 expectedValidate ++; 1000 } 1001 if (mPostLayoutPos >= 0) { 1002 expectedValidate ++; 1003 } 1004 assertEquals("should run all validates", expectedValidate, mValidateCount); 1005 } 1006 1007 @Override 1008 public String toString() { 1009 return "Cons{" + 1010 "t=" + mType.name() + 1011 ", old=" + mOldPos + 1012 ", pre=" + mPreLayoutPos + 1013 ", post=" + mPostLayoutPos + 1014 '}'; 1015 } 1016 1017 public void validate(RecyclerView.State state, CollectPositionResult result, String log) { 1018 mValidateCount ++; 1019 assertNotNull(this + ": result should not be null\n" + log, result); 1020 RecyclerView.ViewHolder viewHolder; 1021 if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) { 1022 assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult); 1023 viewHolder = result.scrapResult; 1024 } else { 1025 assertNotNull(this + ": result should come from adapter\n" + log, 1026 result.adapterResult); 1027 assertEquals(this + ": old position should be none when it came from adapter\n" + log, 1028 RecyclerView.NO_POSITION, result.adapterResult.getOldPosition()); 1029 viewHolder = result.adapterResult; 1030 } 1031 if (state.isPreLayout()) { 1032 assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos, 1033 viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition : 1034 viewHolder.mPreLayoutPosition); 1035 assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos, 1036 viewHolder.getPosition()); 1037 if (mType == Type.scrap) { 1038 assertEquals(this + ": old position should match\n" + log, mOldPos, 1039 result.scrapResult.getOldPosition()); 1040 } 1041 } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult 1042 .isRemoved()) { 1043 assertEquals(this + ": post-layout position should match\n" + log + "\n\n" 1044 + viewHolder, mPostLayoutPos, viewHolder.getPosition()); 1045 } 1046 } 1047 } 1048} 1049