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