1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package android.support.v7.widget; 19 20 21 import android.graphics.Rect; 22 import android.os.Looper; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.support.annotation.Nullable; 26 import android.support.v4.view.AccessibilityDelegateCompat; 27 import android.support.v4.view.accessibility.AccessibilityEventCompat; 28 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 29 import android.util.Log; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.accessibility.AccessibilityEvent; 33 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.BitSet; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.LinkedHashMap; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.UUID; 43 import java.util.concurrent.CountDownLatch; 44 import java.util.concurrent.TimeUnit; 45 import java.util.concurrent.atomic.AtomicInteger; 46 47 import static android.support.v7.widget.LayoutState.*; 48 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 49 import static android.support.v7.widget.StaggeredGridLayoutManager.*; 50 51 public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 52 53 private static final boolean DEBUG = false; 54 55 private static final int AVG_ITEM_PER_VIEW = 3; 56 57 private static final String TAG = "StaggeredGridLayoutManagerTest"; 58 59 volatile WrappedLayoutManager mLayoutManager; 60 61 GridTestAdapter mAdapter; 62 63 final List<Config> mBaseVariations = new ArrayList<Config>(); 64 65 @Override 66 protected void setUp() throws Exception { 67 super.setUp(); 68 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 69 for (boolean reverseLayout : new boolean[]{false, true}) { 70 for (int spanCount : new int[]{1, 3}) { 71 for (int gapStrategy : new int[]{GAP_HANDLING_NONE, 72 GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) { 73 mBaseVariations.add(new Config(orientation, reverseLayout, spanCount, 74 gapStrategy)); 75 } 76 } 77 } 78 } 79 } 80 81 void setupByConfig(Config config) throws Throwable { 82 mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation); 83 mRecyclerView = new RecyclerView(getActivity()); 84 mRecyclerView.setAdapter(mAdapter); 85 mRecyclerView.setHasFixedSize(true); 86 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, 87 config.mOrientation); 88 mLayoutManager.setGapStrategy(config.mGapStrategy); 89 mLayoutManager.setReverseLayout(config.mReverseLayout); 90 mRecyclerView.setLayoutManager(mLayoutManager); 91 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 92 @Override 93 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 94 RecyclerView.State state) { 95 try { 96 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 97 assertNotNull("view should have layout params assigned", lp); 98 assertNotNull("when item offsets are requested, view should have a valid span", 99 lp.mSpan); 100 } catch (Throwable t) { 101 postExceptionToInstrumentation(t); 102 } 103 } 104 }); 105 } 106 107 public void testAreAllStartsTheSame() throws Throwable { 108 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300)); 109 waitFirstLayout(); 110 smoothScrollToPosition(100); 111 mLayoutManager.expectLayouts(1); 112 mAdapter.deleteAndNotify(0, 2); 113 mLayoutManager.waitForLayout(2); 114 smoothScrollToPosition(0); 115 assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual()); 116 } 117 118 public void testAreAllEndsTheSame() throws Throwable { 119 setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300)); 120 waitFirstLayout(); 121 smoothScrollToPosition(100); 122 mLayoutManager.expectLayouts(1); 123 mAdapter.deleteAndNotify(0, 2); 124 mLayoutManager.waitForLayout(2); 125 smoothScrollToPosition(0); 126 assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual()); 127 } 128 129 public void testFindLastInUnevenDistribution() throws Throwable { 130 setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) 131 .itemCount(5)); 132 mAdapter.mOnBindCallback = new OnBindCallback() { 133 @Override 134 void onBoundItem(TestViewHolder vh, int position) { 135 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams(); 136 if (position == 1) { 137 lp.height = mRecyclerView.getHeight() - 10; 138 } else { 139 lp.height = 5; 140 } 141 vh.itemView.setMinimumHeight(0); 142 } 143 }; 144 waitFirstLayout(); 145 int[] into = new int[2]; 146 mLayoutManager.findFirstCompletelyVisibleItemPositions(into); 147 assertEquals("first completely visible item from span 0 should be 0", 0, into[0]); 148 assertEquals("first completely visible item from span 1 should be 1", 1, into[1]); 149 mLayoutManager.findLastCompletelyVisibleItemPositions(into); 150 assertEquals("last completely visible item from span 0 should be 4", 4, into[0]); 151 assertEquals("last completely visible item from span 1 should be 1", 1, into[1]); 152 assertEquals("first fully visible child should be at position", 153 0, mRecyclerView.getChildViewHolder(mLayoutManager. 154 findFirstVisibleItemClosestToStart(true, true)).getPosition()); 155 assertEquals("last fully visible child should be at position", 156 4, mRecyclerView.getChildViewHolder(mLayoutManager. 157 findFirstVisibleItemClosestToEnd(true, true)).getPosition()); 158 159 assertEquals("first visible child should be at position", 160 0, mRecyclerView.getChildViewHolder(mLayoutManager. 161 findFirstVisibleItemClosestToStart(false, true)).getPosition()); 162 assertEquals("last visible child should be at position", 163 4, mRecyclerView.getChildViewHolder(mLayoutManager. 164 findFirstVisibleItemClosestToEnd(false, true)).getPosition()); 165 166 } 167 168 public void testCustomWidthInHorizontal() throws Throwable { 169 customSizeInScrollDirectionTest( 170 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 171 } 172 173 public void testCustomHeightInVertical() throws Throwable { 174 customSizeInScrollDirectionTest( 175 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 176 } 177 178 public void customSizeInScrollDirectionTest(final Config config) throws Throwable { 179 setupByConfig(config); 180 final Map<View, Integer> sizeMap = new HashMap<View, Integer>(); 181 mAdapter.mOnBindCallback = new OnBindCallback() { 182 @Override 183 void onBoundItem(TestViewHolder vh, int position) { 184 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams(); 185 final int size = 1 + position * 5; 186 if (config.mOrientation == HORIZONTAL) { 187 layoutParams.width = size; 188 } else { 189 layoutParams.height = size; 190 } 191 sizeMap.put(vh.itemView, size); 192 if (position == 3) { 193 getLp(vh.itemView).setFullSpan(true); 194 } 195 } 196 197 @Override 198 boolean assignRandomSize() { 199 return false; 200 } 201 }; 202 waitFirstLayout(); 203 assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0); 204 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 205 View child = mRecyclerView.getChildAt(i); 206 final int size = config.mOrientation == HORIZONTAL ? child.getWidth() 207 : child.getHeight(); 208 assertEquals("child " + i + " should have the size specified in its layout params", 209 sizeMap.get(child).intValue(), size); 210 } 211 checkForMainThreadException(); 212 } 213 214 public void testRTL() throws Throwable { 215 for (boolean changeRtlAfter : new boolean[]{false, true}) { 216 for (Config config : mBaseVariations) { 217 rtlTest(config, changeRtlAfter); 218 removeRecyclerView(); 219 } 220 } 221 } 222 223 void rtlTest(Config config, boolean changeRtlAfter) throws Throwable { 224 if (config.mSpanCount == 1) { 225 config.mSpanCount = 2; 226 } 227 String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter; 228 setupByConfig(config.itemCount(5)); 229 if (changeRtlAfter) { 230 waitFirstLayout(); 231 mLayoutManager.expectLayouts(1); 232 mLayoutManager.setFakeRtl(true); 233 mLayoutManager.waitForLayout(2); 234 } else { 235 mLayoutManager.mFakeRTL = true; 236 waitFirstLayout(); 237 } 238 239 assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL()); 240 OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager); 241 View child0 = mLayoutManager.findViewByPosition(0); 242 View child1 = mLayoutManager.findViewByPosition(config.mOrientation == VERTICAL ? 1 243 : config.mSpanCount); 244 assertNotNull(logPrefix + " child position 0 should be laid out", child0); 245 assertNotNull(logPrefix + " child position 0 should be laid out", child1); 246 logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1); 247 if (config.mOrientation == VERTICAL || !config.mReverseLayout) { 248 assertTrue(logPrefix + " second child should be to the left of first child", 249 helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1)); 250 assertEquals(logPrefix + " first child should be right aligned", 251 helper.getDecoratedEnd(child0), helper.getEndAfterPadding()); 252 } else { 253 assertTrue(logPrefix + " first child should be to the left of second child", 254 helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0)); 255 assertEquals(logPrefix + " first child should be left aligned", 256 helper.getDecoratedStart(child0), helper.getStartAfterPadding()); 257 } 258 checkForMainThreadException(); 259 } 260 261 public void testGapHandlingWhenItemMovesToTop() throws Throwable { 262 gapHandlingWhenItemMovesToTopTest(); 263 } 264 265 public void testGapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable { 266 gapHandlingWhenItemMovesToTopTest(0); 267 } 268 269 public void testGapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable { 270 gapHandlingWhenItemMovesToTopTest(1); 271 } 272 273 public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable { 274 Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 275 config.itemCount(3); 276 setupByConfig(config); 277 mAdapter.mOnBindCallback = new OnBindCallback() { 278 @Override 279 void onBoundItem(TestViewHolder vh, int position) { 280 } 281 282 @Override 283 boolean assignRandomSize() { 284 return false; 285 } 286 }; 287 for (int i : fullSpanIndices) { 288 mAdapter.mFullSpanItems.add(i); 289 } 290 waitFirstLayout(); 291 mLayoutManager.expectLayouts(1); 292 mAdapter.moveItem(1, 0, true); 293 mLayoutManager.waitForLayout(2); 294 final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates(); 295 // move back. 296 mLayoutManager.expectLayouts(1); 297 mAdapter.moveItem(0, 1, true); 298 mLayoutManager.waitForLayout(2); 299 mLayoutManager.expectLayouts(2); 300 mAdapter.moveAndNotify(1, 0); 301 mLayoutManager.waitForLayout(2); 302 Thread.sleep(1000); 303 getInstrumentation().waitForIdleSync(); 304 checkForMainThreadException(); 305 // item should be positioned properly 306 assertRectSetsEqual("final position after a move", desiredPositions, 307 mLayoutManager.collectChildCoordinates()); 308 309 } 310 311 312 public void testScrollBackAndPreservePositions() throws Throwable { 313 for (boolean saveRestore : new boolean[]{false, true}) { 314 for (Config config : mBaseVariations) { 315 scrollBackAndPreservePositionsTest(config, saveRestore); 316 removeRecyclerView(); 317 } 318 } 319 } 320 321 public void scrollBackAndPreservePositionsTest(final Config config, 322 final boolean saveRestoreInBetween) 323 throws Throwable { 324 setupByConfig(config); 325 mAdapter.mOnBindCallback = new OnBindCallback() { 326 @Override 327 public void onBoundItem(TestViewHolder vh, int position) { 328 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams(); 329 lp.setFullSpan((position * 7) % (config.mSpanCount + 1) == 0); 330 } 331 }; 332 waitFirstLayout(); 333 final int[] globalPositions = new int[mAdapter.getItemCount()]; 334 Arrays.fill(globalPositions, Integer.MIN_VALUE); 335 final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10) 336 * (config.mReverseLayout ? -1 : 1); 337 338 final int[] globalPos = new int[1]; 339 runTestOnUiThread(new Runnable() { 340 @Override 341 public void run() { 342 int globalScrollPosition = 0; 343 while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) { 344 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 345 View child = mRecyclerView.getChildAt(i); 346 final int pos = mRecyclerView.getChildLayoutPosition(child); 347 if (globalPositions[pos] != Integer.MIN_VALUE) { 348 continue; 349 } 350 if (config.mReverseLayout) { 351 globalPositions[pos] = globalScrollPosition + 352 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child); 353 } else { 354 globalPositions[pos] = globalScrollPosition + 355 mLayoutManager.mPrimaryOrientation.getDecoratedStart(child); 356 } 357 } 358 globalScrollPosition += mLayoutManager.scrollBy(scrollStep, 359 mRecyclerView.mRecycler, mRecyclerView.mState); 360 } 361 if (DEBUG) { 362 Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions)); 363 } 364 globalPos[0] = globalScrollPosition; 365 } 366 }); 367 checkForMainThreadException(); 368 369 if (saveRestoreInBetween) { 370 saveRestore(config); 371 } 372 373 checkForMainThreadException(); 374 runTestOnUiThread(new Runnable() { 375 @Override 376 public void run() { 377 int globalScrollPosition = globalPos[0]; 378 // now scroll back and make sure global positions match 379 BitSet shouldTest = new BitSet(mAdapter.getItemCount()); 380 shouldTest.set(0, mAdapter.getItemCount() - 1, true); 381 String assertPrefix = config + ", restored in between:" + saveRestoreInBetween 382 + " global pos must match when scrolling in reverse for position "; 383 int scrollAmount = Integer.MAX_VALUE; 384 while (!shouldTest.isEmpty() && scrollAmount != 0) { 385 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 386 View child = mRecyclerView.getChildAt(i); 387 int pos = mRecyclerView.getChildLayoutPosition(child); 388 if (!shouldTest.get(pos)) { 389 continue; 390 } 391 shouldTest.clear(pos); 392 int globalPos; 393 if (config.mReverseLayout) { 394 globalPos = globalScrollPosition + 395 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child); 396 } else { 397 globalPos = globalScrollPosition + 398 mLayoutManager.mPrimaryOrientation.getDecoratedStart(child); 399 } 400 assertEquals(assertPrefix + pos, 401 globalPositions[pos], globalPos); 402 } 403 scrollAmount = mLayoutManager.scrollBy(-scrollStep, 404 mRecyclerView.mRecycler, mRecyclerView.mState); 405 globalScrollPosition += scrollAmount; 406 } 407 assertTrue("all views should be seen", shouldTest.isEmpty()); 408 } 409 }); 410 checkForMainThreadException(); 411 } 412 413 public void testScrollToPositionWithPredictive() throws Throwable { 414 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 415 removeRecyclerView(); 416 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 417 LinearLayoutManager.INVALID_OFFSET); 418 removeRecyclerView(); 419 scrollToPositionWithPredictive(9, 20); 420 removeRecyclerView(); 421 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 422 423 } 424 425 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 426 throws Throwable { 427 setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL, 428 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE)); 429 waitFirstLayout(); 430 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 431 @Override 432 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 433 RecyclerView rv = mLayoutManager.mRecyclerView; 434 if (state.isPreLayout()) { 435 assertEquals("pending scroll position should still be pending", 436 scrollPosition, mLayoutManager.mPendingScrollPosition); 437 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 438 assertEquals("pending scroll position offset should still be pending", 439 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 440 } 441 } else { 442 RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition); 443 assertNotNull("scroll to position should work", vh); 444 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 445 assertEquals("scroll offset should be applied properly", 446 mLayoutManager.getPaddingTop() + scrollOffset 447 + ((RecyclerView.LayoutParams) vh.itemView 448 .getLayoutParams()).topMargin, 449 mLayoutManager.getDecoratedTop(vh.itemView)); 450 } 451 } 452 } 453 }; 454 mLayoutManager.expectLayouts(2); 455 runTestOnUiThread(new Runnable() { 456 @Override 457 public void run() { 458 try { 459 mAdapter.addAndNotify(0, 1); 460 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 461 mLayoutManager.scrollToPosition(scrollPosition); 462 } else { 463 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 464 scrollOffset); 465 } 466 467 } catch (Throwable throwable) { 468 throwable.printStackTrace(); 469 } 470 471 } 472 }); 473 mLayoutManager.waitForLayout(2); 474 checkForMainThreadException(); 475 } 476 477 LayoutParams getLp(View view) { 478 return (LayoutParams) view.getLayoutParams(); 479 } 480 481 public void testGetFirstLastChildrenTest() throws Throwable { 482 for (boolean provideArr : new boolean[]{true, false}) { 483 for (Config config : mBaseVariations) { 484 getFirstLastChildrenTest(config, provideArr); 485 removeRecyclerView(); 486 } 487 } 488 } 489 490 public void getFirstLastChildrenTest(final Config config, final boolean provideArr) 491 throws Throwable { 492 setupByConfig(config); 493 waitFirstLayout(); 494 Runnable viewInBoundsTest = new Runnable() { 495 @Override 496 public void run() { 497 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); 498 final String boundsLog = mLayoutManager.getBoundsLog(); 499 VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount()); 500 queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager 501 .findFirstVisibleItemClosestToStart(false, true); 502 queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager 503 .findFirstVisibleItemClosestToEnd(false, true); 504 queryResult.firstFullyVisiblePositions = mLayoutManager 505 .findFirstCompletelyVisibleItemPositions( 506 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 507 queryResult.firstVisiblePositions = mLayoutManager 508 .findFirstVisibleItemPositions( 509 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 510 queryResult.lastFullyVisiblePositions = mLayoutManager 511 .findLastCompletelyVisibleItemPositions( 512 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 513 queryResult.lastVisiblePositions = mLayoutManager 514 .findLastVisibleItemPositions( 515 provideArr ? new int[mLayoutManager.getSpanCount()] : null); 516 assertEquals(config + ":\nfirst visible child should match traversal result\n" 517 + "traversed:" + visibleChildren + "\n" 518 + "queried:" + queryResult + "\n" 519 + boundsLog, visibleChildren, queryResult 520 ); 521 } 522 }; 523 runTestOnUiThread(viewInBoundsTest); 524 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 525 // case 526 final int scrollPosition = mAdapter.getItemCount(); 527 runTestOnUiThread(new Runnable() { 528 @Override 529 public void run() { 530 mRecyclerView.smoothScrollToPosition(scrollPosition); 531 } 532 }); 533 while (mLayoutManager.isSmoothScrolling() || 534 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 535 runTestOnUiThread(viewInBoundsTest); 536 checkForMainThreadException(); 537 Thread.sleep(400); 538 } 539 // delete all items 540 mLayoutManager.expectLayouts(2); 541 mAdapter.deleteAndNotify(0, mAdapter.getItemCount()); 542 mLayoutManager.waitForLayout(2); 543 // test empty case 544 runTestOnUiThread(viewInBoundsTest); 545 // set a new adapter with huge items to test full bounds check 546 mLayoutManager.expectLayouts(1); 547 final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace(); 548 final TestAdapter newAdapter = new TestAdapter(100) { 549 @Override 550 public void onBindViewHolder(TestViewHolder holder, 551 int position) { 552 super.onBindViewHolder(holder, position); 553 if (config.mOrientation == LinearLayoutManager.HORIZONTAL) { 554 holder.itemView.setMinimumWidth(totalSpace + 100); 555 } else { 556 holder.itemView.setMinimumHeight(totalSpace + 100); 557 } 558 } 559 }; 560 runTestOnUiThread(new Runnable() { 561 @Override 562 public void run() { 563 mRecyclerView.setAdapter(newAdapter); 564 } 565 }); 566 mLayoutManager.waitForLayout(2); 567 runTestOnUiThread(viewInBoundsTest); 568 checkForMainThreadException(); 569 570 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 571 // case 572 runTestOnUiThread(new Runnable() { 573 @Override 574 public void run() { 575 final int diff; 576 if (config.mReverseLayout) { 577 diff = -1; 578 } else { 579 diff = 1; 580 } 581 final int distance = diff * 10; 582 if (config.mOrientation == HORIZONTAL) { 583 mRecyclerView.scrollBy(distance, 0); 584 } else { 585 mRecyclerView.scrollBy(0, distance); 586 } 587 } 588 }); 589 runTestOnUiThread(viewInBoundsTest); 590 checkForMainThreadException(); 591 } 592 593 public void testMoveGapHandling() throws Throwable { 594 Config config = new Config().spanCount(2).itemCount(40); 595 setupByConfig(config); 596 waitFirstLayout(); 597 mLayoutManager.expectLayouts(2); 598 mAdapter.moveAndNotify(4, 1); 599 mLayoutManager.waitForLayout(2); 600 assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix()); 601 } 602 603 public void testUpdateAfterFullSpan() throws Throwable { 604 updateAfterFullSpanGapHandlingTest(0); 605 } 606 607 public void testUpdateAfterFullSpan2() throws Throwable { 608 updateAfterFullSpanGapHandlingTest(20); 609 } 610 611 public void testTemporaryGapHandling() throws Throwable { 612 int fullSpanIndex = 200; 613 setupByConfig(new Config().spanCount(2).itemCount(500)); 614 mAdapter.mFullSpanItems.add(fullSpanIndex); 615 waitFirstLayout(); 616 smoothScrollToPosition(fullSpanIndex + 200);// go far away 617 assertNull("test sanity. full span item should not be visible", 618 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex)); 619 mLayoutManager.expectLayouts(1); 620 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 621 mLayoutManager.waitForLayout(1); 622 smoothScrollToPosition(0); 623 mLayoutManager.expectLayouts(1); 624 smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1)); 625 String log = mLayoutManager.layoutToString("post gap"); 626 mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a " 627 + "relayout " + log, 2); 628 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 629 assertNotNull("full span item should be there:\n" + log, fullSpan); 630 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 631 assertNotNull("next view should be there\n" + log, view1); 632 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 633 assertNotNull("+2 view should be there\n" + log, view2); 634 635 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 636 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 637 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 638 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 639 assertEquals("no gap between span and view 1", 640 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 641 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 642 assertEquals("no gap between span and view 2", 643 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 644 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 645 } 646 647 public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable { 648 setupByConfig(new Config().spanCount(2).itemCount(100)); 649 mAdapter.mFullSpanItems.add(fullSpanIndex); 650 waitFirstLayout(); 651 smoothScrollToPosition(fullSpanIndex + 30); 652 mLayoutManager.expectLayouts(1); 653 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 654 mLayoutManager.waitForLayout(1); 655 smoothScrollToPosition(fullSpanIndex); 656 // give it some time to fix the gap 657 Thread.sleep(500); 658 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 659 660 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 661 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 662 663 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 664 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 665 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 666 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 667 assertEquals("no gap between span and view 1", 668 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 669 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 670 assertEquals("no gap between span and view 2", 671 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 672 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 673 } 674 675 public void testInnerGapHandling() throws Throwable { 676 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE); 677 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 678 } 679 680 public void innerGapHandlingTest(int strategy) throws Throwable { 681 Config config = new Config().spanCount(3).itemCount(500); 682 setupByConfig(config); 683 mLayoutManager.setGapStrategy(strategy); 684 mAdapter.mFullSpanItems.add(100); 685 mAdapter.mFullSpanItems.add(104); 686 mAdapter.mViewsHaveEqualSize = true; 687 mAdapter.mOnBindCallback = new OnBindCallback() { 688 @Override 689 void onBoundItem(TestViewHolder vh, int position) { 690 691 } 692 693 @Override 694 void onCreatedViewHolder(TestViewHolder vh) { 695 super.onCreatedViewHolder(vh); 696 //make sure we have enough views 697 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5; 698 } 699 }; 700 waitFirstLayout(); 701 mLayoutManager.expectLayouts(1); 702 scrollToPosition(400); 703 mLayoutManager.waitForLayout(2); 704 View view400 = mLayoutManager.findViewByPosition(400); 705 assertNotNull("test sanity, scrollToPos should succeed", view400); 706 assertTrue("test sanity, view should be visible top", 707 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >= 708 mLayoutManager.mPrimaryOrientation.getStartAfterPadding()); 709 assertTrue("test sanity, view should be visible bottom", 710 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <= 711 mLayoutManager.mPrimaryOrientation.getEndAfterPadding()); 712 mLayoutManager.expectLayouts(2); 713 mAdapter.addAndNotify(101, 1); 714 mLayoutManager.waitForLayout(2); 715 checkForMainThreadException(); 716 if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 717 mLayoutManager.expectLayouts(1); 718 } 719 // state 720 // now smooth scroll to 99 to trigger a layout around 100 721 mLayoutManager.validateChildren(); 722 smoothScrollToPosition(99); 723 switch (strategy) { 724 case GAP_HANDLING_NONE: 725 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0}, 726 new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2}, 727 new int[]{105, 0}); 728 break; 729 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS: 730 mLayoutManager.waitForLayout(2); 731 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0}, 732 new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0}); 733 break; 734 } 735 736 } 737 738 public void testFullSizeSpans() throws Throwable { 739 Config config = new Config().spanCount(5).itemCount(30); 740 setupByConfig(config); 741 mAdapter.mFullSpanItems.add(3); 742 waitFirstLayout(); 743 assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2}, 744 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2}, 745 new int[]{7, 3}, new int[]{8, 4}); 746 } 747 748 void assertSpans(String msg, int[]... childSpanTuples) { 749 msg = msg + mLayoutManager.layoutToString("\n\n"); 750 for (int i = 0; i < childSpanTuples.length; i++) { 751 assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]); 752 } 753 } 754 755 void assertSpan(String msg, int childPosition, int expectedSpan) { 756 View view = mLayoutManager.findViewByPosition(childPosition); 757 assertNotNull(msg + " view at position " + childPosition + " should exists", view); 758 assertEquals(msg + "[child:" + childPosition + "]", expectedSpan, 759 getLp(view).mSpan.mIndex); 760 } 761 762 public void testGapAtTheBeginning() throws Throwable { 763 for (Config config : mBaseVariations) { 764 for (int deleteCount = 1; deleteCount < config.mSpanCount * 2; deleteCount++) { 765 for (int deletePosition = config.mSpanCount - 1; 766 deletePosition < config.mSpanCount + 2; deletePosition++) { 767 gapAtTheBeginningOfTheListTest(config, deletePosition, deleteCount); 768 removeRecyclerView(); 769 } 770 } 771 } 772 } 773 774 public void gapAtTheBeginningOfTheListTest(final Config config, int deletePosition, 775 int deleteCount) throws Throwable { 776 if (config.mSpanCount < 2 || config.mGapStrategy == GAP_HANDLING_NONE) { 777 return; 778 } 779 if (config.mItemCount < 100) { 780 config.itemCount(100); 781 } 782 final String logPrefix = config + ", deletePos:" + deletePosition + ", deleteCount:" 783 + deleteCount; 784 setupByConfig(config); 785 final RecyclerView.Adapter adapter = mAdapter; 786 waitFirstLayout(); 787 // scroll far away 788 smoothScrollToPosition(config.mItemCount / 2); 789 checkForMainThreadException(); 790 // assert to be deleted child is not visible 791 assertNull(logPrefix + " test sanity, to be deleted child should be invisible", 792 mRecyclerView.findViewHolderForLayoutPosition(deletePosition)); 793 // delete the child and notify 794 mAdapter.deleteAndNotify(deletePosition, deleteCount); 795 getInstrumentation().waitForIdleSync(); 796 mLayoutManager.expectLayouts(1); 797 smoothScrollToPosition(0); 798 mLayoutManager.waitForLayout(2); 799 checkForMainThreadException(); 800 // due to data changes, first item may become visible before others which will cause 801 // smooth scrolling to stop. Triggering it twice more is a naive hack. 802 // Until we have time to consider it as a bug, this is the only workaround. 803 smoothScrollToPosition(0); 804 checkForMainThreadException(); 805 Thread.sleep(300); 806 smoothScrollToPosition(0); 807 checkForMainThreadException(); 808 Thread.sleep(500); 809 // some animations should happen and we should recover layout 810 final Map<Item, Rect> actualCoords = mLayoutManager.collectChildCoordinates(); 811 812 // now layout another RV with same adapter 813 removeRecyclerView(); 814 setupByConfig(config); 815 mRecyclerView.setAdapter(adapter);// use same adapter so that items can be matched 816 waitFirstLayout(); 817 final Map<Item, Rect> desiredCoords = mLayoutManager.collectChildCoordinates(); 818 assertRectSetsEqual(logPrefix + " when an item from the start of the list is deleted, " 819 + "layout should recover the state once scrolling is stopped", 820 desiredCoords, actualCoords); 821 checkForMainThreadException(); 822 } 823 824 public void testPartialSpanInvalidation() throws Throwable { 825 Config config = new Config().spanCount(5).itemCount(100); 826 setupByConfig(config); 827 for (int i = 20; i < mAdapter.getItemCount(); i += 20) { 828 mAdapter.mFullSpanItems.add(i); 829 } 830 waitFirstLayout(); 831 smoothScrollToPosition(50); 832 int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30]; 833 mAdapter.changeAndNotify(15, 2); 834 Thread.sleep(200); 835 assertEquals("Invalidation should happen within full span item boundaries", prevSpanId, 836 mLayoutManager.mLazySpanLookup.mData[30]); 837 assertEquals("item in invalidated range should have clear span id", 838 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 839 smoothScrollToPosition(85); 840 int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85); 841 mAdapter.deleteAndNotify(55, 2); 842 Thread.sleep(200); 843 assertEquals("item in invalidated range should have clear span id", 844 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 845 int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83); 846 assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans, 847 newSpans, 0, 0, newSpans.length); 848 } 849 850 // Same as Arrays.copyOfRange but for API 7 851 private int[] copyOfRange(int[] original, int from, int to) { 852 int newLength = to - from; 853 if (newLength < 0) { 854 throw new IllegalArgumentException(from + " > " + to); 855 } 856 int[] copy = new int[newLength]; 857 System.arraycopy(original, from, copy, 0, 858 Math.min(original.length - from, newLength)); 859 return copy; 860 } 861 862 public void testSpanReassignmentsOnItemChange() throws Throwable { 863 Config config = new Config().spanCount(5); 864 setupByConfig(config); 865 waitFirstLayout(); 866 smoothScrollToPosition(mAdapter.getItemCount() / 2); 867 final int changePosition = mAdapter.getItemCount() / 4; 868 mLayoutManager.expectLayouts(1); 869 mAdapter.changeAndNotify(changePosition, 1); 870 mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated", 871 1); 872 // delete an item before visible area 873 int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2; 874 assertTrue("test sanity", deletedPosition >= 0); 875 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 876 if (DEBUG) { 877 Log.d(TAG, "before:"); 878 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 879 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue()); 880 } 881 } 882 mLayoutManager.expectLayouts(1); 883 mAdapter.deleteAndNotify(deletedPosition, 1); 884 mLayoutManager.waitForLayout(2); 885 assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it " 886 + "should not affect the layout if it is not visible", before, 887 mLayoutManager.collectChildCoordinates() 888 ); 889 deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2)); 890 mLayoutManager.expectLayouts(1); 891 mAdapter.deleteAndNotify(deletedPosition, 1); 892 mLayoutManager.waitForLayout(2); 893 assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the " 894 + "layout", before, mLayoutManager.collectChildCoordinates()); 895 } 896 897 void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start, int end) { 898 for (int i = start; i < end; i++) { 899 assertEquals(msg + " ind:" + i, set1[i], set2[i]); 900 } 901 } 902 903 void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2, 904 int length) { 905 for (int i = 0; i < length; i++) { 906 assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i], 907 set2[start2 + i]); 908 } 909 } 910 911 public void testViewSnapping() throws Throwable { 912 for (Config config : mBaseVariations) { 913 viewSnapTest(config.itemCount(config.mSpanCount + 1)); 914 removeRecyclerView(); 915 } 916 } 917 918 public void viewSnapTest(final Config config) throws Throwable { 919 setupByConfig(config); 920 mAdapter.mOnBindCallback = new OnBindCallback() { 921 @Override 922 void onBoundItem(TestViewHolder vh, int position) { 923 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams(); 924 if (config.mOrientation == HORIZONTAL) { 925 lp.width = mRecyclerView.getWidth() / 3; 926 } else { 927 lp.height = mRecyclerView.getHeight() / 3; 928 } 929 } 930 @Override 931 boolean assignRandomSize() { 932 return false; 933 } 934 }; 935 waitFirstLayout(); 936 // run these tests twice. once initial layout, once after scroll 937 String logSuffix = ""; 938 for (int i = 0; i < 2; i++) { 939 Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates(); 940 Rect recyclerViewBounds = getDecoratedRecyclerViewBounds(); 941 // workaround for SGLM's span distribution issue. Right now, it may leave gaps so we 942 // avoid it by setting its layout params directly 943 if(config.mOrientation == HORIZONTAL) { 944 recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount; 945 } else { 946 recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount; 947 } 948 949 Rect usedLayoutBounds = new Rect(); 950 for (Rect rect : itemRectMap.values()) { 951 usedLayoutBounds.union(rect); 952 } 953 954 if (DEBUG) { 955 Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config); 956 } 957 if (config.mOrientation == VERTICAL) { 958 assertEquals(config + " there should be no gap on left" + logSuffix, 959 usedLayoutBounds.left, recyclerViewBounds.left); 960 assertEquals(config + " there should be no gap on right" + logSuffix, 961 usedLayoutBounds.right, recyclerViewBounds.right); 962 if (config.mReverseLayout) { 963 assertEquals(config + " there should be no gap on bottom" + logSuffix, 964 usedLayoutBounds.bottom, recyclerViewBounds.bottom); 965 assertTrue(config + " there should be some gap on top" + logSuffix, 966 usedLayoutBounds.top > recyclerViewBounds.top); 967 } else { 968 assertEquals(config + " there should be no gap on top" + logSuffix, 969 usedLayoutBounds.top, recyclerViewBounds.top); 970 assertTrue(config + " there should be some gap at the bottom" + logSuffix, 971 usedLayoutBounds.bottom < recyclerViewBounds.bottom); 972 } 973 } else { 974 assertEquals(config + " there should be no gap on top" + logSuffix, 975 usedLayoutBounds.top, recyclerViewBounds.top); 976 assertEquals(config + " there should be no gap at the bottom" + logSuffix, 977 usedLayoutBounds.bottom, recyclerViewBounds.bottom); 978 if (config.mReverseLayout) { 979 assertEquals(config + " there should be no on right" + logSuffix, 980 usedLayoutBounds.right, recyclerViewBounds.right); 981 assertTrue(config + " there should be some gap on left" + logSuffix, 982 usedLayoutBounds.left > recyclerViewBounds.left); 983 } else { 984 assertEquals(config + " there should be no gap on left" + logSuffix, 985 usedLayoutBounds.left, recyclerViewBounds.left); 986 assertTrue(config + " there should be some gap on right" + logSuffix, 987 usedLayoutBounds.right < recyclerViewBounds.right); 988 } 989 } 990 final int scroll = config.mReverseLayout ? -500 : 500; 991 scrollBy(scroll); 992 logSuffix = " scrolled " + scroll; 993 } 994 995 } 996 997 public void testSpanCountChangeOnRestoreSavedState() throws Throwable { 998 Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE); 999 setupByConfig(config); 1000 waitFirstLayout(); 1001 1002 int beforeChildCount = mLayoutManager.getChildCount(); 1003 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 1004 // we append a suffix to the parcelable to test out of bounds 1005 String parcelSuffix = UUID.randomUUID().toString(); 1006 Parcel parcel = Parcel.obtain(); 1007 savedState.writeToParcel(parcel, 0); 1008 parcel.writeString(parcelSuffix); 1009 removeRecyclerView(); 1010 // reset for reading 1011 parcel.setDataPosition(0); 1012 // re-create 1013 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 1014 removeRecyclerView(); 1015 1016 RecyclerView restored = new RecyclerView(getActivity()); 1017 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation); 1018 mLayoutManager.setReverseLayout(config.mReverseLayout); 1019 mLayoutManager.setGapStrategy(config.mGapStrategy); 1020 restored.setLayoutManager(mLayoutManager); 1021 // use the same adapter for Rect matching 1022 restored.setAdapter(mAdapter); 1023 restored.onRestoreInstanceState(savedState); 1024 mLayoutManager.setSpanCount(1); 1025 mLayoutManager.expectLayouts(1); 1026 setRecyclerView(restored); 1027 mLayoutManager.waitForLayout(2); 1028 assertEquals("on saved state, reverse layout should be preserved", 1029 config.mReverseLayout, mLayoutManager.getReverseLayout()); 1030 assertEquals("on saved state, orientation should be preserved", 1031 config.mOrientation, mLayoutManager.getOrientation()); 1032 assertEquals("after setting new span count, layout manager should keep new value", 1033 1, mLayoutManager.getSpanCount()); 1034 assertEquals("on saved state, gap strategy should be preserved", 1035 config.mGapStrategy, mLayoutManager.getGapStrategy()); 1036 assertTrue("when span count is dramatically changed after restore, # of child views " 1037 + "should change", beforeChildCount > mLayoutManager.getChildCount()); 1038 // make sure LLM can layout all children. is some span info is leaked, this would crash 1039 smoothScrollToPosition(mAdapter.getItemCount() - 1); 1040 } 1041 1042 public void testSavedState() throws Throwable { 1043 PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ 1044 new PostLayoutRunnable() { 1045 @Override 1046 public void run() throws Throwable { 1047 // do nothing 1048 } 1049 1050 @Override 1051 public String describe() { 1052 return "doing nothing"; 1053 } 1054 }, 1055 new PostLayoutRunnable() { 1056 @Override 1057 public void run() throws Throwable { 1058 mLayoutManager.expectLayouts(1); 1059 scrollToPosition(mAdapter.getItemCount() * 3 / 4); 1060 mLayoutManager.waitForLayout(2); 1061 } 1062 1063 @Override 1064 public String describe() { 1065 return "scroll to position " + (mAdapter == null ? "" : 1066 mAdapter.getItemCount() * 3 / 4); 1067 } 1068 }, 1069 new PostLayoutRunnable() { 1070 @Override 1071 public void run() throws Throwable { 1072 mLayoutManager.expectLayouts(1); 1073 scrollToPositionWithOffset(mAdapter.getItemCount() / 3, 1074 50); 1075 mLayoutManager.waitForLayout(2); 1076 } 1077 1078 @Override 1079 public String describe() { 1080 return "scroll to position " + (mAdapter == null ? "" : 1081 mAdapter.getItemCount() / 3) + "with positive offset"; 1082 } 1083 }, 1084 new PostLayoutRunnable() { 1085 @Override 1086 public void run() throws Throwable { 1087 mLayoutManager.expectLayouts(1); 1088 scrollToPositionWithOffset(mAdapter.getItemCount() * 2 / 3, 1089 -50); 1090 mLayoutManager.waitForLayout(2); 1091 } 1092 1093 @Override 1094 public String describe() { 1095 return "scroll to position with negative offset"; 1096 } 1097 } 1098 }; 1099 boolean[] waitForLayoutOptions = new boolean[]{false, true}; 1100 boolean[] loadDataAfterRestoreOptions = new boolean[]{false, true}; 1101 List<Config> testVariations = new ArrayList<Config>(); 1102 testVariations.addAll(mBaseVariations); 1103 for (Config config : mBaseVariations) { 1104 if (config.mSpanCount < 2) { 1105 continue; 1106 } 1107 final Config clone = (Config) config.clone(); 1108 clone.mItemCount = clone.mSpanCount - 1; 1109 testVariations.add(clone); 1110 } 1111 for (Config config : testVariations) { 1112 for (PostLayoutRunnable runnable : postLayoutOptions) { 1113 for (boolean waitForLayout : waitForLayoutOptions) { 1114 for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) { 1115 savedStateTest(config, waitForLayout, loadDataAfterRestore, runnable); 1116 removeRecyclerView(); 1117 checkForMainThreadException(); 1118 } 1119 } 1120 } 1121 } 1122 } 1123 1124 private void saveRestore(final Config config) throws Throwable { 1125 runTestOnUiThread(new Runnable() { 1126 @Override 1127 public void run() { 1128 try { 1129 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 1130 // we append a suffix to the parcelable to test out of bounds 1131 String parcelSuffix = UUID.randomUUID().toString(); 1132 Parcel parcel = Parcel.obtain(); 1133 savedState.writeToParcel(parcel, 0); 1134 parcel.writeString(parcelSuffix); 1135 removeRecyclerView(); 1136 // reset for reading 1137 parcel.setDataPosition(0); 1138 // re-create 1139 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 1140 RecyclerView restored = new RecyclerView(getActivity()); 1141 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, 1142 config.mOrientation); 1143 mLayoutManager.setGapStrategy(config.mGapStrategy); 1144 restored.setLayoutManager(mLayoutManager); 1145 // use the same adapter for Rect matching 1146 restored.setAdapter(mAdapter); 1147 restored.onRestoreInstanceState(savedState); 1148 if (Looper.myLooper() == Looper.getMainLooper()) { 1149 mLayoutManager.expectLayouts(1); 1150 setRecyclerView(restored); 1151 } else { 1152 mLayoutManager.expectLayouts(1); 1153 setRecyclerView(restored); 1154 mLayoutManager.waitForLayout(2); 1155 } 1156 } catch (Throwable t) { 1157 postExceptionToInstrumentation(t); 1158 } 1159 } 1160 }); 1161 checkForMainThreadException(); 1162 } 1163 1164 public void savedStateTest(Config config, boolean waitForLayout, boolean loadDataAfterRestore, 1165 PostLayoutRunnable postLayoutOperations) 1166 throws Throwable { 1167 if (DEBUG) { 1168 Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " 1169 + config + " post layout action " + postLayoutOperations.describe()); 1170 } 1171 setupByConfig(config); 1172 if (loadDataAfterRestore) { 1173 // We are going to re-create items, force non-random item size. 1174 mAdapter.mOnBindCallback = new OnBindCallback() { 1175 @Override 1176 void onBoundItem(TestViewHolder vh, int position) { 1177 } 1178 1179 boolean assignRandomSize() { 1180 return false; 1181 } 1182 }; 1183 } 1184 waitFirstLayout(); 1185 if (waitForLayout) { 1186 postLayoutOperations.run(); 1187 // ugly thread sleep but since post op is anything, we need to give it time to settle. 1188 Thread.sleep(500); 1189 } 1190 getInstrumentation().waitForIdleSync(); 1191 final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt(); 1192 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1193 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 1194 // we append a suffix to the parcelable to test out of bounds 1195 String parcelSuffix = UUID.randomUUID().toString(); 1196 Parcel parcel = Parcel.obtain(); 1197 savedState.writeToParcel(parcel, 0); 1198 parcel.writeString(parcelSuffix); 1199 removeRecyclerView(); 1200 // reset for reading 1201 parcel.setDataPosition(0); 1202 // re-create 1203 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 1204 removeRecyclerView(); 1205 1206 final int itemCount = mAdapter.getItemCount(); 1207 if (loadDataAfterRestore) { 1208 mAdapter.deleteAndNotify(0, itemCount); 1209 } 1210 1211 RecyclerView restored = new RecyclerView(getActivity()); 1212 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation); 1213 mLayoutManager.setGapStrategy(config.mGapStrategy); 1214 restored.setLayoutManager(mLayoutManager); 1215 // use the same adapter for Rect matching 1216 restored.setAdapter(mAdapter); 1217 restored.onRestoreInstanceState(savedState); 1218 1219 if (loadDataAfterRestore) { 1220 mAdapter.addAndNotify(itemCount); 1221 } 1222 1223 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 1224 parcel.readString()); 1225 mLayoutManager.expectLayouts(1); 1226 setRecyclerView(restored); 1227 mLayoutManager.waitForLayout(2); 1228 assertEquals(config + " on saved state, reverse layout should be preserved", 1229 config.mReverseLayout, mLayoutManager.getReverseLayout()); 1230 assertEquals(config + " on saved state, orientation should be preserved", 1231 config.mOrientation, mLayoutManager.getOrientation()); 1232 assertEquals(config + " on saved state, span count should be preserved", 1233 config.mSpanCount, mLayoutManager.getSpanCount()); 1234 assertEquals(config + " on saved state, gap strategy should be preserved", 1235 config.mGapStrategy, mLayoutManager.getGapStrategy()); 1236 assertEquals(config + " on saved state, first completely visible child position should" 1237 + " be preserved", firstCompletelyVisiblePosition, 1238 mLayoutManager.findFirstVisibleItemPositionInt()); 1239 if (waitForLayout) { 1240 final boolean strictItemEquality = !loadDataAfterRestore; 1241 assertRectSetsEqual(config + "\npost layout op:" + postLayoutOperations.describe() 1242 + ": on restore, previous view positions should be preserved", 1243 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 1244 } 1245 // TODO add tests for changing values after restore before layout 1246 } 1247 1248 public void testScrollAndClear() throws Throwable { 1249 setupByConfig(new Config()); 1250 waitFirstLayout(); 1251 1252 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 1253 1254 mLayoutManager.expectLayouts(1); 1255 runTestOnUiThread(new Runnable() { 1256 @Override 1257 public void run() { 1258 mLayoutManager.scrollToPositionWithOffset(1, 0); 1259 mAdapter.clearOnUIThread(); 1260 } 1261 }); 1262 mLayoutManager.waitForLayout(2); 1263 1264 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 1265 } 1266 1267 public void testScrollToPositionWithOffset() throws Throwable { 1268 for (Config config : mBaseVariations) { 1269 scrollToPositionWithOffsetTest(config); 1270 removeRecyclerView(); 1271 } 1272 } 1273 1274 public void scrollToPositionWithOffsetTest(Config config) throws Throwable { 1275 setupByConfig(config); 1276 waitFirstLayout(); 1277 OrientationHelper orientationHelper = OrientationHelper 1278 .createOrientationHelper(mLayoutManager, config.mOrientation); 1279 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 1280 // try scrolling towards head, should not affect anything 1281 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1282 scrollToPositionWithOffset(0, 20); 1283 assertRectSetsEqual(config + " trying to over scroll with offset should be no-op", 1284 before, mLayoutManager.collectChildCoordinates()); 1285 // try offsetting some visible children 1286 int testCount = 10; 1287 while (testCount-- > 0) { 1288 // get middle child 1289 final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2); 1290 final int position = mRecyclerView.getChildLayoutPosition(child); 1291 final int startOffset = config.mReverseLayout ? 1292 orientationHelper.getEndAfterPadding() - orientationHelper 1293 .getDecoratedEnd(child) 1294 : orientationHelper.getDecoratedStart(child) - orientationHelper 1295 .getStartAfterPadding(); 1296 final int scrollOffset = startOffset / 2; 1297 mLayoutManager.expectLayouts(1); 1298 scrollToPositionWithOffset(position, scrollOffset); 1299 mLayoutManager.waitForLayout(2); 1300 final int finalOffset = config.mReverseLayout ? 1301 orientationHelper.getEndAfterPadding() - orientationHelper 1302 .getDecoratedEnd(child) 1303 : orientationHelper.getDecoratedStart(child) - orientationHelper 1304 .getStartAfterPadding(); 1305 assertEquals(config + " scroll with offset on a visible child should work fine", 1306 scrollOffset, finalOffset); 1307 } 1308 1309 // try scrolling to invisible children 1310 testCount = 10; 1311 // we test above and below, one by one 1312 int offsetMultiplier = -1; 1313 while (testCount-- > 0) { 1314 final TargetTuple target = findInvisibleTarget(config); 1315 mLayoutManager.expectLayouts(1); 1316 final int offset = offsetMultiplier 1317 * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3; 1318 scrollToPositionWithOffset(target.mPosition, offset); 1319 mLayoutManager.waitForLayout(2); 1320 final View child = mLayoutManager.findViewByPosition(target.mPosition); 1321 assertNotNull(config + " scrolling to a mPosition with offset " + offset 1322 + " should layout it", child); 1323 final Rect bounds = mLayoutManager.getViewBounds(child); 1324 if (DEBUG) { 1325 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in " 1326 + layoutBounds + " with offset " + offset); 1327 } 1328 1329 if (config.mReverseLayout) { 1330 assertEquals(config + " when scrolling with offset to an invisible in reverse " 1331 + "layout, its end should align with recycler view's end - offset", 1332 orientationHelper.getEndAfterPadding() - offset, 1333 orientationHelper.getDecoratedEnd(child) 1334 ); 1335 } else { 1336 assertEquals(config + " when scrolling with offset to an invisible child in normal" 1337 + " layout its start should align with recycler view's start + " 1338 + "offset", 1339 orientationHelper.getStartAfterPadding() + offset, 1340 orientationHelper.getDecoratedStart(child) 1341 ); 1342 } 1343 offsetMultiplier *= -1; 1344 } 1345 } 1346 1347 public void testScrollToPosition() throws Throwable { 1348 for (Config config : mBaseVariations) { 1349 scrollToPositionTest(config); 1350 removeRecyclerView(); 1351 } 1352 } 1353 1354 private TargetTuple findInvisibleTarget(Config config) { 1355 int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE; 1356 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 1357 View child = mLayoutManager.getChildAt(i); 1358 int position = mRecyclerView.getChildLayoutPosition(child); 1359 if (position < minPosition) { 1360 minPosition = position; 1361 } 1362 if (position > maxPosition) { 1363 maxPosition = position; 1364 } 1365 } 1366 final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2; 1367 final int headTarget = minPosition / 2; 1368 final int target; 1369 // where will the child come from ? 1370 final int itemLayoutDirection; 1371 if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) { 1372 target = tailTarget; 1373 itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END; 1374 } else { 1375 target = headTarget; 1376 itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START; 1377 } 1378 if (DEBUG) { 1379 Log.d(TAG, 1380 config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition); 1381 } 1382 return new TargetTuple(target, itemLayoutDirection); 1383 } 1384 1385 public void scrollToPositionTest(Config config) throws Throwable { 1386 setupByConfig(config); 1387 waitFirstLayout(); 1388 OrientationHelper orientationHelper = OrientationHelper 1389 .createOrientationHelper(mLayoutManager, config.mOrientation); 1390 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 1391 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 1392 View view = mLayoutManager.getChildAt(i); 1393 Rect bounds = mLayoutManager.getViewBounds(view); 1394 if (layoutBounds.contains(bounds)) { 1395 Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates(); 1396 final int position = mRecyclerView.getChildLayoutPosition(view); 1397 LayoutParams layoutParams 1398 = (LayoutParams) (view.getLayoutParams()); 1399 TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder; 1400 assertEquals("recycler view mPosition should match adapter mPosition", position, 1401 vh.mBoundItem.mAdapterIndex); 1402 if (DEBUG) { 1403 Log.d(TAG, "testing scroll to visible mPosition at " + position 1404 + " " + bounds + " inside " + layoutBounds); 1405 } 1406 mLayoutManager.expectLayouts(1); 1407 scrollToPosition(position); 1408 mLayoutManager.waitForLayout(2); 1409 if (DEBUG) { 1410 view = mLayoutManager.findViewByPosition(position); 1411 Rect newBounds = mLayoutManager.getViewBounds(view); 1412 Log.d(TAG, "after scrolling to visible mPosition " + 1413 bounds + " equals " + newBounds); 1414 } 1415 1416 assertRectSetsEqual( 1417 config + "scroll to mPosition on fully visible child should be no-op", 1418 initialBounds, mLayoutManager.collectChildCoordinates()); 1419 } else { 1420 final int position = mRecyclerView.getChildLayoutPosition(view); 1421 if (DEBUG) { 1422 Log.d(TAG, 1423 "child(" + position + ") not fully visible " + bounds + " not inside " 1424 + layoutBounds 1425 + mRecyclerView.getChildLayoutPosition(view) 1426 ); 1427 } 1428 mLayoutManager.expectLayouts(1); 1429 runTestOnUiThread(new Runnable() { 1430 @Override 1431 public void run() { 1432 mLayoutManager.scrollToPosition(position); 1433 } 1434 }); 1435 mLayoutManager.waitForLayout(2); 1436 view = mLayoutManager.findViewByPosition(position); 1437 bounds = mLayoutManager.getViewBounds(view); 1438 if (DEBUG) { 1439 Log.d(TAG, "after scroll to partially visible child " + bounds + " in " 1440 + layoutBounds); 1441 } 1442 assertTrue(config 1443 + " after scrolling to a partially visible child, it should become fully " 1444 + " visible. " + bounds + " not inside " + layoutBounds, 1445 layoutBounds.contains(bounds) 1446 ); 1447 assertTrue(config + " when scrolling to a partially visible item, one of its edges " 1448 + "should be on the boundaries", orientationHelper.getStartAfterPadding() == 1449 orientationHelper.getDecoratedStart(view) 1450 || orientationHelper.getEndAfterPadding() == 1451 orientationHelper.getDecoratedEnd(view)); 1452 } 1453 } 1454 1455 // try scrolling to invisible children 1456 int testCount = 10; 1457 while (testCount-- > 0) { 1458 final TargetTuple target = findInvisibleTarget(config); 1459 mLayoutManager.expectLayouts(1); 1460 scrollToPosition(target.mPosition); 1461 mLayoutManager.waitForLayout(2); 1462 final View child = mLayoutManager.findViewByPosition(target.mPosition); 1463 assertNotNull(config + " scrolling to a mPosition should lay it out", child); 1464 final Rect bounds = mLayoutManager.getViewBounds(child); 1465 if (DEBUG) { 1466 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in " 1467 + layoutBounds); 1468 } 1469 assertTrue(config + " scrolling to a mPosition should make it fully visible", 1470 layoutBounds.contains(bounds)); 1471 if (target.mLayoutDirection == LAYOUT_START) { 1472 assertEquals( 1473 config + " when scrolling to an invisible child above, its start should" 1474 + " align with recycler view's start", 1475 orientationHelper.getStartAfterPadding(), 1476 orientationHelper.getDecoratedStart(child) 1477 ); 1478 } else { 1479 assertEquals(config + " when scrolling to an invisible child below, its end " 1480 + "should align with recycler view's end", 1481 orientationHelper.getEndAfterPadding(), 1482 orientationHelper.getDecoratedEnd(child) 1483 ); 1484 } 1485 } 1486 } 1487 1488 private void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 1489 runTestOnUiThread(new Runnable() { 1490 @Override 1491 public void run() { 1492 mLayoutManager.scrollToPositionWithOffset(position, offset); 1493 } 1494 }); 1495 } 1496 1497 public void testLayoutOrder() throws Throwable { 1498 for (Config config : mBaseVariations) { 1499 layoutOrderTest(config); 1500 removeRecyclerView(); 1501 } 1502 } 1503 1504 public void layoutOrderTest(Config config) throws Throwable { 1505 setupByConfig(config); 1506 assertViewPositions(config); 1507 } 1508 1509 void assertViewPositions(Config config) { 1510 ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan(); 1511 OrientationHelper orientationHelper = OrientationHelper 1512 .createOrientationHelper(mLayoutManager, config.mOrientation); 1513 for (ArrayList<View> span : viewsBySpan) { 1514 // validate all children's order. first child should have min start mPosition 1515 final int count = span.size(); 1516 for (int i = 0, j = 1; j < count; i++, j++) { 1517 View prev = span.get(i); 1518 View next = span.get(j); 1519 assertTrue(config + " prev item should be above next item", 1520 orientationHelper.getDecoratedEnd(prev) <= orientationHelper 1521 .getDecoratedStart(next) 1522 ); 1523 1524 } 1525 } 1526 } 1527 1528 public void testScrollBy() throws Throwable { 1529 for (Config config : mBaseVariations) { 1530 scollByTest(config); 1531 removeRecyclerView(); 1532 } 1533 } 1534 1535 void waitFirstLayout() throws Throwable { 1536 mLayoutManager.expectLayouts(1); 1537 setRecyclerView(mRecyclerView); 1538 mLayoutManager.waitForLayout(2); 1539 getInstrumentation().waitForIdleSync(); 1540 } 1541 1542 public void scollByTest(Config config) throws Throwable { 1543 setupByConfig(config); 1544 waitFirstLayout(); 1545 // try invalid scroll. should not happen 1546 final View first = mLayoutManager.getChildAt(0); 1547 OrientationHelper primaryOrientation = OrientationHelper 1548 .createOrientationHelper(mLayoutManager, config.mOrientation); 1549 int scrollDist; 1550 if (config.mReverseLayout) { 1551 scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2; 1552 } else { 1553 scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2; 1554 } 1555 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1556 scrollBy(scrollDist); 1557 Map<Item, Rect> after = mLayoutManager.collectChildCoordinates(); 1558 assertRectSetsEqual( 1559 config + " if there are no more items, scroll should not happen (dt:" + scrollDist 1560 + ")", 1561 before, after 1562 ); 1563 1564 scrollDist = -scrollDist * 3; 1565 before = mLayoutManager.collectChildCoordinates(); 1566 scrollBy(scrollDist); 1567 after = mLayoutManager.collectChildCoordinates(); 1568 int layoutStart = primaryOrientation.getStartAfterPadding(); 1569 int layoutEnd = primaryOrientation.getEndAfterPadding(); 1570 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1571 Rect afterRect = after.get(entry.getKey()); 1572 // offset rect 1573 if (config.mOrientation == VERTICAL) { 1574 entry.getValue().offset(0, -scrollDist); 1575 } else { 1576 entry.getValue().offset(-scrollDist, 0); 1577 } 1578 if (afterRect == null || afterRect.isEmpty()) { 1579 // assert item is out of bounds 1580 int start, end; 1581 if (config.mOrientation == VERTICAL) { 1582 start = entry.getValue().top; 1583 end = entry.getValue().bottom; 1584 } else { 1585 start = entry.getValue().left; 1586 end = entry.getValue().right; 1587 } 1588 assertTrue( 1589 config + " if item is missing after relayout, it should be out of bounds." 1590 + "item start: " + start + ", end:" + end + " layout start:" 1591 + layoutStart + 1592 ", layout end:" + layoutEnd, 1593 start <= layoutStart && end <= layoutEnd || 1594 start >= layoutEnd && end >= layoutEnd 1595 ); 1596 } else { 1597 assertEquals(config + " Item should be laid out at the scroll offset coordinates", 1598 entry.getValue(), 1599 afterRect); 1600 } 1601 } 1602 assertViewPositions(config); 1603 } 1604 1605 public void testAccessibilityPositions() throws Throwable { 1606 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE)); 1607 waitFirstLayout(); 1608 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 1609 .getCompatAccessibilityDelegate(); 1610 final AccessibilityEvent event = AccessibilityEvent.obtain(); 1611 runTestOnUiThread(new Runnable() { 1612 @Override 1613 public void run() { 1614 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 1615 } 1616 }); 1617 final AccessibilityRecordCompat record = AccessibilityEventCompat 1618 .asRecord(event); 1619 final int start = mRecyclerView 1620 .getChildLayoutPosition( 1621 mLayoutManager.findFirstVisibleItemClosestToStart(false, true)); 1622 final int end = mRecyclerView 1623 .getChildLayoutPosition( 1624 mLayoutManager.findFirstVisibleItemClosestToEnd(false, true)); 1625 assertEquals("first item position should match", 1626 Math.min(start, end), record.getFromIndex()); 1627 assertEquals("last item position should match", 1628 Math.max(start, end), record.getToIndex()); 1629 1630 } 1631 1632 public void testConsistentRelayout() throws Throwable { 1633 for (Config config : mBaseVariations) { 1634 for (boolean firstChildMultiSpan : new boolean[]{false, true}) { 1635 consistentRelayoutTest(config, firstChildMultiSpan); 1636 } 1637 removeRecyclerView(); 1638 } 1639 } 1640 1641 public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan) 1642 throws Throwable { 1643 setupByConfig(config); 1644 if (firstChildMultiSpan) { 1645 mAdapter.mFullSpanItems.add(0); 1646 } 1647 waitFirstLayout(); 1648 // record all child positions 1649 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1650 requestLayoutOnUIThread(mRecyclerView); 1651 Map<Item, Rect> after = mLayoutManager.collectChildCoordinates(); 1652 assertRectSetsEqual( 1653 config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before, 1654 after); 1655 // scroll some to create inconsistency 1656 View firstChild = mLayoutManager.getChildAt(0); 1657 final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation 1658 .getDecoratedStart(firstChild); 1659 int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2; 1660 if (config.mReverseLayout) { 1661 distance *= -1; 1662 } 1663 scrollBy(distance); 1664 waitForMainThread(2); 1665 assertTrue("scroll by should move children", firstChildStartBeforeScroll != 1666 mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild)); 1667 before = mLayoutManager.collectChildCoordinates(); 1668 mLayoutManager.expectLayouts(1); 1669 requestLayoutOnUIThread(mRecyclerView); 1670 mLayoutManager.waitForLayout(2); 1671 after = mLayoutManager.collectChildCoordinates(); 1672 assertRectSetsEqual(config + " simple re-layout after scroll", before, after); 1673 } 1674 1675 /** 1676 * enqueues an empty runnable to main thread so that we can be assured it did run 1677 * 1678 * @param count Number of times to run 1679 */ 1680 private void waitForMainThread(int count) throws Throwable { 1681 final AtomicInteger i = new AtomicInteger(count); 1682 while (i.get() > 0) { 1683 runTestOnUiThread(new Runnable() { 1684 @Override 1685 public void run() { 1686 i.decrementAndGet(); 1687 } 1688 }); 1689 } 1690 } 1691 1692 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 1693 Map<Item, Rect> after) { 1694 Throwable throwable = null; 1695 try { 1696 assertRectSetsEqual("NOT " + message, before, after); 1697 } catch (Throwable t) { 1698 throwable = t; 1699 } 1700 assertNotNull(message + " two layout should be different", throwable); 1701 } 1702 1703 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 1704 assertRectSetsEqual(message, before, after, true); 1705 } 1706 1707 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, 1708 boolean strictItemEquality) { 1709 StringBuilder log = new StringBuilder(); 1710 if (DEBUG) { 1711 log.append("checking rectangle equality.\n"); 1712 log.append("total space:" + mLayoutManager.mPrimaryOrientation.getTotalSpace()); 1713 log.append("before:"); 1714 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1715 log.append("\n").append(entry.getKey().mAdapterIndex).append(":") 1716 .append(entry.getValue()); 1717 } 1718 log.append("\nafter:"); 1719 for (Map.Entry<Item, Rect> entry : after.entrySet()) { 1720 log.append("\n").append(entry.getKey().mAdapterIndex).append(":") 1721 .append(entry.getValue()); 1722 } 1723 message += "\n\n" + log.toString(); 1724 } 1725 assertEquals(message + ": item counts should be equal", before.size() 1726 , after.size()); 1727 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1728 final Item beforeItem = entry.getKey(); 1729 Rect afterRect = null; 1730 if (strictItemEquality) { 1731 afterRect = after.get(beforeItem); 1732 assertNotNull(message + ": Same item should be visible after simple re-layout", 1733 afterRect); 1734 } else { 1735 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) { 1736 final Item afterItem = afterEntry.getKey(); 1737 if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) { 1738 afterRect = afterEntry.getValue(); 1739 break; 1740 } 1741 } 1742 assertNotNull(message + ": Item with same adapter index should be visible " + 1743 "after simple re-layout", 1744 afterRect); 1745 } 1746 assertEquals(message + ": Item should be laid out at the same coordinates", 1747 entry.getValue(), 1748 afterRect); 1749 } 1750 } 1751 1752 // test layout params assignment 1753 1754 static class OnLayoutListener { 1755 1756 void before(RecyclerView.Recycler recycler, RecyclerView.State state) { 1757 } 1758 1759 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 1760 } 1761 } 1762 1763 class WrappedLayoutManager extends StaggeredGridLayoutManager { 1764 1765 CountDownLatch layoutLatch; 1766 OnLayoutListener mOnLayoutListener; 1767 // gradle does not yet let us customize manifest for tests which is necessary to test RTL. 1768 // until bug is fixed, we'll fake it. 1769 // public issue id: 57819 1770 Boolean mFakeRTL; 1771 1772 @Override 1773 boolean isLayoutRTL() { 1774 return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL; 1775 } 1776 1777 public void expectLayouts(int count) { 1778 layoutLatch = new CountDownLatch(count); 1779 } 1780 1781 public void waitForLayout(long timeout) throws InterruptedException { 1782 waitForLayout(timeout * (DEBUG ? 1000 : 1), TimeUnit.SECONDS); 1783 } 1784 1785 public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException { 1786 layoutLatch.await(timeout, timeUnit); 1787 assertEquals("all expected layouts should be executed at the expected time", 1788 0, layoutLatch.getCount()); 1789 } 1790 1791 public void assertNoLayout(String msg, long timeout) throws Throwable { 1792 layoutLatch.await(timeout, TimeUnit.SECONDS); 1793 assertFalse(msg, layoutLatch.getCount() == 0); 1794 } 1795 1796 @Override 1797 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1798 String before; 1799 if (DEBUG) { 1800 before = layoutToString("before"); 1801 } else { 1802 before = "enable DEBUG"; 1803 } 1804 try { 1805 if (mOnLayoutListener != null) { 1806 mOnLayoutListener.before(recycler, state); 1807 } 1808 super.onLayoutChildren(recycler, state); 1809 if (mOnLayoutListener != null) { 1810 mOnLayoutListener.after(recycler, state); 1811 } 1812 validateChildren(before); 1813 } catch (Throwable t) { 1814 postExceptionToInstrumentation(t); 1815 } 1816 1817 layoutLatch.countDown(); 1818 } 1819 1820 @Override 1821 int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 1822 try { 1823 int result = super.scrollBy(dt, recycler, state); 1824 validateChildren(); 1825 return result; 1826 } catch (Throwable t) { 1827 postExceptionToInstrumentation(t); 1828 } 1829 1830 return 0; 1831 } 1832 1833 public WrappedLayoutManager(int spanCount, int orientation) { 1834 super(spanCount, orientation); 1835 } 1836 1837 ArrayList<ArrayList<View>> collectChildrenBySpan() { 1838 ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>(); 1839 for (int i = 0; i < getSpanCount(); i++) { 1840 viewsBySpan.add(new ArrayList<View>()); 1841 } 1842 for (int i = 0; i < getChildCount(); i++) { 1843 View view = getChildAt(i); 1844 LayoutParams lp 1845 = (LayoutParams) view 1846 .getLayoutParams(); 1847 viewsBySpan.get(lp.mSpan.mIndex).add(view); 1848 } 1849 return viewsBySpan; 1850 } 1851 1852 @Nullable 1853 @Override 1854 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 1855 RecyclerView.State state) { 1856 View result = null; 1857 try { 1858 result = super.onFocusSearchFailed(focused, direction, recycler, state); 1859 validateChildren(); 1860 } catch (Throwable t) { 1861 postExceptionToInstrumentation(t); 1862 } 1863 return result; 1864 } 1865 1866 Rect getViewBounds(View view) { 1867 if (getOrientation() == HORIZONTAL) { 1868 return new Rect( 1869 mPrimaryOrientation.getDecoratedStart(view), 1870 mSecondaryOrientation.getDecoratedStart(view), 1871 mPrimaryOrientation.getDecoratedEnd(view), 1872 mSecondaryOrientation.getDecoratedEnd(view)); 1873 } else { 1874 return new Rect( 1875 mSecondaryOrientation.getDecoratedStart(view), 1876 mPrimaryOrientation.getDecoratedStart(view), 1877 mSecondaryOrientation.getDecoratedEnd(view), 1878 mPrimaryOrientation.getDecoratedEnd(view)); 1879 } 1880 } 1881 1882 public String getBoundsLog() { 1883 StringBuilder sb = new StringBuilder(); 1884 sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding()) 1885 .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding()); 1886 sb.append("\nchildren bounds\n"); 1887 final int childCount = getChildCount(); 1888 for (int i = 0; i < childCount; i++) { 1889 View child = getChildAt(i); 1890 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 1891 .append("[").append("start:").append( 1892 mPrimaryOrientation.getDecoratedStart(child)).append(", end:") 1893 .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n"); 1894 } 1895 return sb.toString(); 1896 } 1897 1898 public VisibleChildren traverseAndFindVisibleChildren() { 1899 int childCount = getChildCount(); 1900 final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount()); 1901 final int start = mPrimaryOrientation.getStartAfterPadding(); 1902 final int end = mPrimaryOrientation.getEndAfterPadding(); 1903 for (int i = 0; i < childCount; i++) { 1904 View child = getChildAt(i); 1905 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 1906 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 1907 final boolean fullyVisible = childStart >= start && childEnd <= end; 1908 final boolean hidden = childEnd <= start || childStart >= end; 1909 if (hidden) { 1910 continue; 1911 } 1912 final int position = getPosition(child); 1913 final int span = getLp(child).getSpanIndex(); 1914 if (fullyVisible) { 1915 if (position < visibleChildren.firstFullyVisiblePositions[span] || 1916 visibleChildren.firstFullyVisiblePositions[span] 1917 == RecyclerView.NO_POSITION) { 1918 visibleChildren.firstFullyVisiblePositions[span] = position; 1919 } 1920 1921 if (position > visibleChildren.lastFullyVisiblePositions[span]) { 1922 visibleChildren.lastFullyVisiblePositions[span] = position; 1923 } 1924 } 1925 1926 if (position < visibleChildren.firstVisiblePositions[span] || 1927 visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) { 1928 visibleChildren.firstVisiblePositions[span] = position; 1929 } 1930 1931 if (position > visibleChildren.lastVisiblePositions[span]) { 1932 visibleChildren.lastVisiblePositions[span] = position; 1933 } 1934 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) { 1935 visibleChildren.findFirstPartialVisibleClosestToStart = child; 1936 } 1937 visibleChildren.findFirstPartialVisibleClosestToEnd = child; 1938 } 1939 return visibleChildren; 1940 } 1941 1942 Map<Item, Rect> collectChildCoordinates() throws Throwable { 1943 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 1944 runTestOnUiThread(new Runnable() { 1945 @Override 1946 public void run() { 1947 final int childCount = getChildCount(); 1948 for (int i = 0; i < childCount; i++) { 1949 View child = getChildAt(i); 1950 // do it if and only if child is visible 1951 if (child.getRight() < 0 || child.getBottom() < 0 || 1952 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) { 1953 // invisible children may be drawn in cases like scrolling so we should 1954 // ignore them 1955 continue; 1956 } 1957 LayoutParams lp = (LayoutParams) child 1958 .getLayoutParams(); 1959 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 1960 items.put(vh.mBoundItem, getViewBounds(child)); 1961 } 1962 } 1963 }); 1964 return items; 1965 } 1966 1967 1968 public void setFakeRtl(Boolean fakeRtl) { 1969 mFakeRTL = fakeRtl; 1970 try { 1971 requestLayoutOnUIThread(mRecyclerView); 1972 } catch (Throwable throwable) { 1973 postExceptionToInstrumentation(throwable); 1974 } 1975 } 1976 1977 private String layoutToString(String hint) { 1978 StringBuilder sb = new StringBuilder(); 1979 sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n"); 1980 for (int i = 0; i < getChildCount(); i++) { 1981 final View view = getChildAt(i); 1982 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 1983 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s", 1984 i, getPosition(view), 1985 mPrimaryOrientation.getDecoratedStart(view), 1986 mPrimaryOrientation.getDecoratedEnd(view), 1987 layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n"); 1988 } 1989 return sb.toString(); 1990 } 1991 1992 private void validateChildren() { 1993 validateChildren(null); 1994 } 1995 1996 private void validateChildren(String msg) { 1997 if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) { 1998 return; 1999 } 2000 final int dir = mShouldReverseLayout ? -1 : 1; 2001 int i = 0; 2002 int pos = -1; 2003 while (i < getChildCount()) { 2004 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 2005 if (lp.isItemRemoved()) { 2006 i++; 2007 continue; 2008 } 2009 pos = getPosition(getChildAt(i)); 2010 break; 2011 } 2012 if (pos == -1) { 2013 return; 2014 } 2015 while (++i < getChildCount()) { 2016 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 2017 if (lp.isItemRemoved()) { 2018 continue; 2019 } 2020 pos += dir; 2021 if (getPosition(getChildAt(i)) != pos) { 2022 throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" + 2023 layoutToString("ERROR") + "\n msg:" + msg); 2024 } 2025 } 2026 } 2027 } 2028 2029 static class VisibleChildren { 2030 2031 int[] firstVisiblePositions; 2032 2033 int[] firstFullyVisiblePositions; 2034 2035 int[] lastVisiblePositions; 2036 2037 int[] lastFullyVisiblePositions; 2038 2039 View findFirstPartialVisibleClosestToStart; 2040 View findFirstPartialVisibleClosestToEnd; 2041 2042 VisibleChildren(int spanCount) { 2043 firstFullyVisiblePositions = new int[spanCount]; 2044 firstVisiblePositions = new int[spanCount]; 2045 lastVisiblePositions = new int[spanCount]; 2046 lastFullyVisiblePositions = new int[spanCount]; 2047 for (int i = 0; i < spanCount; i++) { 2048 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION; 2049 firstVisiblePositions[i] = RecyclerView.NO_POSITION; 2050 lastVisiblePositions[i] = RecyclerView.NO_POSITION; 2051 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION; 2052 } 2053 } 2054 2055 @Override 2056 public boolean equals(Object o) { 2057 if (this == o) { 2058 return true; 2059 } 2060 if (o == null || getClass() != o.getClass()) { 2061 return false; 2062 } 2063 2064 VisibleChildren that = (VisibleChildren) o; 2065 2066 if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) { 2067 return false; 2068 } 2069 if (findFirstPartialVisibleClosestToStart 2070 != null ? !findFirstPartialVisibleClosestToStart 2071 .equals(that.findFirstPartialVisibleClosestToStart) 2072 : that.findFirstPartialVisibleClosestToStart != null) { 2073 return false; 2074 } 2075 if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) { 2076 return false; 2077 } 2078 if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) { 2079 return false; 2080 } 2081 if (findFirstPartialVisibleClosestToEnd != null ? !findFirstPartialVisibleClosestToEnd 2082 .equals(that.findFirstPartialVisibleClosestToEnd) 2083 : that.findFirstPartialVisibleClosestToEnd 2084 != null) { 2085 return false; 2086 } 2087 if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) { 2088 return false; 2089 } 2090 2091 return true; 2092 } 2093 2094 @Override 2095 public int hashCode() { 2096 int result = Arrays.hashCode(firstVisiblePositions); 2097 result = 31 * result + Arrays.hashCode(firstFullyVisiblePositions); 2098 result = 31 * result + Arrays.hashCode(lastVisiblePositions); 2099 result = 31 * result + Arrays.hashCode(lastFullyVisiblePositions); 2100 result = 31 * result + (findFirstPartialVisibleClosestToStart != null 2101 ? findFirstPartialVisibleClosestToStart 2102 .hashCode() : 0); 2103 result = 31 * result + (findFirstPartialVisibleClosestToEnd != null 2104 ? findFirstPartialVisibleClosestToEnd 2105 .hashCode() 2106 : 0); 2107 return result; 2108 } 2109 2110 @Override 2111 public String toString() { 2112 return "VisibleChildren{" + 2113 "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) + 2114 ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) + 2115 ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) + 2116 ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) + 2117 ", findFirstPartialVisibleClosestToStart=" + 2118 viewToString(findFirstPartialVisibleClosestToStart) + 2119 ", findFirstPartialVisibleClosestToEnd=" + 2120 viewToString(findFirstPartialVisibleClosestToEnd) + 2121 '}'; 2122 } 2123 2124 private String viewToString(View view) { 2125 if (view == null) { 2126 return null; 2127 } 2128 ViewGroup.LayoutParams lp = view.getLayoutParams(); 2129 if (lp instanceof RecyclerView.LayoutParams == false) { 2130 return System.identityHashCode(view) + "(?)"; 2131 } 2132 RecyclerView.LayoutParams rvlp = (RecyclerView.LayoutParams) lp; 2133 return System.identityHashCode(view) + "(" + rvlp.getViewAdapterPosition() + ")"; 2134 } 2135 } 2136 2137 class GridTestAdapter extends TestAdapter { 2138 2139 int mOrientation; 2140 int mRecyclerViewWidth; 2141 int mRecyclerViewHeight; 2142 Integer mSizeReference = null; 2143 2144 // original ids of items that should be full span 2145 HashSet<Integer> mFullSpanItems = new HashSet<Integer>(); 2146 2147 private boolean mViewsHaveEqualSize = false; // size in the scrollable direction 2148 2149 private OnBindCallback mOnBindCallback; 2150 2151 GridTestAdapter(int count, int orientation) { 2152 super(count); 2153 mOrientation = orientation; 2154 } 2155 2156 @Override 2157 public TestViewHolder onCreateViewHolder(ViewGroup parent, 2158 int viewType) { 2159 mRecyclerViewWidth = parent.getWidth(); 2160 mRecyclerViewHeight = parent.getHeight(); 2161 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 2162 if (mOnBindCallback != null) { 2163 mOnBindCallback.onCreatedViewHolder(vh); 2164 } 2165 return vh; 2166 } 2167 2168 @Override 2169 public void offsetOriginalIndices(int start, int offset) { 2170 if (mFullSpanItems.size() > 0) { 2171 HashSet<Integer> old = mFullSpanItems; 2172 mFullSpanItems = new HashSet<Integer>(); 2173 for (Integer i : old) { 2174 if (i < start) { 2175 mFullSpanItems.add(i); 2176 } else if (offset > 0 || (start + Math.abs(offset)) <= i) { 2177 mFullSpanItems.add(i + offset); 2178 } else if (DEBUG) { 2179 Log.d(TAG, "removed full span item " + i); 2180 } 2181 } 2182 } 2183 super.offsetOriginalIndices(start, offset); 2184 } 2185 2186 @Override 2187 protected void moveInUIThread(int from, int to) { 2188 boolean setAsFullSpanAgain = mFullSpanItems.contains(from); 2189 super.moveInUIThread(from, to); 2190 if (setAsFullSpanAgain) { 2191 mFullSpanItems.add(to); 2192 } 2193 } 2194 2195 @Override 2196 public void onBindViewHolder(TestViewHolder holder, 2197 int position) { 2198 if (mSizeReference == null) { 2199 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth 2200 / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW; 2201 } 2202 super.onBindViewHolder(holder, position); 2203 Item item = mItems.get(position); 2204 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 2205 .getLayoutParams(); 2206 if (lp instanceof LayoutParams) { 2207 ((LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 2208 } else { 2209 LayoutParams slp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2210 ViewGroup.LayoutParams.WRAP_CONTENT); 2211 holder.itemView.setLayoutParams(slp); 2212 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 2213 lp = slp; 2214 } 2215 2216 if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) { 2217 final int minSize = mViewsHaveEqualSize ? mSizeReference : 2218 mSizeReference + 20 * (item.mId % 10); 2219 if (mOrientation == OrientationHelper.HORIZONTAL) { 2220 holder.itemView.setMinimumWidth(minSize); 2221 } else { 2222 holder.itemView.setMinimumHeight(minSize); 2223 } 2224 lp.topMargin = 3; 2225 lp.leftMargin = 5; 2226 lp.rightMargin = 7; 2227 lp.bottomMargin = 9; 2228 } 2229 2230 if (mOnBindCallback != null) { 2231 mOnBindCallback.onBoundItem(holder, position); 2232 } 2233 } 2234 } 2235 2236 abstract static class OnBindCallback { 2237 2238 abstract void onBoundItem(TestViewHolder vh, int position); 2239 2240 boolean assignRandomSize() { 2241 return true; 2242 } 2243 2244 void onCreatedViewHolder(TestViewHolder vh) { 2245 } 2246 } 2247 2248 static class Config implements Cloneable { 2249 2250 private static final int DEFAULT_ITEM_COUNT = 300; 2251 2252 int mOrientation = OrientationHelper.VERTICAL; 2253 2254 boolean mReverseLayout = false; 2255 2256 int mSpanCount = 3; 2257 2258 int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 2259 2260 int mItemCount = DEFAULT_ITEM_COUNT; 2261 2262 Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) { 2263 mOrientation = orientation; 2264 mReverseLayout = reverseLayout; 2265 mSpanCount = spanCount; 2266 mGapStrategy = gapStrategy; 2267 } 2268 2269 public Config() { 2270 2271 } 2272 2273 Config orientation(int orientation) { 2274 mOrientation = orientation; 2275 return this; 2276 } 2277 2278 Config reverseLayout(boolean reverseLayout) { 2279 mReverseLayout = reverseLayout; 2280 return this; 2281 } 2282 2283 Config spanCount(int spanCount) { 2284 mSpanCount = spanCount; 2285 return this; 2286 } 2287 2288 Config gapStrategy(int gapStrategy) { 2289 mGapStrategy = gapStrategy; 2290 return this; 2291 } 2292 2293 public Config itemCount(int itemCount) { 2294 mItemCount = itemCount; 2295 return this; 2296 } 2297 2298 @Override 2299 public String toString() { 2300 return "[CONFIG:" + 2301 " span:" + mSpanCount + "," + 2302 " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") + 2303 " reverse:" + (mReverseLayout ? "T" : "F") + 2304 " itemCount:" + mItemCount + 2305 " gap strategy: " + gapStrategyName(mGapStrategy); 2306 } 2307 2308 private static String gapStrategyName(int gapStrategy) { 2309 switch (gapStrategy) { 2310 case GAP_HANDLING_NONE: 2311 return "none"; 2312 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS: 2313 return "move spans"; 2314 } 2315 return "gap strategy: unknown"; 2316 } 2317 2318 @Override 2319 public Object clone() throws CloneNotSupportedException { 2320 return super.clone(); 2321 } 2322 } 2323 2324 private interface PostLayoutRunnable { 2325 2326 void run() throws Throwable; 2327 2328 String describe(); 2329 } 2330 2331 } 2332