BaseStaggeredGridLayoutManagerTest.java revision c587f7dba5a337169e854e235da59f595255d6cc
1package android.support.v7.widget; 2 3import android.graphics.Rect; 4import android.support.annotation.Nullable; 5import android.util.Log; 6import android.view.View; 7import android.view.ViewGroup; 8 9import java.lang.reflect.Field; 10import java.util.ArrayList; 11import java.util.Arrays; 12import java.util.HashSet; 13import java.util.LinkedHashMap; 14import java.util.List; 15import java.util.Map; 16import java.util.concurrent.CountDownLatch; 17import java.util.concurrent.TimeUnit; 18import java.util.concurrent.atomic.AtomicInteger; 19 20import static android.support.v7.widget.LayoutState.LAYOUT_END; 21import static android.support.v7.widget.LayoutState.LAYOUT_START; 22import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 23import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 24import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE; 25import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL; 26import static org.junit.Assert.assertEquals; 27import static org.junit.Assert.assertFalse; 28import static org.junit.Assert.assertNotNull; 29import static org.junit.Assert.assertTrue; 30 31import static java.util.concurrent.TimeUnit.SECONDS; 32 33import org.hamcrest.CoreMatchers; 34import org.hamcrest.MatcherAssert; 35 36public class BaseStaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 37 38 protected static final boolean DEBUG = false; 39 protected static final int AVG_ITEM_PER_VIEW = 3; 40 protected static final String TAG = "StaggeredGridLayoutManagerTest"; 41 volatile WrappedLayoutManager mLayoutManager; 42 GridTestAdapter mAdapter; 43 44 protected static List<Config> createBaseVariations() { 45 List<Config> variations = new ArrayList<>(); 46 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 47 for (boolean reverseLayout : new boolean[]{false, true}) { 48 for (int spanCount : new int[]{1, 3}) { 49 for (int gapStrategy : new int[]{GAP_HANDLING_NONE, 50 GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) { 51 for (boolean wrap : new boolean[]{true, false}) { 52 variations.add(new Config(orientation, reverseLayout, spanCount, 53 gapStrategy).wrap(wrap)); 54 } 55 56 } 57 } 58 } 59 } 60 return variations; 61 } 62 63 protected static List<Config> addConfigVariation(List<Config> base, String fieldName, 64 Object... variations) 65 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 66 List<Config> newConfigs = new ArrayList<Config>(); 67 Field field = Config.class.getDeclaredField(fieldName); 68 for (Config config : base) { 69 for (Object variation : variations) { 70 Config newConfig = (Config) config.clone(); 71 field.set(newConfig, variation); 72 newConfigs.add(newConfig); 73 } 74 } 75 return newConfigs; 76 } 77 78 void setupByConfig(Config config) throws Throwable { 79 setupByConfig(config, new GridTestAdapter(config.mItemCount, config.mOrientation)); 80 } 81 82 void setupByConfig(Config config, GridTestAdapter adapter) throws Throwable { 83 mAdapter = adapter; 84 mRecyclerView = new RecyclerView(getActivity()); 85 mRecyclerView.setAdapter(mAdapter); 86 mRecyclerView.setHasFixedSize(true); 87 mLayoutManager = new WrappedLayoutManager(config.mSpanCount, 88 config.mOrientation); 89 mLayoutManager.setGapStrategy(config.mGapStrategy); 90 mLayoutManager.setReverseLayout(config.mReverseLayout); 91 mRecyclerView.setLayoutManager(mLayoutManager); 92 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 93 @Override 94 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 95 RecyclerView.State state) { 96 try { 97 StaggeredGridLayoutManager.LayoutParams 98 lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); 99 assertNotNull("view should have layout params assigned", lp); 100 assertNotNull("when item offsets are requested, view should have a valid span", 101 lp.mSpan); 102 } catch (Throwable t) { 103 postExceptionToInstrumentation(t); 104 } 105 } 106 }); 107 } 108 109 StaggeredGridLayoutManager.LayoutParams getLp(View view) { 110 return (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams(); 111 } 112 113 void waitFirstLayout() throws Throwable { 114 mLayoutManager.expectLayouts(1); 115 setRecyclerView(mRecyclerView); 116 mLayoutManager.waitForLayout(3); 117 getInstrumentation().waitForIdleSync(); 118 } 119 120 /** 121 * enqueues an empty runnable to main thread so that we can be assured it did run 122 * 123 * @param count Number of times to run 124 */ 125 protected void waitForMainThread(int count) throws Throwable { 126 final AtomicInteger i = new AtomicInteger(count); 127 while (i.get() > 0) { 128 runTestOnUiThread(new Runnable() { 129 @Override 130 public void run() { 131 i.decrementAndGet(); 132 } 133 }); 134 } 135 } 136 137 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 138 Map<Item, Rect> after) { 139 Throwable throwable = null; 140 try { 141 assertRectSetsEqual("NOT " + message, before, after); 142 } catch (Throwable t) { 143 throwable = t; 144 } 145 assertNotNull(message + " two layout should be different", throwable); 146 } 147 148 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 149 assertRectSetsEqual(message, before, after, true); 150 } 151 152 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, 153 boolean strictItemEquality) { 154 StringBuilder log = new StringBuilder(); 155 if (DEBUG) { 156 log.append("checking rectangle equality.\n"); 157 log.append("total space:" + mLayoutManager.mPrimaryOrientation.getTotalSpace()); 158 log.append("before:"); 159 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 160 log.append("\n").append(entry.getKey().mAdapterIndex).append(":") 161 .append(entry.getValue()); 162 } 163 log.append("\nafter:"); 164 for (Map.Entry<Item, Rect> entry : after.entrySet()) { 165 log.append("\n").append(entry.getKey().mAdapterIndex).append(":") 166 .append(entry.getValue()); 167 } 168 message += "\n\n" + log.toString(); 169 } 170 assertEquals(message + ": item counts should be equal", before.size() 171 , after.size()); 172 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 173 final Item beforeItem = entry.getKey(); 174 Rect afterRect = null; 175 if (strictItemEquality) { 176 afterRect = after.get(beforeItem); 177 assertNotNull(message + ": Same item should be visible after simple re-layout", 178 afterRect); 179 } else { 180 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) { 181 final Item afterItem = afterEntry.getKey(); 182 if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) { 183 afterRect = afterEntry.getValue(); 184 break; 185 } 186 } 187 assertNotNull(message + ": Item with same adapter index should be visible " + 188 "after simple re-layout", 189 afterRect); 190 } 191 assertEquals(message + ": Item should be laid out at the same coordinates", 192 entry.getValue(), 193 afterRect); 194 } 195 } 196 197 protected void assertViewPositions(Config config) { 198 ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan(); 199 OrientationHelper orientationHelper = OrientationHelper 200 .createOrientationHelper(mLayoutManager, config.mOrientation); 201 for (ArrayList<View> span : viewsBySpan) { 202 // validate all children's order. first child should have min start mPosition 203 final int count = span.size(); 204 for (int i = 0, j = 1; j < count; i++, j++) { 205 View prev = span.get(i); 206 View next = span.get(j); 207 assertTrue(config + " prev item should be above next item", 208 orientationHelper.getDecoratedEnd(prev) <= orientationHelper 209 .getDecoratedStart(next) 210 ); 211 212 } 213 } 214 } 215 216 protected TargetTuple findInvisibleTarget(Config config) { 217 int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE; 218 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 219 View child = mLayoutManager.getChildAt(i); 220 int position = mRecyclerView.getChildLayoutPosition(child); 221 if (position < minPosition) { 222 minPosition = position; 223 } 224 if (position > maxPosition) { 225 maxPosition = position; 226 } 227 } 228 final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2; 229 final int headTarget = minPosition / 2; 230 final int target; 231 // where will the child come from ? 232 final int itemLayoutDirection; 233 if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) { 234 target = tailTarget; 235 itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END; 236 } else { 237 target = headTarget; 238 itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START; 239 } 240 if (DEBUG) { 241 Log.d(TAG, 242 config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition); 243 } 244 return new TargetTuple(target, itemLayoutDirection); 245 } 246 247 protected void scrollToPositionWithOffset(final int position, final int offset) 248 throws Throwable { 249 runTestOnUiThread(new Runnable() { 250 @Override 251 public void run() { 252 mLayoutManager.scrollToPositionWithOffset(position, offset); 253 } 254 }); 255 } 256 257 static class OnLayoutListener { 258 259 void before(RecyclerView.Recycler recycler, RecyclerView.State state) { 260 } 261 262 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 263 } 264 } 265 266 static class VisibleChildren { 267 268 int[] firstVisiblePositions; 269 270 int[] firstFullyVisiblePositions; 271 272 int[] lastVisiblePositions; 273 274 int[] lastFullyVisiblePositions; 275 276 View findFirstPartialVisibleClosestToStart; 277 View findFirstPartialVisibleClosestToEnd; 278 279 VisibleChildren(int spanCount) { 280 firstFullyVisiblePositions = new int[spanCount]; 281 firstVisiblePositions = new int[spanCount]; 282 lastVisiblePositions = new int[spanCount]; 283 lastFullyVisiblePositions = new int[spanCount]; 284 for (int i = 0; i < spanCount; i++) { 285 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION; 286 firstVisiblePositions[i] = RecyclerView.NO_POSITION; 287 lastVisiblePositions[i] = RecyclerView.NO_POSITION; 288 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION; 289 } 290 } 291 292 @Override 293 public boolean equals(Object o) { 294 if (this == o) { 295 return true; 296 } 297 if (o == null || getClass() != o.getClass()) { 298 return false; 299 } 300 301 VisibleChildren that = (VisibleChildren) o; 302 303 if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) { 304 return false; 305 } 306 if (findFirstPartialVisibleClosestToStart 307 != null ? !findFirstPartialVisibleClosestToStart 308 .equals(that.findFirstPartialVisibleClosestToStart) 309 : that.findFirstPartialVisibleClosestToStart != null) { 310 return false; 311 } 312 if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) { 313 return false; 314 } 315 if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) { 316 return false; 317 } 318 if (findFirstPartialVisibleClosestToEnd != null ? !findFirstPartialVisibleClosestToEnd 319 .equals(that.findFirstPartialVisibleClosestToEnd) 320 : that.findFirstPartialVisibleClosestToEnd 321 != null) { 322 return false; 323 } 324 if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) { 325 return false; 326 } 327 328 return true; 329 } 330 331 @Override 332 public int hashCode() { 333 int result = Arrays.hashCode(firstVisiblePositions); 334 result = 31 * result + Arrays.hashCode(firstFullyVisiblePositions); 335 result = 31 * result + Arrays.hashCode(lastVisiblePositions); 336 result = 31 * result + Arrays.hashCode(lastFullyVisiblePositions); 337 result = 31 * result + (findFirstPartialVisibleClosestToStart != null 338 ? findFirstPartialVisibleClosestToStart 339 .hashCode() : 0); 340 result = 31 * result + (findFirstPartialVisibleClosestToEnd != null 341 ? findFirstPartialVisibleClosestToEnd 342 .hashCode() 343 : 0); 344 return result; 345 } 346 347 @Override 348 public String toString() { 349 return "VisibleChildren{" + 350 "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) + 351 ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) + 352 ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) + 353 ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) + 354 ", findFirstPartialVisibleClosestToStart=" + 355 viewToString(findFirstPartialVisibleClosestToStart) + 356 ", findFirstPartialVisibleClosestToEnd=" + 357 viewToString(findFirstPartialVisibleClosestToEnd) + 358 '}'; 359 } 360 361 private String viewToString(View view) { 362 if (view == null) { 363 return null; 364 } 365 ViewGroup.LayoutParams lp = view.getLayoutParams(); 366 if (lp instanceof RecyclerView.LayoutParams == false) { 367 return System.identityHashCode(view) + "(?)"; 368 } 369 RecyclerView.LayoutParams rvlp = (RecyclerView.LayoutParams) lp; 370 return System.identityHashCode(view) + "(" + rvlp.getViewAdapterPosition() + ")"; 371 } 372 } 373 374 abstract static class OnBindCallback { 375 376 abstract void onBoundItem(TestViewHolder vh, int position); 377 378 boolean assignRandomSize() { 379 return true; 380 } 381 382 void onCreatedViewHolder(TestViewHolder vh) { 383 } 384 } 385 386 static class Config implements Cloneable { 387 388 static final int DEFAULT_ITEM_COUNT = 300; 389 390 int mOrientation = OrientationHelper.VERTICAL; 391 392 boolean mReverseLayout = false; 393 394 int mSpanCount = 3; 395 396 int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 397 398 int mItemCount = DEFAULT_ITEM_COUNT; 399 400 boolean mWrap = false; 401 402 Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) { 403 mOrientation = orientation; 404 mReverseLayout = reverseLayout; 405 mSpanCount = spanCount; 406 mGapStrategy = gapStrategy; 407 } 408 409 public Config() { 410 411 } 412 413 Config orientation(int orientation) { 414 mOrientation = orientation; 415 return this; 416 } 417 418 Config reverseLayout(boolean reverseLayout) { 419 mReverseLayout = reverseLayout; 420 return this; 421 } 422 423 Config spanCount(int spanCount) { 424 mSpanCount = spanCount; 425 return this; 426 } 427 428 Config gapStrategy(int gapStrategy) { 429 mGapStrategy = gapStrategy; 430 return this; 431 } 432 433 public Config itemCount(int itemCount) { 434 mItemCount = itemCount; 435 return this; 436 } 437 438 public Config wrap(boolean wrap) { 439 mWrap = wrap; 440 return this; 441 } 442 443 @Override 444 public String toString() { 445 return "[CONFIG:" + 446 " span:" + mSpanCount + "," + 447 " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") + 448 " reverse:" + (mReverseLayout ? "T" : "F") + 449 " itemCount:" + mItemCount + 450 " wrapContent:" + mWrap + 451 " gap strategy: " + gapStrategyName(mGapStrategy); 452 } 453 454 protected static String gapStrategyName(int gapStrategy) { 455 switch (gapStrategy) { 456 case GAP_HANDLING_NONE: 457 return "none"; 458 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS: 459 return "move spans"; 460 } 461 return "gap strategy: unknown"; 462 } 463 464 @Override 465 public Object clone() throws CloneNotSupportedException { 466 return super.clone(); 467 } 468 } 469 470 class WrappedLayoutManager extends StaggeredGridLayoutManager { 471 472 CountDownLatch layoutLatch; 473 OnLayoutListener mOnLayoutListener; 474 // gradle does not yet let us customize manifest for tests which is necessary to test RTL. 475 // until bug is fixed, we'll fake it. 476 // public issue id: 57819 477 Boolean mFakeRTL; 478 CountDownLatch snapLatch; 479 480 @Override 481 boolean isLayoutRTL() { 482 return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL; 483 } 484 485 public void expectLayouts(int count) { 486 layoutLatch = new CountDownLatch(count); 487 } 488 489 public void waitForLayout(int seconds) throws Throwable { 490 layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 491 checkForMainThreadException(); 492 MatcherAssert.assertThat("all layouts should complete on time", 493 layoutLatch.getCount(), CoreMatchers.is(0L)); 494 // use a runnable to ensure RV layout is finished 495 getInstrumentation().runOnMainSync(new Runnable() { 496 @Override 497 public void run() { 498 } 499 }); 500 } 501 502 public void expectIdleState(int count) { 503 snapLatch = new CountDownLatch(count); 504 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 505 @Override 506 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 507 super.onScrollStateChanged(recyclerView, newState); 508 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 509 snapLatch.countDown(); 510 if (snapLatch.getCount() == 0L) { 511 mRecyclerView.removeOnScrollListener(this); 512 } 513 } 514 } 515 }); 516 } 517 518 public void waitForSnap(int seconds) throws Throwable { 519 snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 520 checkForMainThreadException(); 521 MatcherAssert.assertThat("all scrolling should complete on time", 522 snapLatch.getCount(), CoreMatchers.is(0L)); 523 // use a runnable to ensure RV layout is finished 524 getInstrumentation().runOnMainSync(new Runnable() { 525 @Override 526 public void run() { 527 } 528 }); 529 } 530 531 public void assertNoLayout(String msg, long timeout) throws Throwable { 532 layoutLatch.await(timeout, TimeUnit.SECONDS); 533 assertFalse(msg, layoutLatch.getCount() == 0); 534 } 535 536 @Override 537 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 538 String before; 539 if (DEBUG) { 540 before = layoutToString("before"); 541 } else { 542 before = "enable DEBUG"; 543 } 544 try { 545 if (mOnLayoutListener != null) { 546 mOnLayoutListener.before(recycler, state); 547 } 548 super.onLayoutChildren(recycler, state); 549 if (mOnLayoutListener != null) { 550 mOnLayoutListener.after(recycler, state); 551 } 552 validateChildren(before); 553 } catch (Throwable t) { 554 postExceptionToInstrumentation(t); 555 } 556 557 layoutLatch.countDown(); 558 } 559 560 @Override 561 int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 562 try { 563 int result = super.scrollBy(dt, recycler, state); 564 validateChildren(); 565 return result; 566 } catch (Throwable t) { 567 postExceptionToInstrumentation(t); 568 } 569 570 return 0; 571 } 572 573 View findFirstVisibleItemClosestToCenter() { 574 final int boundsStart = mPrimaryOrientation.getStartAfterPadding(); 575 final int boundsEnd = mPrimaryOrientation.getEndAfterPadding(); 576 final int boundsCenter = (boundsStart + boundsEnd) / 2; 577 final Rect childBounds = new Rect(); 578 int minDist = Integer.MAX_VALUE; 579 View closestChild = null; 580 for (int i = getChildCount() - 1; i >= 0; i--) { 581 final View child = getChildAt(i); 582 childBounds.setEmpty(); 583 getDecoratedBoundsWithMargins(child, childBounds); 584 int childCenter = canScrollHorizontally() 585 ? childBounds.centerX() : childBounds.centerY(); 586 int dist = Math.abs(boundsCenter - childCenter); 587 if (dist < minDist) { 588 minDist = dist; 589 closestChild = child; 590 } 591 } 592 return closestChild; 593 } 594 595 public WrappedLayoutManager(int spanCount, int orientation) { 596 super(spanCount, orientation); 597 } 598 599 ArrayList<ArrayList<View>> collectChildrenBySpan() { 600 ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>(); 601 for (int i = 0; i < getSpanCount(); i++) { 602 viewsBySpan.add(new ArrayList<View>()); 603 } 604 for (int i = 0; i < getChildCount(); i++) { 605 View view = getChildAt(i); 606 LayoutParams lp 607 = (LayoutParams) view 608 .getLayoutParams(); 609 viewsBySpan.get(lp.mSpan.mIndex).add(view); 610 } 611 return viewsBySpan; 612 } 613 614 @Nullable 615 @Override 616 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 617 RecyclerView.State state) { 618 View result = null; 619 try { 620 result = super.onFocusSearchFailed(focused, direction, recycler, state); 621 validateChildren(); 622 } catch (Throwable t) { 623 postExceptionToInstrumentation(t); 624 } 625 return result; 626 } 627 628 Rect getViewBounds(View view) { 629 if (getOrientation() == HORIZONTAL) { 630 return new Rect( 631 mPrimaryOrientation.getDecoratedStart(view), 632 mSecondaryOrientation.getDecoratedStart(view), 633 mPrimaryOrientation.getDecoratedEnd(view), 634 mSecondaryOrientation.getDecoratedEnd(view)); 635 } else { 636 return new Rect( 637 mSecondaryOrientation.getDecoratedStart(view), 638 mPrimaryOrientation.getDecoratedStart(view), 639 mSecondaryOrientation.getDecoratedEnd(view), 640 mPrimaryOrientation.getDecoratedEnd(view)); 641 } 642 } 643 644 public String getBoundsLog() { 645 StringBuilder sb = new StringBuilder(); 646 sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding()) 647 .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding()); 648 sb.append("\nchildren bounds\n"); 649 final int childCount = getChildCount(); 650 for (int i = 0; i < childCount; i++) { 651 View child = getChildAt(i); 652 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 653 .append("[").append("start:").append( 654 mPrimaryOrientation.getDecoratedStart(child)).append(", end:") 655 .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n"); 656 } 657 return sb.toString(); 658 } 659 660 public VisibleChildren traverseAndFindVisibleChildren() { 661 int childCount = getChildCount(); 662 final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount()); 663 final int start = mPrimaryOrientation.getStartAfterPadding(); 664 final int end = mPrimaryOrientation.getEndAfterPadding(); 665 for (int i = 0; i < childCount; i++) { 666 View child = getChildAt(i); 667 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 668 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 669 final boolean fullyVisible = childStart >= start && childEnd <= end; 670 final boolean hidden = childEnd <= start || childStart >= end; 671 if (hidden) { 672 continue; 673 } 674 final int position = getPosition(child); 675 final int span = getLp(child).getSpanIndex(); 676 if (fullyVisible) { 677 if (position < visibleChildren.firstFullyVisiblePositions[span] || 678 visibleChildren.firstFullyVisiblePositions[span] 679 == RecyclerView.NO_POSITION) { 680 visibleChildren.firstFullyVisiblePositions[span] = position; 681 } 682 683 if (position > visibleChildren.lastFullyVisiblePositions[span]) { 684 visibleChildren.lastFullyVisiblePositions[span] = position; 685 } 686 } 687 688 if (position < visibleChildren.firstVisiblePositions[span] || 689 visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) { 690 visibleChildren.firstVisiblePositions[span] = position; 691 } 692 693 if (position > visibleChildren.lastVisiblePositions[span]) { 694 visibleChildren.lastVisiblePositions[span] = position; 695 } 696 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) { 697 visibleChildren.findFirstPartialVisibleClosestToStart = child; 698 } 699 visibleChildren.findFirstPartialVisibleClosestToEnd = child; 700 } 701 return visibleChildren; 702 } 703 704 Map<Item, Rect> collectChildCoordinates() throws Throwable { 705 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 706 runTestOnUiThread(new Runnable() { 707 @Override 708 public void run() { 709 final int childCount = getChildCount(); 710 for (int i = 0; i < childCount; i++) { 711 View child = getChildAt(i); 712 // do it if and only if child is visible 713 if (child.getRight() < 0 || child.getBottom() < 0 || 714 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) { 715 // invisible children may be drawn in cases like scrolling so we should 716 // ignore them 717 continue; 718 } 719 LayoutParams lp = (LayoutParams) child 720 .getLayoutParams(); 721 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 722 items.put(vh.mBoundItem, getViewBounds(child)); 723 } 724 } 725 }); 726 return items; 727 } 728 729 730 public void setFakeRtl(Boolean fakeRtl) { 731 mFakeRTL = fakeRtl; 732 try { 733 requestLayoutOnUIThread(mRecyclerView); 734 } catch (Throwable throwable) { 735 postExceptionToInstrumentation(throwable); 736 } 737 } 738 739 String layoutToString(String hint) { 740 StringBuilder sb = new StringBuilder(); 741 sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n"); 742 for (int i = 0; i < getChildCount(); i++) { 743 final View view = getChildAt(i); 744 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 745 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s", 746 i, getPosition(view), 747 mPrimaryOrientation.getDecoratedStart(view), 748 mPrimaryOrientation.getDecoratedEnd(view), 749 layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n"); 750 } 751 return sb.toString(); 752 } 753 754 protected void validateChildren() { 755 validateChildren(null); 756 } 757 758 private void validateChildren(String msg) { 759 if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) { 760 return; 761 } 762 final int dir = mShouldReverseLayout ? -1 : 1; 763 int i = 0; 764 int pos = -1; 765 while (i < getChildCount()) { 766 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 767 if (lp.isItemRemoved()) { 768 i++; 769 continue; 770 } 771 pos = getPosition(getChildAt(i)); 772 break; 773 } 774 if (pos == -1) { 775 return; 776 } 777 while (++i < getChildCount()) { 778 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 779 if (lp.isItemRemoved()) { 780 continue; 781 } 782 pos += dir; 783 if (getPosition(getChildAt(i)) != pos) { 784 throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" + 785 layoutToString("ERROR") + "\n msg:" + msg); 786 } 787 } 788 } 789 } 790 791 class GridTestAdapter extends TestAdapter { 792 793 int mOrientation; 794 int mRecyclerViewWidth; 795 int mRecyclerViewHeight; 796 Integer mSizeReference = null; 797 798 // original ids of items that should be full span 799 HashSet<Integer> mFullSpanItems = new HashSet<Integer>(); 800 801 protected boolean mViewsHaveEqualSize = false; // size in the scrollable direction 802 803 protected OnBindCallback mOnBindCallback; 804 805 GridTestAdapter(int count, int orientation) { 806 super(count); 807 mOrientation = orientation; 808 } 809 810 @Override 811 public TestViewHolder onCreateViewHolder(ViewGroup parent, 812 int viewType) { 813 mRecyclerViewWidth = parent.getWidth(); 814 mRecyclerViewHeight = parent.getHeight(); 815 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 816 if (mOnBindCallback != null) { 817 mOnBindCallback.onCreatedViewHolder(vh); 818 } 819 return vh; 820 } 821 822 @Override 823 public void offsetOriginalIndices(int start, int offset) { 824 if (mFullSpanItems.size() > 0) { 825 HashSet<Integer> old = mFullSpanItems; 826 mFullSpanItems = new HashSet<Integer>(); 827 for (Integer i : old) { 828 if (i < start) { 829 mFullSpanItems.add(i); 830 } else if (offset > 0 || (start + Math.abs(offset)) <= i) { 831 mFullSpanItems.add(i + offset); 832 } else if (DEBUG) { 833 Log.d(TAG, "removed full span item " + i); 834 } 835 } 836 } 837 super.offsetOriginalIndices(start, offset); 838 } 839 840 @Override 841 protected void moveInUIThread(int from, int to) { 842 boolean setAsFullSpanAgain = mFullSpanItems.contains(from); 843 super.moveInUIThread(from, to); 844 if (setAsFullSpanAgain) { 845 mFullSpanItems.add(to); 846 } 847 } 848 849 @Override 850 public void onBindViewHolder(TestViewHolder holder, 851 int position) { 852 if (mSizeReference == null) { 853 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth 854 / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW; 855 } 856 super.onBindViewHolder(holder, position); 857 Item item = mItems.get(position); 858 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 859 .getLayoutParams(); 860 if (lp instanceof StaggeredGridLayoutManager.LayoutParams) { 861 ((StaggeredGridLayoutManager.LayoutParams) lp) 862 .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 863 } else { 864 StaggeredGridLayoutManager.LayoutParams slp 865 = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager 866 .generateDefaultLayoutParams(); 867 holder.itemView.setLayoutParams(slp); 868 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 869 lp = slp; 870 } 871 872 if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) { 873 final int minSize = mViewsHaveEqualSize ? mSizeReference : 874 mSizeReference + 20 * (item.mId % 10); 875 if (mOrientation == OrientationHelper.HORIZONTAL) { 876 holder.itemView.setMinimumWidth(minSize); 877 } else { 878 holder.itemView.setMinimumHeight(minSize); 879 } 880 lp.topMargin = 3; 881 lp.leftMargin = 5; 882 lp.rightMargin = 7; 883 lp.bottomMargin = 9; 884 } 885 886 if (mOnBindCallback != null) { 887 mOnBindCallback.onBoundItem(holder, position); 888 } 889 } 890 } 891} 892