1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.v7.widget; 18 19 20 import org.junit.Test; 21 import org.junit.runner.RunWith; 22 import org.junit.runners.Parameterized; 23 24 import android.graphics.Rect; 25 import android.os.Looper; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.support.v4.view.ViewCompat; 29 import android.test.suitebuilder.annotation.MediumTest; 30 import android.util.Log; 31 import android.view.View; 32 import android.view.ViewParent; 33 34 import java.util.Arrays; 35 import java.util.BitSet; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.UUID; 39 40 import static android.support.v7.widget.LayoutState.LAYOUT_START; 41 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 42 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL; 43 44 import static org.hamcrest.CoreMatchers.hasItem; 45 import static org.hamcrest.CoreMatchers.is; 46 import static org.hamcrest.CoreMatchers.not; 47 import static org.hamcrest.CoreMatchers.sameInstance; 48 import static org.junit.Assert.assertEquals; 49 import static org.junit.Assert.assertNotNull; 50 import static org.junit.Assert.assertThat; 51 import static org.junit.Assert.assertTrue; 52 53 @RunWith(Parameterized.class) 54 @MediumTest 55 public class StaggeredGridLayoutManagerBaseConfigSetTest 56 extends BaseStaggeredGridLayoutManagerTest { 57 58 @Parameterized.Parameters(name = "{0}") 59 public static List<Config> getParams() { 60 return createBaseVariations(); 61 } 62 63 private final Config mConfig; 64 65 public StaggeredGridLayoutManagerBaseConfigSetTest(Config config) 66 throws CloneNotSupportedException { 67 mConfig = (Config) config.clone(); 68 } 69 70 @Test 71 public void rTL() throws Throwable { 72 rtlTest(false, false); 73 } 74 75 @Test 76 public void rTLChangeAfter() throws Throwable { 77 rtlTest(true, false); 78 } 79 80 @Test 81 public void rTLItemWrapContent() throws Throwable { 82 rtlTest(false, true); 83 } 84 85 @Test 86 public void rTLChangeAfterItemWrapContent() throws Throwable { 87 rtlTest(true, true); 88 } 89 90 void rtlTest(boolean changeRtlAfter, final boolean wrapContent) throws Throwable { 91 if (mConfig.mSpanCount == 1) { 92 mConfig.mSpanCount = 2; 93 } 94 String logPrefix = mConfig + ", changeRtlAfterLayout:" + changeRtlAfter; 95 setupByConfig(mConfig.itemCount(5), 96 new GridTestAdapter(mConfig.mItemCount, mConfig.mOrientation) { 97 @Override 98 public void onBindViewHolder(TestViewHolder holder, 99 int position) { 100 super.onBindViewHolder(holder, position); 101 if (wrapContent) { 102 if (mOrientation == HORIZONTAL) { 103 holder.itemView.getLayoutParams().height 104 = RecyclerView.LayoutParams.WRAP_CONTENT; 105 } else { 106 holder.itemView.getLayoutParams().width 107 = RecyclerView.LayoutParams.MATCH_PARENT; 108 } 109 } 110 } 111 }); 112 if (changeRtlAfter) { 113 waitFirstLayout(); 114 mLayoutManager.expectLayouts(1); 115 mLayoutManager.setFakeRtl(true); 116 mLayoutManager.waitForLayout(2); 117 } else { 118 mLayoutManager.mFakeRTL = true; 119 waitFirstLayout(); 120 } 121 122 assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL()); 123 OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager); 124 View child0 = mLayoutManager.findViewByPosition(0); 125 View child1 = mLayoutManager.findViewByPosition(mConfig.mOrientation == VERTICAL ? 1 126 : mConfig.mSpanCount); 127 assertNotNull(logPrefix + " child position 0 should be laid out", child0); 128 assertNotNull(logPrefix + " child position 0 should be laid out", child1); 129 logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1); 130 if (mConfig.mOrientation == VERTICAL || !mConfig.mReverseLayout) { 131 assertTrue(logPrefix + " second child should be to the left of first child", 132 helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1)); 133 assertEquals(logPrefix + " first child should be right aligned", 134 helper.getDecoratedEnd(child0), helper.getEndAfterPadding()); 135 } else { 136 assertTrue(logPrefix + " first child should be to the left of second child", 137 helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0)); 138 assertEquals(logPrefix + " first child should be left aligned", 139 helper.getDecoratedStart(child0), helper.getStartAfterPadding()); 140 } 141 checkForMainThreadException(); 142 } 143 144 @Test 145 public void scrollBackAndPreservePositions() throws Throwable { 146 scrollBackAndPreservePositionsTest(false); 147 } 148 149 @Test 150 public void scrollBackAndPreservePositionsWithRestore() throws Throwable { 151 scrollBackAndPreservePositionsTest(true); 152 } 153 154 public void scrollBackAndPreservePositionsTest(final boolean saveRestoreInBetween) 155 throws Throwable { 156 setupByConfig(mConfig); 157 mAdapter.mOnBindCallback = new OnBindCallback() { 158 @Override 159 public void onBoundItem(TestViewHolder vh, int position) { 160 StaggeredGridLayoutManager.LayoutParams 161 lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView 162 .getLayoutParams(); 163 lp.setFullSpan((position * 7) % (mConfig.mSpanCount + 1) == 0); 164 } 165 }; 166 waitFirstLayout(); 167 final int[] globalPositions = new int[mAdapter.getItemCount()]; 168 Arrays.fill(globalPositions, Integer.MIN_VALUE); 169 final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10) 170 * (mConfig.mReverseLayout ? -1 : 1); 171 172 final int[] globalPos = new int[1]; 173 runTestOnUiThread(new Runnable() { 174 @Override 175 public void run() { 176 int globalScrollPosition = 0; 177 while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) { 178 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 179 View child = mRecyclerView.getChildAt(i); 180 final int pos = mRecyclerView.getChildLayoutPosition(child); 181 if (globalPositions[pos] != Integer.MIN_VALUE) { 182 continue; 183 } 184 if (mConfig.mReverseLayout) { 185 globalPositions[pos] = globalScrollPosition + 186 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child); 187 } else { 188 globalPositions[pos] = globalScrollPosition + 189 mLayoutManager.mPrimaryOrientation.getDecoratedStart(child); 190 } 191 } 192 globalScrollPosition += mLayoutManager.scrollBy(scrollStep, 193 mRecyclerView.mRecycler, mRecyclerView.mState); 194 } 195 if (DEBUG) { 196 Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions)); 197 } 198 globalPos[0] = globalScrollPosition; 199 } 200 }); 201 checkForMainThreadException(); 202 203 if (saveRestoreInBetween) { 204 saveRestore(mConfig); 205 } 206 207 checkForMainThreadException(); 208 runTestOnUiThread(new Runnable() { 209 @Override 210 public void run() { 211 int globalScrollPosition = globalPos[0]; 212 // now scroll back and make sure global positions match 213 BitSet shouldTest = new BitSet(mAdapter.getItemCount()); 214 shouldTest.set(0, mAdapter.getItemCount() - 1, true); 215 String assertPrefix = mConfig + ", restored in between:" + saveRestoreInBetween 216 + " global pos must match when scrolling in reverse for position "; 217 int scrollAmount = Integer.MAX_VALUE; 218 while (!shouldTest.isEmpty() && scrollAmount != 0) { 219 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 220 View child = mRecyclerView.getChildAt(i); 221 int pos = mRecyclerView.getChildLayoutPosition(child); 222 if (!shouldTest.get(pos)) { 223 continue; 224 } 225 shouldTest.clear(pos); 226 int globalPos; 227 if (mConfig.mReverseLayout) { 228 globalPos = globalScrollPosition + 229 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child); 230 } else { 231 globalPos = globalScrollPosition + 232 mLayoutManager.mPrimaryOrientation.getDecoratedStart(child); 233 } 234 assertEquals(assertPrefix + pos, 235 globalPositions[pos], globalPos); 236 } 237 scrollAmount = mLayoutManager.scrollBy(-scrollStep, 238 mRecyclerView.mRecycler, mRecyclerView.mState); 239 globalScrollPosition += scrollAmount; 240 } 241 assertTrue("all views should be seen", shouldTest.isEmpty()); 242 } 243 }); 244 checkForMainThreadException(); 245 } 246 247 private void saveRestore(final Config config) throws Throwable { 248 runTestOnUiThread(new Runnable() { 249 @Override 250 public void run() { 251 try { 252 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 253 // we append a suffix to the parcelable to test out of bounds 254 String parcelSuffix = UUID.randomUUID().toString(); 255 Parcel parcel = Parcel.obtain(); 256 savedState.writeToParcel(parcel, 0); 257 parcel.writeString(parcelSuffix); 258 removeRecyclerView(); 259 // reset for reading 260 parcel.setDataPosition(0); 261 // re-create 262 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 263 RecyclerView restored = new RecyclerView(getActivity()); 264 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, 265 config.mOrientation); 266 mLayoutManager.setGapStrategy(config.mGapStrategy); 267 restored.setLayoutManager(mLayoutManager); 268 // use the same adapter for Rect matching 269 restored.setAdapter(mAdapter); 270 restored.onRestoreInstanceState(savedState); 271 if (Looper.myLooper() == Looper.getMainLooper()) { 272 mLayoutManager.expectLayouts(1); 273 setRecyclerView(restored); 274 } else { 275 mLayoutManager.expectLayouts(1); 276 setRecyclerView(restored); 277 mLayoutManager.waitForLayout(2); 278 } 279 } catch (Throwable t) { 280 postExceptionToInstrumentation(t); 281 } 282 } 283 }); 284 checkForMainThreadException(); 285 } 286 287 @Test 288 public void getFirstLastChildrenTest() throws Throwable { 289 getFirstLastChildrenTest(false); 290 } 291 292 @Test 293 public void getFirstLastChildrenTestProvideArray() throws Throwable { 294 getFirstLastChildrenTest(true); 295 } 296 297 public void getFirstLastChildrenTest(final boolean provideArr) throws Throwable { 298 setupByConfig(mConfig); 299 waitFirstLayout(); 300 Runnable viewInBoundsTest = new Runnable() { 301 @Override 302 public void run() { 303 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); 304 final String boundsLog = mLayoutManager.getBoundsLog(); 305 VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount()); 306 queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager 307 .findFirstVisibleItemClosestToStart(false, true); 308 queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager 309 .findFirstVisibleItemClosestToEnd(false, true); 310 queryResult.firstFullyVisiblePositions = mLayoutManager 311 .findFirstCompletelyVisibleItemPositions( 312 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 313 queryResult.firstVisiblePositions = mLayoutManager 314 .findFirstVisibleItemPositions( 315 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 316 queryResult.lastFullyVisiblePositions = mLayoutManager 317 .findLastCompletelyVisibleItemPositions( 318 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 319 queryResult.lastVisiblePositions = mLayoutManager 320 .findLastVisibleItemPositions( 321 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 322 assertEquals(mConfig + ":\nfirst visible child should match traversal result\n" 323 + "traversed:" + visibleChildren + "\n" 324 + "queried:" + queryResult + "\n" 325 + boundsLog, visibleChildren, queryResult 326 ); 327 } 328 }; 329 runTestOnUiThread(viewInBoundsTest); 330 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 331 // case 332 final int scrollPosition = mAdapter.getItemCount(); 333 runTestOnUiThread(new Runnable() { 334 @Override 335 public void run() { 336 mRecyclerView.smoothScrollToPosition(scrollPosition); 337 } 338 }); 339 while (mLayoutManager.isSmoothScrolling() || 340 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 341 runTestOnUiThread(viewInBoundsTest); 342 checkForMainThreadException(); 343 Thread.sleep(400); 344 } 345 // delete all items 346 mLayoutManager.expectLayouts(2); 347 mAdapter.deleteAndNotify(0, mAdapter.getItemCount()); 348 mLayoutManager.waitForLayout(2); 349 // test empty case 350 runTestOnUiThread(viewInBoundsTest); 351 // set a new adapter with huge items to test full bounds check 352 mLayoutManager.expectLayouts(1); 353 final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace(); 354 final TestAdapter newAdapter = new TestAdapter(100) { 355 @Override 356 public void onBindViewHolder(TestViewHolder holder, 357 int position) { 358 super.onBindViewHolder(holder, position); 359 if (mConfig.mOrientation == LinearLayoutManager.HORIZONTAL) { 360 holder.itemView.setMinimumWidth(totalSpace + 100); 361 } else { 362 holder.itemView.setMinimumHeight(totalSpace + 100); 363 } 364 } 365 }; 366 runTestOnUiThread(new Runnable() { 367 @Override 368 public void run() { 369 mRecyclerView.setAdapter(newAdapter); 370 } 371 }); 372 mLayoutManager.waitForLayout(2); 373 runTestOnUiThread(viewInBoundsTest); 374 checkForMainThreadException(); 375 376 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 377 // case 378 runTestOnUiThread(new Runnable() { 379 @Override 380 public void run() { 381 final int diff; 382 if (mConfig.mReverseLayout) { 383 diff = -1; 384 } else { 385 diff = 1; 386 } 387 final int distance = diff * 10; 388 if (mConfig.mOrientation == HORIZONTAL) { 389 mRecyclerView.scrollBy(distance, 0); 390 } else { 391 mRecyclerView.scrollBy(0, distance); 392 } 393 } 394 }); 395 runTestOnUiThread(viewInBoundsTest); 396 checkForMainThreadException(); 397 } 398 399 @Test 400 public void viewSnapTest() throws Throwable { 401 final Config config = ((Config) mConfig.clone()).itemCount(mConfig.mSpanCount + 1); 402 setupByConfig(config); 403 mAdapter.mOnBindCallback = new OnBindCallback() { 404 @Override 405 void onBoundItem(TestViewHolder vh, int position) { 406 StaggeredGridLayoutManager.LayoutParams 407 lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView 408 .getLayoutParams(); 409 if (config.mOrientation == HORIZONTAL) { 410 lp.width = mRecyclerView.getWidth() / 3; 411 } else { 412 lp.height = mRecyclerView.getHeight() / 3; 413 } 414 } 415 416 @Override 417 boolean assignRandomSize() { 418 return false; 419 } 420 }; 421 waitFirstLayout(); 422 // run these tests twice. once initial layout, once after scroll 423 String logSuffix = ""; 424 for (int i = 0; i < 2; i++) { 425 Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates(); 426 Rect recyclerViewBounds = getDecoratedRecyclerViewBounds(); 427 // workaround for SGLM's span distribution issue. Right now, it may leave gaps so we 428 // avoid it by setting its layout params directly 429 if (config.mOrientation == HORIZONTAL) { 430 recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount; 431 } else { 432 recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount; 433 } 434 435 Rect usedLayoutBounds = new Rect(); 436 for (Rect rect : itemRectMap.values()) { 437 usedLayoutBounds.union(rect); 438 } 439 440 if (DEBUG) { 441 Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config); 442 } 443 if (config.mOrientation == VERTICAL) { 444 assertEquals(config + " there should be no gap on left" + logSuffix, 445 usedLayoutBounds.left, recyclerViewBounds.left); 446 assertEquals(config + " there should be no gap on right" + logSuffix, 447 usedLayoutBounds.right, recyclerViewBounds.right); 448 if (config.mReverseLayout) { 449 assertEquals(config + " there should be no gap on bottom" + logSuffix, 450 usedLayoutBounds.bottom, recyclerViewBounds.bottom); 451 assertTrue(config + " there should be some gap on top" + logSuffix, 452 usedLayoutBounds.top > recyclerViewBounds.top); 453 } else { 454 assertEquals(config + " there should be no gap on top" + logSuffix, 455 usedLayoutBounds.top, recyclerViewBounds.top); 456 assertTrue(config + " there should be some gap at the bottom" + logSuffix, 457 usedLayoutBounds.bottom < recyclerViewBounds.bottom); 458 } 459 } else { 460 assertEquals(config + " there should be no gap on top" + logSuffix, 461 usedLayoutBounds.top, recyclerViewBounds.top); 462 assertEquals(config + " there should be no gap at the bottom" + logSuffix, 463 usedLayoutBounds.bottom, recyclerViewBounds.bottom); 464 if (config.mReverseLayout) { 465 assertEquals(config + " there should be no on right" + logSuffix, 466 usedLayoutBounds.right, recyclerViewBounds.right); 467 assertTrue(config + " there should be some gap on left" + logSuffix, 468 usedLayoutBounds.left > recyclerViewBounds.left); 469 } else { 470 assertEquals(config + " there should be no gap on left" + logSuffix, 471 usedLayoutBounds.left, recyclerViewBounds.left); 472 assertTrue(config + " there should be some gap on right" + logSuffix, 473 usedLayoutBounds.right < recyclerViewBounds.right); 474 } 475 } 476 final int scroll = config.mReverseLayout ? -500 : 500; 477 scrollBy(scroll); 478 logSuffix = " scrolled " + scroll; 479 } 480 } 481 482 @Test 483 public void scrollToPositionWithOffsetTest() throws Throwable { 484 setupByConfig(mConfig); 485 waitFirstLayout(); 486 OrientationHelper orientationHelper = OrientationHelper 487 .createOrientationHelper(mLayoutManager, mConfig.mOrientation); 488 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 489 // try scrolling towards head, should not affect anything 490 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 491 scrollToPositionWithOffset(0, 20); 492 assertRectSetsEqual(mConfig + " trying to over scroll with offset should be no-op", 493 before, mLayoutManager.collectChildCoordinates()); 494 // try offsetting some visible children 495 int testCount = 10; 496 while (testCount-- > 0) { 497 // get middle child 498 final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2); 499 final int position = mRecyclerView.getChildLayoutPosition(child); 500 final int startOffset = mConfig.mReverseLayout ? 501 orientationHelper.getEndAfterPadding() - orientationHelper 502 .getDecoratedEnd(child) 503 : orientationHelper.getDecoratedStart(child) - orientationHelper 504 .getStartAfterPadding(); 505 final int scrollOffset = startOffset / 2; 506 mLayoutManager.expectLayouts(1); 507 scrollToPositionWithOffset(position, scrollOffset); 508 mLayoutManager.waitForLayout(2); 509 final int finalOffset = mConfig.mReverseLayout ? 510 orientationHelper.getEndAfterPadding() - orientationHelper 511 .getDecoratedEnd(child) 512 : orientationHelper.getDecoratedStart(child) - orientationHelper 513 .getStartAfterPadding(); 514 assertEquals(mConfig + " scroll with offset on a visible child should work fine", 515 scrollOffset, finalOffset); 516 } 517 518 // try scrolling to invisible children 519 testCount = 10; 520 // we test above and below, one by one 521 int offsetMultiplier = -1; 522 while (testCount-- > 0) { 523 final TargetTuple target = findInvisibleTarget(mConfig); 524 mLayoutManager.expectLayouts(1); 525 final int offset = offsetMultiplier 526 * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3; 527 scrollToPositionWithOffset(target.mPosition, offset); 528 mLayoutManager.waitForLayout(2); 529 final View child = mLayoutManager.findViewByPosition(target.mPosition); 530 assertNotNull(mConfig + " scrolling to a mPosition with offset " + offset 531 + " should layout it", child); 532 final Rect bounds = mLayoutManager.getViewBounds(child); 533 if (DEBUG) { 534 Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in " 535 + layoutBounds + " with offset " + offset); 536 } 537 538 if (mConfig.mReverseLayout) { 539 assertEquals(mConfig + " when scrolling with offset to an invisible in reverse " 540 + "layout, its end should align with recycler view's end - offset", 541 orientationHelper.getEndAfterPadding() - offset, 542 orientationHelper.getDecoratedEnd(child) 543 ); 544 } else { 545 assertEquals(mConfig + " when scrolling with offset to an invisible child in normal" 546 + " layout its start should align with recycler view's start + " 547 + "offset", 548 orientationHelper.getStartAfterPadding() + offset, 549 orientationHelper.getDecoratedStart(child) 550 ); 551 } 552 offsetMultiplier *= -1; 553 } 554 } 555 556 @Test 557 public void scrollToPositionTest() throws Throwable { 558 setupByConfig(mConfig); 559 waitFirstLayout(); 560 OrientationHelper orientationHelper = OrientationHelper 561 .createOrientationHelper(mLayoutManager, mConfig.mOrientation); 562 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 563 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 564 View view = mLayoutManager.getChildAt(i); 565 Rect bounds = mLayoutManager.getViewBounds(view); 566 if (layoutBounds.contains(bounds)) { 567 Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates(); 568 final int position = mRecyclerView.getChildLayoutPosition(view); 569 StaggeredGridLayoutManager.LayoutParams layoutParams 570 = (StaggeredGridLayoutManager.LayoutParams) (view.getLayoutParams()); 571 TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder; 572 assertEquals("recycler view mPosition should match adapter mPosition", position, 573 vh.mBoundItem.mAdapterIndex); 574 if (DEBUG) { 575 Log.d(TAG, "testing scroll to visible mPosition at " + position 576 + " " + bounds + " inside " + layoutBounds); 577 } 578 mLayoutManager.expectLayouts(1); 579 scrollToPosition(position); 580 mLayoutManager.waitForLayout(2); 581 if (DEBUG) { 582 view = mLayoutManager.findViewByPosition(position); 583 Rect newBounds = mLayoutManager.getViewBounds(view); 584 Log.d(TAG, "after scrolling to visible mPosition " + 585 bounds + " equals " + newBounds); 586 } 587 588 assertRectSetsEqual( 589 mConfig + "scroll to mPosition on fully visible child should be no-op", 590 initialBounds, mLayoutManager.collectChildCoordinates()); 591 } else { 592 final int position = mRecyclerView.getChildLayoutPosition(view); 593 if (DEBUG) { 594 Log.d(TAG, 595 "child(" + position + ") not fully visible " + bounds + " not inside " 596 + layoutBounds 597 + mRecyclerView.getChildLayoutPosition(view) 598 ); 599 } 600 mLayoutManager.expectLayouts(1); 601 runTestOnUiThread(new Runnable() { 602 @Override 603 public void run() { 604 mLayoutManager.scrollToPosition(position); 605 } 606 }); 607 mLayoutManager.waitForLayout(2); 608 view = mLayoutManager.findViewByPosition(position); 609 bounds = mLayoutManager.getViewBounds(view); 610 if (DEBUG) { 611 Log.d(TAG, "after scroll to partially visible child " + bounds + " in " 612 + layoutBounds); 613 } 614 assertTrue(mConfig 615 + " after scrolling to a partially visible child, it should become fully " 616 + " visible. " + bounds + " not inside " + layoutBounds, 617 layoutBounds.contains(bounds) 618 ); 619 assertTrue( 620 mConfig + " when scrolling to a partially visible item, one of its edges " 621 + "should be on the boundaries", 622 orientationHelper.getStartAfterPadding() == 623 orientationHelper.getDecoratedStart(view) 624 || orientationHelper.getEndAfterPadding() == 625 orientationHelper.getDecoratedEnd(view)); 626 } 627 } 628 629 // try scrolling to invisible children 630 int testCount = 10; 631 while (testCount-- > 0) { 632 final TargetTuple target = findInvisibleTarget(mConfig); 633 mLayoutManager.expectLayouts(1); 634 scrollToPosition(target.mPosition); 635 mLayoutManager.waitForLayout(2); 636 final View child = mLayoutManager.findViewByPosition(target.mPosition); 637 assertNotNull(mConfig + " scrolling to a mPosition should lay it out", child); 638 final Rect bounds = mLayoutManager.getViewBounds(child); 639 if (DEBUG) { 640 Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in " 641 + layoutBounds); 642 } 643 assertTrue(mConfig + " scrolling to a mPosition should make it fully visible", 644 layoutBounds.contains(bounds)); 645 if (target.mLayoutDirection == LAYOUT_START) { 646 assertEquals( 647 mConfig + " when scrolling to an invisible child above, its start should" 648 + " align with recycler view's start", 649 orientationHelper.getStartAfterPadding(), 650 orientationHelper.getDecoratedStart(child) 651 ); 652 } else { 653 assertEquals(mConfig + " when scrolling to an invisible child below, its end " 654 + "should align with recycler view's end", 655 orientationHelper.getEndAfterPadding(), 656 orientationHelper.getDecoratedEnd(child) 657 ); 658 } 659 } 660 } 661 662 @Test 663 public void scollByTest() throws Throwable { 664 setupByConfig(mConfig); 665 waitFirstLayout(); 666 // try invalid scroll. should not happen 667 final View first = mLayoutManager.getChildAt(0); 668 OrientationHelper primaryOrientation = OrientationHelper 669 .createOrientationHelper(mLayoutManager, mConfig.mOrientation); 670 int scrollDist; 671 if (mConfig.mReverseLayout) { 672 scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2; 673 } else { 674 scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2; 675 } 676 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 677 scrollBy(scrollDist); 678 Map<Item, Rect> after = mLayoutManager.collectChildCoordinates(); 679 assertRectSetsEqual( 680 mConfig + " if there are no more items, scroll should not happen (dt:" + scrollDist 681 + ")", 682 before, after 683 ); 684 685 scrollDist = -scrollDist * 3; 686 before = mLayoutManager.collectChildCoordinates(); 687 scrollBy(scrollDist); 688 after = mLayoutManager.collectChildCoordinates(); 689 int layoutStart = primaryOrientation.getStartAfterPadding(); 690 int layoutEnd = primaryOrientation.getEndAfterPadding(); 691 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 692 Rect afterRect = after.get(entry.getKey()); 693 // offset rect 694 if (mConfig.mOrientation == VERTICAL) { 695 entry.getValue().offset(0, -scrollDist); 696 } else { 697 entry.getValue().offset(-scrollDist, 0); 698 } 699 if (afterRect == null || afterRect.isEmpty()) { 700 // assert item is out of bounds 701 int start, end; 702 if (mConfig.mOrientation == VERTICAL) { 703 start = entry.getValue().top; 704 end = entry.getValue().bottom; 705 } else { 706 start = entry.getValue().left; 707 end = entry.getValue().right; 708 } 709 assertTrue( 710 mConfig + " if item is missing after relayout, it should be out of bounds." 711 + "item start: " + start + ", end:" + end + " layout start:" 712 + layoutStart + 713 ", layout end:" + layoutEnd, 714 start <= layoutStart && end <= layoutEnd || 715 start >= layoutEnd && end >= layoutEnd 716 ); 717 } else { 718 assertEquals(mConfig + " Item should be laid out at the scroll offset coordinates", 719 entry.getValue(), 720 afterRect); 721 } 722 } 723 assertViewPositions(mConfig); 724 } 725 726 @Test 727 public void layoutOrderTest() throws Throwable { 728 setupByConfig(mConfig); 729 assertViewPositions(mConfig); 730 } 731 732 @Test 733 public void consistentRelayout() throws Throwable { 734 consistentRelayoutTest(mConfig, false); 735 } 736 737 @Test 738 public void consistentRelayoutWithFullSpanFirstChild() throws Throwable { 739 consistentRelayoutTest(mConfig, true); 740 } 741 742 @Test 743 public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable { 744 final Config config = ((Config) mConfig.clone()).itemCount(1000); 745 setupByConfig(config); 746 waitFirstLayout(); 747 // pick position from child count so that it is not too far away 748 int pos = mRecyclerView.getChildCount() * 2; 749 smoothScrollToPosition(pos, true); 750 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos); 751 OrientationHelper helper = mLayoutManager.mPrimaryOrientation; 752 int gap = helper.getDecoratedStart(vh.itemView); 753 scrollBy(gap); 754 gap = helper.getDecoratedStart(vh.itemView); 755 assertThat("test sanity", gap, is(0)); 756 757 final int size = helper.getDecoratedMeasurement(vh.itemView); 758 AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); 759 runTestOnUiThread(new Runnable() { 760 @Override 761 public void run() { 762 if (mConfig.mOrientation == HORIZONTAL) { 763 ViewCompat.setTranslationX(vh.itemView, size * 2); 764 } else { 765 ViewCompat.setTranslationY(vh.itemView, size * 2); 766 } 767 } 768 }); 769 scrollBy(size * 2); 770 assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); 771 assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); 772 assertThat(vh.getAdapterPosition(), is(pos)); 773 scrollBy(size * 2); 774 assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); 775 } 776 777 @Test 778 public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable { 779 final Config config = ((Config) mConfig.clone()).itemCount(1000); 780 setupByConfig(config); 781 waitFirstLayout(); 782 // pick position from child count so that it is not too far away 783 int pos = mRecyclerView.getChildCount() * 2; 784 mLayoutManager.expectLayouts(1); 785 scrollToPosition(pos); 786 mLayoutManager.waitForLayout(2); 787 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos); 788 OrientationHelper helper = mLayoutManager.mPrimaryOrientation; 789 int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); 790 scrollBy(-gap); 791 gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView); 792 assertThat("test sanity", gap, is(0)); 793 794 final int size = helper.getDecoratedMeasurement(vh.itemView); 795 AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView); 796 runTestOnUiThread(new Runnable() { 797 @Override 798 public void run() { 799 if (mConfig.mOrientation == HORIZONTAL) { 800 ViewCompat.setTranslationX(vh.itemView, -size * 2); 801 } else { 802 ViewCompat.setTranslationY(vh.itemView, -size * 2); 803 } 804 } 805 }); 806 scrollBy(-size * 2); 807 assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView)))); 808 assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView)); 809 assertThat(vh.getAdapterPosition(), is(pos)); 810 scrollBy(-size * 2); 811 assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView))); 812 } 813 814 public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan) 815 throws Throwable { 816 setupByConfig(config); 817 if (firstChildMultiSpan) { 818 mAdapter.mFullSpanItems.add(0); 819 } 820 waitFirstLayout(); 821 // record all child positions 822 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 823 requestLayoutOnUIThread(mRecyclerView); 824 Map<Item, Rect> after = mLayoutManager.collectChildCoordinates(); 825 assertRectSetsEqual( 826 config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before, 827 after); 828 // scroll some to create inconsistency 829 View firstChild = mLayoutManager.getChildAt(0); 830 final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation 831 .getDecoratedStart(firstChild); 832 int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2; 833 if (config.mReverseLayout) { 834 distance *= -1; 835 } 836 scrollBy(distance); 837 waitForMainThread(2); 838 assertTrue("scroll by should move children", firstChildStartBeforeScroll != 839 mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild)); 840 before = mLayoutManager.collectChildCoordinates(); 841 mLayoutManager.expectLayouts(1); 842 requestLayoutOnUIThread(mRecyclerView); 843 mLayoutManager.waitForLayout(2); 844 after = mLayoutManager.collectChildCoordinates(); 845 assertRectSetsEqual(config + " simple re-layout after scroll", before, after); 846 } 847 } 848