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