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