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