1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.v7.widget; 18 19 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 20 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 21 22 import static org.hamcrest.CoreMatchers.is; 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertFalse; 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.assertSame; 27 import static org.junit.Assert.assertThat; 28 import static org.junit.Assert.assertTrue; 29 30 import android.graphics.Color; 31 import android.graphics.drawable.ColorDrawable; 32 import android.graphics.drawable.StateListDrawable; 33 import android.os.Build; 34 import android.support.test.filters.LargeTest; 35 import android.support.test.filters.SdkSuppress; 36 import android.support.test.runner.AndroidJUnit4; 37 import android.support.v4.view.AccessibilityDelegateCompat; 38 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 39 import android.test.UiThreadTest; 40 import android.util.SparseIntArray; 41 import android.util.StateSet; 42 import android.view.View; 43 import android.view.ViewGroup; 44 45 import org.hamcrest.CoreMatchers; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.concurrent.atomic.AtomicBoolean; 54 55 @LargeTest 56 @RunWith(AndroidJUnit4.class) 57 public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { 58 59 @Test 60 public void focusSearchFailureUp() throws Throwable { 61 focusSearchFailure(false); 62 } 63 64 @Test 65 public void focusSearchFailureDown() throws Throwable { 66 focusSearchFailure(true); 67 } 68 69 @Test 70 public void scrollToBadOffset() throws Throwable { 71 scrollToBadOffset(false); 72 } 73 74 @Test 75 public void scrollToBadOffsetReverse() throws Throwable { 76 scrollToBadOffset(true); 77 } 78 79 private void scrollToBadOffset(boolean reverseLayout) throws Throwable { 80 final int w = 500; 81 final int h = 1000; 82 RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout), 83 new GridTestAdapter(100) { 84 @Override 85 public void onBindViewHolder(TestViewHolder holder, 86 int position) { 87 super.onBindViewHolder(holder, position); 88 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 89 if (lp == null) { 90 lp = new ViewGroup.LayoutParams(w / 2, h / 2); 91 holder.itemView.setLayoutParams(lp); 92 } else { 93 lp.width = w / 2; 94 lp.height = h / 2; 95 holder.itemView.setLayoutParams(lp); 96 } 97 } 98 }); 99 TestedFrameLayout.FullControlLayoutParams lp 100 = new TestedFrameLayout.FullControlLayoutParams(w, h); 101 recyclerView.setLayoutParams(lp); 102 waitForFirstLayout(recyclerView); 103 mGlm.expectLayout(1); 104 scrollToPosition(11); 105 mGlm.waitForLayout(2); 106 // assert spans and position etc 107 for (int i = 0; i < mGlm.getChildCount(); i++) { 108 View child = mGlm.getChildAt(i); 109 GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child 110 .getLayoutParams(); 111 assertThat("span index for child at " + i + " with position " + params 112 .getViewAdapterPosition(), 113 params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2)); 114 } 115 // assert spans and positions etc. 116 int lastVisible = mGlm.findLastVisibleItemPosition(); 117 // this should be the scrolled child 118 assertThat(lastVisible, CoreMatchers.is(11)); 119 } 120 121 private void focusSearchFailure(boolean scrollDown) throws Throwable { 122 final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown) 123 , new GridTestAdapter(31, 1) { 124 RecyclerView mAttachedRv; 125 126 @Override 127 public TestViewHolder onCreateViewHolder(ViewGroup parent, 128 int viewType) { 129 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 130 testViewHolder.itemView.setFocusable(true); 131 testViewHolder.itemView.setFocusableInTouchMode(true); 132 // Good to have colors for debugging 133 StateListDrawable stl = new StateListDrawable(); 134 stl.addState(new int[]{android.R.attr.state_focused}, 135 new ColorDrawable(Color.RED)); 136 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 137 //noinspection deprecation using this for kitkat tests 138 testViewHolder.itemView.setBackgroundDrawable(stl); 139 return testViewHolder; 140 } 141 142 @Override 143 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 144 mAttachedRv = recyclerView; 145 } 146 147 @Override 148 public void onBindViewHolder(TestViewHolder holder, 149 int position) { 150 super.onBindViewHolder(holder, position); 151 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3); 152 } 153 }); 154 waitForFirstLayout(recyclerView); 155 156 View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView; 157 assertTrue(requestFocus(viewToFocus, true)); 158 assertSame(viewToFocus, recyclerView.getFocusedChild()); 159 int pos = 1; 160 View focusedView = viewToFocus; 161 while (pos < 31) { 162 focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP); 163 waitForIdleScroll(recyclerView); 164 focusedView = recyclerView.getFocusedChild(); 165 assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1), 166 recyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 167 pos += 3; 168 } 169 } 170 171 172 @Test 173 public void topUnfocusableViewsVisibility() throws Throwable { 174 // The maximum number of rows that can be fully in-bounds of RV. 175 final int visibleRowCount = 5; 176 final int spanCount = 3; 177 final int consecutiveFocusableRowsCount = 4; 178 final int consecutiveUnFocusableRowsCount = 8; 179 final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount) 180 * spanCount; 181 182 final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) 183 .reverseLayout(true), 184 new GridTestAdapter(itemCount, 1) { 185 RecyclerView mAttachedRv; 186 187 @Override 188 public TestViewHolder onCreateViewHolder(ViewGroup parent, 189 int viewType) { 190 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 191 // Good to have colors for debugging 192 StateListDrawable stl = new StateListDrawable(); 193 stl.addState(new int[]{android.R.attr.state_focused}, 194 new ColorDrawable(Color.RED)); 195 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 196 //noinspection deprecation using this for kitkat tests 197 testViewHolder.itemView.setBackgroundDrawable(stl); 198 return testViewHolder; 199 } 200 201 @Override 202 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 203 mAttachedRv = recyclerView; 204 } 205 206 @Override 207 public void onBindViewHolder(TestViewHolder holder, 208 int position) { 209 super.onBindViewHolder(holder, position); 210 if (position < spanCount * consecutiveFocusableRowsCount) { 211 holder.itemView.setFocusable(true); 212 holder.itemView.setFocusableInTouchMode(true); 213 } else { 214 holder.itemView.setFocusable(false); 215 holder.itemView.setFocusableInTouchMode(false); 216 } 217 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); 218 } 219 }); 220 waitForFirstLayout(recyclerView); 221 222 // adapter position of the currently focused item. 223 int focusIndex = 1; 224 RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 225 View viewToFocus = toFocus.itemView; 226 assertTrue(requestFocus(viewToFocus, true)); 227 assertSame(viewToFocus, recyclerView.getFocusedChild()); 228 229 // adapter position of the item (whether focusable or not) that just becomes fully 230 // visible after focusSearch. 231 int visibleIndex = focusIndex; 232 // The VH of the above adapter position 233 RecyclerView.ViewHolder toVisible = null; 234 235 int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex; 236 int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2) 237 * spanCount + visibleIndex; 238 239 // Navigate up through the focusable and unfocusable rows. The focusable rows should 240 // become focused one by one until hitting the last focusable row, at which point, 241 // unfocusable rows should become visible on the screen until the currently focused row 242 // stays on the screen. 243 int pos = focusIndex + spanCount; 244 while (pos < itemCount) { 245 focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true); 246 waitForIdleScroll(recyclerView); 247 focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); 248 toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 249 visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); 250 toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); 251 252 assertThat("Child at position " + focusIndex + " should be focused", 253 toFocus.itemView.hasFocus(), is(true)); 254 assertTrue("Focused child should be at least partially visible.", 255 isViewPartiallyInBound(recyclerView, toFocus.itemView)); 256 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 257 isViewFullyInBound(recyclerView, toVisible.itemView)); 258 pos += spanCount; 259 } 260 } 261 262 @Test 263 public void bottomUnfocusableViewsVisibility() throws Throwable { 264 // The maximum number of rows that can be fully in-bounds of RV. 265 final int visibleRowCount = 5; 266 final int spanCount = 3; 267 final int consecutiveFocusableRowsCount = 4; 268 final int consecutiveUnFocusableRowsCount = 8; 269 final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount) 270 * spanCount; 271 272 final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) 273 .reverseLayout(false), 274 new GridTestAdapter(itemCount, 1) { 275 RecyclerView mAttachedRv; 276 277 @Override 278 public TestViewHolder onCreateViewHolder(ViewGroup parent, 279 int viewType) { 280 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 281 // Good to have colors for debugging 282 StateListDrawable stl = new StateListDrawable(); 283 stl.addState(new int[]{android.R.attr.state_focused}, 284 new ColorDrawable(Color.RED)); 285 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 286 //noinspection deprecation using this for kitkat tests 287 testViewHolder.itemView.setBackgroundDrawable(stl); 288 return testViewHolder; 289 } 290 291 @Override 292 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 293 mAttachedRv = recyclerView; 294 } 295 296 @Override 297 public void onBindViewHolder(TestViewHolder holder, 298 int position) { 299 super.onBindViewHolder(holder, position); 300 if (position < spanCount * consecutiveFocusableRowsCount) { 301 holder.itemView.setFocusable(true); 302 holder.itemView.setFocusableInTouchMode(true); 303 } else { 304 holder.itemView.setFocusable(false); 305 holder.itemView.setFocusableInTouchMode(false); 306 } 307 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount); 308 } 309 }); 310 waitForFirstLayout(recyclerView); 311 312 // adapter position of the currently focused item. 313 int focusIndex = 1; 314 RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 315 View viewToFocus = toFocus.itemView; 316 assertTrue(requestFocus(viewToFocus, true)); 317 assertSame(viewToFocus, recyclerView.getFocusedChild()); 318 319 // adapter position of the item (whether focusable or not) that just becomes fully 320 // visible after focusSearch. 321 int visibleIndex = focusIndex; 322 // The VH of the above adapter position 323 RecyclerView.ViewHolder toVisible = null; 324 325 int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex; 326 int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2) 327 * spanCount + visibleIndex; 328 329 // Navigate down through the focusable and unfocusable rows. The focusable rows should 330 // become focused one by one until hitting the last focusable row, at which point, 331 // unfocusable rows should become visible on the screen until the currently focused row 332 // stays on the screen. 333 int pos = focusIndex + spanCount; 334 while (pos < itemCount) { 335 focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true); 336 waitForIdleScroll(recyclerView); 337 focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); 338 toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 339 visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); 340 toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); 341 342 assertThat("Child at position " + focusIndex + " should be focused", 343 toFocus.itemView.hasFocus(), is(true)); 344 assertTrue("Focused child should be at least partially visible.", 345 isViewPartiallyInBound(recyclerView, toFocus.itemView)); 346 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 347 isViewFullyInBound(recyclerView, toVisible.itemView)); 348 pos += spanCount; 349 } 350 } 351 352 @Test 353 public void leftUnfocusableViewsVisibility() throws Throwable { 354 // The maximum number of columns that can be fully in-bounds of RV. 355 final int visibleColCount = 5; 356 final int spanCount = 3; 357 final int consecutiveFocusableColsCount = 4; 358 final int consecutiveUnFocusableColsCount = 8; 359 final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount) 360 * spanCount; 361 362 final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) 363 .orientation(HORIZONTAL).reverseLayout(true), 364 new GridTestAdapter(itemCount, 1) { 365 RecyclerView mAttachedRv; 366 367 @Override 368 public TestViewHolder onCreateViewHolder(ViewGroup parent, 369 int viewType) { 370 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 371 // Good to have colors for debugging 372 StateListDrawable stl = new StateListDrawable(); 373 stl.addState(new int[]{android.R.attr.state_focused}, 374 new ColorDrawable(Color.RED)); 375 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 376 //noinspection deprecation using this for kitkat tests 377 testViewHolder.itemView.setBackgroundDrawable(stl); 378 return testViewHolder; 379 } 380 381 @Override 382 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 383 mAttachedRv = recyclerView; 384 } 385 386 @Override 387 public void onBindViewHolder(TestViewHolder holder, 388 int position) { 389 super.onBindViewHolder(holder, position); 390 if (position < spanCount * consecutiveFocusableColsCount) { 391 holder.itemView.setFocusable(true); 392 holder.itemView.setFocusableInTouchMode(true); 393 } else { 394 holder.itemView.setFocusable(false); 395 holder.itemView.setFocusableInTouchMode(false); 396 } 397 holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); 398 } 399 }); 400 waitForFirstLayout(recyclerView); 401 402 // adapter position of the currently focused item. 403 int focusIndex = 1; 404 RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 405 View viewToFocus = toFocus.itemView; 406 assertTrue(requestFocus(viewToFocus, true)); 407 assertSame(viewToFocus, recyclerView.getFocusedChild()); 408 409 // adapter position of the item (whether focusable or not) that just becomes fully 410 // visible after focusSearch. 411 int visibleIndex = focusIndex; 412 // The VH of the above adapter position 413 RecyclerView.ViewHolder toVisible = null; 414 415 int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex; 416 int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2) 417 * spanCount + visibleIndex; 418 419 // Navigate left through the focusable and unfocusable columns. The focusable columns should 420 // become focused one by one until hitting the last focusable column, at which point, 421 // unfocusable columns should become visible on the screen until the currently focused 422 // column stays on the screen. 423 int pos = focusIndex + spanCount; 424 while (pos < itemCount) { 425 focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true); 426 waitForIdleScroll(recyclerView); 427 focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); 428 toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 429 visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); 430 toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); 431 432 assertThat("Child at position " + focusIndex + " should be focused", 433 toFocus.itemView.hasFocus(), is(true)); 434 assertTrue("Focused child should be at least partially visible.", 435 isViewPartiallyInBound(recyclerView, toFocus.itemView)); 436 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 437 isViewFullyInBound(recyclerView, toVisible.itemView)); 438 pos += spanCount; 439 } 440 } 441 442 @Test 443 public void rightUnfocusableViewsVisibility() throws Throwable { 444 // The maximum number of columns that can be fully in-bounds of RV. 445 final int visibleColCount = 5; 446 final int spanCount = 3; 447 final int consecutiveFocusableColsCount = 4; 448 final int consecutiveUnFocusableColsCount = 8; 449 final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount) 450 * spanCount; 451 452 final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount) 453 .orientation(HORIZONTAL).reverseLayout(false), 454 new GridTestAdapter(itemCount, 1) { 455 RecyclerView mAttachedRv; 456 457 @Override 458 public TestViewHolder onCreateViewHolder(ViewGroup parent, 459 int viewType) { 460 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 461 // Good to have colors for debugging 462 StateListDrawable stl = new StateListDrawable(); 463 stl.addState(new int[]{android.R.attr.state_focused}, 464 new ColorDrawable(Color.RED)); 465 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 466 //noinspection deprecation using this for kitkat tests 467 testViewHolder.itemView.setBackgroundDrawable(stl); 468 return testViewHolder; 469 } 470 471 @Override 472 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 473 mAttachedRv = recyclerView; 474 } 475 476 @Override 477 public void onBindViewHolder(TestViewHolder holder, 478 int position) { 479 super.onBindViewHolder(holder, position); 480 if (position < spanCount * consecutiveFocusableColsCount) { 481 holder.itemView.setFocusable(true); 482 holder.itemView.setFocusableInTouchMode(true); 483 } else { 484 holder.itemView.setFocusable(false); 485 holder.itemView.setFocusableInTouchMode(false); 486 } 487 holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount); 488 } 489 }); 490 waitForFirstLayout(recyclerView); 491 492 // adapter position of the currently focused item. 493 int focusIndex = 1; 494 RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 495 View viewToFocus = toFocus.itemView; 496 assertTrue(requestFocus(viewToFocus, true)); 497 assertSame(viewToFocus, recyclerView.getFocusedChild()); 498 499 // adapter position of the item (whether focusable or not) that just becomes fully 500 // visible after focusSearch. 501 int visibleIndex = focusIndex; 502 // The VH of the above adapter position 503 RecyclerView.ViewHolder toVisible = null; 504 505 int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex; 506 int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2) 507 * spanCount + visibleIndex; 508 509 // Navigate right through the focusable and unfocusable columns. The focusable columns 510 // should become focused one by one until hitting the last focusable column, at which point, 511 // unfocusable columns should become visible on the screen until the currently focused 512 // column stays on the screen. 513 int pos = focusIndex + spanCount; 514 while (pos < itemCount) { 515 focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true); 516 waitForIdleScroll(recyclerView); 517 focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount)); 518 toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex); 519 visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount)); 520 toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex); 521 522 assertThat("Child at position " + focusIndex + " should be focused", 523 toFocus.itemView.hasFocus(), is(true)); 524 assertTrue("Focused child should be at least partially visible.", 525 isViewPartiallyInBound(recyclerView, toFocus.itemView)); 526 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 527 isViewFullyInBound(recyclerView, toVisible.itemView)); 528 pos += spanCount; 529 } 530 } 531 532 @UiThreadTest 533 @Test 534 public void scrollWithoutLayout() throws Throwable { 535 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 536 mGlm.expectLayout(1); 537 setRecyclerView(recyclerView); 538 mGlm.setSpanCount(5); 539 recyclerView.scrollBy(0, 10); 540 } 541 542 @Test 543 public void scrollWithoutLayoutAfterInvalidate() throws Throwable { 544 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 545 waitForFirstLayout(recyclerView); 546 mActivityRule.runOnUiThread(new Runnable() { 547 @Override 548 public void run() { 549 mGlm.setSpanCount(5); 550 recyclerView.scrollBy(0, 10); 551 } 552 }); 553 } 554 555 @Test 556 public void predictiveSpanLookup1() throws Throwable { 557 predictiveSpanLookupTest(0, false); 558 } 559 560 @Test 561 public void predictiveSpanLookup2() throws Throwable { 562 predictiveSpanLookupTest(0, true); 563 } 564 565 @Test 566 public void predictiveSpanLookup3() throws Throwable { 567 predictiveSpanLookupTest(1, false); 568 } 569 570 @Test 571 public void predictiveSpanLookup4() throws Throwable { 572 predictiveSpanLookupTest(1, true); 573 } 574 575 public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable { 576 RecyclerView recyclerView = setupBasic(new Config(3, 10)); 577 mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 578 @Override 579 public int getSpanSize(int position) { 580 if (position < 0 || position >= mAdapter.getItemCount()) { 581 postExceptionToInstrumentation(new AssertionError("position is not within " + 582 "adapter range. pos:" + position + ", adapter size:" + 583 mAdapter.getItemCount())); 584 } 585 return 1; 586 } 587 588 @Override 589 public int getSpanIndex(int position, int spanCount) { 590 if (position < 0 || position >= mAdapter.getItemCount()) { 591 postExceptionToInstrumentation(new AssertionError("position is not within " + 592 "adapter range. pos:" + position + ", adapter size:" + 593 mAdapter.getItemCount())); 594 } 595 return super.getSpanIndex(position, spanCount); 596 } 597 }); 598 waitForFirstLayout(recyclerView); 599 checkForMainThreadException(); 600 assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations()); 601 mGlm.expectLayout(2); 602 int deleteCnt = 10 - remaining; 603 int deleteStart = removeFromStart ? 0 : remaining; 604 mAdapter.deleteAndNotify(deleteStart, deleteCnt); 605 mGlm.waitForLayout(2); 606 checkForMainThreadException(); 607 } 608 609 @Test 610 public void movingAGroupOffScreenForAddedItems() throws Throwable { 611 final RecyclerView rv = setupBasic(new Config(3, 100)); 612 final int[] maxId = new int[1]; 613 maxId[0] = -1; 614 final SparseIntArray spanLookups = new SparseIntArray(); 615 final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false); 616 mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 617 @Override 618 public int getSpanSize(int position) { 619 if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) { 620 return 1; 621 } else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) { 622 spanLookups.put(position, spanLookups.get(position, 0) + 1); 623 } 624 return 3; 625 } 626 }); 627 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true); 628 waitForFirstLayout(rv); 629 View lastView = rv.getChildAt(rv.getChildCount() - 1); 630 final int lastPos = rv.getChildAdapterPosition(lastView); 631 maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId; 632 // now add a lot of items below this and those new views should have span size 3 633 enableSpanLookupLogging.set(true); 634 mGlm.expectLayout(2); 635 mAdapter.addAndNotify(lastPos - 2, 30); 636 mGlm.waitForLayout(2); 637 checkForMainThreadException(); 638 639 assertEquals("last items span count should be queried twice", 2, 640 spanLookups.get(lastPos + 30)); 641 642 } 643 644 @Test 645 public void layoutParams() throws Throwable { 646 layoutParamsTest(GridLayoutManager.HORIZONTAL); 647 removeRecyclerView(); 648 layoutParamsTest(GridLayoutManager.VERTICAL); 649 } 650 651 @Test 652 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) 653 public void horizontalAccessibilitySpanIndices() throws Throwable { 654 accessibilitySpanIndicesTest(HORIZONTAL); 655 } 656 657 @Test 658 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) 659 public void verticalAccessibilitySpanIndices() throws Throwable { 660 accessibilitySpanIndicesTest(VERTICAL); 661 } 662 663 public void accessibilitySpanIndicesTest(int orientation) throws Throwable { 664 final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false)); 665 waitForFirstLayout(recyclerView); 666 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 667 .getCompatAccessibilityDelegate().getItemDelegate(); 668 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 669 final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2); 670 final int position = recyclerView.getChildLayoutPosition(chosen); 671 mActivityRule.runOnUiThread(new Runnable() { 672 @Override 673 public void run() { 674 delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info); 675 } 676 }); 677 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 678 AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info 679 .getCollectionItemInfo(); 680 assertNotNull(itemInfo); 681 assertEquals("result should have span group position", 682 ssl.getSpanGroupIndex(position, mGlm.getSpanCount()), 683 orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex()); 684 assertEquals("result should have span index", 685 ssl.getSpanIndex(position, mGlm.getSpanCount()), 686 orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex()); 687 assertEquals("result should have span size", 688 ssl.getSpanSize(position), 689 orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan()); 690 } 691 692 public GridLayoutManager.LayoutParams ensureGridLp(View view) { 693 ViewGroup.LayoutParams lp = view.getLayoutParams(); 694 GridLayoutManager.LayoutParams glp; 695 if (lp instanceof GridLayoutManager.LayoutParams) { 696 glp = (GridLayoutManager.LayoutParams) lp; 697 } else if (lp == null) { 698 glp = (GridLayoutManager.LayoutParams) mGlm 699 .generateDefaultLayoutParams(); 700 view.setLayoutParams(glp); 701 } else { 702 glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp); 703 view.setLayoutParams(glp); 704 } 705 return glp; 706 } 707 708 public void layoutParamsTest(final int orientation) throws Throwable { 709 final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation), 710 new GridTestAdapter(100) { 711 @Override 712 public void onBindViewHolder(TestViewHolder holder, 713 int position) { 714 super.onBindViewHolder(holder, position); 715 GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 716 int val = 0; 717 switch (position % 5) { 718 case 0: 719 val = 10; 720 break; 721 case 1: 722 val = 30; 723 break; 724 case 2: 725 val = GridLayoutManager.LayoutParams.WRAP_CONTENT; 726 break; 727 case 3: 728 val = GridLayoutManager.LayoutParams.MATCH_PARENT; 729 break; 730 case 4: 731 val = 200; 732 break; 733 } 734 if (orientation == GridLayoutManager.VERTICAL) { 735 glp.height = val; 736 } else { 737 glp.width = val; 738 } 739 holder.itemView.setLayoutParams(glp); 740 } 741 }); 742 waitForFirstLayout(rv); 743 final OrientationHelper helper = mGlm.mOrientationHelper; 744 final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2))); 745 assertEquals(firstRowSize, 746 helper.getDecoratedMeasurement(mGlm.findViewByPosition(0))); 747 assertEquals(firstRowSize, 748 helper.getDecoratedMeasurement(mGlm.findViewByPosition(1))); 749 assertEquals(firstRowSize, 750 helper.getDecoratedMeasurement(mGlm.findViewByPosition(2))); 751 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0))); 752 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1))); 753 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2))); 754 755 final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3))); 756 assertEquals(secondRowSize, 757 helper.getDecoratedMeasurement(mGlm.findViewByPosition(3))); 758 assertEquals(secondRowSize, 759 helper.getDecoratedMeasurement(mGlm.findViewByPosition(4))); 760 assertEquals(secondRowSize, 761 helper.getDecoratedMeasurement(mGlm.findViewByPosition(5))); 762 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3))); 763 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4))); 764 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5))); 765 } 766 767 @Test 768 public void anchorUpdate() throws InterruptedException { 769 GridLayoutManager glm = new GridLayoutManager(getActivity(), 11); 770 final GridLayoutManager.SpanSizeLookup spanSizeLookup 771 = new GridLayoutManager.SpanSizeLookup() { 772 @Override 773 public int getSpanSize(int position) { 774 if (position > 200) { 775 return 100; 776 } 777 if (position > 20) { 778 return 2; 779 } 780 return 1; 781 } 782 }; 783 glm.setSpanSizeLookup(spanSizeLookup); 784 glm.mAnchorInfo.mPosition = 11; 785 RecyclerView.State state = new RecyclerView.State(); 786 mRecyclerView = new RecyclerView(getActivity()); 787 state.mItemCount = 1000; 788 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 789 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 790 assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition); 791 792 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 793 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 794 assertEquals("gm should keep anchor in last span in the row", 20, 795 glm.mAnchorInfo.mPosition); 796 797 glm.mAnchorInfo.mPosition = 5; 798 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 799 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 800 assertEquals("gm should keep anchor in last span in the row", 10, 801 glm.mAnchorInfo.mPosition); 802 803 glm.mAnchorInfo.mPosition = 13; 804 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 805 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 806 assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition); 807 808 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 809 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 810 assertEquals("gm should keep anchor in last span in the row", 20, 811 glm.mAnchorInfo.mPosition); 812 813 glm.mAnchorInfo.mPosition = 23; 814 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 815 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 816 assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition); 817 818 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 819 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 820 assertEquals("gm should keep anchor in last span in the row", 25, 821 glm.mAnchorInfo.mPosition); 822 823 glm.mAnchorInfo.mPosition = 35; 824 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 825 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 826 assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition); 827 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 828 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 829 assertEquals("gm should keep anchor in last span in the row", 35, 830 glm.mAnchorInfo.mPosition); 831 } 832 833 @Test 834 public void spanLookup() { 835 spanLookupTest(false); 836 } 837 838 @Test 839 public void spanLookupWithCache() { 840 spanLookupTest(true); 841 } 842 843 @Test 844 public void spanLookupCache() { 845 final GridLayoutManager.SpanSizeLookup ssl 846 = new GridLayoutManager.SpanSizeLookup() { 847 @Override 848 public int getSpanSize(int position) { 849 if (position > 6) { 850 return 2; 851 } 852 return 1; 853 } 854 }; 855 ssl.setSpanIndexCacheEnabled(true); 856 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2)); 857 ssl.getCachedSpanIndex(4, 5); 858 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3)); 859 // this should not happen and if happens, it is better to return -1 860 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 861 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5)); 862 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100)); 863 ssl.getCachedSpanIndex(6, 5); 864 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 865 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6)); 866 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 867 ssl.getCachedSpanIndex(12, 5); 868 assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13)); 869 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12)); 870 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 871 for (int i = 0; i < 6; i++) { 872 ssl.getCachedSpanIndex(i, 5); 873 } 874 875 for (int i = 1; i < 7; i++) { 876 assertEquals("reference child right before " + i, i - 1, 877 ssl.findReferenceIndexFromCache(i)); 878 } 879 assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0)); 880 } 881 882 public void spanLookupTest(boolean enableCache) { 883 final GridLayoutManager.SpanSizeLookup ssl 884 = new GridLayoutManager.SpanSizeLookup() { 885 @Override 886 public int getSpanSize(int position) { 887 if (position > 200) { 888 return 100; 889 } 890 if (position > 6) { 891 return 2; 892 } 893 return 1; 894 } 895 }; 896 ssl.setSpanIndexCacheEnabled(enableCache); 897 assertEquals(0, ssl.getCachedSpanIndex(0, 5)); 898 assertEquals(4, ssl.getCachedSpanIndex(4, 5)); 899 assertEquals(0, ssl.getCachedSpanIndex(5, 5)); 900 assertEquals(1, ssl.getCachedSpanIndex(6, 5)); 901 assertEquals(2, ssl.getCachedSpanIndex(7, 5)); 902 assertEquals(2, ssl.getCachedSpanIndex(9, 5)); 903 assertEquals(0, ssl.getCachedSpanIndex(8, 5)); 904 } 905 906 @Test 907 public void removeAnchorItem() throws Throwable { 908 removeAnchorItemTest( 909 new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0); 910 } 911 912 @Test 913 public void removeAnchorItemReverse() throws Throwable { 914 removeAnchorItemTest( 915 new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100, 916 0); 917 } 918 919 @Test 920 public void removeAnchorItemHorizontal() throws Throwable { 921 removeAnchorItemTest( 922 new Config(3, 0).orientation(HORIZONTAL).reverseLayout( 923 false), 100, 0); 924 } 925 926 @Test 927 public void removeAnchorItemReverseHorizontal() throws Throwable { 928 removeAnchorItemTest( 929 new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true), 930 100, 0); 931 } 932 933 /** 934 * This tests a regression where predictive animations were not working as expected when the 935 * first item is removed and there aren't any more items to add from that direction. 936 * First item refers to the default anchor item. 937 */ 938 public void removeAnchorItemTest(final Config config, int adapterSize, 939 final int removePos) throws Throwable { 940 GridTestAdapter adapter = new GridTestAdapter(adapterSize) { 941 @Override 942 public void onBindViewHolder(TestViewHolder holder, 943 int position) { 944 super.onBindViewHolder(holder, position); 945 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 946 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 947 lp = new ViewGroup.MarginLayoutParams(0, 0); 948 holder.itemView.setLayoutParams(lp); 949 } 950 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 951 final int maxSize; 952 if (config.mOrientation == HORIZONTAL) { 953 maxSize = mRecyclerView.getWidth(); 954 mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 955 } else { 956 maxSize = mRecyclerView.getHeight(); 957 mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT; 958 } 959 960 final int desiredSize; 961 if (position == removePos) { 962 // make it large 963 desiredSize = maxSize / 4; 964 } else { 965 // make it small 966 desiredSize = maxSize / 8; 967 } 968 if (config.mOrientation == HORIZONTAL) { 969 mlp.width = desiredSize; 970 } else { 971 mlp.height = desiredSize; 972 } 973 } 974 }; 975 RecyclerView recyclerView = setupBasic(config, adapter); 976 waitForFirstLayout(recyclerView); 977 final int childCount = mGlm.getChildCount(); 978 RecyclerView.ViewHolder toBeRemoved = null; 979 List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>(); 980 for (int i = 0; i < childCount; i++) { 981 View child = mGlm.getChildAt(i); 982 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 983 if (holder.getAdapterPosition() == removePos) { 984 toBeRemoved = holder; 985 } else { 986 toBeMoved.add(holder); 987 } 988 } 989 assertNotNull("test sanity", toBeRemoved); 990 assertEquals("test sanity", childCount - 1, toBeMoved.size()); 991 LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator(); 992 mRecyclerView.setItemAnimator(loggingItemAnimator); 993 loggingItemAnimator.reset(); 994 loggingItemAnimator.expectRunPendingAnimationsCall(1); 995 mGlm.expectLayout(2); 996 adapter.deleteAndNotify(removePos, 1); 997 mGlm.waitForLayout(1); 998 loggingItemAnimator.waitForPendingAnimationsCall(2); 999 assertTrue("removed child should receive remove animation", 1000 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved)); 1001 for (RecyclerView.ViewHolder vh : toBeMoved) { 1002 assertTrue("view holder should be in moved list", 1003 loggingItemAnimator.mMoveVHs.contains(vh)); 1004 } 1005 List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>(); 1006 for (int i = 0; i < mGlm.getChildCount(); i++) { 1007 View child = mGlm.getChildAt(i); 1008 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 1009 if (toBeRemoved != holder && !toBeMoved.contains(holder)) { 1010 newHolders.add(holder); 1011 } 1012 } 1013 assertTrue("some new children should show up for the new space", newHolders.size() > 0); 1014 assertEquals("no items should receive animate add since they are not new", 0, 1015 loggingItemAnimator.mAddVHs.size()); 1016 for (RecyclerView.ViewHolder holder : newHolders) { 1017 assertTrue("new holder should receive a move animation", 1018 loggingItemAnimator.mMoveVHs.contains(holder)); 1019 } 1020 // for removed view, 3 for new row 1021 assertTrue("control against adding too many children due to bad layout state preparation." 1022 + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(), 1023 mRecyclerView.getChildCount() <= childCount + 1 + 3); 1024 } 1025 1026 @Test 1027 public void spanGroupIndex() { 1028 final GridLayoutManager.SpanSizeLookup ssl 1029 = new GridLayoutManager.SpanSizeLookup() { 1030 @Override 1031 public int getSpanSize(int position) { 1032 if (position > 200) { 1033 return 100; 1034 } 1035 if (position > 6) { 1036 return 2; 1037 } 1038 return 1; 1039 } 1040 }; 1041 assertEquals(0, ssl.getSpanGroupIndex(0, 5)); 1042 assertEquals(0, ssl.getSpanGroupIndex(4, 5)); 1043 assertEquals(1, ssl.getSpanGroupIndex(5, 5)); 1044 assertEquals(1, ssl.getSpanGroupIndex(6, 5)); 1045 assertEquals(1, ssl.getSpanGroupIndex(7, 5)); 1046 assertEquals(2, ssl.getSpanGroupIndex(9, 5)); 1047 assertEquals(2, ssl.getSpanGroupIndex(8, 5)); 1048 } 1049 1050 @Test 1051 public void notifyDataSetChange() throws Throwable { 1052 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 1053 final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup(); 1054 ssl.setSpanIndexCacheEnabled(true); 1055 waitForFirstLayout(recyclerView); 1056 assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0); 1057 final Callback callback = new Callback() { 1058 @Override 1059 public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 1060 if (!state.isPreLayout()) { 1061 assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size()); 1062 } 1063 } 1064 1065 @Override 1066 public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 1067 if (!state.isPreLayout()) { 1068 assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0); 1069 } 1070 } 1071 }; 1072 mGlm.mCallbacks.add(callback); 1073 mGlm.expectLayout(2); 1074 mAdapter.deleteAndNotify(2, 3); 1075 mGlm.waitForLayout(2); 1076 checkForMainThreadException(); 1077 } 1078 1079 @Test 1080 public void unevenHeights() throws Throwable { 1081 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 1082 new HashMap<Integer, RecyclerView.ViewHolder>(); 1083 RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) { 1084 @Override 1085 public void onBindViewHolder(TestViewHolder holder, 1086 int position) { 1087 super.onBindViewHolder(holder, position); 1088 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 1089 glp.height = 50 + position * 50; 1090 viewHolderMap.put(position, holder); 1091 } 1092 }); 1093 waitForFirstLayout(recyclerView); 1094 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 1095 assertEquals("all items should get max height", 150, 1096 vh.itemView.getHeight()); 1097 } 1098 1099 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 1100 assertEquals("all items should have measured the max height", 150, 1101 vh.itemView.getMeasuredHeight()); 1102 } 1103 } 1104 1105 @Test 1106 public void unevenWidths() throws Throwable { 1107 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 1108 new HashMap<Integer, RecyclerView.ViewHolder>(); 1109 RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false), 1110 new GridTestAdapter(3) { 1111 @Override 1112 public void onBindViewHolder(TestViewHolder holder, 1113 int position) { 1114 super.onBindViewHolder(holder, position); 1115 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 1116 glp.width = 50 + position * 50; 1117 viewHolderMap.put(position, holder); 1118 } 1119 }); 1120 waitForFirstLayout(recyclerView); 1121 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 1122 assertEquals("all items should get max width", 150, 1123 vh.itemView.getWidth()); 1124 } 1125 1126 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 1127 assertEquals("all items should have measured the max width", 150, 1128 vh.itemView.getMeasuredWidth()); 1129 } 1130 } 1131 1132 @Test 1133 public void spanSizeChange() throws Throwable { 1134 final RecyclerView rv = setupBasic(new Config(3, 100)); 1135 waitForFirstLayout(rv); 1136 assertTrue(mGlm.supportsPredictiveItemAnimations()); 1137 mGlm.expectLayout(1); 1138 mActivityRule.runOnUiThread(new Runnable() { 1139 @Override 1140 public void run() { 1141 mGlm.setSpanCount(5); 1142 assertFalse(mGlm.supportsPredictiveItemAnimations()); 1143 } 1144 }); 1145 mGlm.waitForLayout(2); 1146 mGlm.expectLayout(2); 1147 mAdapter.deleteAndNotify(3, 2); 1148 mGlm.waitForLayout(2); 1149 assertTrue(mGlm.supportsPredictiveItemAnimations()); 1150 } 1151 1152 @Test 1153 public void cacheSpanIndices() throws Throwable { 1154 final RecyclerView rv = setupBasic(new Config(3, 100)); 1155 mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true); 1156 waitForFirstLayout(rv); 1157 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 1158 assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0); 1159 assertEquals("item index 5 should be in span 2", 2, 1160 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 1161 mGlm.expectLayout(2); 1162 mAdapter.mFullSpanItems.add(4); 1163 mAdapter.changeAndNotify(4, 1); 1164 mGlm.waitForLayout(2); 1165 assertEquals("item index 5 should be in span 2", 0, 1166 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 1167 } 1168 } 1169