1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.recyclerview.widget; 18 19 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 20 import static androidx.recyclerview.widget.StaggeredGridLayoutManager 21 .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 22 import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE; 23 import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL; 24 import static androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams; 25 26 import static org.hamcrest.CoreMatchers.equalTo; 27 import static org.junit.Assert.assertEquals; 28 import static org.junit.Assert.assertFalse; 29 import static org.junit.Assert.assertNotNull; 30 import static org.junit.Assert.assertNull; 31 import static org.junit.Assert.assertSame; 32 import static org.junit.Assert.assertThat; 33 import static org.junit.Assert.assertTrue; 34 35 import android.graphics.Color; 36 import android.graphics.Rect; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.StateListDrawable; 39 import android.os.Parcel; 40 import android.os.Parcelable; 41 import android.support.test.filters.LargeTest; 42 import android.text.TextUtils; 43 import android.util.Log; 44 import android.util.StateSet; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.view.accessibility.AccessibilityEvent; 48 import android.widget.EditText; 49 import android.widget.FrameLayout; 50 51 import androidx.annotation.NonNull; 52 import androidx.core.view.AccessibilityDelegateCompat; 53 54 import org.hamcrest.CoreMatchers; 55 import org.hamcrest.MatcherAssert; 56 import org.junit.Test; 57 58 import java.util.HashMap; 59 import java.util.Map; 60 import java.util.UUID; 61 62 @LargeTest 63 public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest { 64 65 @Test 66 public void layout_rvHasPaddingChildIsMatchParentVertical_childrenAreInsideParent() 67 throws Throwable { 68 layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, false); 69 } 70 71 @Test 72 public void layout_rvHasPaddingChildIsMatchParentHorizontal_childrenAreInsideParent() 73 throws Throwable { 74 layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, false); 75 } 76 77 @Test 78 public void layout_rvHasPaddingChildIsMatchParentVerticalFullSpan_childrenAreInsideParent() 79 throws Throwable { 80 layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, true); 81 } 82 83 @Test 84 public void layout_rvHasPaddingChildIsMatchParentHorizontalFullSpan_childrenAreInsideParent() 85 throws Throwable { 86 layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, true); 87 } 88 89 private void layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent( 90 final int orientation, final boolean fullSpan) 91 throws Throwable { 92 93 setupByConfig(new Config(orientation, false, 1, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 94 new GridTestAdapter(10, orientation) { 95 96 @NonNull 97 @Override 98 public TestViewHolder onCreateViewHolder( 99 @NonNull ViewGroup parent, int viewType) { 100 View view = new View(parent.getContext()); 101 StaggeredGridLayoutManager.LayoutParams layoutParams = 102 new StaggeredGridLayoutManager.LayoutParams( 103 ViewGroup.LayoutParams.MATCH_PARENT, 104 ViewGroup.LayoutParams.MATCH_PARENT); 105 layoutParams.setFullSpan(fullSpan); 106 view.setLayoutParams(layoutParams); 107 return new TestViewHolder(view); 108 } 109 110 @Override 111 public void onBindViewHolder(@NonNull TestViewHolder holder, int position) { 112 // No actual binding needed, but we need to override this to prevent default 113 // behavior of GridTestAdapter. 114 } 115 }); 116 mRecyclerView.setPadding(1, 2, 3, 4); 117 118 waitFirstLayout(); 119 120 mActivityRule.runOnUiThread(new Runnable() { 121 @Override 122 public void run() { 123 int childDimension; 124 int recyclerViewDimensionMinusPadding; 125 if (orientation == VERTICAL) { 126 childDimension = mRecyclerView.getChildAt(0).getHeight(); 127 recyclerViewDimensionMinusPadding = mRecyclerView.getHeight() 128 - mRecyclerView.getPaddingTop() 129 - mRecyclerView.getPaddingBottom(); 130 } else { 131 childDimension = mRecyclerView.getChildAt(0).getWidth(); 132 recyclerViewDimensionMinusPadding = mRecyclerView.getWidth() 133 - mRecyclerView.getPaddingLeft() 134 - mRecyclerView.getPaddingRight(); 135 } 136 assertThat(childDimension, equalTo(recyclerViewDimensionMinusPadding)); 137 } 138 }); 139 } 140 141 @Test 142 public void forceLayoutOnDetach() throws Throwable { 143 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 144 waitFirstLayout(); 145 assertFalse("test sanity", mRecyclerView.isLayoutRequested()); 146 mActivityRule.runOnUiThread(new Runnable() { 147 @Override 148 public void run() { 149 mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler); 150 } 151 }); 152 assertTrue(mRecyclerView.isLayoutRequested()); 153 } 154 155 @Test 156 public void areAllStartsTheSame() throws Throwable { 157 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300)); 158 waitFirstLayout(); 159 smoothScrollToPosition(100); 160 mLayoutManager.expectLayouts(1); 161 mAdapter.deleteAndNotify(0, 2); 162 mLayoutManager.waitForLayout(2000); 163 smoothScrollToPosition(0); 164 assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual()); 165 } 166 167 @Test 168 public void areAllEndsTheSame() throws Throwable { 169 setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300)); 170 waitFirstLayout(); 171 smoothScrollToPosition(100); 172 mLayoutManager.expectLayouts(1); 173 mAdapter.deleteAndNotify(0, 2); 174 mLayoutManager.waitForLayout(2); 175 smoothScrollToPosition(0); 176 assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual()); 177 } 178 179 @Test 180 public void getPositionsBeforeInitialization() throws Throwable { 181 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 182 int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null); 183 MatcherAssert.assertThat(positions, 184 CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION, 185 RecyclerView.NO_POSITION})); 186 } 187 188 @Test 189 public void findLastInUnevenDistribution() throws Throwable { 190 setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) 191 .itemCount(5)); 192 mAdapter.mOnBindCallback = new OnBindCallback() { 193 @Override 194 void onBoundItem(TestViewHolder vh, int position) { 195 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams(); 196 if (position == 1) { 197 lp.height = mRecyclerView.getHeight() - 10; 198 } else { 199 lp.height = 5; 200 } 201 vh.itemView.setMinimumHeight(0); 202 } 203 }; 204 waitFirstLayout(); 205 int[] into = new int[2]; 206 mLayoutManager.findFirstCompletelyVisibleItemPositions(into); 207 assertEquals("first completely visible item from span 0 should be 0", 0, into[0]); 208 assertEquals("first completely visible item from span 1 should be 1", 1, into[1]); 209 mLayoutManager.findLastCompletelyVisibleItemPositions(into); 210 assertEquals("last completely visible item from span 0 should be 4", 4, into[0]); 211 assertEquals("last completely visible item from span 1 should be 1", 1, into[1]); 212 assertEquals("first fully visible child should be at position", 213 0, mRecyclerView.getChildViewHolder(mLayoutManager. 214 findFirstVisibleItemClosestToStart(true)).getPosition()); 215 assertEquals("last fully visible child should be at position", 216 4, mRecyclerView.getChildViewHolder(mLayoutManager. 217 findFirstVisibleItemClosestToEnd(true)).getPosition()); 218 219 assertEquals("first visible child should be at position", 220 0, mRecyclerView.getChildViewHolder(mLayoutManager. 221 findFirstVisibleItemClosestToStart(false)).getPosition()); 222 assertEquals("last visible child should be at position", 223 4, mRecyclerView.getChildViewHolder(mLayoutManager. 224 findFirstVisibleItemClosestToEnd(false)).getPosition()); 225 226 } 227 228 @Test 229 public void customWidthInHorizontal() throws Throwable { 230 customSizeInScrollDirectionTest( 231 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 232 } 233 234 @Test 235 public void customHeightInVertical() throws Throwable { 236 customSizeInScrollDirectionTest( 237 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)); 238 } 239 240 public void customSizeInScrollDirectionTest(final Config config) throws Throwable { 241 setupByConfig(config); 242 final Map<View, Integer> sizeMap = new HashMap<View, Integer>(); 243 mAdapter.mOnBindCallback = new OnBindCallback() { 244 @Override 245 void onBoundItem(TestViewHolder vh, int position) { 246 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams(); 247 final int size = 1 + position * 5; 248 if (config.mOrientation == HORIZONTAL) { 249 layoutParams.width = size; 250 } else { 251 layoutParams.height = size; 252 } 253 sizeMap.put(vh.itemView, size); 254 if (position == 3) { 255 getLp(vh.itemView).setFullSpan(true); 256 } 257 } 258 259 @Override 260 boolean assignRandomSize() { 261 return false; 262 } 263 }; 264 waitFirstLayout(); 265 assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0); 266 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 267 View child = mRecyclerView.getChildAt(i); 268 final int size = config.mOrientation == HORIZONTAL ? child.getWidth() 269 : child.getHeight(); 270 assertEquals("child " + i + " should have the size specified in its layout params", 271 sizeMap.get(child).intValue(), size); 272 } 273 checkForMainThreadException(); 274 } 275 276 @Test 277 public void gapHandlingWhenItemMovesToTop() throws Throwable { 278 gapHandlingWhenItemMovesToTopTest(); 279 } 280 281 @Test 282 public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable { 283 gapHandlingWhenItemMovesToTopTest(0); 284 } 285 286 @Test 287 public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable { 288 gapHandlingWhenItemMovesToTopTest(1); 289 } 290 291 public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable { 292 Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 293 config.itemCount(3); 294 setupByConfig(config); 295 mAdapter.mOnBindCallback = new OnBindCallback() { 296 @Override 297 void onBoundItem(TestViewHolder vh, int position) { 298 } 299 300 @Override 301 boolean assignRandomSize() { 302 return false; 303 } 304 }; 305 for (int i : fullSpanIndices) { 306 mAdapter.mFullSpanItems.add(i); 307 } 308 waitFirstLayout(); 309 mLayoutManager.expectLayouts(1); 310 mAdapter.moveItem(1, 0, true); 311 mLayoutManager.waitForLayout(2); 312 final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates(); 313 // move back. 314 mLayoutManager.expectLayouts(1); 315 mAdapter.moveItem(0, 1, true); 316 mLayoutManager.waitForLayout(2); 317 mLayoutManager.expectLayouts(2); 318 mAdapter.moveAndNotify(1, 0); 319 mLayoutManager.waitForLayout(2); 320 Thread.sleep(1000); 321 getInstrumentation().waitForIdleSync(); 322 checkForMainThreadException(); 323 // item should be positioned properly 324 assertRectSetsEqual("final position after a move", desiredPositions, 325 mLayoutManager.collectChildCoordinates()); 326 327 } 328 329 @Test 330 public void focusSearchFailureUp() throws Throwable { 331 focusSearchFailure(false); 332 } 333 334 @Test 335 public void focusSearchFailureDown() throws Throwable { 336 focusSearchFailure(true); 337 } 338 339 @Test 340 public void focusSearchFailureFromSubChild() throws Throwable { 341 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 342 new GridTestAdapter(1000, VERTICAL) { 343 344 @NonNull 345 @Override 346 public TestViewHolder onCreateViewHolder( 347 @NonNull ViewGroup parent, int viewType) { 348 FrameLayout fl = new FrameLayout(parent.getContext()); 349 EditText editText = new EditText(parent.getContext()); 350 fl.addView(editText); 351 editText.setEllipsize(TextUtils.TruncateAt.END); 352 return new TestViewHolder(fl); 353 } 354 355 @Override 356 public void onBindViewHolder(@NonNull TestViewHolder holder, int position) { 357 Item item = mItems.get(position); 358 holder.mBoundItem = item; 359 ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText( 360 item.mText + " (" + item.mId + ")"); 361 // Good to have colors for debugging 362 StateListDrawable stl = new StateListDrawable(); 363 stl.addState(new int[]{android.R.attr.state_focused}, 364 new ColorDrawable(Color.RED)); 365 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 366 //noinspection deprecation using this for kitkat tests 367 holder.itemView.setBackgroundDrawable(stl); 368 if (mOnBindCallback != null) { 369 mOnBindCallback.onBoundItem(holder, position); 370 } 371 } 372 }); 373 mLayoutManager.expectLayouts(1); 374 setRecyclerView(mRecyclerView); 375 mLayoutManager.waitForLayout(10); 376 getInstrumentation().waitForIdleSync(); 377 ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt( 378 mRecyclerView.getChildCount() - 1); 379 RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild); 380 View subChildToFocus = lastChild.getChildAt(0); 381 requestFocus(subChildToFocus, true); 382 assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true)); 383 focusSearch(subChildToFocus, View.FOCUS_FORWARD); 384 waitForIdleScroll(mRecyclerView); 385 checkForMainThreadException(); 386 View focusedChild = mRecyclerView.getFocusedChild(); 387 if (focusedChild == subChildToFocus.getParent()) { 388 focusSearch(focusedChild, View.FOCUS_FORWARD); 389 waitForIdleScroll(mRecyclerView); 390 focusedChild = mRecyclerView.getFocusedChild(); 391 } 392 RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder( 393 focusedChild); 394 assertTrue("new focused view should have a larger position " 395 + lastViewHolder.getAdapterPosition() + " vs " 396 + containingViewHolder.getAdapterPosition(), 397 lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition()); 398 } 399 400 public void focusSearchFailure(boolean scrollDown) throws Throwable { 401 int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP; 402 setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) 403 , new GridTestAdapter(31, 1) { 404 RecyclerView mAttachedRv; 405 406 @Override 407 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 408 int viewType) { 409 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 410 testViewHolder.itemView.setFocusable(true); 411 testViewHolder.itemView.setFocusableInTouchMode(true); 412 // Good to have colors for debugging 413 StateListDrawable stl = new StateListDrawable(); 414 stl.addState(new int[]{android.R.attr.state_focused}, 415 new ColorDrawable(Color.RED)); 416 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 417 //noinspection deprecation used to support kitkat tests 418 testViewHolder.itemView.setBackgroundDrawable(stl); 419 return testViewHolder; 420 } 421 422 @Override 423 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 424 mAttachedRv = recyclerView; 425 } 426 427 @Override 428 public void onBindViewHolder(@NonNull TestViewHolder holder, 429 int position) { 430 super.onBindViewHolder(holder, position); 431 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3); 432 } 433 }); 434 /** 435 * 0 1 2 436 * 3 4 5 437 * 6 7 8 438 * 9 10 11 439 * 12 13 14 440 * 15 16 17 441 * 18 18 18 442 * 19 443 * 20 20 20 444 * 21 22 445 * 23 23 23 446 * 24 25 26 447 * 27 28 29 448 * 30 449 */ 450 mAdapter.mFullSpanItems.add(18); 451 mAdapter.mFullSpanItems.add(20); 452 mAdapter.mFullSpanItems.add(23); 453 waitFirstLayout(); 454 View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView; 455 assertTrue(requestFocus(viewToFocus, true)); 456 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 457 int pos = 1; 458 View focusedView = viewToFocus; 459 while (pos < 16) { 460 focusSearchAndWaitForScroll(focusedView, focusDir); 461 focusedView = mRecyclerView.getFocusedChild(); 462 assertEquals(pos + 3, 463 mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 464 pos += 3; 465 } 466 for (int i : new int[]{18, 19, 20, 21, 23, 24}) { 467 focusSearchAndWaitForScroll(focusedView, focusDir); 468 focusedView = mRecyclerView.getFocusedChild(); 469 assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 470 } 471 // now move right 472 focusSearch(focusedView, View.FOCUS_RIGHT); 473 waitForIdleScroll(mRecyclerView); 474 focusedView = mRecyclerView.getFocusedChild(); 475 assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 476 for (int i : new int[]{28, 30}) { 477 focusSearchAndWaitForScroll(focusedView, focusDir); 478 focusedView = mRecyclerView.getFocusedChild(); 479 assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 480 } 481 } 482 483 private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable { 484 focusSearch(focused, dir); 485 waitForIdleScroll(mRecyclerView); 486 } 487 488 @Test 489 public void topUnfocusableViewsVisibility() throws Throwable { 490 // The maximum number of rows that can be fully in-bounds of RV. 491 final int visibleRowCount = 5; 492 final int spanCount = 3; 493 final int lastFocusableIndex = 6; 494 495 setupByConfig(new Config(VERTICAL, true, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 496 new GridTestAdapter(18, 1) { 497 RecyclerView mAttachedRv; 498 499 @Override 500 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 501 int viewType) { 502 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 503 testViewHolder.itemView.setFocusable(true); 504 testViewHolder.itemView.setFocusableInTouchMode(true); 505 // Good to have colors for debugging 506 StateListDrawable stl = new StateListDrawable(); 507 stl.addState(new int[]{android.R.attr.state_focused}, 508 new ColorDrawable(Color.RED)); 509 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 510 //noinspection deprecation used to support kitkat tests 511 testViewHolder.itemView.setBackgroundDrawable(stl); 512 return testViewHolder; 513 } 514 515 @Override 516 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 517 mAttachedRv = recyclerView; 518 } 519 520 @Override 521 public void onBindViewHolder(@NonNull TestViewHolder holder, 522 int position) { 523 super.onBindViewHolder(holder, position); 524 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 525 .getLayoutParams(); 526 if (position <= lastFocusableIndex) { 527 holder.itemView.setFocusable(true); 528 holder.itemView.setFocusableInTouchMode(true); 529 } else { 530 holder.itemView.setFocusable(false); 531 holder.itemView.setFocusableInTouchMode(false); 532 } 533 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); 534 lp.topMargin = 0; 535 lp.leftMargin = 0; 536 lp.rightMargin = 0; 537 lp.bottomMargin = 0; 538 if (position == 11) { 539 lp.bottomMargin = 9; 540 } 541 } 542 }); 543 544 /** 545 * 546 * 15 16 17 547 * 12 13 14 548 * 11 11 11 549 * 9 10 550 * 8 8 8 551 * 7 552 * 6 6 6 553 * 3 4 5 554 * 0 1 2 555 */ 556 mAdapter.mFullSpanItems.add(6); 557 mAdapter.mFullSpanItems.add(8); 558 mAdapter.mFullSpanItems.add(11); 559 waitFirstLayout(); 560 561 562 // adapter position of the currently focused item. 563 int focusIndex = 1; 564 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 565 focusIndex); 566 View viewToFocus = toFocus.itemView; 567 assertTrue(requestFocus(viewToFocus, true)); 568 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 569 570 // The VH of the unfocusable item that just became fully visible after focusSearch. 571 RecyclerView.ViewHolder toVisible = null; 572 573 View focusedView = viewToFocus; 574 int actualFocusIndex = -1; 575 // First, scroll until the last focusable row. 576 for (int i : new int[]{4, 6}) { 577 focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP); 578 focusedView = mRecyclerView.getFocusedChild(); 579 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 580 assertEquals("Focused view should be at adapter position " + i + " whereas it's at " 581 + actualFocusIndex, i, actualFocusIndex); 582 } 583 584 // Further scroll up in order to make the unfocusable rows visible. This process should 585 // continue until the currently focused item is still visible. The focused item should not 586 // change in this loop. 587 for (int i : new int[]{9, 11, 11, 11}) { 588 focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP); 589 focusedView = mRecyclerView.getFocusedChild(); 590 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 591 toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); 592 593 assertEquals("Focused view should not be changed, whereas it's now at " 594 + actualFocusIndex, 6, actualFocusIndex); 595 assertTrue("Focused child should be at least partially visible.", 596 isViewPartiallyInBound(mRecyclerView, focusedView)); 597 assertTrue("Child view at adapter pos " + i + " should be fully visible.", 598 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 599 } 600 } 601 602 @Test 603 public void bottomUnfocusableViewsVisibility() throws Throwable { 604 // The maximum number of rows that can be fully in-bounds of RV. 605 final int visibleRowCount = 5; 606 final int spanCount = 3; 607 final int lastFocusableIndex = 6; 608 609 setupByConfig(new Config(VERTICAL, false, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 610 new GridTestAdapter(18, 1) { 611 RecyclerView mAttachedRv; 612 613 @Override 614 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 615 int viewType) { 616 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 617 testViewHolder.itemView.setFocusable(true); 618 testViewHolder.itemView.setFocusableInTouchMode(true); 619 // Good to have colors for debugging 620 StateListDrawable stl = new StateListDrawable(); 621 stl.addState(new int[]{android.R.attr.state_focused}, 622 new ColorDrawable(Color.RED)); 623 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 624 //noinspection deprecation used to support kitkat tests 625 testViewHolder.itemView.setBackgroundDrawable(stl); 626 return testViewHolder; 627 } 628 629 @Override 630 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 631 mAttachedRv = recyclerView; 632 } 633 634 @Override 635 public void onBindViewHolder(@NonNull TestViewHolder holder, 636 int position) { 637 super.onBindViewHolder(holder, position); 638 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 639 .getLayoutParams(); 640 if (position <= lastFocusableIndex) { 641 holder.itemView.setFocusable(true); 642 holder.itemView.setFocusableInTouchMode(true); 643 } else { 644 holder.itemView.setFocusable(false); 645 holder.itemView.setFocusableInTouchMode(false); 646 } 647 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); 648 lp.topMargin = 0; 649 lp.leftMargin = 0; 650 lp.rightMargin = 0; 651 lp.bottomMargin = 0; 652 if (position == 11) { 653 lp.topMargin = 9; 654 } 655 } 656 }); 657 658 /** 659 * 0 1 2 660 * 3 4 5 661 * 6 6 6 662 * 7 663 * 8 8 8 664 * 9 10 665 * 11 11 11 666 * 12 13 14 667 * 15 16 17 668 */ 669 mAdapter.mFullSpanItems.add(6); 670 mAdapter.mFullSpanItems.add(8); 671 mAdapter.mFullSpanItems.add(11); 672 waitFirstLayout(); 673 674 675 // adapter position of the currently focused item. 676 int focusIndex = 1; 677 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 678 focusIndex); 679 View viewToFocus = toFocus.itemView; 680 assertTrue(requestFocus(viewToFocus, true)); 681 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 682 683 // The VH of the unfocusable item that just became fully visible after focusSearch. 684 RecyclerView.ViewHolder toVisible = null; 685 686 View focusedView = viewToFocus; 687 int actualFocusIndex = -1; 688 // First, scroll until the last focusable row. 689 for (int i : new int[]{4, 6}) { 690 focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN); 691 focusedView = mRecyclerView.getFocusedChild(); 692 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 693 assertEquals("Focused view should be at adapter position " + i + " whereas it's at " 694 + actualFocusIndex, i, actualFocusIndex); 695 } 696 697 // Further scroll down in order to make the unfocusable rows visible. This process should 698 // continue until the currently focused item is still visible. The focused item should not 699 // change in this loop. 700 for (int i : new int[]{9, 11, 11, 11}) { 701 focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN); 702 focusedView = mRecyclerView.getFocusedChild(); 703 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 704 toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); 705 706 assertEquals("Focused view should not be changed, whereas it's now at " 707 + actualFocusIndex, 6, actualFocusIndex); 708 assertTrue("Focused child should be at least partially visible.", 709 isViewPartiallyInBound(mRecyclerView, focusedView)); 710 assertTrue("Child view at adapter pos " + i + " should be fully visible.", 711 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 712 } 713 } 714 715 @Test 716 public void leftUnfocusableViewsVisibility() throws Throwable { 717 // The maximum number of columns that can be fully in-bounds of RV. 718 final int visibleColCount = 5; 719 final int spanCount = 3; 720 final int lastFocusableIndex = 6; 721 722 // Reverse layout so that views are placed from right to left. 723 setupByConfig(new Config(HORIZONTAL, true, spanCount, 724 GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 725 new GridTestAdapter(18, 1) { 726 RecyclerView mAttachedRv; 727 728 @Override 729 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 730 int viewType) { 731 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 732 testViewHolder.itemView.setFocusable(true); 733 testViewHolder.itemView.setFocusableInTouchMode(true); 734 // Good to have colors for debugging 735 StateListDrawable stl = new StateListDrawable(); 736 stl.addState(new int[]{android.R.attr.state_focused}, 737 new ColorDrawable(Color.RED)); 738 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 739 //noinspection deprecation used to support kitkat tests 740 testViewHolder.itemView.setBackgroundDrawable(stl); 741 return testViewHolder; 742 } 743 744 @Override 745 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 746 mAttachedRv = recyclerView; 747 } 748 749 @Override 750 public void onBindViewHolder(@NonNull TestViewHolder holder, 751 int position) { 752 super.onBindViewHolder(holder, position); 753 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 754 .getLayoutParams(); 755 if (position <= lastFocusableIndex) { 756 holder.itemView.setFocusable(true); 757 holder.itemView.setFocusableInTouchMode(true); 758 } else { 759 holder.itemView.setFocusable(false); 760 holder.itemView.setFocusableInTouchMode(false); 761 } 762 holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); 763 lp.topMargin = 0; 764 lp.leftMargin = 0; 765 lp.rightMargin = 0; 766 lp.bottomMargin = 0; 767 if (position == 11) { 768 lp.rightMargin = 9; 769 } 770 } 771 }); 772 773 /** 774 * 15 12 11 9 8 7 6 3 0 775 * 16 13 11 10 8 6 4 1 776 * 17 14 11 8 6 5 2 777 */ 778 mAdapter.mFullSpanItems.add(6); 779 mAdapter.mFullSpanItems.add(8); 780 mAdapter.mFullSpanItems.add(11); 781 waitFirstLayout(); 782 783 784 // adapter position of the currently focused item. 785 int focusIndex = 1; 786 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 787 focusIndex); 788 View viewToFocus = toFocus.itemView; 789 assertTrue(requestFocus(viewToFocus, true)); 790 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 791 792 // The VH of the unfocusable item that just became fully visible after focusSearch. 793 RecyclerView.ViewHolder toVisible = null; 794 795 View focusedView = viewToFocus; 796 int actualFocusIndex = -1; 797 // First, scroll until the last focusable column. 798 for (int i : new int[]{4, 6}) { 799 focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT); 800 focusedView = mRecyclerView.getFocusedChild(); 801 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 802 assertEquals("Focused view should be at adapter position " + i + " whereas it's at " 803 + actualFocusIndex, i, actualFocusIndex); 804 } 805 806 // Further scroll left in order to make the unfocusable columns visible. This process should 807 // continue until the currently focused item is still visible. The focused item should not 808 // change in this loop. 809 for (int i : new int[]{9, 11, 11, 11}) { 810 focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT); 811 focusedView = mRecyclerView.getFocusedChild(); 812 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 813 toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); 814 815 assertEquals("Focused view should not be changed, whereas it's now at " 816 + actualFocusIndex, 6, actualFocusIndex); 817 assertTrue("Focused child should be at least partially visible.", 818 isViewPartiallyInBound(mRecyclerView, focusedView)); 819 assertTrue("Child view at adapter pos " + i + " should be fully visible.", 820 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 821 } 822 } 823 824 @Test 825 public void rightUnfocusableViewsVisibility() throws Throwable { 826 // The maximum number of columns that can be fully in-bounds of RV. 827 final int visibleColCount = 5; 828 final int spanCount = 3; 829 final int lastFocusableIndex = 6; 830 831 setupByConfig(new Config(HORIZONTAL, false, spanCount, 832 GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS), 833 new GridTestAdapter(18, 1) { 834 RecyclerView mAttachedRv; 835 836 @Override 837 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 838 int viewType) { 839 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 840 testViewHolder.itemView.setFocusable(true); 841 testViewHolder.itemView.setFocusableInTouchMode(true); 842 // Good to have colors for debugging 843 StateListDrawable stl = new StateListDrawable(); 844 stl.addState(new int[]{android.R.attr.state_focused}, 845 new ColorDrawable(Color.RED)); 846 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 847 //noinspection deprecation used to support kitkat tests 848 testViewHolder.itemView.setBackgroundDrawable(stl); 849 return testViewHolder; 850 } 851 852 @Override 853 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 854 mAttachedRv = recyclerView; 855 } 856 857 @Override 858 public void onBindViewHolder(@NonNull TestViewHolder holder, 859 int position) { 860 super.onBindViewHolder(holder, position); 861 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 862 .getLayoutParams(); 863 if (position <= lastFocusableIndex) { 864 holder.itemView.setFocusable(true); 865 holder.itemView.setFocusableInTouchMode(true); 866 } else { 867 holder.itemView.setFocusable(false); 868 holder.itemView.setFocusableInTouchMode(false); 869 } 870 holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); 871 lp.topMargin = 0; 872 lp.leftMargin = 0; 873 lp.rightMargin = 0; 874 lp.bottomMargin = 0; 875 if (position == 11) { 876 lp.leftMargin = 9; 877 } 878 } 879 }); 880 881 /** 882 * 0 3 6 7 8 9 11 12 15 883 * 1 4 6 8 10 11 13 16 884 * 2 5 6 8 11 14 17 885 */ 886 mAdapter.mFullSpanItems.add(6); 887 mAdapter.mFullSpanItems.add(8); 888 mAdapter.mFullSpanItems.add(11); 889 waitFirstLayout(); 890 891 892 // adapter position of the currently focused item. 893 int focusIndex = 1; 894 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 895 focusIndex); 896 View viewToFocus = toFocus.itemView; 897 assertTrue(requestFocus(viewToFocus, true)); 898 assertSame(viewToFocus, mRecyclerView.getFocusedChild()); 899 900 // The VH of the unfocusable item that just became fully visible after focusSearch. 901 RecyclerView.ViewHolder toVisible = null; 902 903 View focusedView = viewToFocus; 904 int actualFocusIndex = -1; 905 // First, scroll until the last focusable column. 906 for (int i : new int[]{4, 6}) { 907 focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT); 908 focusedView = mRecyclerView.getFocusedChild(); 909 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 910 assertEquals("Focused view should be at adapter position " + i + " whereas it's at " 911 + actualFocusIndex, i, actualFocusIndex); 912 } 913 914 // Further scroll right in order to make the unfocusable rows visible. This process should 915 // continue until the currently focused item is still visible. The focused item should not 916 // change in this loop. 917 for (int i : new int[]{9, 11, 11, 11}) { 918 focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT); 919 focusedView = mRecyclerView.getFocusedChild(); 920 actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition(); 921 toVisible = mRecyclerView.findViewHolderForAdapterPosition(i); 922 923 assertEquals("Focused view should not be changed, whereas it's now at " 924 + actualFocusIndex, 6, actualFocusIndex); 925 assertTrue("Focused child should be at least partially visible.", 926 isViewPartiallyInBound(mRecyclerView, focusedView)); 927 assertTrue("Child view at adapter pos " + i + " should be fully visible.", 928 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 929 } 930 } 931 932 @Test 933 public void scrollToPositionWithPredictive() throws Throwable { 934 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 935 removeRecyclerView(); 936 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 937 LinearLayoutManager.INVALID_OFFSET); 938 removeRecyclerView(); 939 scrollToPositionWithPredictive(9, 20); 940 removeRecyclerView(); 941 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 942 943 } 944 945 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 946 throws Throwable { 947 setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL, 948 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE)); 949 waitFirstLayout(); 950 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 951 @Override 952 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 953 RecyclerView rv = mLayoutManager.mRecyclerView; 954 if (state.isPreLayout()) { 955 assertEquals("pending scroll position should still be pending", 956 scrollPosition, mLayoutManager.mPendingScrollPosition); 957 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 958 assertEquals("pending scroll position offset should still be pending", 959 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 960 } 961 } else { 962 RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition); 963 assertNotNull("scroll to position should work", vh); 964 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 965 assertEquals("scroll offset should be applied properly", 966 mLayoutManager.getPaddingTop() + scrollOffset 967 + ((RecyclerView.LayoutParams) vh.itemView 968 .getLayoutParams()).topMargin, 969 mLayoutManager.getDecoratedTop(vh.itemView)); 970 } 971 } 972 } 973 }; 974 mLayoutManager.expectLayouts(2); 975 mActivityRule.runOnUiThread(new Runnable() { 976 @Override 977 public void run() { 978 try { 979 mAdapter.addAndNotify(0, 1); 980 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 981 mLayoutManager.scrollToPosition(scrollPosition); 982 } else { 983 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 984 scrollOffset); 985 } 986 987 } catch (Throwable throwable) { 988 throwable.printStackTrace(); 989 } 990 991 } 992 }); 993 mLayoutManager.waitForLayout(2); 994 checkForMainThreadException(); 995 } 996 997 @Test 998 public void moveGapHandling() throws Throwable { 999 Config config = new Config().spanCount(2).itemCount(40); 1000 setupByConfig(config); 1001 waitFirstLayout(); 1002 mLayoutManager.expectLayouts(2); 1003 mAdapter.moveAndNotify(4, 1); 1004 mLayoutManager.waitForLayout(2); 1005 assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix()); 1006 } 1007 1008 @Test 1009 public void updateAfterFullSpan() throws Throwable { 1010 updateAfterFullSpanGapHandlingTest(0); 1011 } 1012 1013 @Test 1014 public void updateAfterFullSpan2() throws Throwable { 1015 updateAfterFullSpanGapHandlingTest(20); 1016 } 1017 1018 @Test 1019 public void temporaryGapHandling() throws Throwable { 1020 int fullSpanIndex = 200; 1021 setupByConfig(new Config().spanCount(2).itemCount(500)); 1022 mAdapter.mFullSpanItems.add(fullSpanIndex); 1023 waitFirstLayout(); 1024 smoothScrollToPosition(fullSpanIndex + 200);// go far away 1025 assertNull("test sanity. full span item should not be visible", 1026 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex)); 1027 mLayoutManager.expectLayouts(1); 1028 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 1029 mLayoutManager.waitForLayout(1); 1030 smoothScrollToPosition(0); 1031 mLayoutManager.expectLayouts(1); 1032 smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1)); 1033 String log = mLayoutManager.layoutToString("post gap"); 1034 mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a " 1035 + "relayout " + log, 2); 1036 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 1037 assertNotNull("full span item should be there:\n" + log, fullSpan); 1038 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 1039 assertNotNull("next view should be there\n" + log, view1); 1040 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 1041 assertNotNull("+2 view should be there\n" + log, view2); 1042 1043 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 1044 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 1045 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 1046 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 1047 assertEquals("no gap between span and view 1", 1048 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 1049 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 1050 assertEquals("no gap between span and view 2", 1051 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 1052 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 1053 } 1054 1055 public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable { 1056 setupByConfig(new Config().spanCount(2).itemCount(100)); 1057 mAdapter.mFullSpanItems.add(fullSpanIndex); 1058 waitFirstLayout(); 1059 smoothScrollToPosition(fullSpanIndex + 30); 1060 mLayoutManager.expectLayouts(1); 1061 mAdapter.deleteAndNotify(fullSpanIndex + 1, 3); 1062 mLayoutManager.waitForLayout(1); 1063 smoothScrollToPosition(fullSpanIndex); 1064 // give it some time to fix the gap 1065 Thread.sleep(500); 1066 View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex); 1067 1068 View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1); 1069 View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2); 1070 1071 LayoutParams lp1 = (LayoutParams) view1.getLayoutParams(); 1072 LayoutParams lp2 = (LayoutParams) view2.getLayoutParams(); 1073 assertEquals("view 1 span index", 0, lp1.getSpanIndex()); 1074 assertEquals("view 2 span index", 1, lp2.getSpanIndex()); 1075 assertEquals("no gap between span and view 1", 1076 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 1077 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1)); 1078 assertEquals("no gap between span and view 2", 1079 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan), 1080 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2)); 1081 } 1082 1083 @Test 1084 public void innerGapHandling() throws Throwable { 1085 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE); 1086 innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); 1087 } 1088 1089 public void innerGapHandlingTest(int strategy) throws Throwable { 1090 Config config = new Config().spanCount(3).itemCount(500); 1091 setupByConfig(config); 1092 mLayoutManager.setGapStrategy(strategy); 1093 mAdapter.mFullSpanItems.add(100); 1094 mAdapter.mFullSpanItems.add(104); 1095 mAdapter.mViewsHaveEqualSize = true; 1096 mAdapter.mOnBindCallback = new OnBindCallback() { 1097 @Override 1098 void onBoundItem(TestViewHolder vh, int position) { 1099 1100 } 1101 1102 @Override 1103 void onCreatedViewHolder(TestViewHolder vh) { 1104 super.onCreatedViewHolder(vh); 1105 //make sure we have enough views 1106 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5; 1107 } 1108 }; 1109 waitFirstLayout(); 1110 mLayoutManager.expectLayouts(1); 1111 scrollToPosition(400); 1112 mLayoutManager.waitForLayout(2); 1113 View view400 = mLayoutManager.findViewByPosition(400); 1114 assertNotNull("test sanity, scrollToPos should succeed", view400); 1115 assertTrue("test sanity, view should be visible top", 1116 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >= 1117 mLayoutManager.mPrimaryOrientation.getStartAfterPadding()); 1118 assertTrue("test sanity, view should be visible bottom", 1119 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <= 1120 mLayoutManager.mPrimaryOrientation.getEndAfterPadding()); 1121 mLayoutManager.expectLayouts(2); 1122 mAdapter.addAndNotify(101, 1); 1123 mLayoutManager.waitForLayout(2); 1124 checkForMainThreadException(); 1125 if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) { 1126 mLayoutManager.expectLayouts(1); 1127 } 1128 // state 1129 // now smooth scroll to 99 to trigger a layout around 100 1130 mLayoutManager.validateChildren(); 1131 smoothScrollToPosition(99); 1132 switch (strategy) { 1133 case GAP_HANDLING_NONE: 1134 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0}, 1135 new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2}, 1136 new int[]{105, 0}); 1137 break; 1138 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS: 1139 mLayoutManager.waitForLayout(2); 1140 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0}, 1141 new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0}); 1142 break; 1143 } 1144 1145 } 1146 1147 @Test 1148 public void fullSizeSpans() throws Throwable { 1149 Config config = new Config().spanCount(5).itemCount(30); 1150 setupByConfig(config); 1151 mAdapter.mFullSpanItems.add(3); 1152 waitFirstLayout(); 1153 assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2}, 1154 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2}, 1155 new int[]{7, 3}, new int[]{8, 4}); 1156 } 1157 1158 void assertSpans(String msg, int[]... childSpanTuples) { 1159 msg = msg + mLayoutManager.layoutToString("\n\n"); 1160 for (int i = 0; i < childSpanTuples.length; i++) { 1161 assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]); 1162 } 1163 } 1164 1165 void assertSpan(String msg, int childPosition, int expectedSpan) { 1166 View view = mLayoutManager.findViewByPosition(childPosition); 1167 assertNotNull(msg + " view at position " + childPosition + " should exists", view); 1168 assertEquals(msg + "[child:" + childPosition + "]", expectedSpan, 1169 getLp(view).mSpan.mIndex); 1170 } 1171 1172 @Test 1173 public void partialSpanInvalidation() throws Throwable { 1174 Config config = new Config().spanCount(5).itemCount(100); 1175 setupByConfig(config); 1176 for (int i = 20; i < mAdapter.getItemCount(); i += 20) { 1177 mAdapter.mFullSpanItems.add(i); 1178 } 1179 waitFirstLayout(); 1180 smoothScrollToPosition(50); 1181 int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30]; 1182 mAdapter.changeAndNotify(15, 2); 1183 Thread.sleep(200); 1184 assertEquals("Invalidation should happen within full span item boundaries", prevSpanId, 1185 mLayoutManager.mLazySpanLookup.mData[30]); 1186 assertEquals("item in invalidated range should have clear span id", 1187 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 1188 smoothScrollToPosition(85); 1189 int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85); 1190 mAdapter.deleteAndNotify(55, 2); 1191 Thread.sleep(200); 1192 assertEquals("item in invalidated range should have clear span id", 1193 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]); 1194 int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83); 1195 assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans, 1196 newSpans, 0, 0, newSpans.length); 1197 } 1198 1199 // Same as Arrays.copyOfRange but for API 7 1200 private int[] copyOfRange(int[] original, int from, int to) { 1201 int newLength = to - from; 1202 if (newLength < 0) { 1203 throw new IllegalArgumentException(from + " > " + to); 1204 } 1205 int[] copy = new int[newLength]; 1206 System.arraycopy(original, from, copy, 0, 1207 Math.min(original.length - from, newLength)); 1208 return copy; 1209 } 1210 1211 @Test 1212 public void spanReassignmentsOnItemChange() throws Throwable { 1213 Config config = new Config().spanCount(5); 1214 setupByConfig(config); 1215 waitFirstLayout(); 1216 smoothScrollToPosition(mAdapter.getItemCount() / 2); 1217 final int changePosition = mAdapter.getItemCount() / 4; 1218 mLayoutManager.expectLayouts(1); 1219 if (RecyclerView.POST_UPDATES_ON_ANIMATION) { 1220 mAdapter.changeAndNotify(changePosition, 1); 1221 mLayoutManager.assertNoLayout("no layout should happen when an invisible child is " 1222 + "updated", 1); 1223 } else { 1224 mAdapter.changeAndNotify(changePosition, 1); 1225 mLayoutManager.waitForLayout(1); 1226 } 1227 1228 // delete an item before visible area 1229 int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2; 1230 assertTrue("test sanity", deletedPosition >= 0); 1231 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1232 if (DEBUG) { 1233 Log.d(TAG, "before:"); 1234 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1235 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue()); 1236 } 1237 } 1238 mLayoutManager.expectLayouts(1); 1239 mAdapter.deleteAndNotify(deletedPosition, 1); 1240 mLayoutManager.waitForLayout(2); 1241 assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it " 1242 + "should not affect the layout if it is not visible", before, 1243 mLayoutManager.collectChildCoordinates() 1244 ); 1245 deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2)); 1246 mLayoutManager.expectLayouts(1); 1247 mAdapter.deleteAndNotify(deletedPosition, 1); 1248 mLayoutManager.waitForLayout(2); 1249 assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the " 1250 + "layout", before, mLayoutManager.collectChildCoordinates()); 1251 } 1252 1253 void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2, 1254 int length) { 1255 for (int i = 0; i < length; i++) { 1256 assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i], 1257 set2[start2 + i]); 1258 } 1259 } 1260 1261 @Test 1262 public void spanCountChangeOnRestoreSavedState() throws Throwable { 1263 Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE).itemCount(50); 1264 setupByConfig(config); 1265 waitFirstLayout(); 1266 1267 int beforeChildCount = mLayoutManager.getChildCount(); 1268 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 1269 // we append a suffix to the parcelable to test out of bounds 1270 String parcelSuffix = UUID.randomUUID().toString(); 1271 Parcel parcel = Parcel.obtain(); 1272 savedState.writeToParcel(parcel, 0); 1273 parcel.writeString(parcelSuffix); 1274 removeRecyclerView(); 1275 // reset for reading 1276 parcel.setDataPosition(0); 1277 // re-create 1278 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 1279 removeRecyclerView(); 1280 1281 RecyclerView restored = new RecyclerView(getActivity()); 1282 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation); 1283 mLayoutManager.setReverseLayout(config.mReverseLayout); 1284 mLayoutManager.setGapStrategy(config.mGapStrategy); 1285 restored.setLayoutManager(mLayoutManager); 1286 // use the same adapter for Rect matching 1287 restored.setAdapter(mAdapter); 1288 restored.onRestoreInstanceState(savedState); 1289 mLayoutManager.setSpanCount(1); 1290 mLayoutManager.expectLayouts(1); 1291 setRecyclerView(restored); 1292 mLayoutManager.waitForLayout(2); 1293 assertEquals("on saved state, reverse layout should be preserved", 1294 config.mReverseLayout, mLayoutManager.getReverseLayout()); 1295 assertEquals("on saved state, orientation should be preserved", 1296 config.mOrientation, mLayoutManager.getOrientation()); 1297 assertEquals("after setting new span count, layout manager should keep new value", 1298 1, mLayoutManager.getSpanCount()); 1299 assertEquals("on saved state, gap strategy should be preserved", 1300 config.mGapStrategy, mLayoutManager.getGapStrategy()); 1301 assertTrue("when span count is dramatically changed after restore, # of child views " 1302 + "should change", beforeChildCount > mLayoutManager.getChildCount()); 1303 // make sure SGLM can layout all children. is some span info is leaked, this would crash 1304 smoothScrollToPosition(mAdapter.getItemCount() - 1); 1305 } 1306 1307 @Test 1308 public void scrollAndClear() throws Throwable { 1309 setupByConfig(new Config()); 1310 waitFirstLayout(); 1311 1312 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 1313 1314 mLayoutManager.expectLayouts(1); 1315 mActivityRule.runOnUiThread(new Runnable() { 1316 @Override 1317 public void run() { 1318 mLayoutManager.scrollToPositionWithOffset(1, 0); 1319 mAdapter.clearOnUIThread(); 1320 } 1321 }); 1322 mLayoutManager.waitForLayout(2); 1323 1324 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 1325 } 1326 1327 @Test 1328 public void accessibilityPositions() throws Throwable { 1329 setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE)); 1330 waitFirstLayout(); 1331 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 1332 .getCompatAccessibilityDelegate(); 1333 final AccessibilityEvent event = AccessibilityEvent.obtain(); 1334 mActivityRule.runOnUiThread(new Runnable() { 1335 @Override 1336 public void run() { 1337 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 1338 } 1339 }); 1340 final int start = mRecyclerView 1341 .getChildLayoutPosition( 1342 mLayoutManager.findFirstVisibleItemClosestToStart(false)); 1343 final int end = mRecyclerView 1344 .getChildLayoutPosition( 1345 mLayoutManager.findFirstVisibleItemClosestToEnd(false)); 1346 assertEquals("first item position should match", 1347 Math.min(start, end), event.getFromIndex()); 1348 assertEquals("last item position should match", 1349 Math.max(start, end), event.getToIndex()); 1350 1351 } 1352 } 1353