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 package androidx.recyclerview.widget; 17 18 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 19 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 20 21 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL; 22 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 23 24 import static org.junit.Assert.assertEquals; 25 import static org.junit.Assert.assertFalse; 26 import static org.junit.Assert.assertNotNull; 27 28 import static java.util.concurrent.TimeUnit.SECONDS; 29 30 import android.content.Context; 31 import android.graphics.Rect; 32 import android.util.Log; 33 import android.view.View; 34 import android.view.ViewGroup; 35 36 import org.hamcrest.CoreMatchers; 37 import org.hamcrest.MatcherAssert; 38 import org.jspecify.annotations.NonNull; 39 import org.jspecify.annotations.Nullable; 40 41 import java.lang.reflect.Field; 42 import java.util.ArrayList; 43 import java.util.LinkedHashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.concurrent.CountDownLatch; 47 import java.util.concurrent.TimeUnit; 48 49 public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 50 51 protected static final boolean DEBUG = false; 52 protected static final String TAG = "LinearLayoutManagerTest"; 53 createBaseVariations()54 protected static List<Config> createBaseVariations() { 55 List<Config> variations = new ArrayList<>(); 56 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 57 for (boolean reverseLayout : new boolean[]{false, true}) { 58 for (boolean stackFromBottom : new boolean[]{false, true}) { 59 for (boolean wrap : new boolean[]{false, true}) { 60 variations.add( 61 new Config(orientation, reverseLayout, stackFromBottom).wrap(wrap)); 62 } 63 64 } 65 } 66 } 67 return variations; 68 } 69 70 WrappedLinearLayoutManager mLayoutManager; 71 TestAdapter mTestAdapter; 72 addConfigVariation(List<Config> base, String fieldName, Object... variations)73 protected static List<Config> addConfigVariation(List<Config> base, String fieldName, 74 Object... variations) 75 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 76 List<Config> newConfigs = new ArrayList<Config>(); 77 Field field = Config.class.getDeclaredField(fieldName); 78 for (Config config : base) { 79 for (Object variation : variations) { 80 Config newConfig = (Config) config.clone(); 81 field.set(newConfig, variation); 82 newConfigs.add(newConfig); 83 } 84 } 85 return newConfigs; 86 } 87 setupByConfig(Config config, boolean waitForFirstLayout)88 void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable { 89 setupByConfig(config, waitForFirstLayout, null, null); 90 } 91 setupByConfig(Config config, boolean waitForFirstLayout, RecyclerView.@Nullable LayoutParams childLayoutParams, RecyclerView.@Nullable LayoutParams parentLayoutParams)92 void setupByConfig(Config config, boolean waitForFirstLayout, 93 RecyclerView.@Nullable LayoutParams childLayoutParams, 94 RecyclerView.@Nullable LayoutParams parentLayoutParams) throws Throwable { 95 mRecyclerView = inflateWrappedRV(); 96 97 mRecyclerView.setHasFixedSize(true); 98 mTestAdapter = config.mTestAdapter == null 99 ? new TestAdapter(config.mItemCount, childLayoutParams) 100 : config.mTestAdapter; 101 mRecyclerView.setAdapter(mTestAdapter); 102 mLayoutManager = config.mTestLayoutManager == null 103 ? new WrappedLinearLayoutManager( 104 getActivity(), config.mOrientation, config.mReverseLayout) 105 : config.mTestLayoutManager; 106 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 107 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 108 mRecyclerView.setLayoutManager(mLayoutManager); 109 if (config.mWrap) { 110 mRecyclerView.setLayoutParams( 111 new ViewGroup.LayoutParams( 112 config.mOrientation == HORIZONTAL ? WRAP_CONTENT : MATCH_PARENT, 113 config.mOrientation == VERTICAL ? WRAP_CONTENT : MATCH_PARENT 114 ) 115 ); 116 } 117 if (parentLayoutParams != null) { 118 mRecyclerView.setLayoutParams(parentLayoutParams); 119 } 120 121 if (waitForFirstLayout) { 122 waitForFirstLayout(); 123 } 124 } 125 scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)126 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 127 throws Throwable { 128 setupByConfig(new Config(VERTICAL, false, false), true); 129 130 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 131 @Override 132 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 133 if (state.isPreLayout()) { 134 assertEquals("pending scroll position should still be pending", 135 scrollPosition, mLayoutManager.mPendingScrollPosition); 136 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 137 assertEquals("pending scroll position offset should still be pending", 138 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 139 } 140 } else { 141 RecyclerView.ViewHolder vh = 142 mRecyclerView.findViewHolderForLayoutPosition(scrollPosition); 143 assertNotNull("scroll to position should work", vh); 144 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 145 assertEquals("scroll offset should be applied properly", 146 mLayoutManager.getPaddingTop() + scrollOffset + 147 ((RecyclerView.LayoutParams) vh.itemView 148 .getLayoutParams()).topMargin, 149 mLayoutManager.getDecoratedTop(vh.itemView)); 150 } 151 } 152 } 153 }; 154 mLayoutManager.expectLayouts(2); 155 mActivityRule.runOnUiThread(new Runnable() { 156 @Override 157 public void run() { 158 try { 159 mTestAdapter.addAndNotify(0, 1); 160 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 161 mLayoutManager.scrollToPosition(scrollPosition); 162 } else { 163 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 164 scrollOffset); 165 } 166 167 } catch (Throwable throwable) { 168 throwable.printStackTrace(); 169 } 170 171 } 172 }); 173 mLayoutManager.waitForLayout(2); 174 checkForMainThreadException(); 175 } 176 waitForFirstLayout()177 protected void waitForFirstLayout() throws Throwable { 178 mLayoutManager.expectLayouts(1); 179 setRecyclerView(mRecyclerView); 180 mLayoutManager.waitForLayout(2); 181 } 182 scrollToPositionWithOffset(final int position, final int offset)183 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 184 mActivityRule.runOnUiThread(new Runnable() { 185 @Override 186 public void run() { 187 mLayoutManager.scrollToPositionWithOffset(position, offset); 188 } 189 }); 190 } 191 assertRectSetsNotEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)192 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 193 Map<Item, Rect> after, boolean strictItemEquality) { 194 final LayoutEquality layoutEquality = new LayoutEquality( 195 message, before, after, strictItemEquality 196 ); 197 Inequality inequality = layoutEquality.findInequality(); 198 if (inequality == null) { 199 throw new AssertionError( 200 layoutEquality.buildErrorLog("two layout should be different") 201 ); 202 } 203 } 204 assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)205 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 206 assertRectSetsEqual(message, before, after, true); 207 } 208 assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)209 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, 210 boolean strictItemEquality) { 211 final LayoutEquality layoutEquality = new LayoutEquality( 212 message, before, after, strictItemEquality 213 ); 214 Inequality inequality = layoutEquality.findInequality(); 215 if (inequality != null) { 216 inequality.throwAsError(); 217 } 218 } 219 220 private static class LayoutEquality { 221 private final String mMessagePrefix; 222 private final Map<Item, Rect> mBefore; 223 private final Map<Item, Rect> mAfter; 224 private boolean mStrictEquality; 225 private @Nullable String mPositionsLog; 226 LayoutEquality(String messagePrefix, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictEquality)227 private LayoutEquality(String messagePrefix, 228 Map<Item, Rect> before, 229 Map<Item, Rect> after, boolean strictEquality) { 230 mMessagePrefix = messagePrefix; 231 mBefore = before; 232 mAfter = after; 233 mStrictEquality = strictEquality; 234 } 235 findInequality()236 public @Nullable Inequality findInequality() { 237 if (mBefore.size() != mAfter.size()) { 238 return new Inequality(this, 239 "item counts should be equal " + mBefore.size() + " vs " + mAfter.size()); 240 } 241 for (Map.Entry<Item, Rect> entry : mBefore.entrySet()) { 242 final Item beforeItem = entry.getKey(); 243 Rect afterRect = null; 244 if (mStrictEquality) { 245 afterRect = mAfter.get(beforeItem); 246 if (afterRect == null) { 247 return new Inequality(this, 248 "Same item should be visible after" 249 + " simple re-layout, rectangle for " + beforeItem + " is " 250 + "missing"); 251 } 252 } else { 253 for (Map.Entry<Item, Rect> afterEntry : mAfter.entrySet()) { 254 final Item afterItem = afterEntry.getKey(); 255 if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) { 256 afterRect = afterEntry.getValue(); 257 break; 258 } 259 } 260 if (afterRect == null) { 261 return new Inequality(this, 262 "Item with same adapter index should be visible after " 263 + "simple re-layout," + beforeItem + " is missing"); 264 } 265 } 266 if (!entry.getValue().equals(afterRect)) { 267 return new Inequality(this, 268 "Item should be laid out at the same coordinates. Before:" 269 + entry.getValue() + ", after:" + afterRect); 270 } 271 } 272 return null; 273 } getPositionsLog()274 private @NonNull String getPositionsLog() { 275 if (mPositionsLog == null) { 276 mPositionsLog = buildPositionLog(); 277 } 278 return mPositionsLog; 279 } 280 buildPositionLog()281 private String buildPositionLog() { 282 StringBuilder sb = new StringBuilder(); 283 sb.append("checking rectangle equality.\n"); 284 sb.append("before:\n"); 285 for (Map.Entry<Item, Rect> entry : mBefore.entrySet()) { 286 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 287 } 288 sb.append("after:\n"); 289 for (Map.Entry<Item, Rect> entry : mAfter.entrySet()) { 290 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 291 } 292 return sb.toString(); 293 } 294 buildErrorLog(String error)295 private String buildErrorLog(String error) { 296 return error + "\n" + mMessagePrefix + "\n" + getPositionsLog(); 297 } 298 } 299 300 private static class Inequality { 301 private final String mMessage; 302 private final LayoutEquality mLayoutEquality; 303 Inequality(LayoutEquality layoutEquality, String message)304 private Inequality(LayoutEquality layoutEquality, 305 String message) { 306 mMessage = message; 307 mLayoutEquality = layoutEquality; 308 } 309 throwAsError()310 void throwAsError() { 311 throw new AssertionError(mLayoutEquality.buildErrorLog(mMessage)); 312 } 313 } 314 315 static class VisibleChildren { 316 317 int firstVisiblePosition = RecyclerView.NO_POSITION; 318 319 int firstFullyVisiblePosition = RecyclerView.NO_POSITION; 320 321 int lastVisiblePosition = RecyclerView.NO_POSITION; 322 323 int lastFullyVisiblePosition = RecyclerView.NO_POSITION; 324 325 @Override toString()326 public String toString() { 327 return "VisibleChildren{" + 328 "firstVisiblePosition=" + firstVisiblePosition + 329 ", firstFullyVisiblePosition=" + firstFullyVisiblePosition + 330 ", lastVisiblePosition=" + lastVisiblePosition + 331 ", lastFullyVisiblePosition=" + lastFullyVisiblePosition + 332 '}'; 333 } 334 } 335 336 static class OnLayoutListener { 337 before(RecyclerView.Recycler recycler, RecyclerView.State state)338 void before(RecyclerView.Recycler recycler, RecyclerView.State state) { 339 } 340 after(RecyclerView.Recycler recycler, RecyclerView.State state)341 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 342 } 343 } 344 345 public static class Config implements Cloneable { 346 347 static final int DEFAULT_ITEM_COUNT = 250; 348 349 boolean mStackFromEnd; 350 351 int mOrientation = VERTICAL; 352 353 boolean mReverseLayout = false; 354 355 boolean mRecycleChildrenOnDetach = false; 356 357 int mItemCount = DEFAULT_ITEM_COUNT; 358 359 boolean mWrap = false; 360 361 TestAdapter mTestAdapter; 362 363 WrappedLinearLayoutManager mTestLayoutManager; 364 Config(int orientation, boolean reverseLayout, boolean stackFromEnd)365 Config(int orientation, boolean reverseLayout, boolean stackFromEnd) { 366 mOrientation = orientation; 367 mReverseLayout = reverseLayout; 368 mStackFromEnd = stackFromEnd; 369 } 370 Config()371 public Config() { 372 373 } 374 adapter(TestAdapter adapter)375 Config adapter(TestAdapter adapter) { 376 mTestAdapter = adapter; 377 return this; 378 } 379 recycleChildrenOnDetach(boolean recycleChildrenOnDetach)380 Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 381 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 382 return this; 383 } 384 orientation(int orientation)385 Config orientation(int orientation) { 386 mOrientation = orientation; 387 return this; 388 } 389 stackFromBottom(boolean stackFromBottom)390 Config stackFromBottom(boolean stackFromBottom) { 391 mStackFromEnd = stackFromBottom; 392 return this; 393 } 394 reverseLayout(boolean reverseLayout)395 Config reverseLayout(boolean reverseLayout) { 396 mReverseLayout = reverseLayout; 397 return this; 398 } 399 itemCount(int itemCount)400 public Config itemCount(int itemCount) { 401 mItemCount = itemCount; 402 return this; 403 } 404 405 // required by convention 406 @Override clone()407 public Object clone() throws CloneNotSupportedException { 408 return super.clone(); 409 } 410 411 @Override toString()412 public String toString() { 413 return "Config{" 414 + "mStackFromEnd=" + mStackFromEnd 415 + ",mOrientation=" + mOrientation 416 + ",mReverseLayout=" + mReverseLayout 417 + ",mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach 418 + ",mItemCount=" + mItemCount 419 + ",wrap=" + mWrap 420 + '}'; 421 } 422 wrap(boolean wrap)423 public Config wrap(boolean wrap) { 424 mWrap = wrap; 425 return this; 426 } 427 } 428 429 class WrappedLinearLayoutManager extends LinearLayoutManager { 430 431 CountDownLatch layoutLatch; 432 CountDownLatch snapLatch; 433 CountDownLatch prefetchLatch; 434 CountDownLatch callbackLatch; 435 436 OrientationHelper mSecondaryOrientation; 437 438 OnLayoutListener mOnLayoutListener; 439 440 RecyclerView.OnScrollListener mSnapListener = new RecyclerView.OnScrollListener() { 441 @Override 442 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 443 super.onScrollStateChanged(recyclerView, newState); 444 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 445 snapLatch.countDown(); 446 if (snapLatch.getCount() == 0L) { 447 removeOnScrollListener(this); 448 } 449 } 450 } 451 }; 452 453 RecyclerView.OnScrollListener mCallbackListener = new RecyclerView.OnScrollListener() { 454 @Override 455 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 456 super.onScrollStateChanged(recyclerView, newState); 457 callbackLatch.countDown(); 458 if (callbackLatch.getCount() == 0L) { 459 removeOnScrollListener(this); 460 } 461 } 462 }; 463 WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout)464 public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 465 super(context, orientation, reverseLayout); 466 } 467 expectLayouts(int count)468 public void expectLayouts(int count) { 469 layoutLatch = new CountDownLatch(count); 470 } 471 expectCallbacks(int count)472 public void expectCallbacks(int count) throws Throwable { 473 callbackLatch = new CountDownLatch(count); 474 mRecyclerView.addOnScrollListener(mCallbackListener); 475 } 476 removeOnScrollListener(RecyclerView.OnScrollListener listener)477 private void removeOnScrollListener(RecyclerView.OnScrollListener listener) { 478 mRecyclerView.removeOnScrollListener(listener); 479 } 480 waitForLayout(int seconds)481 public void waitForLayout(int seconds) throws Throwable { 482 layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 483 checkForMainThreadException(); 484 MatcherAssert.assertThat("all layouts should complete on time", 485 layoutLatch.getCount(), CoreMatchers.is(0L)); 486 // use a runnable to ensure RV layout is finished 487 getInstrumentation().runOnMainSync(new Runnable() { 488 @Override 489 public void run() { 490 } 491 }); 492 } 493 assertNoCallbacks(String msg, long timeout)494 public void assertNoCallbacks(String msg, long timeout) throws Throwable { 495 callbackLatch.await(timeout, TimeUnit.SECONDS); 496 long latchCount = callbackLatch.getCount(); 497 assertFalse(msg + " :" + latchCount, latchCount == 0); 498 removeOnScrollListener(mCallbackListener); 499 } 500 expectPrefetch(int count)501 public void expectPrefetch(int count) { 502 prefetchLatch = new CountDownLatch(count); 503 } 504 waitForPrefetch(int seconds)505 public void waitForPrefetch(int seconds) throws Throwable { 506 prefetchLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 507 checkForMainThreadException(); 508 MatcherAssert.assertThat("all prefetches should complete on time", 509 prefetchLatch.getCount(), CoreMatchers.is(0L)); 510 // use a runnable to ensure RV layout is finished 511 getInstrumentation().runOnMainSync(new Runnable() { 512 @Override 513 public void run() { 514 } 515 }); 516 } 517 expectIdleState(int count)518 public void expectIdleState(int count) { 519 // Remove listener if it was still there 520 mRecyclerView.removeOnScrollListener(mSnapListener); 521 snapLatch = new CountDownLatch(count); 522 mRecyclerView.addOnScrollListener(mSnapListener); 523 } 524 waitForSnap(int seconds)525 public void waitForSnap(int seconds) throws Throwable { 526 snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS); 527 checkForMainThreadException(); 528 MatcherAssert.assertThat("all scrolling should complete on time", 529 snapLatch.getCount(), CoreMatchers.is(0L)); 530 // use a runnable to ensure RV layout is finished 531 getInstrumentation().runOnMainSync(new Runnable() { 532 @Override 533 public void run() {} 534 }); 535 } 536 537 @Override setOrientation(int orientation)538 public void setOrientation(int orientation) { 539 super.setOrientation(orientation); 540 mSecondaryOrientation = null; 541 } 542 543 @Override removeAndRecycleView(View child, RecyclerView.Recycler recycler)544 public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) { 545 if (DEBUG) { 546 Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child)); 547 } 548 super.removeAndRecycleView(child, recycler); 549 } 550 551 @Override removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler)552 public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) { 553 if (DEBUG) { 554 Log.d(TAG, 555 "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index))); 556 } 557 super.removeAndRecycleViewAt(index, recycler); 558 } 559 560 @Override ensureLayoutState()561 void ensureLayoutState() { 562 super.ensureLayoutState(); 563 if (mSecondaryOrientation == null) { 564 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this, 565 1 - getOrientation()); 566 } 567 } 568 569 @Override createLayoutState()570 LayoutState createLayoutState() { 571 return new LayoutState() { 572 @Override 573 View next(RecyclerView.Recycler recycler) { 574 final boolean hadMore = hasMore(mRecyclerView.mState); 575 final int position = mCurrentPosition; 576 View next = super.next(recycler); 577 assertEquals("if has more, should return a view", hadMore, next != null); 578 assertEquals("position of the returned view must match current position", 579 position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition()); 580 return next; 581 } 582 }; 583 } 584 getBoundsLog()585 public String getBoundsLog() { 586 StringBuilder sb = new StringBuilder(); 587 sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding()) 588 .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding()); 589 sb.append("\nchildren bounds\n"); 590 final int childCount = getChildCount(); 591 for (int i = 0; i < childCount; i++) { 592 View child = getChildAt(i); 593 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 594 .append("[").append("start:").append( 595 mOrientationHelper.getDecoratedStart(child)).append(", end:") 596 .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n"); 597 } 598 return sb.toString(); 599 } 600 waitForAnimationsToEnd(int timeoutInSeconds)601 public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException { 602 RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); 603 if (itemAnimator == null) { 604 return; 605 } 606 final CountDownLatch latch = new CountDownLatch(1); 607 final boolean running = itemAnimator.isRunning( 608 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 609 @Override 610 public void onAnimationsFinished() { 611 latch.countDown(); 612 } 613 } 614 ); 615 if (running) { 616 latch.await(timeoutInSeconds, TimeUnit.SECONDS); 617 } 618 } 619 traverseAndFindVisibleChildren()620 public VisibleChildren traverseAndFindVisibleChildren() { 621 int childCount = getChildCount(); 622 final VisibleChildren visibleChildren = new VisibleChildren(); 623 final int start = mOrientationHelper.getStartAfterPadding(); 624 final int end = mOrientationHelper.getEndAfterPadding(); 625 for (int i = 0; i < childCount; i++) { 626 View child = getChildAt(i); 627 final int childStart = mOrientationHelper.getDecoratedStart(child); 628 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 629 final boolean fullyVisible = childStart >= start && childEnd <= end; 630 final boolean hidden = childEnd <= start || childStart >= end; 631 if (hidden) { 632 continue; 633 } 634 final int position = getPosition(child); 635 if (fullyVisible) { 636 if (position < visibleChildren.firstFullyVisiblePosition || 637 visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) { 638 visibleChildren.firstFullyVisiblePosition = position; 639 } 640 641 if (position > visibleChildren.lastFullyVisiblePosition) { 642 visibleChildren.lastFullyVisiblePosition = position; 643 } 644 } 645 646 if (position < visibleChildren.firstVisiblePosition || 647 visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) { 648 visibleChildren.firstVisiblePosition = position; 649 } 650 651 if (position > visibleChildren.lastVisiblePosition) { 652 visibleChildren.lastVisiblePosition = position; 653 } 654 655 } 656 return visibleChildren; 657 } 658 getViewBounds(View view)659 Rect getViewBounds(View view) { 660 if (getOrientation() == HORIZONTAL) { 661 return new Rect( 662 mOrientationHelper.getDecoratedStart(view), 663 mSecondaryOrientation.getDecoratedStart(view), 664 mOrientationHelper.getDecoratedEnd(view), 665 mSecondaryOrientation.getDecoratedEnd(view)); 666 } else { 667 return new Rect( 668 mSecondaryOrientation.getDecoratedStart(view), 669 mOrientationHelper.getDecoratedStart(view), 670 mSecondaryOrientation.getDecoratedEnd(view), 671 mOrientationHelper.getDecoratedEnd(view)); 672 } 673 674 } 675 collectChildCoordinates()676 Map<Item, Rect> collectChildCoordinates() throws Throwable { 677 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 678 mActivityRule.runOnUiThread(new Runnable() { 679 @Override 680 public void run() { 681 final int childCount = getChildCount(); 682 Rect layoutBounds = new Rect(0, 0, 683 mLayoutManager.getWidth(), mLayoutManager.getHeight()); 684 for (int i = 0; i < childCount; i++) { 685 View child = getChildAt(i); 686 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child 687 .getLayoutParams(); 688 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 689 Rect childBounds = getViewBounds(child); 690 if (new Rect(childBounds).intersect(layoutBounds)) { 691 items.put(vh.mBoundItem, childBounds); 692 } 693 } 694 } 695 }); 696 return items; 697 } 698 699 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)700 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 701 try { 702 if (mOnLayoutListener != null) { 703 mOnLayoutListener.before(recycler, state); 704 } 705 super.onLayoutChildren(recycler, state); 706 if (mOnLayoutListener != null) { 707 mOnLayoutListener.after(recycler, state); 708 } 709 } catch (Throwable t) { 710 postExceptionToInstrumentation(t); 711 } 712 layoutLatch.countDown(); 713 } 714 715 @Override collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, LayoutPrefetchRegistry layoutPrefetchRegistry)716 public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, 717 LayoutPrefetchRegistry layoutPrefetchRegistry) { 718 if (prefetchLatch != null) prefetchLatch.countDown(); 719 super.collectAdjacentPrefetchPositions(dx, dy, state, layoutPrefetchRegistry); 720 } 721 } 722 } 723