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 17package android.support.v7.widget; 18 19import android.content.Context; 20import android.support.v4.view.AccessibilityDelegateCompat; 21import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 22import android.util.Log; 23import android.view.View; 24import android.view.ViewGroup; 25 26import java.util.ArrayList; 27import java.util.Arrays; 28import java.util.BitSet; 29import java.util.HashMap; 30import java.util.HashSet; 31import java.util.List; 32import java.util.Map; 33import java.util.Set; 34import java.util.concurrent.CountDownLatch; 35 36import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 37import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 38import static java.util.concurrent.TimeUnit.SECONDS; 39 40public class GridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 41 42 static final String TAG = "GridLayoutManagerTest"; 43 44 static final boolean DEBUG = false; 45 46 WrappedGridLayoutManager mGlm; 47 48 GridTestAdapter mAdapter; 49 50 final List<Config> mBaseVariations = new ArrayList<Config>(); 51 52 @Override 53 protected void setUp() throws Exception { 54 super.setUp(); 55 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 56 for (boolean reverseLayout : new boolean[]{false, true}) { 57 for (int spanCount : new int[]{1, 3, 4}) { 58 mBaseVariations.add(new Config(spanCount, orientation, reverseLayout)); 59 } 60 } 61 } 62 } 63 64 public RecyclerView setupBasic(Config config) throws Throwable { 65 return setupBasic(config, new GridTestAdapter(config.mItemCount)); 66 } 67 68 public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable { 69 RecyclerView recyclerView = new RecyclerView(getActivity()); 70 mAdapter = testAdapter; 71 mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation, 72 config.mReverseLayout); 73 mAdapter.assignSpanSizeLookup(mGlm); 74 recyclerView.setAdapter(mAdapter); 75 recyclerView.setLayoutManager(mGlm); 76 return recyclerView; 77 } 78 79 public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable { 80 mGlm.expectLayout(1); 81 setRecyclerView(recyclerView); 82 mGlm.waitForLayout(2); 83 } 84 85 public void testCustomWidthInHorizontal() throws Throwable { 86 customSizeInScrollDirectionTest(new Config(3, HORIZONTAL, false)); 87 } 88 89 public void testCustomHeightInVertical() throws Throwable { 90 customSizeInScrollDirectionTest(new Config(3, VERTICAL, false)); 91 } 92 93 public void customSizeInScrollDirectionTest(final Config config) throws Throwable { 94 final int[] sizePerPosition = new int[]{3, 5, 9, 21, 3, 5, 9, 6, 9, 1}; 95 final int[] expectedSizePerPosition = new int[]{9, 9, 9, 21, 3, 5, 9, 9, 9, 1}; 96 final GridTestAdapter testAdapter = new GridTestAdapter(10) { 97 @Override 98 public void onBindViewHolder(TestViewHolder holder, 99 int position) { 100 super.onBindViewHolder(holder, position); 101 ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); 102 if (layoutParams == null) { 103 layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 104 ViewGroup.LayoutParams.WRAP_CONTENT); 105 holder.itemView.setLayoutParams(layoutParams); 106 } 107 final int size = sizePerPosition[position]; 108 if (config.mOrientation == HORIZONTAL) { 109 layoutParams.width = size; 110 } else { 111 layoutParams.height = size; 112 } 113 } 114 }; 115 testAdapter.setFullSpan(3, 5); 116 final RecyclerView rv = setupBasic(config, testAdapter); 117 waitForFirstLayout(rv); 118 119 assertTrue("[test sanity] some views should be laid out", mRecyclerView.getChildCount() > 0); 120 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 121 View child = mRecyclerView.getChildAt(i); 122 final int size = config.mOrientation == HORIZONTAL ? child.getWidth() 123 : child.getHeight(); 124 assertEquals("child " + i + " should have the size specified in its layout params", 125 expectedSizePerPosition[i], size); 126 } 127 checkForMainThreadException(); 128 } 129 130 public void testLayoutParams() throws Throwable { 131 layoutParamsTest(GridLayoutManager.HORIZONTAL); 132 removeRecyclerView(); 133 layoutParamsTest(GridLayoutManager.VERTICAL); 134 } 135 136 public void testHorizontalAccessibilitySpanIndices() throws Throwable { 137 accessibilitySpanIndicesTest(HORIZONTAL); 138 } 139 140 public void testVerticalAccessibilitySpanIndices() throws Throwable { 141 accessibilitySpanIndicesTest(VERTICAL); 142 } 143 144 public void accessibilitySpanIndicesTest(int orientation) throws Throwable { 145 final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false)); 146 waitForFirstLayout(recyclerView); 147 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 148 .getCompatAccessibilityDelegate().getItemDelegate(); 149 final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); 150 final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2); 151 final int position = recyclerView.getChildLayoutPosition(chosen); 152 runTestOnUiThread(new Runnable() { 153 @Override 154 public void run() { 155 delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info); 156 } 157 }); 158 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 159 AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info 160 .getCollectionItemInfo(); 161 assertNotNull(itemInfo); 162 assertEquals("result should have span group position", 163 ssl.getSpanGroupIndex(position, mGlm.getSpanCount()), 164 orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex()); 165 assertEquals("result should have span index", 166 ssl.getSpanIndex(position, mGlm.getSpanCount()), 167 orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex()); 168 assertEquals("result should have span size", 169 ssl.getSpanSize(position), 170 orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan()); 171 } 172 173 public GridLayoutManager.LayoutParams ensureGridLp(View view) { 174 ViewGroup.LayoutParams lp = view.getLayoutParams(); 175 GridLayoutManager.LayoutParams glp; 176 if (lp instanceof GridLayoutManager.LayoutParams) { 177 glp = (GridLayoutManager.LayoutParams) lp; 178 } else if (lp == null) { 179 glp = (GridLayoutManager.LayoutParams) mGlm 180 .generateDefaultLayoutParams(); 181 view.setLayoutParams(glp); 182 } else { 183 glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp); 184 view.setLayoutParams(glp); 185 } 186 return glp; 187 } 188 189 public void layoutParamsTest(final int orientation) throws Throwable { 190 final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation), 191 new GridTestAdapter(100) { 192 @Override 193 public void onBindViewHolder(TestViewHolder holder, 194 int position) { 195 super.onBindViewHolder(holder, position); 196 GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 197 int val = 0; 198 switch (position % 5) { 199 case 0: 200 val = 10; 201 break; 202 case 1: 203 val = 30; 204 break; 205 case 2: 206 val = GridLayoutManager.LayoutParams.WRAP_CONTENT; 207 break; 208 case 3: 209 val = GridLayoutManager.LayoutParams.FILL_PARENT; 210 break; 211 case 4: 212 val = 200; 213 break; 214 } 215 if (orientation == GridLayoutManager.VERTICAL) { 216 glp.height = val; 217 } else { 218 glp.width = val; 219 } 220 holder.itemView.setLayoutParams(glp); 221 } 222 }); 223 waitForFirstLayout(rv); 224 final OrientationHelper helper = mGlm.mOrientationHelper; 225 final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2))); 226 assertEquals(firstRowSize, 227 helper.getDecoratedMeasurement(mGlm.findViewByPosition(0))); 228 assertEquals(firstRowSize, 229 helper.getDecoratedMeasurement(mGlm.findViewByPosition(1))); 230 assertEquals(firstRowSize, 231 helper.getDecoratedMeasurement(mGlm.findViewByPosition(2))); 232 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0))); 233 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1))); 234 assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2))); 235 236 final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3))); 237 assertEquals(secondRowSize, 238 helper.getDecoratedMeasurement(mGlm.findViewByPosition(3))); 239 assertEquals(secondRowSize, 240 helper.getDecoratedMeasurement(mGlm.findViewByPosition(4))); 241 assertEquals(secondRowSize, 242 helper.getDecoratedMeasurement(mGlm.findViewByPosition(5))); 243 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3))); 244 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4))); 245 assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5))); 246 } 247 248 private int getSize(View view) { 249 if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) { 250 return view.getWidth(); 251 } 252 return view.getHeight(); 253 } 254 255 public void testAnchorUpdate() throws InterruptedException { 256 GridLayoutManager glm = new GridLayoutManager(getActivity(), 11); 257 final GridLayoutManager.SpanSizeLookup spanSizeLookup 258 = new GridLayoutManager.SpanSizeLookup() { 259 @Override 260 public int getSpanSize(int position) { 261 if (position > 200) { 262 return 100; 263 } 264 if (position > 20) { 265 return 2; 266 } 267 return 1; 268 } 269 }; 270 glm.setSpanSizeLookup(spanSizeLookup); 271 glm.mAnchorInfo.mPosition = 11; 272 RecyclerView.State state = new RecyclerView.State(); 273 state.mItemCount = 1000; 274 glm.onAnchorReady(state, glm.mAnchorInfo); 275 assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition); 276 277 glm.mAnchorInfo.mPosition = 13; 278 glm.onAnchorReady(state, glm.mAnchorInfo); 279 assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition); 280 281 glm.mAnchorInfo.mPosition = 23; 282 glm.onAnchorReady(state, glm.mAnchorInfo); 283 assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition); 284 285 glm.mAnchorInfo.mPosition = 35; 286 glm.onAnchorReady(state, glm.mAnchorInfo); 287 assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition); 288 } 289 290 public void testSpanLookup() { 291 spanLookupTest(false); 292 } 293 294 public void testSpanLookupWithCache() { 295 spanLookupTest(true); 296 } 297 298 public void testSpanLookupCache() { 299 final GridLayoutManager.SpanSizeLookup ssl 300 = new GridLayoutManager.SpanSizeLookup() { 301 @Override 302 public int getSpanSize(int position) { 303 if (position > 6) { 304 return 2; 305 } 306 return 1; 307 } 308 }; 309 ssl.setSpanIndexCacheEnabled(true); 310 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2)); 311 ssl.getCachedSpanIndex(4, 5); 312 assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3)); 313 // this should not happen and if happens, it is better to return -1 314 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 315 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5)); 316 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100)); 317 ssl.getCachedSpanIndex(6, 5); 318 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 319 assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6)); 320 assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4)); 321 ssl.getCachedSpanIndex(12, 5); 322 assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13)); 323 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12)); 324 assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7)); 325 for (int i = 0; i < 6; i++) { 326 ssl.getCachedSpanIndex(i, 5); 327 } 328 329 for (int i = 1; i < 7; i++) { 330 assertEquals("reference child right before " + i, i - 1, 331 ssl.findReferenceIndexFromCache(i)); 332 } 333 assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0)); 334 } 335 336 public void spanLookupTest(boolean enableCache) { 337 final GridLayoutManager.SpanSizeLookup ssl 338 = new GridLayoutManager.SpanSizeLookup() { 339 @Override 340 public int getSpanSize(int position) { 341 if (position > 200) { 342 return 100; 343 } 344 if (position > 6) { 345 return 2; 346 } 347 return 1; 348 } 349 }; 350 ssl.setSpanIndexCacheEnabled(enableCache); 351 assertEquals(0, ssl.getCachedSpanIndex(0, 5)); 352 assertEquals(4, ssl.getCachedSpanIndex(4, 5)); 353 assertEquals(0, ssl.getCachedSpanIndex(5, 5)); 354 assertEquals(1, ssl.getCachedSpanIndex(6, 5)); 355 assertEquals(2, ssl.getCachedSpanIndex(7, 5)); 356 assertEquals(2, ssl.getCachedSpanIndex(9, 5)); 357 assertEquals(0, ssl.getCachedSpanIndex(8, 5)); 358 } 359 360 public void testSpanGroupIndex() { 361 final GridLayoutManager.SpanSizeLookup ssl 362 = new GridLayoutManager.SpanSizeLookup() { 363 @Override 364 public int getSpanSize(int position) { 365 if (position > 200) { 366 return 100; 367 } 368 if (position > 6) { 369 return 2; 370 } 371 return 1; 372 } 373 }; 374 assertEquals(0, ssl.getSpanGroupIndex(0, 5)); 375 assertEquals(0, ssl.getSpanGroupIndex(4, 5)); 376 assertEquals(1, ssl.getSpanGroupIndex(5, 5)); 377 assertEquals(1, ssl.getSpanGroupIndex(6, 5)); 378 assertEquals(1, ssl.getSpanGroupIndex(7, 5)); 379 assertEquals(2, ssl.getSpanGroupIndex(9, 5)); 380 assertEquals(2, ssl.getSpanGroupIndex(8, 5)); 381 } 382 383 public void testNotifyDataSetChange() throws Throwable { 384 final RecyclerView recyclerView = setupBasic(new Config(3, 100)); 385 final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup(); 386 ssl.setSpanIndexCacheEnabled(true); 387 waitForFirstLayout(recyclerView); 388 assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0); 389 final Callback callback = new Callback() { 390 @Override 391 public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 392 if (!state.isPreLayout()) { 393 assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size()); 394 } 395 } 396 397 @Override 398 public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 399 if (!state.isPreLayout()) { 400 assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0); 401 } 402 } 403 }; 404 mGlm.mCallbacks.add(callback); 405 mGlm.expectLayout(2); 406 mAdapter.deleteAndNotify(2, 3); 407 mGlm.waitForLayout(2); 408 checkForMainThreadException(); 409 } 410 411 public void testUnevenHeights() throws Throwable { 412 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 413 new HashMap<Integer, RecyclerView.ViewHolder>(); 414 RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) { 415 @Override 416 public void onBindViewHolder(TestViewHolder holder, 417 int position) { 418 super.onBindViewHolder(holder, position); 419 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 420 glp.height = 50 + position * 50; 421 viewHolderMap.put(position, holder); 422 } 423 }); 424 waitForFirstLayout(recyclerView); 425 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 426 assertEquals("all items should get max height", 150, 427 vh.itemView.getHeight()); 428 } 429 430 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 431 assertEquals("all items should have measured the max height", 150, 432 vh.itemView.getMeasuredHeight()); 433 } 434 } 435 436 public void testUnevenWidths() throws Throwable { 437 final Map<Integer, RecyclerView.ViewHolder> viewHolderMap = 438 new HashMap<Integer, RecyclerView.ViewHolder>(); 439 RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false), 440 new GridTestAdapter(3) { 441 @Override 442 public void onBindViewHolder(TestViewHolder holder, 443 int position) { 444 super.onBindViewHolder(holder, position); 445 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView); 446 glp.width = 50 + position * 50; 447 viewHolderMap.put(position, holder); 448 } 449 }); 450 waitForFirstLayout(recyclerView); 451 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 452 assertEquals("all items should get max width", 150, 453 vh.itemView.getWidth()); 454 } 455 456 for (RecyclerView.ViewHolder vh : viewHolderMap.values()) { 457 assertEquals("all items should have measured the max width", 150, 458 vh.itemView.getMeasuredWidth()); 459 } 460 } 461 462 public void testScrollBackAndPreservePositions() throws Throwable { 463 for (Config config : mBaseVariations) { 464 config.mItemCount = 150; 465 scrollBackAndPreservePositionsTest(config); 466 removeRecyclerView(); 467 } 468 } 469 470 public void testCacheSpanIndices() throws Throwable { 471 final RecyclerView rv = setupBasic(new Config(3, 100)); 472 mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true); 473 waitForFirstLayout(rv); 474 GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup; 475 assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0); 476 assertEquals("item index 5 should be in span 2", 2, 477 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 478 mGlm.expectLayout(2); 479 mAdapter.mFullSpanItems.add(4); 480 mAdapter.changeAndNotify(4, 1); 481 mGlm.waitForLayout(2); 482 assertEquals("item index 5 should be in span 2", 0, 483 getLp(mGlm.findViewByPosition(5)).getSpanIndex()); 484 } 485 486 GridLayoutManager.LayoutParams getLp(View view) { 487 return (GridLayoutManager.LayoutParams) view.getLayoutParams(); 488 } 489 490 public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable { 491 final RecyclerView rv = setupBasic(config); 492 for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) { 493 mAdapter.setFullSpan(i); 494 } 495 waitForFirstLayout(rv); 496 final int[] globalPositions = new int[mAdapter.getItemCount()]; 497 Arrays.fill(globalPositions, Integer.MIN_VALUE); 498 final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20) 499 * (config.mReverseLayout ? -1 : 1); 500 final String logPrefix = config.toString(); 501 final int[] globalPos = new int[1]; 502 runTestOnUiThread(new Runnable() { 503 @Override 504 public void run() { 505 int globalScrollPosition = 0; 506 int visited = 0; 507 while (visited < mAdapter.getItemCount()) { 508 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 509 View child = mRecyclerView.getChildAt(i); 510 final int pos = mRecyclerView.getChildLayoutPosition(child); 511 if (globalPositions[pos] != Integer.MIN_VALUE) { 512 continue; 513 } 514 visited++; 515 GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) 516 child.getLayoutParams(); 517 if (config.mReverseLayout) { 518 globalPositions[pos] = globalScrollPosition + 519 mGlm.mOrientationHelper.getDecoratedEnd(child); 520 } else { 521 globalPositions[pos] = globalScrollPosition + 522 mGlm.mOrientationHelper.getDecoratedStart(child); 523 } 524 assertEquals(logPrefix + " span index should match", 525 mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()), 526 lp.getSpanIndex()); 527 } 528 int scrolled = mGlm.scrollBy(scrollStep, 529 mRecyclerView.mRecycler, mRecyclerView.mState); 530 globalScrollPosition += scrolled; 531 if (scrolled == 0) { 532 assertEquals( 533 logPrefix + " If scroll is complete, all views should be visited", 534 visited, mAdapter.getItemCount()); 535 } 536 } 537 if (DEBUG) { 538 Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions)); 539 } 540 globalPos[0] = globalScrollPosition; 541 } 542 }); 543 checkForMainThreadException(); 544 runTestOnUiThread(new Runnable() { 545 @Override 546 public void run() { 547 int globalScrollPosition = globalPos[0]; 548 // now scroll back and make sure global positions match 549 BitSet shouldTest = new BitSet(mAdapter.getItemCount()); 550 shouldTest.set(0, mAdapter.getItemCount() - 1, true); 551 String assertPrefix = config 552 + " global pos must match when scrolling in reverse for position "; 553 int scrollAmount = Integer.MAX_VALUE; 554 while (!shouldTest.isEmpty() && scrollAmount != 0) { 555 for (int i = 0; i < mRecyclerView.getChildCount(); i++) { 556 View child = mRecyclerView.getChildAt(i); 557 int pos = mRecyclerView.getChildLayoutPosition(child); 558 if (!shouldTest.get(pos)) { 559 continue; 560 } 561 GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) 562 child.getLayoutParams(); 563 shouldTest.clear(pos); 564 int globalPos; 565 if (config.mReverseLayout) { 566 globalPos = globalScrollPosition + 567 mGlm.mOrientationHelper.getDecoratedEnd(child); 568 } else { 569 globalPos = globalScrollPosition + 570 mGlm.mOrientationHelper.getDecoratedStart(child); 571 } 572 assertEquals(assertPrefix + pos, 573 globalPositions[pos], globalPos); 574 assertEquals("span index should match", 575 mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()), 576 lp.getSpanIndex()); 577 } 578 scrollAmount = mGlm.scrollBy(-scrollStep, 579 mRecyclerView.mRecycler, mRecyclerView.mState); 580 globalScrollPosition += scrollAmount; 581 } 582 assertTrue("all views should be seen", shouldTest.isEmpty()); 583 } 584 }); 585 checkForMainThreadException(); 586 } 587 588 class WrappedGridLayoutManager extends GridLayoutManager { 589 590 CountDownLatch mLayoutLatch; 591 592 List<Callback> mCallbacks = new ArrayList<Callback>(); 593 594 public WrappedGridLayoutManager(Context context, int spanCount) { 595 super(context, spanCount); 596 } 597 598 public WrappedGridLayoutManager(Context context, int spanCount, int orientation, 599 boolean reverseLayout) { 600 super(context, spanCount, orientation, reverseLayout); 601 } 602 603 @Override 604 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 605 try { 606 for (Callback callback : mCallbacks) { 607 callback.onBeforeLayout(recycler, state); 608 } 609 super.onLayoutChildren(recycler, state); 610 for (Callback callback : mCallbacks) { 611 callback.onAfterLayout(recycler, state); 612 } 613 } catch (Throwable t) { 614 postExceptionToInstrumentation(t); 615 } 616 mLayoutLatch.countDown(); 617 } 618 619 public void expectLayout(int layoutCount) { 620 mLayoutLatch = new CountDownLatch(layoutCount); 621 } 622 623 public void waitForLayout(int seconds) throws InterruptedException { 624 mLayoutLatch.await(seconds, SECONDS); 625 } 626 } 627 628 class Config { 629 630 int mSpanCount; 631 int mOrientation = GridLayoutManager.VERTICAL; 632 int mItemCount = 1000; 633 boolean mReverseLayout = false; 634 635 Config(int spanCount, int itemCount) { 636 mSpanCount = spanCount; 637 mItemCount = itemCount; 638 } 639 640 public Config(int spanCount, int orientation, boolean reverseLayout) { 641 mSpanCount = spanCount; 642 mOrientation = orientation; 643 mReverseLayout = reverseLayout; 644 } 645 646 Config orientation(int orientation) { 647 mOrientation = orientation; 648 return this; 649 } 650 651 @Override 652 public String toString() { 653 return "Config{" + 654 "mSpanCount=" + mSpanCount + 655 ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") + 656 ", mItemCount=" + mItemCount + 657 ", mReverseLayout=" + mReverseLayout + 658 '}'; 659 } 660 } 661 662 class GridTestAdapter extends TestAdapter { 663 664 Set<Integer> mFullSpanItems = new HashSet<Integer>(); 665 666 GridTestAdapter(int count) { 667 super(count); 668 } 669 670 void setFullSpan(int... items) { 671 for (int i : items) { 672 mFullSpanItems.add(i); 673 } 674 } 675 676 void assignSpanSizeLookup(final GridLayoutManager glm) { 677 glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { 678 @Override 679 public int getSpanSize(int position) { 680 return mFullSpanItems.contains(position) ? glm.getSpanCount() : 1; 681 } 682 }); 683 } 684 } 685 686 class Callback { 687 688 public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 689 } 690 691 public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) { 692 } 693 } 694} 695