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