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.junit.Assert.assertEquals; 23 import static org.junit.Assert.assertFalse; 24 import static org.junit.Assert.assertNotNull; 25 import static org.junit.Assert.assertSame; 26 import static org.junit.Assert.assertThat; 27 import static org.junit.Assert.assertTrue; 28 29 import android.graphics.Color; 30 import android.graphics.drawable.ColorDrawable; 31 import android.graphics.drawable.StateListDrawable; 32 import android.support.test.runner.AndroidJUnit4; 33 import android.support.v4.view.AccessibilityDelegateCompat; 34 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 35 import android.test.UiThreadTest; 36 import android.test.suitebuilder.annotation.MediumTest; 37 import android.util.SparseIntArray; 38 import android.util.StateSet; 39 import android.view.View; 40 import android.view.ViewGroup; 41 42 import org.hamcrest.CoreMatchers; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 46 import java.util.ArrayList; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Map; 50 import java.util.concurrent.atomic.AtomicBoolean; 51 52 @MediumTest 53 @RunWith(AndroidJUnit4.class) 54 public class GridLayoutManagerTest extends BaseGridLayoutManagerTest { 55 56 @Test focusSearchFailureUp()57 public void focusSearchFailureUp() throws Throwable { 58 focusSearchFailure(false); 59 } 60 61 @Test focusSearchFailureDown()62 public void focusSearchFailureDown() throws Throwable { 63 focusSearchFailure(true); 64 } 65 66 @Test scrollToBadOffset()67 public void scrollToBadOffset() throws Throwable { 68 scrollToBadOffset(false); 69 } 70 71 @Test scrollToBadOffsetReverse()72 public void scrollToBadOffsetReverse() throws Throwable { 73 scrollToBadOffset(true); 74 } 75 scrollToBadOffset(boolean reverseLayout)76 private void scrollToBadOffset(boolean reverseLayout) throws Throwable { 77 final int w = 500; 78 final int h = 1000; 79 RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout), 80 new GridTestAdapter(100) { 81 @Override 82 public void onBindViewHolder(TestViewHolder holder, 83 int position) { 84 super.onBindViewHolder(holder, position); 85 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 86 if (lp == null) { 87 lp = new ViewGroup.LayoutParams(w / 2, h / 2); 88 holder.itemView.setLayoutParams(lp); 89 } else { 90 lp.width = w / 2; 91 lp.height = h / 2; 92 holder.itemView.setLayoutParams(lp); 93 } 94 } 95 }); 96 TestedFrameLayout.FullControlLayoutParams lp 97 = new TestedFrameLayout.FullControlLayoutParams(w, h); 98 recyclerView.setLayoutParams(lp); 99 waitForFirstLayout(recyclerView); 100 mGlm.expectLayout(1); 101 scrollToPosition(11); 102 mGlm.waitForLayout(2); 103 // assert spans and position etc 104 for (int i = 0; i < mGlm.getChildCount(); i++) { 105 View child = mGlm.getChildAt(i); 106 GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child 107 .getLayoutParams(); 108 assertThat("span index for child at " + i + " with position " + params 109 .getViewAdapterPosition(), 110 params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2)); 111 } 112 // assert spans and positions etc. 113 int lastVisible = mGlm.findLastVisibleItemPosition(); 114 // this should be the scrolled child 115 assertThat(lastVisible, CoreMatchers.is(11)); 116 } 117 focusSearchFailure(boolean scrollDown)118 private void focusSearchFailure(boolean scrollDown) throws Throwable { 119 final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown) 120 , new GridTestAdapter(31, 1) { 121 RecyclerView mAttachedRv; 122 123 @Override 124 public TestViewHolder onCreateViewHolder(ViewGroup parent, 125 int viewType) { 126 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 127 testViewHolder.itemView.setFocusable(true); 128 testViewHolder.itemView.setFocusableInTouchMode(true); 129 // Good to have colors for debugging 130 StateListDrawable stl = new StateListDrawable(); 131 stl.addState(new int[]{android.R.attr.state_focused}, 132 new ColorDrawable(Color.RED)); 133 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 134 testViewHolder.itemView.setBackground(stl); 135 return testViewHolder; 136 } 137 138 @Override 139 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 140 mAttachedRv = recyclerView; 141 } 142 143 @Override 144 public void onBindViewHolder(TestViewHolder holder, 145 int position) { 146 super.onBindViewHolder(holder, position); 147 holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3); 148 } 149 }); 150 waitForFirstLayout(recyclerView); 151 152 View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView; 153 assertTrue(requestFocus(viewToFocus, true)); 154 assertSame(viewToFocus, recyclerView.getFocusedChild()); 155 int pos = 1; 156 View focusedView = viewToFocus; 157 while (pos < 31) { 158 focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP); 159 waitForIdleScroll(recyclerView); 160 focusedView = recyclerView.getFocusedChild(); 161 assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1), 162 recyclerView.getChildViewHolder(focusedView).getAdapterPosition()); 163 pos += 3; 164 } 165 } 166 167 @UiThreadTest 168 @Test scrollWithoutLayout()169 public void scrollWithoutLayout() throws Throwable { 170 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 171 mGlm.expectLayout(1); 172 setRecyclerView(recyclerView); 173 mGlm.setSpanCount(5); 174 recyclerView.scrollBy(0, 10); 175 } 176 177 @Test scrollWithoutLayoutAfterInvalidate()178 public void scrollWithoutLayoutAfterInvalidate() throws Throwable { 179 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 180 waitForFirstLayout(recyclerView); 181 runTestOnUiThread(new Runnable() { 182 @Override 183 public void run() { 184 mGlm.setSpanCount(5); 185 recyclerView.scrollBy(0, 10); 186 } 187 }); 188 } 189 190 @Test predictiveSpanLookup1()191 public void predictiveSpanLookup1() throws Throwable { 192 predictiveSpanLookupTest(0, false); 193 } 194 195 @Test predictiveSpanLookup2()196 public void predictiveSpanLookup2() throws Throwable { 197 predictiveSpanLookupTest(0, true); 198 } 199 200 @Test predictiveSpanLookup3()201 public void predictiveSpanLookup3() throws Throwable { 202 predictiveSpanLookupTest(1, false); 203 } 204 205 @Test predictiveSpanLookup4()206 public void predictiveSpanLookup4() throws Throwable { 207 predictiveSpanLookupTest(1, true); 208 } 209 predictiveSpanLookupTest(int remaining, boolean removeFromStart)210 public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable { 211 RecyclerView recyclerView = setupBasic(new Config(3, 10)); 212 mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 213 @Override 214 public int getSpanSize(int position) { 215 if (position < 0 || position >= mAdapter.getItemCount()) { 216 postExceptionToInstrumentation(new AssertionError("position is not within " + 217 "adapter range. pos:" + position + ", adapter size:" + 218 mAdapter.getItemCount())); 219 } 220 return 1; 221 } 222 223 @Override 224 public int getSpanIndex(int position, int spanCount) { 225 if (position < 0 || position >= mAdapter.getItemCount()) { 226 postExceptionToInstrumentation(new AssertionError("position is not within " + 227 "adapter range. pos:" + position + ", adapter size:" + 228 mAdapter.getItemCount())); 229 } 230 return super.getSpanIndex(position, spanCount); 231 } 232 }); 233 waitForFirstLayout(recyclerView); 234 checkForMainThreadException(); 235 assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations()); 236 mGlm.expectLayout(2); 237 int deleteCnt = 10 - remaining; 238 int deleteStart = removeFromStart ? 0 : remaining; 239 mAdapter.deleteAndNotify(deleteStart, deleteCnt); 240 mGlm.waitForLayout(2); 241 checkForMainThreadException(); 242 } 243 244 @Test movingAGroupOffScreenForAddedItems()245 public void movingAGroupOffScreenForAddedItems() throws Throwable { 246 final RecyclerView rv = setupBasic(new Config(3, 100)); 247 final int[] maxId = new int[1]; 248 maxId[0] = -1; 249 final SparseIntArray spanLookups = new SparseIntArray(); 250 final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false); 251 mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 252 @Override 253 public int getSpanSize(int position) { 254 if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) { 255 return 1; 256 } else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) { 257 spanLookups.put(position, spanLookups.get(position, 0) + 1); 258 } 259 return 3; 260 } 261 }); 262 ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true); 263 waitForFirstLayout(rv); 264 View lastView = rv.getChildAt(rv.getChildCount() - 1); 265 final int lastPos = rv.getChildAdapterPosition(lastView); 266 maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId; 267 // now add a lot of items below this and those new views should have span size 3 268 enableSpanLookupLogging.set(true); 269 mGlm.expectLayout(2); 270 mAdapter.addAndNotify(lastPos - 2, 30); 271 mGlm.waitForLayout(2); 272 checkForMainThreadException(); 273 274 assertEquals("last items span count should be queried twice", 2, 275 spanLookups.get(lastPos + 30)); 276 277 } 278 279 @Test layoutParams()280 public void layoutParams() throws Throwable { 281 layoutParamsTest(GridLayoutManager.HORIZONTAL); 282 removeRecyclerView(); 283 layoutParamsTest(GridLayoutManager.VERTICAL); 284 } 285 286 @Test horizontalAccessibilitySpanIndices()287 public void horizontalAccessibilitySpanIndices() throws Throwable { 288 accessibilitySpanIndicesTest(HORIZONTAL); 289 } 290 291 @Test verticalAccessibilitySpanIndices()292 public void verticalAccessibilitySpanIndices() throws Throwable { 293 accessibilitySpanIndicesTest(VERTICAL); 294 } 295 accessibilitySpanIndicesTest(int orientation)296 public void accessibilitySpanIndicesTest(int orientation) throws Throwable { 297 final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false)); 298 waitForFirstLayout(recyclerView); 299 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 300 .getCompatAccessibilityDelegate().getItemDelegate(); 301 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 302 final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2); 303 final int position = recyclerView.getChildLayoutPosition(chosen); 304 runTestOnUiThread(new Runnable() { 305 @Override 306 public void run() { 307 delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info); 308 } 309 }); 310 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 311 AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info 312 .getCollectionItemInfo(); 313 assertNotNull(itemInfo); 314 assertEquals("result should have span group position", 315 ssl.getSpanGroupIndex(position, mGlm.getSpanCount()), 316 orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex()); 317 assertEquals("result should have span index", 318 ssl.getSpanIndex(position, mGlm.getSpanCount()), 319 orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex()); 320 assertEquals("result should have span size", 321 ssl.getSpanSize(position), 322 orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan()); 323 } 324 ensureGridLp(View view)325 public GridLayoutManager.LayoutParams ensureGridLp(View view) { 326 ViewGroup.LayoutParams lp = view.getLayoutParams(); 327 GridLayoutManager.LayoutParams glp; 328 if (lp instanceof GridLayoutManager.LayoutParams) { 329 glp = (GridLayoutManager.LayoutParams) lp; 330 } else if (lp == null) { 331 glp = (GridLayoutManager.LayoutParams) mGlm 332 .generateDefaultLayoutParams(); 333 view.setLayoutParams(glp); 334 } else { 335 glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp); 336 view.setLayoutParams(glp); 337 } 338 return glp; 339 } 340 layoutParamsTest(final int orientation)341 public void layoutParamsTest(final int orientation) throws Throwable { 342 final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation), 343 new GridTestAdapter(100) { 344 @Override 345 public void onBindViewHolder(TestViewHolder holder, 346 int position) { 347 super.onBindViewHolder(holder, position); 348 GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 349 int val = 0; 350 switch (position % 5) { 351 case 0: 352 val = 10; 353 break; 354 case 1: 355 val = 30; 356 break; 357 case 2: 358 val = GridLayoutManager.LayoutParams.WRAP_CONTENT; 359 break; 360 case 3: 361 val = GridLayoutManager.LayoutParams.MATCH_PARENT; 362 break; 363 case 4: 364 val = 200; 365 break; 366 } 367 if (orientation == GridLayoutManager.VERTICAL) { 368 glp.height = val; 369 } else { 370 glp.width = val; 371 } 372 holder.itemView.setLayoutParams(glp); 373 } 374 }); 375 waitForFirstLayout(rv); 376 final OrientationHelper helper = mGlm.mOrientationHelper; 377 final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2))); 378 assertEquals(firstRowSize, 379 helper.getDecoratedMeasurement(mGlm.findViewByPosition(0))); 380 assertEquals(firstRowSize, 381 helper.getDecoratedMeasurement(mGlm.findViewByPosition(1))); 382 assertEquals(firstRowSize, 383 helper.getDecoratedMeasurement(mGlm.findViewByPosition(2))); 384 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0))); 385 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1))); 386 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2))); 387 388 final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3))); 389 assertEquals(secondRowSize, 390 helper.getDecoratedMeasurement(mGlm.findViewByPosition(3))); 391 assertEquals(secondRowSize, 392 helper.getDecoratedMeasurement(mGlm.findViewByPosition(4))); 393 assertEquals(secondRowSize, 394 helper.getDecoratedMeasurement(mGlm.findViewByPosition(5))); 395 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3))); 396 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4))); 397 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5))); 398 } 399 400 @Test anchorUpdate()401 public void anchorUpdate() throws InterruptedException { 402 GridLayoutManager glm = new GridLayoutManager(getActivity(), 11); 403 final GridLayoutManager.SpanSizeLookup spanSizeLookup 404 = new GridLayoutManager.SpanSizeLookup() { 405 @Override 406 public int getSpanSize(int position) { 407 if (position > 200) { 408 return 100; 409 } 410 if (position > 20) { 411 return 2; 412 } 413 return 1; 414 } 415 }; 416 glm.setSpanSizeLookup(spanSizeLookup); 417 glm.mAnchorInfo.mPosition = 11; 418 RecyclerView.State state = new RecyclerView.State(); 419 mRecyclerView = new RecyclerView(getActivity()); 420 state.mItemCount = 1000; 421 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 422 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 423 assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition); 424 425 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 426 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 427 assertEquals("gm should keep anchor in last span in the row", 20, 428 glm.mAnchorInfo.mPosition); 429 430 glm.mAnchorInfo.mPosition = 5; 431 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 432 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 433 assertEquals("gm should keep anchor in last span in the row", 10, 434 glm.mAnchorInfo.mPosition); 435 436 glm.mAnchorInfo.mPosition = 13; 437 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 438 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 439 assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition); 440 441 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 442 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 443 assertEquals("gm should keep anchor in last span in the row", 20, 444 glm.mAnchorInfo.mPosition); 445 446 glm.mAnchorInfo.mPosition = 23; 447 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 448 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 449 assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition); 450 451 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 452 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 453 assertEquals("gm should keep anchor in last span in the row", 25, 454 glm.mAnchorInfo.mPosition); 455 456 glm.mAnchorInfo.mPosition = 35; 457 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 458 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL); 459 assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition); 460 glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo, 461 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD); 462 assertEquals("gm should keep anchor in last span in the row", 35, 463 glm.mAnchorInfo.mPosition); 464 } 465 466 @Test spanLookup()467 public void spanLookup() { 468 spanLookupTest(false); 469 } 470 471 @Test spanLookupWithCache()472 public void spanLookupWithCache() { 473 spanLookupTest(true); 474 } 475 476 @Test spanLookupCache()477 public void spanLookupCache() { 478 final GridLayoutManager.SpanSizeLookup ssl 479 = new GridLayoutManager.SpanSizeLookup() { 480 @Override 481 public int getSpanSize(int position) { 482 if (position > 6) { 483 return 2; 484 } 485 return 1; 486 } 487 }; 488 ssl.setSpanIndexCacheEnabled(true); 489 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2)); 490 ssl.getCachedSpanIndex(4, 5); 491 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3)); 492 // this should not happen and if happens, it is better to return -1 493 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 494 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5)); 495 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100)); 496 ssl.getCachedSpanIndex(6, 5); 497 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 498 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6)); 499 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 500 ssl.getCachedSpanIndex(12, 5); 501 assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13)); 502 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12)); 503 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 504 for (int i = 0; i < 6; i++) { 505 ssl.getCachedSpanIndex(i, 5); 506 } 507 508 for (int i = 1; i < 7; i++) { 509 assertEquals("reference child right before " + i, i - 1, 510 ssl.findReferenceIndexFromCache(i)); 511 } 512 assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0)); 513 } 514 spanLookupTest(boolean enableCache)515 public void spanLookupTest(boolean enableCache) { 516 final GridLayoutManager.SpanSizeLookup ssl 517 = new GridLayoutManager.SpanSizeLookup() { 518 @Override 519 public int getSpanSize(int position) { 520 if (position > 200) { 521 return 100; 522 } 523 if (position > 6) { 524 return 2; 525 } 526 return 1; 527 } 528 }; 529 ssl.setSpanIndexCacheEnabled(enableCache); 530 assertEquals(0, ssl.getCachedSpanIndex(0, 5)); 531 assertEquals(4, ssl.getCachedSpanIndex(4, 5)); 532 assertEquals(0, ssl.getCachedSpanIndex(5, 5)); 533 assertEquals(1, ssl.getCachedSpanIndex(6, 5)); 534 assertEquals(2, ssl.getCachedSpanIndex(7, 5)); 535 assertEquals(2, ssl.getCachedSpanIndex(9, 5)); 536 assertEquals(0, ssl.getCachedSpanIndex(8, 5)); 537 } 538 539 @Test removeAnchorItem()540 public void removeAnchorItem() throws Throwable { 541 removeAnchorItemTest( 542 new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0); 543 } 544 545 @Test removeAnchorItemReverse()546 public void removeAnchorItemReverse() throws Throwable { 547 removeAnchorItemTest( 548 new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100, 549 0); 550 } 551 552 @Test removeAnchorItemHorizontal()553 public void removeAnchorItemHorizontal() throws Throwable { 554 removeAnchorItemTest( 555 new Config(3, 0).orientation(HORIZONTAL).reverseLayout( 556 false), 100, 0); 557 } 558 559 @Test removeAnchorItemReverseHorizontal()560 public void removeAnchorItemReverseHorizontal() throws Throwable { 561 removeAnchorItemTest( 562 new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true), 563 100, 0); 564 } 565 566 /** 567 * This tests a regression where predictive animations were not working as expected when the 568 * first item is removed and there aren't any more items to add from that direction. 569 * First item refers to the default anchor item. 570 */ removeAnchorItemTest(final Config config, int adapterSize, final int removePos)571 public void removeAnchorItemTest(final Config config, int adapterSize, 572 final int removePos) throws Throwable { 573 GridTestAdapter adapter = new GridTestAdapter(adapterSize) { 574 @Override 575 public void onBindViewHolder(TestViewHolder holder, 576 int position) { 577 super.onBindViewHolder(holder, position); 578 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 579 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 580 lp = new ViewGroup.MarginLayoutParams(0, 0); 581 holder.itemView.setLayoutParams(lp); 582 } 583 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 584 final int maxSize; 585 if (config.mOrientation == HORIZONTAL) { 586 maxSize = mRecyclerView.getWidth(); 587 mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 588 } else { 589 maxSize = mRecyclerView.getHeight(); 590 mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT; 591 } 592 593 final int desiredSize; 594 if (position == removePos) { 595 // make it large 596 desiredSize = maxSize / 4; 597 } else { 598 // make it small 599 desiredSize = maxSize / 8; 600 } 601 if (config.mOrientation == HORIZONTAL) { 602 mlp.width = desiredSize; 603 } else { 604 mlp.height = desiredSize; 605 } 606 } 607 }; 608 RecyclerView recyclerView = setupBasic(config, adapter); 609 waitForFirstLayout(recyclerView); 610 final int childCount = mGlm.getChildCount(); 611 RecyclerView.ViewHolder toBeRemoved = null; 612 List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>(); 613 for (int i = 0; i < childCount; i++) { 614 View child = mGlm.getChildAt(i); 615 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 616 if (holder.getAdapterPosition() == removePos) { 617 toBeRemoved = holder; 618 } else { 619 toBeMoved.add(holder); 620 } 621 } 622 assertNotNull("test sanity", toBeRemoved); 623 assertEquals("test sanity", childCount - 1, toBeMoved.size()); 624 LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator(); 625 mRecyclerView.setItemAnimator(loggingItemAnimator); 626 loggingItemAnimator.reset(); 627 loggingItemAnimator.expectRunPendingAnimationsCall(1); 628 mGlm.expectLayout(2); 629 adapter.deleteAndNotify(removePos, 1); 630 mGlm.waitForLayout(1); 631 loggingItemAnimator.waitForPendingAnimationsCall(2); 632 assertTrue("removed child should receive remove animation", 633 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved)); 634 for (RecyclerView.ViewHolder vh : toBeMoved) { 635 assertTrue("view holder should be in moved list", 636 loggingItemAnimator.mMoveVHs.contains(vh)); 637 } 638 List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>(); 639 for (int i = 0; i < mGlm.getChildCount(); i++) { 640 View child = mGlm.getChildAt(i); 641 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 642 if (toBeRemoved != holder && !toBeMoved.contains(holder)) { 643 newHolders.add(holder); 644 } 645 } 646 assertTrue("some new children should show up for the new space", newHolders.size() > 0); 647 assertEquals("no items should receive animate add since they are not new", 0, 648 loggingItemAnimator.mAddVHs.size()); 649 for (RecyclerView.ViewHolder holder : newHolders) { 650 assertTrue("new holder should receive a move animation", 651 loggingItemAnimator.mMoveVHs.contains(holder)); 652 } 653 // for removed view, 3 for new row 654 assertTrue("control against adding too many children due to bad layout state preparation." 655 + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(), 656 mRecyclerView.getChildCount() <= childCount + 1 + 3); 657 } 658 659 @Test spanGroupIndex()660 public void spanGroupIndex() { 661 final GridLayoutManager.SpanSizeLookup ssl 662 = new GridLayoutManager.SpanSizeLookup() { 663 @Override 664 public int getSpanSize(int position) { 665 if (position > 200) { 666 return 100; 667 } 668 if (position > 6) { 669 return 2; 670 } 671 return 1; 672 } 673 }; 674 assertEquals(0, ssl.getSpanGroupIndex(0, 5)); 675 assertEquals(0, ssl.getSpanGroupIndex(4, 5)); 676 assertEquals(1, ssl.getSpanGroupIndex(5, 5)); 677 assertEquals(1, ssl.getSpanGroupIndex(6, 5)); 678 assertEquals(1, ssl.getSpanGroupIndex(7, 5)); 679 assertEquals(2, ssl.getSpanGroupIndex(9, 5)); 680 assertEquals(2, ssl.getSpanGroupIndex(8, 5)); 681 } 682 683 @Test notifyDataSetChange()684 public void notifyDataSetChange() throws Throwable { 685 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 686 final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup(); 687 ssl.setSpanIndexCacheEnabled(true); 688 waitForFirstLayout(recyclerView); 689 assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0); 690 final Callback callback = new Callback() { 691 @Override 692 public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 693 if (!state.isPreLayout()) { 694 assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size()); 695 } 696 } 697 698 @Override 699 public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 700 if (!state.isPreLayout()) { 701 assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0); 702 } 703 } 704 }; 705 mGlm.mCallbacks.add(callback); 706 mGlm.expectLayout(2); 707 mAdapter.deleteAndNotify(2, 3); 708 mGlm.waitForLayout(2); 709 checkForMainThreadException(); 710 } 711 712 @Test unevenHeights()713 public void unevenHeights() throws Throwable { 714 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 715 new HashMap<Integer, RecyclerView.ViewHolder>(); 716 RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) { 717 @Override 718 public void onBindViewHolder(TestViewHolder holder, 719 int position) { 720 super.onBindViewHolder(holder, position); 721 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 722 glp.height = 50 + position * 50; 723 viewHolderMap.put(position, holder); 724 } 725 }); 726 waitForFirstLayout(recyclerView); 727 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 728 assertEquals("all items should get max height", 150, 729 vh.itemView.getHeight()); 730 } 731 732 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 733 assertEquals("all items should have measured the max height", 150, 734 vh.itemView.getMeasuredHeight()); 735 } 736 } 737 738 @Test unevenWidths()739 public void unevenWidths() throws Throwable { 740 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 741 new HashMap<Integer, RecyclerView.ViewHolder>(); 742 RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false), 743 new GridTestAdapter(3) { 744 @Override 745 public void onBindViewHolder(TestViewHolder holder, 746 int position) { 747 super.onBindViewHolder(holder, position); 748 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 749 glp.width = 50 + position * 50; 750 viewHolderMap.put(position, holder); 751 } 752 }); 753 waitForFirstLayout(recyclerView); 754 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 755 assertEquals("all items should get max width", 150, 756 vh.itemView.getWidth()); 757 } 758 759 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 760 assertEquals("all items should have measured the max width", 150, 761 vh.itemView.getMeasuredWidth()); 762 } 763 } 764 765 @Test spanSizeChange()766 public void spanSizeChange() throws Throwable { 767 final RecyclerView rv = setupBasic(new Config(3, 100)); 768 waitForFirstLayout(rv); 769 assertTrue(mGlm.supportsPredictiveItemAnimations()); 770 mGlm.expectLayout(1); 771 runTestOnUiThread(new Runnable() { 772 @Override 773 public void run() { 774 mGlm.setSpanCount(5); 775 assertFalse(mGlm.supportsPredictiveItemAnimations()); 776 } 777 }); 778 mGlm.waitForLayout(2); 779 mGlm.expectLayout(2); 780 mAdapter.deleteAndNotify(3, 2); 781 mGlm.waitForLayout(2); 782 assertTrue(mGlm.supportsPredictiveItemAnimations()); 783 } 784 785 @Test cacheSpanIndices()786 public void cacheSpanIndices() throws Throwable { 787 final RecyclerView rv = setupBasic(new Config(3, 100)); 788 mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true); 789 waitForFirstLayout(rv); 790 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 791 assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0); 792 assertEquals("item index 5 should be in span 2", 2, 793 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 794 mGlm.expectLayout(2); 795 mAdapter.mFullSpanItems.add(4); 796 mAdapter.changeAndNotify(4, 1); 797 mGlm.waitForLayout(2); 798 assertEquals("item index 5 should be in span 2", 0, 799 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 800 } 801 } 802