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 18package android.support.v7.widget; 19 20 21import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 22import static android.support.v7.widget.StaggeredGridLayoutManager 23 .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 24import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE; 25import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL; 26import static android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams; 27 28import static org.junit.Assert.assertEquals; 29import static org.junit.Assert.assertFalse; 30import static org.junit.Assert.assertNotNull; 31import static org.junit.Assert.assertNull; 32import static org.junit.Assert.assertSame; 33import static org.junit.Assert.assertThat; 34import static org.junit.Assert.assertTrue; 35 36import android.graphics.Color; 37import android.graphics.Rect; 38import android.graphics.drawable.ColorDrawable; 39import android.graphics.drawable.StateListDrawable; 40import android.os.Parcel; 41import android.os.Parcelable; 42import android.support.v4.view.AccessibilityDelegateCompat; 43import android.support.v4.view.accessibility.AccessibilityEventCompat; 44import android.support.v4.view.accessibility.AccessibilityRecordCompat; 45import android.test.suitebuilder.annotation.MediumTest; 46import android.text.TextUtils; 47import android.util.Log; 48import android.util.StateSet; 49import android.view.View; 50import android.view.ViewGroup; 51import android.view.accessibility.AccessibilityEvent; 52import android.widget.EditText; 53import android.widget.FrameLayout; 54 55import org.hamcrest.CoreMatchers; 56import org.hamcrest.MatcherAssert; 57import org.junit.Test; 58 59import java.util.HashMap; 60import java.util.Map; 61import java.util.UUID; 62 63 64@MediumTest 65public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest { 66 @Test 67 public void forceLayoutOnDetach() throws Throwable { 68 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 69 waitFirstLayout(); 70 assertFalse("test sanity", mRecyclerView.isLayoutRequested()); 71 runTestOnUiThread(new Runnable() { 72 @Override 73 public void run() { 74 mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler); 75 } 76 }); 77 assertTrue(mRecyclerView.isLayoutRequested()); 78 } 79 @Test 80 public void areAllStartsTheSame() throws Throwable { 81 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300)); 82 waitFirstLayout(); 83 smoothScrollToPosition(100); 84 mLayoutManager.expectLayouts(1); 85 mAdapter.deleteAndNotify(0, 2); 86 mLayoutManager.waitForLayout(2); 87 smoothScrollToPosition(0); 88 assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual()); 89 } 90 91 @Test 92 public void areAllEndsTheSame() throws Throwable { 93 setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300)); 94 waitFirstLayout(); 95 smoothScrollToPosition(100); 96 mLayoutManager.expectLayouts(1); 97 mAdapter.deleteAndNotify(0, 2); 98 mLayoutManager.waitForLayout(2); 99 smoothScrollToPosition(0); 100 assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual()); 101 } 102 103 @Test 104 public void getPositionsBeforeInitialization() throws Throwable { 105 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 106 int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null); 107 MatcherAssert.assertThat(positions, 108 CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION, 109 RecyclerView.NO_POSITION})); 110 } 111 112 @Test 113 public void findLastInUnevenDistribution() throws Throwable { 114 setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) 115 .itemCount(5)); 116 mAdapter.mOnBindCallback = new OnBindCallback() { 117 @Override 118 void onBoundItem(TestViewHolder vh, int position) { 119 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams(); 120 if (position == 1) { 121 lp.height = mRecyclerView.getHeight() - 10; 122 } else { 123 lp.height = 5; 124 } 125 vh.itemView.setMinimumHeight(0); 126 } 127 }; 128 waitFirstLayout(); 129 int[] into = new int[2]; 130 mLayoutManager.findFirstCompletelyVisibleItemPositions(into); 131 assertEquals("first completely visible item from span 0 should be 0", 0, into[0]); 132 assertEquals("first completely visible item from span 1 should be 1", 1, into[1]); 133 mLayoutManager.findLastCompletelyVisibleItemPositions(into); 134 assertEquals("last completely visible item from span 0 should be 4", 4, into[0]); 135 assertEquals("last completely visible item from span 1 should be 1", 1, into[1]); 136 assertEquals("first fully visible child should be at position", 137 0, mRecyclerView.getChildViewHolder(mLayoutManager. 138 findFirstVisibleItemClosestToStart(true, true)).getPosition()); 139 assertEquals("last fully visible child should be at position", 140 4, mRecyclerView.getChildViewHolder(mLayoutManager. 141 findFirstVisibleItemClosestToEnd(true, true)).getPosition()); 142 143 assertEquals("first visible child should be at position", 144 0, mRecyclerView.getChildViewHolder(mLayoutManager. 145 findFirstVisibleItemClosestToStart(false, true)).getPosition()); 146 assertEquals("last visible child should be at position", 147 4, mRecyclerView.getChildViewHolder(mLayoutManager. 148 findFirstVisibleItemClosestToEnd(false, true)).getPosition()); 149 150 } 151 152 @Test 153 public void customWidthInHorizontal() throws Throwable { 154 customSizeInScrollDirectionTest( 155 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 156 } 157 158 @Test 159 public void customHeightInVertical() throws Throwable { 160 customSizeInScrollDirectionTest( 161 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 162 } 163 164 public void customSizeInScrollDirectionTest(final Config config) throws Throwable { 165 setupByConfig(config); 166 final Map<View, Integer> sizeMap = new HashMap<View, Integer>(); 167 mAdapter.mOnBindCallback = new OnBindCallback() { 168 @Override 169 void onBoundItem(TestViewHolder vh, int position) { 170 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams(); 171 final int size = 1 + position * 5; 172 if (config.mOrientation == HORIZONTAL) { 173 layoutParams.width = size; 174 } else { 175 layoutParams.height = size; 176 } 177 sizeMap.put(vh.itemView, size); 178 if (position == 3) { 179 getLp(vh.itemView).setFullSpan(true); 180 } 181 } 182 183 @Override 184 boolean assignRandomSize() { 185 return false; 186 } 187 }; 188 waitFirstLayout(); 189 assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0); 190 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 191 View child = mRecyclerView.getChildAt(i); 192 final int size = config.mOrientation == HORIZONTAL ? child.getWidth() 193 : child.getHeight(); 194 assertEquals("child " + i + " should have the size specified in its layout params", 195 sizeMap.get(child).intValue(), size); 196 } 197 checkForMainThreadException(); 198 } 199 200 @Test 201 public void gapHandlingWhenItemMovesToTop() throws Throwable { 202 gapHandlingWhenItemMovesToTopTest(); 203 } 204 205 @Test 206 public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable { 207 gapHandlingWhenItemMovesToTopTest(0); 208 } 209 210 @Test 211 public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable { 212 gapHandlingWhenItemMovesToTopTest(1); 213 } 214 215 public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable { 216 Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 217 config.itemCount(3); 218 setupByConfig(config); 219 mAdapter.mOnBindCallback = new OnBindCallback() { 220 @Override 221 void onBoundItem(TestViewHolder vh, int position) { 222 } 223 224 @Override 225 boolean assignRandomSize() { 226 return false; 227 } 228 }; 229 for (int i : fullSpanIndices) { 230 mAdapter.mFullSpanItems.add(i); 231 } 232 waitFirstLayout(); 233 mLayoutManager.expectLayouts(1); 234 mAdapter.moveItem(1, 0, true); 235 mLayoutManager.waitForLayout(2); 236 final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates(); 237 // move back. 238 mLayoutManager.expectLayouts(1); 239 mAdapter.moveItem(0, 1, true); 240 mLayoutManager.waitForLayout(2); 241 mLayoutManager.expectLayouts(2); 242 mAdapter.moveAndNotify(1, 0); 243 mLayoutManager.waitForLayout(2); 244 Thread.sleep(1000); 245 getInstrumentation().waitForIdleSync(); 246 checkForMainThreadException(); 247 // item should be positioned properly 248 assertRectSetsEqual("final position after a move", desiredPositions, 249 mLayoutManager.collectChildCoordinates()); 250 251 } 252 253 @Test 254 public void focusSearchFailureUp() throws Throwable { 255 focusSearchFailure(false); 256 } 257 258 @Test 259 public void focusSearchFailureDown() throws Throwable { 260 focusSearchFailure(true); 261 } 262 263 @Test 264 public void focusSearchFailureFromSubChild() throws Throwable { 265 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 266 new GridTestAdapter(1000, VERTICAL) { 267 268 @Override 269 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 270 FrameLayout fl = new FrameLayout(parent.getContext()); 271 EditText editText = new EditText(parent.getContext()); 272 fl.addView(editText); 273 editText.setEllipsize(TextUtils.TruncateAt.END); 274 return new TestViewHolder(fl); 275 } 276 277 @Override 278 public void onBindViewHolder(TestViewHolder holder, int position) { 279 Item item = mItems.get(position); 280 holder.mBoundItem = item; 281 ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText( 282 item.mText + " (" + item.mId + ")"); 283 } 284 }); 285 waitFirstLayout(); 286 ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt( 287 mRecyclerView.getChildCount() - 1); 288 RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild); 289 View subChildToFocus = lastChild.getChildAt(0); 290 requestFocus(subChildToFocus, true); 291 assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true)); 292 focusSearch(subChildToFocus, View.FOCUS_FORWARD); 293 waitForIdleScroll(mRecyclerView); 294 checkForMainThreadException(); 295 View focusedChild = mRecyclerView.getFocusedChild(); 296 if (focusedChild == subChildToFocus.getParent()) { 297 focusSearch(focusedChild, View.FOCUS_FORWARD); 298 waitForIdleScroll(mRecyclerView); 299 focusedChild = mRecyclerView.getFocusedChild(); 300 } 301 RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder( 302 focusedChild); 303 assertTrue("new focused view should have a larger position " 304 + lastViewHolder.getAdapterPosition() + " vs " 305 + containingViewHolder.getAdapterPosition(), 306 lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition()); 307 } 308 309 public void focusSearchFailure(boolean scrollDown) throws Throwable { 310 int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP; 311 setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) 312 , new GridTestAdapter(31, 1) { 313 RecyclerView mAttachedRv; 314 315 @Override 316 public TestViewHolder onCreateViewHolder(ViewGroup parent, 317 int viewType) { 318 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 319 testViewHolder.itemView.setFocusable(true); 320 testViewHolder.itemView.setFocusableInTouchMode(true); 321 // Good to have colors for debugging 322 StateListDrawable stl = new StateListDrawable(); 323 stl.addState(new int[]{android.R.attr.state_focused}, 324 new ColorDrawable(Color.RED)); 325 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 326 testViewHolder.itemView.setBackground(stl); 327 return testViewHolder; 328 } 329 330 @Override 331 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 332 mAttachedRv = recyclerView; 333 } 334 335 @Override 336 public void onBindViewHolder(TestViewHolder holder, 337 int position) { 338 super.onBindViewHolder(holder, position); 339 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3); 340 } 341 }); 342 /** 343 * 0 1 2 344 * 3 4 5 345 * 6 7 8 346 * 9 10 11 347 * 12 13 14 348 * 15 16 17 349 * 18 18 18 350 * 19 351 * 20 20 20 352 * 21 22 353 * 23 23 23 354 * 24 25 26 355 * 27 28 29 356 * 30 357 */ 358 mAdapter.mFullSpanItems.add(18); 359 mAdapter.mFullSpanItems.add(20); 360 mAdapter.mFullSpanItems.add(23); 361 waitFirstLayout(); 362 View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView; 363 assertTrue(requestFocus(viewToFocus, true)); 364 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 365 int pos = 1; 366 View focusedView = viewToFocus; 367 while (pos < 16) { 368 focusSearchAndWaitForScroll(focusedView, focusDir); 369 focusedView = mRecyclerView.getFocusedChild(); 370 assertEquals(pos + 3, 371 mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 372 pos += 3; 373 } 374 for (int i : new int[]{18, 19, 20, 21, 23, 24}) { 375 focusSearchAndWaitForScroll(focusedView, focusDir); 376 focusedView = mRecyclerView.getFocusedChild(); 377 assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 378 } 379 // now move right 380 focusSearch(focusedView, View.FOCUS_RIGHT); 381 waitForIdleScroll(mRecyclerView); 382 focusedView = mRecyclerView.getFocusedChild(); 383 assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 384 for (int i : new int[]{28, 30}) { 385 focusSearchAndWaitForScroll(focusedView, focusDir); 386 focusedView = mRecyclerView.getFocusedChild(); 387 assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 388 } 389 } 390 391 private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable { 392 focusSearch(focused, dir); 393 waitForIdleScroll(mRecyclerView); 394 } 395 396 397 @Test 398 public void scrollToPositionWithPredictive() throws Throwable { 399 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 400 removeRecyclerView(); 401 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 402 LinearLayoutManager.INVALID_OFFSET); 403 removeRecyclerView(); 404 scrollToPositionWithPredictive(9, 20); 405 removeRecyclerView(); 406 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 407 408 } 409 410 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 411 throws Throwable { 412 setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL, 413 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE)); 414 waitFirstLayout(); 415 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 416 @Override 417 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 418 RecyclerView rv = mLayoutManager.mRecyclerView; 419 if (state.isPreLayout()) { 420 assertEquals("pending scroll position should still be pending", 421 scrollPosition, mLayoutManager.mPendingScrollPosition); 422 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 423 assertEquals("pending scroll position offset should still be pending", 424 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 425 } 426 } else { 427 RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition); 428 assertNotNull("scroll to position should work", vh); 429 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 430 assertEquals("scroll offset should be applied properly", 431 mLayoutManager.getPaddingTop() + scrollOffset 432 + ((RecyclerView.LayoutParams) vh.itemView 433 .getLayoutParams()).topMargin, 434 mLayoutManager.getDecoratedTop(vh.itemView)); 435 } 436 } 437 } 438 }; 439 mLayoutManager.expectLayouts(2); 440 runTestOnUiThread(new Runnable() { 441 @Override 442 public void run() { 443 try { 444 mAdapter.addAndNotify(0, 1); 445 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 446 mLayoutManager.scrollToPosition(scrollPosition); 447 } else { 448 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 449 scrollOffset); 450 } 451 452 } catch (Throwable throwable) { 453 throwable.printStackTrace(); 454 } 455 456 } 457 }); 458 mLayoutManager.waitForLayout(2); 459 checkForMainThreadException(); 460 } 461 462 @Test 463 public void moveGapHandling() throws Throwable { 464 Config config = new Config().spanCount(2).itemCount(40); 465 setupByConfig(config); 466 waitFirstLayout(); 467 mLayoutManager.expectLayouts(2); 468 mAdapter.moveAndNotify(4, 1); 469 mLayoutManager.waitForLayout(2); 470 assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix()); 471 } 472 473 @Test 474 public void updateAfterFullSpan() throws Throwable { 475 updateAfterFullSpanGapHandlingTest(0); 476 } 477 478 @Test 479 public void updateAfterFullSpan2() throws Throwable { 480 updateAfterFullSpanGapHandlingTest(20); 481 } 482 483 @Test 484 public void temporaryGapHandling() throws Throwable { 485 int fullSpanIndex = 200; 486 setupByConfig(new Config().spanCount(2).itemCount(500)); 487 mAdapter.mFullSpanItems.add(fullSpanIndex); 488 waitFirstLayout(); 489 smoothScrollToPosition(fullSpanIndex + 200);// go far away 490 assertNull("test sanity. full span item should not be visible", 491 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex)); 492 mLayoutManager.expectLayouts(1); 493 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 494 mLayoutManager.waitForLayout(1); 495 smoothScrollToPosition(0); 496 mLayoutManager.expectLayouts(1); 497 smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1)); 498 String log = mLayoutManager.layoutToString("post gap"); 499 mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a " 500 + "relayout " + log, 2); 501 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 502 assertNotNull("full span item should be there:\n" + log, fullSpan); 503 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 504 assertNotNull("next view should be there\n" + log, view1); 505 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 506 assertNotNull("+2 view should be there\n" + log, view2); 507 508 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 509 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 510 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 511 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 512 assertEquals("no gap between span and view 1", 513 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 514 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 515 assertEquals("no gap between span and view 2", 516 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 517 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 518 } 519 520 public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable { 521 setupByConfig(new Config().spanCount(2).itemCount(100)); 522 mAdapter.mFullSpanItems.add(fullSpanIndex); 523 waitFirstLayout(); 524 smoothScrollToPosition(fullSpanIndex + 30); 525 mLayoutManager.expectLayouts(1); 526 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 527 mLayoutManager.waitForLayout(1); 528 smoothScrollToPosition(fullSpanIndex); 529 // give it some time to fix the gap 530 Thread.sleep(500); 531 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 532 533 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 534 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 535 536 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 537 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 538 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 539 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 540 assertEquals("no gap between span and view 1", 541 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 542 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 543 assertEquals("no gap between span and view 2", 544 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 545 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 546 } 547 548 @Test 549 public void innerGapHandling() throws Throwable { 550 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE); 551 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 552 } 553 554 public void innerGapHandlingTest(int strategy) throws Throwable { 555 Config config = new Config().spanCount(3).itemCount(500); 556 setupByConfig(config); 557 mLayoutManager.setGapStrategy(strategy); 558 mAdapter.mFullSpanItems.add(100); 559 mAdapter.mFullSpanItems.add(104); 560 mAdapter.mViewsHaveEqualSize = true; 561 mAdapter.mOnBindCallback = new OnBindCallback() { 562 @Override 563 void onBoundItem(TestViewHolder vh, int position) { 564 565 } 566 567 @Override 568 void onCreatedViewHolder(TestViewHolder vh) { 569 super.onCreatedViewHolder(vh); 570 //make sure we have enough views 571 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5; 572 } 573 }; 574 waitFirstLayout(); 575 mLayoutManager.expectLayouts(1); 576 scrollToPosition(400); 577 mLayoutManager.waitForLayout(2); 578 View view400 = mLayoutManager.findViewByPosition(400); 579 assertNotNull("test sanity, scrollToPos should succeed", view400); 580 assertTrue("test sanity, view should be visible top", 581 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >= 582 mLayoutManager.mPrimaryOrientation.getStartAfterPadding()); 583 assertTrue("test sanity, view should be visible bottom", 584 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <= 585 mLayoutManager.mPrimaryOrientation.getEndAfterPadding()); 586 mLayoutManager.expectLayouts(2); 587 mAdapter.addAndNotify(101, 1); 588 mLayoutManager.waitForLayout(2); 589 checkForMainThreadException(); 590 if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 591 mLayoutManager.expectLayouts(1); 592 } 593 // state 594 // now smooth scroll to 99 to trigger a layout around 100 595 mLayoutManager.validateChildren(); 596 smoothScrollToPosition(99); 597 switch (strategy) { 598 case GAP_HANDLING_NONE: 599 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0}, 600 new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2}, 601 new int[]{105, 0}); 602 break; 603 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS: 604 mLayoutManager.waitForLayout(2); 605 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0}, 606 new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0}); 607 break; 608 } 609 610 } 611 612 @Test 613 public void fullSizeSpans() throws Throwable { 614 Config config = new Config().spanCount(5).itemCount(30); 615 setupByConfig(config); 616 mAdapter.mFullSpanItems.add(3); 617 waitFirstLayout(); 618 assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2}, 619 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2}, 620 new int[]{7, 3}, new int[]{8, 4}); 621 } 622 623 void assertSpans(String msg, int[]... childSpanTuples) { 624 msg = msg + mLayoutManager.layoutToString("\n\n"); 625 for (int i = 0; i < childSpanTuples.length; i++) { 626 assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]); 627 } 628 } 629 630 void assertSpan(String msg, int childPosition, int expectedSpan) { 631 View view = mLayoutManager.findViewByPosition(childPosition); 632 assertNotNull(msg + " view at position " + childPosition + " should exists", view); 633 assertEquals(msg + "[child:" + childPosition + "]", expectedSpan, 634 getLp(view).mSpan.mIndex); 635 } 636 637 @Test 638 public void partialSpanInvalidation() throws Throwable { 639 Config config = new Config().spanCount(5).itemCount(100); 640 setupByConfig(config); 641 for (int i = 20; i < mAdapter.getItemCount(); i += 20) { 642 mAdapter.mFullSpanItems.add(i); 643 } 644 waitFirstLayout(); 645 smoothScrollToPosition(50); 646 int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30]; 647 mAdapter.changeAndNotify(15, 2); 648 Thread.sleep(200); 649 assertEquals("Invalidation should happen within full span item boundaries", prevSpanId, 650 mLayoutManager.mLazySpanLookup.mData[30]); 651 assertEquals("item in invalidated range should have clear span id", 652 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 653 smoothScrollToPosition(85); 654 int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85); 655 mAdapter.deleteAndNotify(55, 2); 656 Thread.sleep(200); 657 assertEquals("item in invalidated range should have clear span id", 658 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 659 int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83); 660 assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans, 661 newSpans, 0, 0, newSpans.length); 662 } 663 664 // Same as Arrays.copyOfRange but for API 7 665 private int[] copyOfRange(int[] original, int from, int to) { 666 int newLength = to - from; 667 if (newLength < 0) { 668 throw new IllegalArgumentException(from + " > " + to); 669 } 670 int[] copy = new int[newLength]; 671 System.arraycopy(original, from, copy, 0, 672 Math.min(original.length - from, newLength)); 673 return copy; 674 } 675 676 @Test 677 public void spanReassignmentsOnItemChange() throws Throwable { 678 Config config = new Config().spanCount(5); 679 setupByConfig(config); 680 waitFirstLayout(); 681 smoothScrollToPosition(mAdapter.getItemCount() / 2); 682 final int changePosition = mAdapter.getItemCount() / 4; 683 mLayoutManager.expectLayouts(1); 684 mAdapter.changeAndNotify(changePosition, 1); 685 mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated", 686 1); 687 // delete an item before visible area 688 int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2; 689 assertTrue("test sanity", deletedPosition >= 0); 690 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 691 if (DEBUG) { 692 Log.d(TAG, "before:"); 693 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 694 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue()); 695 } 696 } 697 mLayoutManager.expectLayouts(1); 698 mAdapter.deleteAndNotify(deletedPosition, 1); 699 mLayoutManager.waitForLayout(2); 700 assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it " 701 + "should not affect the layout if it is not visible", before, 702 mLayoutManager.collectChildCoordinates() 703 ); 704 deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2)); 705 mLayoutManager.expectLayouts(1); 706 mAdapter.deleteAndNotify(deletedPosition, 1); 707 mLayoutManager.waitForLayout(2); 708 assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the " 709 + "layout", before, mLayoutManager.collectChildCoordinates()); 710 } 711 712 void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2, 713 int length) { 714 for (int i = 0; i < length; i++) { 715 assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i], 716 set2[start2 + i]); 717 } 718 } 719 720 @Test 721 public void spanCountChangeOnRestoreSavedState() throws Throwable { 722 Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE); 723 setupByConfig(config); 724 waitFirstLayout(); 725 726 int beforeChildCount = mLayoutManager.getChildCount(); 727 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 728 // we append a suffix to the parcelable to test out of bounds 729 String parcelSuffix = UUID.randomUUID().toString(); 730 Parcel parcel = Parcel.obtain(); 731 savedState.writeToParcel(parcel, 0); 732 parcel.writeString(parcelSuffix); 733 removeRecyclerView(); 734 // reset for reading 735 parcel.setDataPosition(0); 736 // re-create 737 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 738 removeRecyclerView(); 739 740 RecyclerView restored = new RecyclerView(getActivity()); 741 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation); 742 mLayoutManager.setReverseLayout(config.mReverseLayout); 743 mLayoutManager.setGapStrategy(config.mGapStrategy); 744 restored.setLayoutManager(mLayoutManager); 745 // use the same adapter for Rect matching 746 restored.setAdapter(mAdapter); 747 restored.onRestoreInstanceState(savedState); 748 mLayoutManager.setSpanCount(1); 749 mLayoutManager.expectLayouts(1); 750 setRecyclerView(restored); 751 mLayoutManager.waitForLayout(2); 752 assertEquals("on saved state, reverse layout should be preserved", 753 config.mReverseLayout, mLayoutManager.getReverseLayout()); 754 assertEquals("on saved state, orientation should be preserved", 755 config.mOrientation, mLayoutManager.getOrientation()); 756 assertEquals("after setting new span count, layout manager should keep new value", 757 1, mLayoutManager.getSpanCount()); 758 assertEquals("on saved state, gap strategy should be preserved", 759 config.mGapStrategy, mLayoutManager.getGapStrategy()); 760 assertTrue("when span count is dramatically changed after restore, # of child views " 761 + "should change", beforeChildCount > mLayoutManager.getChildCount()); 762 // make sure LLM can layout all children. is some span info is leaked, this would crash 763 smoothScrollToPosition(mAdapter.getItemCount() - 1); 764 } 765 766 @Test 767 public void scrollAndClear() throws Throwable { 768 setupByConfig(new Config()); 769 waitFirstLayout(); 770 771 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 772 773 mLayoutManager.expectLayouts(1); 774 runTestOnUiThread(new Runnable() { 775 @Override 776 public void run() { 777 mLayoutManager.scrollToPositionWithOffset(1, 0); 778 mAdapter.clearOnUIThread(); 779 } 780 }); 781 mLayoutManager.waitForLayout(2); 782 783 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 784 } 785 786 @Test 787 public void accessibilityPositions() throws Throwable { 788 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE)); 789 waitFirstLayout(); 790 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 791 .getCompatAccessibilityDelegate(); 792 final AccessibilityEvent event = AccessibilityEvent.obtain(); 793 runTestOnUiThread(new Runnable() { 794 @Override 795 public void run() { 796 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 797 } 798 }); 799 final AccessibilityRecordCompat record = AccessibilityEventCompat 800 .asRecord(event); 801 final int start = mRecyclerView 802 .getChildLayoutPosition( 803 mLayoutManager.findFirstVisibleItemClosestToStart(false, true)); 804 final int end = mRecyclerView 805 .getChildLayoutPosition( 806 mLayoutManager.findFirstVisibleItemClosestToEnd(false, true)); 807 assertEquals("first item position should match", 808 Math.min(start, end), record.getFromIndex()); 809 assertEquals("last item position should match", 810 Math.max(start, end), record.getToIndex()); 811 812 } 813} 814