1 package android.support.v7.widget; 2 3 import android.graphics.Rect; 4 import android.support.annotation.Nullable; 5 import android.util.Log; 6 import android.view.View; 7 import android.view.ViewGroup; 8 9 import java.lang.reflect.Field; 10 import java.util.ArrayList; 11 import java.util.Arrays; 12 import java.util.HashSet; 13 import java.util.LinkedHashMap; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.concurrent.CountDownLatch; 17 import java.util.concurrent.TimeUnit; 18 import java.util.concurrent.atomic.AtomicInteger; 19 20 import static android.support.v7.widget.LayoutState.LAYOUT_END; 21 import static android.support.v7.widget.LayoutState.LAYOUT_START; 22 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 23 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS; 24 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE; 25 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL; 26 import static org.junit.Assert.assertEquals; 27 import static org.junit.Assert.assertFalse; 28 import static org.junit.Assert.assertNotNull; 29 import static org.junit.Assert.assertTrue; 30 31 import static java.util.concurrent.TimeUnit.SECONDS; 32 33 import org.hamcrest.CoreMatchers; 34 import org.hamcrest.MatcherAssert; 35 36 public 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 479 @Override 480 boolean isLayoutRTL() { 481 return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL; 482 } 483 484 public void expectLayouts(int count) { 485 layoutLatch = new CountDownLatch(count); 486 } 487 488 public void waitForLayout(int seconds) throws Throwable { 489 layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 490 checkForMainThreadException(); 491 MatcherAssert.assertThat("all layouts should complete on time", 492 layoutLatch.getCount(), CoreMatchers.is(0L)); 493 // use a runnable to ensure RV layout is finished 494 getInstrumentation().runOnMainSync(new Runnable() { 495 @Override 496 public void run() { 497 } 498 }); 499 } 500 501 public void assertNoLayout(String msg, long timeout) throws Throwable { 502 layoutLatch.await(timeout, TimeUnit.SECONDS); 503 assertFalse(msg, layoutLatch.getCount() == 0); 504 } 505 506 @Override 507 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 508 String before; 509 if (DEBUG) { 510 before = layoutToString("before"); 511 } else { 512 before = "enable DEBUG"; 513 } 514 try { 515 if (mOnLayoutListener != null) { 516 mOnLayoutListener.before(recycler, state); 517 } 518 super.onLayoutChildren(recycler, state); 519 if (mOnLayoutListener != null) { 520 mOnLayoutListener.after(recycler, state); 521 } 522 validateChildren(before); 523 } catch (Throwable t) { 524 postExceptionToInstrumentation(t); 525 } 526 527 layoutLatch.countDown(); 528 } 529 530 @Override 531 int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) { 532 try { 533 int result = super.scrollBy(dt, recycler, state); 534 validateChildren(); 535 return result; 536 } catch (Throwable t) { 537 postExceptionToInstrumentation(t); 538 } 539 540 return 0; 541 } 542 543 public WrappedLayoutManager(int spanCount, int orientation) { 544 super(spanCount, orientation); 545 } 546 547 ArrayList<ArrayList<View>> collectChildrenBySpan() { 548 ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>(); 549 for (int i = 0; i < getSpanCount(); i++) { 550 viewsBySpan.add(new ArrayList<View>()); 551 } 552 for (int i = 0; i < getChildCount(); i++) { 553 View view = getChildAt(i); 554 LayoutParams lp 555 = (LayoutParams) view 556 .getLayoutParams(); 557 viewsBySpan.get(lp.mSpan.mIndex).add(view); 558 } 559 return viewsBySpan; 560 } 561 562 @Nullable 563 @Override 564 public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, 565 RecyclerView.State state) { 566 View result = null; 567 try { 568 result = super.onFocusSearchFailed(focused, direction, recycler, state); 569 validateChildren(); 570 } catch (Throwable t) { 571 postExceptionToInstrumentation(t); 572 } 573 return result; 574 } 575 576 Rect getViewBounds(View view) { 577 if (getOrientation() == HORIZONTAL) { 578 return new Rect( 579 mPrimaryOrientation.getDecoratedStart(view), 580 mSecondaryOrientation.getDecoratedStart(view), 581 mPrimaryOrientation.getDecoratedEnd(view), 582 mSecondaryOrientation.getDecoratedEnd(view)); 583 } else { 584 return new Rect( 585 mSecondaryOrientation.getDecoratedStart(view), 586 mPrimaryOrientation.getDecoratedStart(view), 587 mSecondaryOrientation.getDecoratedEnd(view), 588 mPrimaryOrientation.getDecoratedEnd(view)); 589 } 590 } 591 592 public String getBoundsLog() { 593 StringBuilder sb = new StringBuilder(); 594 sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding()) 595 .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding()); 596 sb.append("\nchildren bounds\n"); 597 final int childCount = getChildCount(); 598 for (int i = 0; i < childCount; i++) { 599 View child = getChildAt(i); 600 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 601 .append("[").append("start:").append( 602 mPrimaryOrientation.getDecoratedStart(child)).append(", end:") 603 .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n"); 604 } 605 return sb.toString(); 606 } 607 608 public VisibleChildren traverseAndFindVisibleChildren() { 609 int childCount = getChildCount(); 610 final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount()); 611 final int start = mPrimaryOrientation.getStartAfterPadding(); 612 final int end = mPrimaryOrientation.getEndAfterPadding(); 613 for (int i = 0; i < childCount; i++) { 614 View child = getChildAt(i); 615 final int childStart = mPrimaryOrientation.getDecoratedStart(child); 616 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child); 617 final boolean fullyVisible = childStart >= start && childEnd <= end; 618 final boolean hidden = childEnd <= start || childStart >= end; 619 if (hidden) { 620 continue; 621 } 622 final int position = getPosition(child); 623 final int span = getLp(child).getSpanIndex(); 624 if (fullyVisible) { 625 if (position < visibleChildren.firstFullyVisiblePositions[span] || 626 visibleChildren.firstFullyVisiblePositions[span] 627 == RecyclerView.NO_POSITION) { 628 visibleChildren.firstFullyVisiblePositions[span] = position; 629 } 630 631 if (position > visibleChildren.lastFullyVisiblePositions[span]) { 632 visibleChildren.lastFullyVisiblePositions[span] = position; 633 } 634 } 635 636 if (position < visibleChildren.firstVisiblePositions[span] || 637 visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) { 638 visibleChildren.firstVisiblePositions[span] = position; 639 } 640 641 if (position > visibleChildren.lastVisiblePositions[span]) { 642 visibleChildren.lastVisiblePositions[span] = position; 643 } 644 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) { 645 visibleChildren.findFirstPartialVisibleClosestToStart = child; 646 } 647 visibleChildren.findFirstPartialVisibleClosestToEnd = child; 648 } 649 return visibleChildren; 650 } 651 652 Map<Item, Rect> collectChildCoordinates() throws Throwable { 653 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 654 runTestOnUiThread(new Runnable() { 655 @Override 656 public void run() { 657 final int childCount = getChildCount(); 658 for (int i = 0; i < childCount; i++) { 659 View child = getChildAt(i); 660 // do it if and only if child is visible 661 if (child.getRight() < 0 || child.getBottom() < 0 || 662 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) { 663 // invisible children may be drawn in cases like scrolling so we should 664 // ignore them 665 continue; 666 } 667 LayoutParams lp = (LayoutParams) child 668 .getLayoutParams(); 669 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 670 items.put(vh.mBoundItem, getViewBounds(child)); 671 } 672 } 673 }); 674 return items; 675 } 676 677 678 public void setFakeRtl(Boolean fakeRtl) { 679 mFakeRTL = fakeRtl; 680 try { 681 requestLayoutOnUIThread(mRecyclerView); 682 } catch (Throwable throwable) { 683 postExceptionToInstrumentation(throwable); 684 } 685 } 686 687 String layoutToString(String hint) { 688 StringBuilder sb = new StringBuilder(); 689 sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n"); 690 for (int i = 0; i < getChildCount(); i++) { 691 final View view = getChildAt(i); 692 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); 693 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s", 694 i, getPosition(view), 695 mPrimaryOrientation.getDecoratedStart(view), 696 mPrimaryOrientation.getDecoratedEnd(view), 697 layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n"); 698 } 699 return sb.toString(); 700 } 701 702 protected void validateChildren() { 703 validateChildren(null); 704 } 705 706 private void validateChildren(String msg) { 707 if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) { 708 return; 709 } 710 final int dir = mShouldReverseLayout ? -1 : 1; 711 int i = 0; 712 int pos = -1; 713 while (i < getChildCount()) { 714 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 715 if (lp.isItemRemoved()) { 716 i++; 717 continue; 718 } 719 pos = getPosition(getChildAt(i)); 720 break; 721 } 722 if (pos == -1) { 723 return; 724 } 725 while (++i < getChildCount()) { 726 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 727 if (lp.isItemRemoved()) { 728 continue; 729 } 730 pos += dir; 731 if (getPosition(getChildAt(i)) != pos) { 732 throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" + 733 layoutToString("ERROR") + "\n msg:" + msg); 734 } 735 } 736 } 737 } 738 739 class GridTestAdapter extends TestAdapter { 740 741 int mOrientation; 742 int mRecyclerViewWidth; 743 int mRecyclerViewHeight; 744 Integer mSizeReference = null; 745 746 // original ids of items that should be full span 747 HashSet<Integer> mFullSpanItems = new HashSet<Integer>(); 748 749 protected boolean mViewsHaveEqualSize = false; // size in the scrollable direction 750 751 protected OnBindCallback mOnBindCallback; 752 753 GridTestAdapter(int count, int orientation) { 754 super(count); 755 mOrientation = orientation; 756 } 757 758 @Override 759 public TestViewHolder onCreateViewHolder(ViewGroup parent, 760 int viewType) { 761 mRecyclerViewWidth = parent.getWidth(); 762 mRecyclerViewHeight = parent.getHeight(); 763 TestViewHolder vh = super.onCreateViewHolder(parent, viewType); 764 if (mOnBindCallback != null) { 765 mOnBindCallback.onCreatedViewHolder(vh); 766 } 767 return vh; 768 } 769 770 @Override 771 public void offsetOriginalIndices(int start, int offset) { 772 if (mFullSpanItems.size() > 0) { 773 HashSet<Integer> old = mFullSpanItems; 774 mFullSpanItems = new HashSet<Integer>(); 775 for (Integer i : old) { 776 if (i < start) { 777 mFullSpanItems.add(i); 778 } else if (offset > 0 || (start + Math.abs(offset)) <= i) { 779 mFullSpanItems.add(i + offset); 780 } else if (DEBUG) { 781 Log.d(TAG, "removed full span item " + i); 782 } 783 } 784 } 785 super.offsetOriginalIndices(start, offset); 786 } 787 788 @Override 789 protected void moveInUIThread(int from, int to) { 790 boolean setAsFullSpanAgain = mFullSpanItems.contains(from); 791 super.moveInUIThread(from, to); 792 if (setAsFullSpanAgain) { 793 mFullSpanItems.add(to); 794 } 795 } 796 797 @Override 798 public void onBindViewHolder(TestViewHolder holder, 799 int position) { 800 if (mSizeReference == null) { 801 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth 802 / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW; 803 } 804 super.onBindViewHolder(holder, position); 805 Item item = mItems.get(position); 806 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView 807 .getLayoutParams(); 808 if (lp instanceof StaggeredGridLayoutManager.LayoutParams) { 809 ((StaggeredGridLayoutManager.LayoutParams) lp) 810 .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 811 } else { 812 StaggeredGridLayoutManager.LayoutParams slp 813 = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager 814 .generateDefaultLayoutParams(); 815 holder.itemView.setLayoutParams(slp); 816 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex)); 817 lp = slp; 818 } 819 820 if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) { 821 final int minSize = mViewsHaveEqualSize ? mSizeReference : 822 mSizeReference + 20 * (item.mId % 10); 823 if (mOrientation == OrientationHelper.HORIZONTAL) { 824 holder.itemView.setMinimumWidth(minSize); 825 } else { 826 holder.itemView.setMinimumHeight(minSize); 827 } 828 lp.topMargin = 3; 829 lp.leftMargin = 5; 830 lp.rightMargin = 7; 831 lp.bottomMargin = 9; 832 } 833 834 if (mOnBindCallback != null) { 835 mOnBindCallback.onBoundItem(holder, position); 836 } 837 } 838 } 839 } 840