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