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