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