LinearLayoutManagerTest.java revision 504c54ea52c1b2aae6f8f4ae128f1dcaac7e3f6a
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.Rect; 21import android.os.Parcel; 22import android.os.Parcelable; 23import android.util.Log; 24import android.view.View; 25import android.view.ViewGroup; 26 27import java.lang.ref.WeakReference; 28import java.lang.reflect.Field; 29import java.util.ArrayList; 30import java.util.LinkedHashMap; 31import java.util.List; 32import java.util.Map; 33import java.util.UUID; 34import java.util.concurrent.CountDownLatch; 35import java.util.concurrent.TimeUnit; 36import java.util.concurrent.atomic.AtomicInteger; 37 38/** 39 * Includes tests for {@link LinearLayoutManager}. 40 * <p> 41 * Since most UI tests are not practical, these tests are focused on internal data representation 42 * and stability of LinearLayoutManager in response to different events (state change, scrolling 43 * etc) where it is very hard to do manual testing. 44 */ 45public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 46 47 private static final boolean DEBUG = false; 48 49 private static final String TAG = "LinearLayoutManagerTest"; 50 51 WrappedLinearLayoutManager mLayoutManager; 52 53 TestAdapter mTestAdapter; 54 55 final List<Config> mBaseVariations = new ArrayList<Config>(); 56 57 @Override 58 protected void setUp() throws Exception { 59 super.setUp(); 60 for (int orientation : new int[]{LinearLayoutManager.VERTICAL, 61 LinearLayoutManager.HORIZONTAL}) { 62 for (boolean reverseLayout : new boolean[]{false, true}) { 63 for (boolean stackFromBottom : new boolean[]{false, true}) { 64 mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom)); 65 } 66 } 67 } 68 } 69 70 protected List<Config> addConfigVariation(List<Config> base, String fieldName, 71 Object... variations) 72 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 73 List<Config> newConfigs = new ArrayList<Config>(); 74 Field field = Config.class.getDeclaredField(fieldName); 75 for (Config config : base) { 76 for (Object variation : variations) { 77 Config newConfig = (Config) config.clone(); 78 field.set(newConfig, variation); 79 newConfigs.add(newConfig); 80 } 81 } 82 return newConfigs; 83 } 84 85 void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable { 86 mRecyclerView = new RecyclerView(getActivity()); 87 mRecyclerView.setHasFixedSize(true); 88 mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount) 89 : config.mTestAdapter; 90 mRecyclerView.setAdapter(mTestAdapter); 91 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation, 92 config.mReverseLayout); 93 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 94 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 95 mRecyclerView.setLayoutManager(mLayoutManager); 96 if (waitForFirstLayout) { 97 waitForFirstLayout(); 98 } 99 } 100 101 private void waitForFirstLayout() throws Throwable { 102 mLayoutManager.expectLayouts(1); 103 setRecyclerView(mRecyclerView); 104 mLayoutManager.waitForLayout(2); 105 } 106 107 public void testRecycleDuringAnimations() throws Throwable { 108 final AtomicInteger childCount = new AtomicInteger(0); 109 final TestAdapter adapter = new TestAdapter(300) { 110 @Override 111 public TestViewHolder onCreateViewHolder(ViewGroup parent, 112 int viewType) { 113 final int cnt = childCount.incrementAndGet(); 114 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 115 if (DEBUG) { 116 Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder); 117 } 118 return testViewHolder; 119 } 120 }; 121 setupByConfig(new Config(LinearLayoutManager.VERTICAL, false, false).itemCount(300) 122 .adapter(adapter), true); 123 124 final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 125 @Override 126 public void putRecycledView(RecyclerView.ViewHolder scrap) { 127 super.putRecycledView(scrap); 128 int cnt = childCount.decrementAndGet(); 129 if (DEBUG) { 130 Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap); 131 } 132 } 133 134 @Override 135 public RecyclerView.ViewHolder getRecycledView(int viewType) { 136 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType); 137 if (recycledView != null) { 138 final int cnt = childCount.incrementAndGet(); 139 if (DEBUG) { 140 Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView); 141 } 142 } 143 return recycledView; 144 } 145 }; 146 pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500); 147 mRecyclerView.setRecycledViewPool(pool); 148 149 150 // now keep adding children to trigger more children being created etc. 151 for (int i = 0; i < 100; i ++) { 152 adapter.addAndNotify(15, 1); 153 Thread.sleep(15); 154 } 155 getInstrumentation().waitForIdleSync(); 156 waitForAnimations(2); 157 assertEquals("Children count should add up", childCount.get(), 158 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 159 160 // now trigger lots of add again, followed by a scroll to position 161 for (int i = 0; i < 100; i ++) { 162 adapter.addAndNotify(5 + (i % 3) * 3, 1); 163 Thread.sleep(25); 164 } 165 smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20); 166 waitForAnimations(2); 167 getInstrumentation().waitForIdleSync(); 168 assertEquals("Children count should add up", childCount.get(), 169 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 170 } 171 172 173 public void testGetFirstLastChildrenTest() throws Throwable { 174 for (Config config : mBaseVariations) { 175 getFirstLastChildrenTest(config); 176 } 177 } 178 179 public void testDontRecycleChildrenOnDetach() throws Throwable { 180 setupByConfig(new Config().recycleChildrenOnDetach(false), true); 181 runTestOnUiThread(new Runnable() { 182 @Override 183 public void run() { 184 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 185 mRecyclerView.setLayoutManager(new TestLayoutManager()); 186 assertEquals("No views are recycled", recyclerSize, 187 mRecyclerView.mRecycler.getRecycledViewPool().size()); 188 } 189 }); 190 } 191 192 public void testRecycleChildrenOnDetach() throws Throwable { 193 setupByConfig(new Config().recycleChildrenOnDetach(true), true); 194 final int childCount = mLayoutManager.getChildCount(); 195 runTestOnUiThread(new Runnable() { 196 @Override 197 public void run() { 198 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 199 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews( 200 mTestAdapter.getItemViewType(0), recyclerSize + childCount); 201 mRecyclerView.setLayoutManager(new TestLayoutManager()); 202 assertEquals("All children should be recycled", childCount + recyclerSize, 203 mRecyclerView.mRecycler.getRecycledViewPool().size()); 204 } 205 }); 206 } 207 208 public void getFirstLastChildrenTest(final Config config) throws Throwable { 209 setupByConfig(config, true); 210 Runnable viewInBoundsTest = new Runnable() { 211 @Override 212 public void run() { 213 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); 214 final String boundsLog = mLayoutManager.getBoundsLog(); 215 assertEquals(config + ":\nfirst visible child should match traversal result\n" 216 + boundsLog, visibleChildren.firstVisiblePosition, 217 mLayoutManager.findFirstVisibleItemPosition() 218 ); 219 assertEquals( 220 config + ":\nfirst fully visible child should match traversal result\n" 221 + boundsLog, visibleChildren.firstFullyVisiblePosition, 222 mLayoutManager.findFirstCompletelyVisibleItemPosition() 223 ); 224 225 assertEquals(config + ":\nlast visible child should match traversal result\n" 226 + boundsLog, visibleChildren.lastVisiblePosition, 227 mLayoutManager.findLastVisibleItemPosition() 228 ); 229 assertEquals( 230 config + ":\nlast fully visible child should match traversal result\n" 231 + boundsLog, visibleChildren.lastFullyVisiblePosition, 232 mLayoutManager.findLastCompletelyVisibleItemPosition() 233 ); 234 } 235 }; 236 runTestOnUiThread(viewInBoundsTest); 237 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 238 // case 239 final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount(); 240 runTestOnUiThread(new Runnable() { 241 @Override 242 public void run() { 243 mRecyclerView.smoothScrollToPosition(scrollPosition); 244 } 245 }); 246 while (mLayoutManager.isSmoothScrolling() || 247 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 248 runTestOnUiThread(viewInBoundsTest); 249 Thread.sleep(400); 250 } 251 // delete all items 252 mLayoutManager.expectLayouts(2); 253 mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount()); 254 mLayoutManager.waitForLayout(2); 255 // test empty case 256 runTestOnUiThread(viewInBoundsTest); 257 // set a new adapter with huge items to test full bounds check 258 mLayoutManager.expectLayouts(1); 259 final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace(); 260 final TestAdapter newAdapter = new TestAdapter(100) { 261 @Override 262 public void onBindViewHolder(TestViewHolder holder, 263 int position) { 264 super.onBindViewHolder(holder, position); 265 if (config.mOrientation == LinearLayoutManager.HORIZONTAL) { 266 holder.itemView.setMinimumWidth(totalSpace + 5); 267 } else { 268 holder.itemView.setMinimumHeight(totalSpace + 5); 269 } 270 } 271 }; 272 runTestOnUiThread(new Runnable() { 273 @Override 274 public void run() { 275 mRecyclerView.setAdapter(newAdapter); 276 } 277 }); 278 mLayoutManager.waitForLayout(2); 279 runTestOnUiThread(viewInBoundsTest); 280 } 281 282 public void testSavedState() throws Throwable { 283 Thread.sleep(5000); 284 PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ 285 new PostLayoutRunnable() { 286 @Override 287 public void run() throws Throwable { 288 // do nothing 289 } 290 291 @Override 292 public String describe() { 293 return "doing nothing"; 294 } 295 }, 296 new PostLayoutRunnable() { 297 @Override 298 public void run() throws Throwable { 299 mLayoutManager.expectLayouts(1); 300 scrollToPosition(mTestAdapter.getItemCount() * 3 / 4); 301 mLayoutManager.waitForLayout(2); 302 } 303 304 @Override 305 public String describe() { 306 return "scroll to position"; 307 } 308 }, 309 new PostLayoutRunnable() { 310 @Override 311 public void run() throws Throwable { 312 mLayoutManager.expectLayouts(1); 313 scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3, 314 50); 315 mLayoutManager.waitForLayout(2); 316 } 317 318 @Override 319 public String describe() { 320 return "scroll to position with positive offset"; 321 } 322 }, 323 new PostLayoutRunnable() { 324 @Override 325 public void run() throws Throwable { 326 mLayoutManager.expectLayouts(1); 327 scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3, 328 -50); 329 mLayoutManager.waitForLayout(2); 330 } 331 332 @Override 333 public String describe() { 334 return "scroll to position with negative offset"; 335 } 336 } 337 }; 338 339 PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{ 340 new PostRestoreRunnable() { 341 @Override 342 public String describe() { 343 return "Doing nothing"; 344 } 345 }, 346 new PostRestoreRunnable() { 347 @Override 348 void onAfterRestore(Config config) throws Throwable { 349 // update config as well so that restore assertions will work 350 config.mOrientation = 1 - config.mOrientation; 351 mLayoutManager.setOrientation(config.mOrientation); 352 } 353 354 @Override 355 boolean shouldLayoutMatch(Config config) { 356 return config.mItemCount == 0; 357 } 358 359 @Override 360 public String describe() { 361 return "Changing orientation"; 362 } 363 }, 364 new PostRestoreRunnable() { 365 @Override 366 void onAfterRestore(Config config) throws Throwable { 367 config.mStackFromEnd = !config.mStackFromEnd; 368 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 369 } 370 371 @Override 372 boolean shouldLayoutMatch(Config config) { 373 return true; //stack from end should not move items on change 374 } 375 376 @Override 377 public String describe() { 378 return "Changing stack from end"; 379 } 380 }, 381 new PostRestoreRunnable() { 382 @Override 383 void onAfterRestore(Config config) throws Throwable { 384 config.mReverseLayout = !config.mReverseLayout; 385 mLayoutManager.setReverseLayout(config.mReverseLayout); 386 } 387 388 @Override 389 boolean shouldLayoutMatch(Config config) { 390 return config.mItemCount == 0; 391 } 392 393 @Override 394 public String describe() { 395 return "Changing reverse layout"; 396 } 397 }, 398 new PostRestoreRunnable() { 399 @Override 400 void onAfterRestore(Config config) throws Throwable { 401 config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach; 402 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 403 } 404 405 @Override 406 boolean shouldLayoutMatch(Config config) { 407 return true; 408 } 409 410 @Override 411 String describe() { 412 return "Change shoudl recycle children"; 413 } 414 }, 415 new PostRestoreRunnable() { 416 int position; 417 @Override 418 void onAfterRestore(Config config) throws Throwable { 419 position = mTestAdapter.getItemCount() / 2; 420 mLayoutManager.scrollToPosition(position); 421 } 422 423 @Override 424 boolean shouldLayoutMatch(Config config) { 425 return mTestAdapter.getItemCount() == 0; 426 } 427 428 @Override 429 String describe() { 430 return "Scroll to position " + position ; 431 } 432 433 @Override 434 void onAfterReLayout(Config config) { 435 if (mTestAdapter.getItemCount() > 0) { 436 assertEquals(config + ":scrolled view should be last completely visible", 437 position, 438 config.mStackFromEnd ? 439 mLayoutManager.findLastCompletelyVisibleItemPosition() 440 : mLayoutManager.findFirstCompletelyVisibleItemPosition()); 441 } 442 } 443 } 444 }; 445 boolean[] waitForLayoutOptions = new boolean[]{true, false}; 446 List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300); 447 variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true); 448 for (Config config : variations) { 449 for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) { 450 for (boolean waitForLayout : waitForLayoutOptions) { 451 for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) { 452 savedStateTest((Config) config.clone(), waitForLayout, postLayoutRunnable, 453 postRestoreRunnable); 454 removeRecyclerView(); 455 } 456 457 } 458 } 459 } 460 } 461 462 public void savedStateTest(Config config, boolean waitForLayout, 463 PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation) 464 throws Throwable { 465 if (DEBUG) { 466 Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " + 467 config + " post layout action " + postLayoutOperation.describe() + 468 "post restore action " + postRestoreOperation.describe()); 469 } 470 setupByConfig(config, false); 471 if (waitForLayout) { 472 waitForFirstLayout(); 473 postLayoutOperation.run(); 474 } 475 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 476 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 477 // we append a suffix to the parcelable to test out of bounds 478 String parcelSuffix = UUID.randomUUID().toString(); 479 Parcel parcel = Parcel.obtain(); 480 savedState.writeToParcel(parcel, 0); 481 parcel.writeString(parcelSuffix); 482 removeRecyclerView(); 483 // reset for reading 484 parcel.setDataPosition(0); 485 // re-create 486 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 487 removeRecyclerView(); 488 489 RecyclerView restored = new RecyclerView(getActivity()); 490 // this config should be no op. 491 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), 492 1 - config.mOrientation, !config.mReverseLayout); 493 mLayoutManager.setStackFromEnd(!config.mStackFromEnd); 494 restored.setLayoutManager(mLayoutManager); 495 // use the same adapter for Rect matching 496 restored.setAdapter(mTestAdapter); 497 restored.onRestoreInstanceState(savedState); 498 postRestoreOperation.onAfterRestore(config); 499 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 500 parcel.readString()); 501 mLayoutManager.expectLayouts(1); 502 setRecyclerView(restored); 503 mLayoutManager.waitForLayout(2); 504 // calculate prefix here instead of above to include post restore changes 505 final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() + 506 "\npostRestore:" + postRestoreOperation.describe() + "\n"; 507 assertEquals(logPrefix + " on saved state, reverse layout should be preserved", 508 config.mReverseLayout, mLayoutManager.getReverseLayout()); 509 assertEquals(logPrefix + " on saved state, orientation should be preserved", 510 config.mOrientation, mLayoutManager.getOrientation()); 511 assertEquals(logPrefix + " on saved state, stack from end should be preserved", 512 config.mStackFromEnd, mLayoutManager.getStackFromEnd()); 513 assertEquals(logPrefix + " on saved state, mRecycleChildrenOnDetach should be preserved", 514 config.mRecycleChildrenOnDetach, mLayoutManager.getRecycleChildrenOnDetach()); 515 if (waitForLayout) { 516 if (postRestoreOperation.shouldLayoutMatch(config)) { 517 assertRectSetsEqual( 518 logPrefix + ": on restore, previous view positions should be preserved", 519 before, mLayoutManager.collectChildCoordinates()); 520 } else { 521 assertRectSetsNotEqual( 522 logPrefix 523 + ": on restore with changes, previous view positions should NOT " 524 + "be preserved", 525 before, mLayoutManager.collectChildCoordinates()); 526 } 527 postRestoreOperation.onAfterReLayout(config); 528 } 529 } 530 531 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 532 runTestOnUiThread(new Runnable() { 533 @Override 534 public void run() { 535 mLayoutManager.scrollToPositionWithOffset(position, offset); 536 } 537 }); 538 } 539 540 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 541 Map<Item, Rect> after) { 542 Throwable throwable = null; 543 try { 544 assertRectSetsEqual("NOT " + message, before, after); 545 } catch (Throwable t) { 546 throwable = t; 547 } 548 assertNotNull(message + "\ntwo layout should be different", throwable); 549 } 550 551 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 552 StringBuilder sb = new StringBuilder(); 553 sb.append("checking rectangle equality."); 554 sb.append("before:\n"); 555 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 556 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 557 } 558 sb.append("after:\n"); 559 for (Map.Entry<Item, Rect> entry : after.entrySet()) { 560 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 561 } 562 message = message + "\n" + sb.toString(); 563 assertEquals(message + ":\nitem counts should be equal", before.size() 564 , after.size()); 565 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 566 Rect afterRect = after.get(entry.getKey()); 567 assertNotNull(message + ":\nSame item should be visible after simple re-layout", 568 afterRect); 569 assertEquals(message + ":\nItem should be laid out at the same coordinates", 570 entry.getValue(), afterRect); 571 } 572 } 573 574 static class VisibleChildren { 575 576 int firstVisiblePosition = RecyclerView.NO_POSITION; 577 578 int firstFullyVisiblePosition = RecyclerView.NO_POSITION; 579 580 int lastVisiblePosition = RecyclerView.NO_POSITION; 581 582 int lastFullyVisiblePosition = RecyclerView.NO_POSITION; 583 584 @Override 585 public String toString() { 586 return "VisibleChildren{" + 587 "firstVisiblePosition=" + firstVisiblePosition + 588 ", firstFullyVisiblePosition=" + firstFullyVisiblePosition + 589 ", lastVisiblePosition=" + lastVisiblePosition + 590 ", lastFullyVisiblePosition=" + lastFullyVisiblePosition + 591 '}'; 592 } 593 } 594 595 abstract private class PostLayoutRunnable { 596 597 abstract void run() throws Throwable; 598 599 abstract String describe(); 600 } 601 602 abstract private class PostRestoreRunnable { 603 604 void onAfterRestore(Config config) throws Throwable { 605 } 606 607 abstract String describe(); 608 609 boolean shouldLayoutMatch(Config config) { 610 return true; 611 } 612 613 void onAfterReLayout(Config config) { 614 615 }; 616 } 617 618 class WrappedLinearLayoutManager extends LinearLayoutManager { 619 620 CountDownLatch layoutLatch; 621 622 OrientationHelper mSecondaryOrientation; 623 624 public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 625 super(context, orientation, reverseLayout); 626 } 627 628 public void expectLayouts(int count) { 629 layoutLatch = new CountDownLatch(count); 630 } 631 632 public void waitForLayout(long timeout) throws InterruptedException { 633 waitForLayout(timeout, TimeUnit.SECONDS); 634 } 635 636 @Override 637 public void setOrientation(int orientation) { 638 super.setOrientation(orientation); 639 mSecondaryOrientation = null; 640 } 641 642 @Override 643 public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) { 644 if (DEBUG) { 645 Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child)); 646 } 647 super.removeAndRecycleView(child, recycler); 648 } 649 650 @Override 651 public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) { 652 if (DEBUG) { 653 Log.d(TAG, "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index))); 654 } 655 super.removeAndRecycleViewAt(index, recycler); 656 } 657 658 @Override 659 void ensureLayoutState() { 660 super.ensureLayoutState(); 661 if (mSecondaryOrientation == null) { 662 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this, 663 1 - getOrientation()); 664 } 665 } 666 667 private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException { 668 layoutLatch.await(timeout, timeUnit); 669 assertEquals("all expected layouts should be executed at the expected time", 670 0, layoutLatch.getCount()); 671 } 672 673 public String getBoundsLog() { 674 StringBuilder sb = new StringBuilder(); 675 sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding()) 676 .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding()); 677 sb.append("\nchildren bounds\n"); 678 final int childCount = getChildCount(); 679 for (int i = 0; i < childCount; i++) { 680 View child = getChildAt(i); 681 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 682 .append("[").append("start:").append( 683 mOrientationHelper.getDecoratedStart(child)).append(", end:") 684 .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n"); 685 } 686 return sb.toString(); 687 } 688 689 public VisibleChildren traverseAndFindVisibleChildren() { 690 int childCount = getChildCount(); 691 final VisibleChildren visibleChildren = new VisibleChildren(); 692 final int start = mOrientationHelper.getStartAfterPadding(); 693 final int end = mOrientationHelper.getEndAfterPadding(); 694 for (int i = 0; i < childCount; i++) { 695 View child = getChildAt(i); 696 final int childStart = mOrientationHelper.getDecoratedStart(child); 697 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 698 final boolean fullyVisible = childStart >= start && childEnd <= end; 699 final boolean hidden = childEnd <= start || childStart >= end; 700 if (hidden) { 701 continue; 702 } 703 final int position = getPosition(child); 704 if (fullyVisible) { 705 if (position < visibleChildren.firstFullyVisiblePosition || 706 visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) { 707 visibleChildren.firstFullyVisiblePosition = position; 708 } 709 710 if (position > visibleChildren.lastFullyVisiblePosition) { 711 visibleChildren.lastFullyVisiblePosition = position; 712 } 713 } 714 715 if (position < visibleChildren.firstVisiblePosition || 716 visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) { 717 visibleChildren.firstVisiblePosition = position; 718 } 719 720 if (position > visibleChildren.lastVisiblePosition) { 721 visibleChildren.lastVisiblePosition = position; 722 } 723 724 } 725 return visibleChildren; 726 } 727 728 Rect getViewBounds(View view) { 729 if (getOrientation() == HORIZONTAL) { 730 return new Rect( 731 mOrientationHelper.getDecoratedStart(view), 732 mSecondaryOrientation.getDecoratedStart(view), 733 mOrientationHelper.getDecoratedEnd(view), 734 mSecondaryOrientation.getDecoratedEnd(view)); 735 } else { 736 return new Rect( 737 mSecondaryOrientation.getDecoratedStart(view), 738 mOrientationHelper.getDecoratedStart(view), 739 mSecondaryOrientation.getDecoratedEnd(view), 740 mOrientationHelper.getDecoratedEnd(view)); 741 } 742 743 } 744 745 Map<Item, Rect> collectChildCoordinates() throws Throwable { 746 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 747 runTestOnUiThread(new Runnable() { 748 @Override 749 public void run() { 750 final int childCount = getChildCount(); 751 for (int i = 0; i < childCount; i++) { 752 View child = getChildAt(i); 753 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child 754 .getLayoutParams(); 755 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 756 items.put(vh.mBindedItem, getViewBounds(child)); 757 } 758 } 759 }); 760 return items; 761 } 762 763 @Override 764 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 765 super.onLayoutChildren(recycler, state); 766 layoutLatch.countDown(); 767 } 768 } 769 770 static class Config implements Cloneable { 771 772 private static final int DEFAULT_ITEM_COUNT = 100; 773 774 private boolean mStackFromEnd; 775 776 int mOrientation = LinearLayoutManager.VERTICAL; 777 778 boolean mReverseLayout = false; 779 780 boolean mRecycleChildrenOnDetach = false; 781 782 int mItemCount = DEFAULT_ITEM_COUNT; 783 784 TestAdapter mTestAdapter; 785 786 Config(int orientation, boolean reverseLayout, boolean stackFromEnd) { 787 mOrientation = orientation; 788 mReverseLayout = reverseLayout; 789 mStackFromEnd = stackFromEnd; 790 } 791 792 public Config() { 793 794 } 795 796 Config adapter(TestAdapter adapter) { 797 mTestAdapter = adapter; 798 return this; 799 } 800 801 Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 802 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 803 return this; 804 } 805 806 Config orientation(int orientation) { 807 mOrientation = orientation; 808 return this; 809 } 810 811 Config stackFromBottom(boolean stackFromBottom) { 812 mStackFromEnd = stackFromBottom; 813 return this; 814 } 815 816 Config reverseLayout(boolean reverseLayout) { 817 mReverseLayout = reverseLayout; 818 return this; 819 } 820 821 public Config itemCount(int itemCount) { 822 mItemCount = itemCount; 823 return this; 824 } 825 826 // required by convention 827 @Override 828 public Object clone() throws CloneNotSupportedException { 829 return super.clone(); 830 } 831 832 @Override 833 public String toString() { 834 return "Config{" + 835 "mStackFromEnd=" + mStackFromEnd + 836 ", mOrientation=" + mOrientation + 837 ", mReverseLayout=" + mReverseLayout + 838 ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach + 839 ", mItemCount=" + mItemCount + 840 '}'; 841 } 842 } 843} 844