1 /* 2 * Copyright (C) 2014 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 android.support.v7.widget; 18 19 import android.content.Context; 20 import android.graphics.Rect; 21 import android.os.Parcel; 22 import android.os.Parcelable; 23 import android.support.v4.view.AccessibilityDelegateCompat; 24 import android.support.v4.view.accessibility.AccessibilityEventCompat; 25 import android.support.v4.view.accessibility.AccessibilityRecordCompat; 26 import android.util.Log; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.widget.FrameLayout; 31 32 import static android.support.v7.widget.LayoutState.LAYOUT_END; 33 import static android.support.v7.widget.LayoutState.LAYOUT_START; 34 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 35 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 36 import java.lang.reflect.Field; 37 import java.util.ArrayList; 38 import java.util.LinkedHashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.UUID; 42 import java.util.concurrent.CountDownLatch; 43 import java.util.concurrent.TimeUnit; 44 import java.util.concurrent.atomic.AtomicInteger; 45 46 /** 47 * Includes tests for {@link LinearLayoutManager}. 48 * <p> 49 * Since most UI tests are not practical, these tests are focused on internal data representation 50 * and stability of LinearLayoutManager in response to different events (state change, scrolling 51 * etc) where it is very hard to do manual testing. 52 */ 53 public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest { 54 55 private static final boolean DEBUG = false; 56 57 private static final String TAG = "LinearLayoutManagerTest"; 58 59 WrappedLinearLayoutManager mLayoutManager; 60 61 TestAdapter mTestAdapter; 62 63 final List<Config> mBaseVariations = new ArrayList<Config>(); 64 65 @Override setUp()66 protected void setUp() throws Exception { 67 super.setUp(); 68 for (int orientation : new int[]{VERTICAL, HORIZONTAL}) { 69 for (boolean reverseLayout : new boolean[]{false, true}) { 70 for (boolean stackFromBottom : new boolean[]{false, true}) { 71 mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom)); 72 } 73 } 74 } 75 } 76 addConfigVariation(List<Config> base, String fieldName, Object... variations)77 protected List<Config> addConfigVariation(List<Config> base, String fieldName, 78 Object... variations) 79 throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { 80 List<Config> newConfigs = new ArrayList<Config>(); 81 Field field = Config.class.getDeclaredField(fieldName); 82 for (Config config : base) { 83 for (Object variation : variations) { 84 Config newConfig = (Config) config.clone(); 85 field.set(newConfig, variation); 86 newConfigs.add(newConfig); 87 } 88 } 89 return newConfigs; 90 } 91 setupByConfig(Config config, boolean waitForFirstLayout)92 void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable { 93 mRecyclerView = inflateWrappedRV(); 94 95 mRecyclerView.setHasFixedSize(true); 96 mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount) 97 : config.mTestAdapter; 98 mRecyclerView.setAdapter(mTestAdapter); 99 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation, 100 config.mReverseLayout); 101 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 102 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 103 mRecyclerView.setLayoutManager(mLayoutManager); 104 if (waitForFirstLayout) { 105 waitForFirstLayout(); 106 } 107 } 108 testRemoveAnchorItem()109 public void testRemoveAnchorItem() throws Throwable { 110 removeAnchorItemTest( 111 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout( 112 false), 100, 0); 113 } 114 testRemoveAnchorItemReverse()115 public void testRemoveAnchorItemReverse() throws Throwable { 116 removeAnchorItemTest( 117 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100, 118 0); 119 } 120 testRemoveAnchorItemStackFromEnd()121 public void testRemoveAnchorItemStackFromEnd() throws Throwable { 122 removeAnchorItemTest( 123 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100, 124 99); 125 } 126 testRemoveAnchorItemStackFromEndAndReverse()127 public void testRemoveAnchorItemStackFromEndAndReverse() throws Throwable { 128 removeAnchorItemTest( 129 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100, 130 99); 131 } 132 testRemoveAnchorItemHorizontal()133 public void testRemoveAnchorItemHorizontal() throws Throwable { 134 removeAnchorItemTest( 135 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout( 136 false), 100, 0); 137 } 138 testRemoveAnchorItemReverseHorizontal()139 public void testRemoveAnchorItemReverseHorizontal() throws Throwable { 140 removeAnchorItemTest( 141 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true), 142 100, 0); 143 } 144 testRemoveAnchorItemStackFromEndHorizontal()145 public void testRemoveAnchorItemStackFromEndHorizontal() throws Throwable { 146 removeAnchorItemTest( 147 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false), 148 100, 99); 149 } 150 testRemoveAnchorItemStackFromEndAndReverseHorizontal()151 public void testRemoveAnchorItemStackFromEndAndReverseHorizontal() throws Throwable { 152 removeAnchorItemTest( 153 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100, 154 99); 155 } 156 157 /** 158 * This tests a regression where predictive animations were not working as expected when the 159 * first item is removed and there aren't any more items to add from that direction. 160 * First item refers to the default anchor item. 161 */ removeAnchorItemTest(final Config config, int adapterSize, final int removePos)162 public void removeAnchorItemTest(final Config config, int adapterSize, 163 final int removePos) throws Throwable { 164 config.adapter(new TestAdapter(adapterSize) { 165 @Override 166 public void onBindViewHolder(TestViewHolder holder, 167 int position) { 168 super.onBindViewHolder(holder, position); 169 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 170 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 171 lp = new ViewGroup.MarginLayoutParams(0, 0); 172 holder.itemView.setLayoutParams(lp); 173 } 174 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 175 final int maxSize; 176 if (config.mOrientation == HORIZONTAL) { 177 maxSize = mRecyclerView.getWidth(); 178 mlp.height = ViewGroup.MarginLayoutParams.FILL_PARENT; 179 } else { 180 maxSize = mRecyclerView.getHeight(); 181 mlp.width = ViewGroup.MarginLayoutParams.FILL_PARENT; 182 } 183 184 final int desiredSize; 185 if (position == removePos) { 186 // make it large 187 desiredSize = maxSize / 4; 188 } else { 189 // make it small 190 desiredSize = maxSize / 8; 191 } 192 if (config.mOrientation == HORIZONTAL) { 193 mlp.width = desiredSize; 194 } else { 195 mlp.height = desiredSize; 196 } 197 } 198 }); 199 setupByConfig(config, true); 200 final int childCount = mLayoutManager.getChildCount(); 201 RecyclerView.ViewHolder toBeRemoved = null; 202 List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>(); 203 for (int i = 0; i < childCount; i++) { 204 View child = mLayoutManager.getChildAt(i); 205 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 206 if (holder.getAdapterPosition() == removePos) { 207 toBeRemoved = holder; 208 } else { 209 toBeMoved.add(holder); 210 } 211 } 212 assertNotNull("test sanity", toBeRemoved); 213 assertEquals("test sanity", childCount - 1, toBeMoved.size()); 214 LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator(); 215 mRecyclerView.setItemAnimator(loggingItemAnimator); 216 loggingItemAnimator.reset(); 217 loggingItemAnimator.expectRunPendingAnimationsCall(1); 218 mLayoutManager.expectLayouts(2); 219 mTestAdapter.deleteAndNotify(removePos, 1); 220 mLayoutManager.waitForLayout(1); 221 loggingItemAnimator.waitForPendingAnimationsCall(2); 222 assertTrue("removed child should receive remove animation", 223 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved)); 224 for (RecyclerView.ViewHolder vh : toBeMoved) { 225 assertTrue("view holder should be in moved list", 226 loggingItemAnimator.mMoveVHs.contains(vh)); 227 } 228 List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>(); 229 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 230 View child = mLayoutManager.getChildAt(i); 231 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 232 if (toBeRemoved != holder && !toBeMoved.contains(holder)) { 233 newHolders.add(holder); 234 } 235 } 236 assertTrue("some new children should show up for the new space", newHolders.size() > 0); 237 assertEquals("no items should receive animate add since they are not new", 0, 238 loggingItemAnimator.mAddVHs.size()); 239 for (RecyclerView.ViewHolder holder : newHolders) { 240 assertTrue("new holder should receive a move animation", 241 loggingItemAnimator.mMoveVHs.contains(holder)); 242 } 243 assertTrue("control against adding too many children due to bad layout state preparation." 244 + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(), 245 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/); 246 } 247 testKeepFocusOnRelayout()248 public void testKeepFocusOnRelayout() throws Throwable { 249 setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true); 250 int center = (mLayoutManager.findLastVisibleItemPosition() 251 - mLayoutManager.findFirstVisibleItemPosition()) / 2; 252 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center); 253 final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView); 254 runTestOnUiThread(new Runnable() { 255 @Override 256 public void run() { 257 vh.itemView.requestFocus(); 258 } 259 }); 260 assertTrue("view should have the focus", vh.itemView.hasFocus()); 261 // add a bunch of items right before that view, make sure it keeps its position 262 mLayoutManager.expectLayouts(2); 263 final int childCountToAdd = mRecyclerView.getChildCount() * 2; 264 mTestAdapter.addAndNotify(center, childCountToAdd); 265 center += childCountToAdd; // offset item 266 mLayoutManager.waitForLayout(2); 267 mLayoutManager.waitForAnimationsToEnd(20); 268 final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center); 269 assertNotNull("focused child should stay in layout", postVH); 270 assertSame("same view holder should be kept for unchanged child", vh, postVH); 271 assertEquals("focused child's screen position should stay unchanged", top, 272 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView)); 273 } 274 testKeepFullFocusOnResize()275 public void testKeepFullFocusOnResize() throws Throwable { 276 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true); 277 } 278 testKeepPartialFocusOnResize()279 public void testKeepPartialFocusOnResize() throws Throwable { 280 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false); 281 } 282 testKeepReverseFullFocusOnResize()283 public void testKeepReverseFullFocusOnResize() throws Throwable { 284 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true); 285 } 286 testKeepReversePartialFocusOnResize()287 public void testKeepReversePartialFocusOnResize() throws Throwable { 288 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false); 289 } 290 testKeepStackFromEndFullFocusOnResize()291 public void testKeepStackFromEndFullFocusOnResize() throws Throwable { 292 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true); 293 } 294 testKeepStackFromEndPartialFocusOnResize()295 public void testKeepStackFromEndPartialFocusOnResize() throws Throwable { 296 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false); 297 } 298 keepFocusOnResizeTest(final Config config, boolean fullyVisible)299 public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable { 300 setupByConfig(config, true); 301 final int targetPosition; 302 if (config.mStackFromEnd) { 303 targetPosition = mLayoutManager.findFirstVisibleItemPosition(); 304 } else { 305 targetPosition = mLayoutManager.findLastVisibleItemPosition(); 306 } 307 final OrientationHelper helper = mLayoutManager.mOrientationHelper; 308 final RecyclerView.ViewHolder vh = mRecyclerView 309 .findViewHolderForLayoutPosition(targetPosition); 310 311 // scroll enough to offset the child 312 int startMargin = helper.getDecoratedStart(vh.itemView) - 313 helper.getStartAfterPadding(); 314 int endMargin = helper.getEndAfterPadding() - 315 helper.getDecoratedEnd(vh.itemView); 316 Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin); 317 requestFocus(vh.itemView); 318 runTestOnUiThread(new Runnable() { 319 @Override 320 public void run() { 321 assertTrue("view should gain the focus", vh.itemView.hasFocus()); 322 } 323 }); 324 do { 325 Thread.sleep(100); 326 } while (mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE); 327 // scroll enough to offset the child 328 startMargin = helper.getDecoratedStart(vh.itemView) - 329 helper.getStartAfterPadding(); 330 endMargin = helper.getEndAfterPadding() - 331 helper.getDecoratedEnd(vh.itemView); 332 333 Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin); 334 assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0); 335 336 int expectedOffset = 0; 337 boolean offsetAtStart = false; 338 if (!fullyVisible) { 339 // move it a bit such that it is no more fully visible 340 final int childSize = helper 341 .getDecoratedMeasurement(vh.itemView); 342 expectedOffset = childSize / 3; 343 if (startMargin < endMargin) { 344 scrollBy(expectedOffset); 345 offsetAtStart = true; 346 } else { 347 scrollBy(-expectedOffset); 348 offsetAtStart = false; 349 } 350 startMargin = helper.getDecoratedStart(vh.itemView) - 351 helper.getStartAfterPadding(); 352 endMargin = helper.getEndAfterPadding() - 353 helper.getDecoratedEnd(vh.itemView); 354 assertTrue("test sanity, view should not be fully visible", startMargin < 0 355 || endMargin < 0); 356 } 357 358 mLayoutManager.expectLayouts(1); 359 runTestOnUiThread(new Runnable() { 360 @Override 361 public void run() { 362 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams(); 363 if (config.mOrientation == HORIZONTAL) { 364 layoutParams.width = mRecyclerView.getWidth() / 2; 365 } else { 366 layoutParams.height = mRecyclerView.getHeight() / 2; 367 } 368 mRecyclerView.setLayoutParams(layoutParams); 369 } 370 }); 371 Thread.sleep(100); 372 // add a bunch of items right before that view, make sure it keeps its position 373 mLayoutManager.waitForLayout(2); 374 mLayoutManager.waitForAnimationsToEnd(20); 375 assertTrue("view should preserve the focus", vh.itemView.hasFocus()); 376 final RecyclerView.ViewHolder postVH = mRecyclerView 377 .findViewHolderForLayoutPosition(targetPosition); 378 assertNotNull("focused child should stay in layout", postVH); 379 assertSame("same view holder should be kept for unchanged child", vh, postVH); 380 View focused = postVH.itemView; 381 382 startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding(); 383 endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused); 384 385 assertTrue("focused child should be somewhat visible", 386 helper.getDecoratedStart(focused) < helper.getEndAfterPadding() 387 && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding()); 388 if (fullyVisible) { 389 assertTrue("focused child end should stay fully visible", 390 endMargin >= 0); 391 assertTrue("focused child start should stay fully visible", 392 startMargin >= 0); 393 } else { 394 if (offsetAtStart) { 395 assertTrue("start should preserve its offset", startMargin < 0); 396 assertTrue("end should be visible", endMargin >= 0); 397 } else { 398 assertTrue("end should preserve its offset", endMargin < 0); 399 assertTrue("start should be visible", startMargin >= 0); 400 } 401 } 402 } 403 testResize()404 public void testResize() throws Throwable { 405 for(Config config : addConfigVariation(mBaseVariations, "mItemCount", 5 406 , Config.DEFAULT_ITEM_COUNT)) { 407 stackFromEndTest(config); 408 removeRecyclerView(); 409 } 410 } 411 testScrollToPositionWithOffset()412 public void testScrollToPositionWithOffset() throws Throwable { 413 for (Config config : mBaseVariations) { 414 scrollToPositionWithOffsetTest(config.itemCount(300)); 415 removeRecyclerView(); 416 } 417 } 418 scrollToPositionWithOffsetTest(Config config)419 public void scrollToPositionWithOffsetTest(Config config) throws Throwable { 420 setupByConfig(config, true); 421 OrientationHelper orientationHelper = OrientationHelper 422 .createOrientationHelper(mLayoutManager, config.mOrientation); 423 Rect layoutBounds = getDecoratedRecyclerViewBounds(); 424 // try scrolling towards head, should not affect anything 425 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 426 if (config.mStackFromEnd) { 427 scrollToPositionWithOffset(mTestAdapter.getItemCount() - 1, 428 mLayoutManager.mOrientationHelper.getEnd() - 500); 429 } else { 430 scrollToPositionWithOffset(0, 20); 431 } 432 assertRectSetsEqual(config + " trying to over scroll with offset should be no-op", 433 before, mLayoutManager.collectChildCoordinates()); 434 // try offsetting some visible children 435 int testCount = 10; 436 while (testCount-- > 0) { 437 // get middle child 438 final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2); 439 final int position = mRecyclerView.getChildLayoutPosition(child); 440 final int startOffset = config.mReverseLayout ? 441 orientationHelper.getEndAfterPadding() - orientationHelper 442 .getDecoratedEnd(child) 443 : orientationHelper.getDecoratedStart(child) - orientationHelper 444 .getStartAfterPadding(); 445 final int scrollOffset = config.mStackFromEnd ? startOffset + startOffset / 2 446 : startOffset / 2; 447 mLayoutManager.expectLayouts(1); 448 scrollToPositionWithOffset(position, scrollOffset); 449 mLayoutManager.waitForLayout(2); 450 final int finalOffset = config.mReverseLayout ? 451 orientationHelper.getEndAfterPadding() - orientationHelper 452 .getDecoratedEnd(child) 453 : orientationHelper.getDecoratedStart(child) - orientationHelper 454 .getStartAfterPadding(); 455 assertEquals(config + " scroll with offset on a visible child should work fine " + 456 " offset:" + finalOffset + " , existing offset:" + startOffset + ", " 457 + "child " + position, 458 scrollOffset, finalOffset); 459 } 460 461 // try scrolling to invisible children 462 testCount = 10; 463 // we test above and below, one by one 464 int offsetMultiplier = -1; 465 while (testCount-- > 0) { 466 final TargetTuple target = findInvisibleTarget(config); 467 final String logPrefix = config + " " + target; 468 mLayoutManager.expectLayouts(1); 469 final int offset = offsetMultiplier 470 * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3; 471 scrollToPositionWithOffset(target.mPosition, offset); 472 mLayoutManager.waitForLayout(2); 473 final View child = mLayoutManager.findViewByPosition(target.mPosition); 474 assertNotNull(logPrefix + " scrolling to a mPosition with offset " + offset 475 + " should layout it", child); 476 final Rect bounds = mLayoutManager.getViewBounds(child); 477 if (DEBUG) { 478 Log.d(TAG, logPrefix + " post scroll to invisible mPosition " + bounds + " in " 479 + layoutBounds + " with offset " + offset); 480 } 481 482 if (config.mReverseLayout) { 483 assertEquals(logPrefix + " when scrolling with offset to an invisible in reverse " 484 + "layout, its end should align with recycler view's end - offset", 485 orientationHelper.getEndAfterPadding() - offset, 486 orientationHelper.getDecoratedEnd(child) 487 ); 488 } else { 489 assertEquals(logPrefix + " when scrolling with offset to an invisible child in normal" 490 + " layout its start should align with recycler view's start + " 491 + "offset", 492 orientationHelper.getStartAfterPadding() + offset, 493 orientationHelper.getDecoratedStart(child) 494 ); 495 } 496 offsetMultiplier *= -1; 497 } 498 } 499 findInvisibleTarget(Config config)500 private TargetTuple findInvisibleTarget(Config config) { 501 int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE; 502 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 503 View child = mLayoutManager.getChildAt(i); 504 int position = mRecyclerView.getChildLayoutPosition(child); 505 if (position < minPosition) { 506 minPosition = position; 507 } 508 if (position > maxPosition) { 509 maxPosition = position; 510 } 511 } 512 final int tailTarget = maxPosition + 513 (mRecyclerView.getAdapter().getItemCount() - maxPosition) / 2; 514 final int headTarget = minPosition / 2; 515 final int target; 516 // where will the child come from ? 517 final int itemLayoutDirection; 518 if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) { 519 target = tailTarget; 520 itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END; 521 } else { 522 target = headTarget; 523 itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START; 524 } 525 if (DEBUG) { 526 Log.d(TAG, 527 config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition); 528 } 529 return new TargetTuple(target, itemLayoutDirection); 530 } 531 stackFromEndTest(final Config config)532 public void stackFromEndTest(final Config config) throws Throwable { 533 final FrameLayout container = getRecyclerViewContainer(); 534 runTestOnUiThread(new Runnable() { 535 @Override 536 public void run() { 537 container.setPadding(0, 0, 0, 0); 538 } 539 }); 540 541 setupByConfig(config, true); 542 int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); 543 int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition(); 544 int lastCompletelyVisibleItemPosition = mLayoutManager.findLastCompletelyVisibleItemPosition(); 545 int firstCompletelyVisibleItemPosition = mLayoutManager.findFirstCompletelyVisibleItemPosition(); 546 mLayoutManager.expectLayouts(1); 547 // resize the recycler view to half 548 runTestOnUiThread(new Runnable() { 549 @Override 550 public void run() { 551 if (config.mOrientation == HORIZONTAL) { 552 container.setPadding(0, 0, container.getWidth() / 2, 0); 553 } else { 554 container.setPadding(0, 0, 0, container.getWidth() / 2); 555 } 556 } 557 }); 558 mLayoutManager.waitForLayout(1); 559 if (config.mStackFromEnd) { 560 assertEquals("[" + config + "]: last visible position should not change.", 561 lastVisibleItemPosition, mLayoutManager.findLastVisibleItemPosition()); 562 assertEquals("[" + config + "]: last completely visible position should not change", 563 lastCompletelyVisibleItemPosition, 564 mLayoutManager.findLastCompletelyVisibleItemPosition()); 565 } else { 566 assertEquals("[" + config + "]: first visible position should not change.", 567 firstVisibleItemPosition, mLayoutManager.findFirstVisibleItemPosition()); 568 assertEquals("[" + config + "]: last completely visible position should not change", 569 firstCompletelyVisibleItemPosition, 570 mLayoutManager.findFirstCompletelyVisibleItemPosition()); 571 } 572 } 573 testScrollToPositionWithPredictive()574 public void testScrollToPositionWithPredictive() throws Throwable { 575 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 576 removeRecyclerView(); 577 scrollToPositionWithPredictive(3, 20); 578 removeRecyclerView(); 579 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 580 LinearLayoutManager.INVALID_OFFSET); 581 removeRecyclerView(); 582 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 583 } 584 scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)585 public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset) 586 throws Throwable { 587 setupByConfig(new Config(VERTICAL, false, false), true); 588 589 mLayoutManager.mOnLayoutListener = new OnLayoutListener() { 590 @Override 591 void after(RecyclerView.Recycler recycler, RecyclerView.State state) { 592 if (state.isPreLayout()) { 593 assertEquals("pending scroll position should still be pending", 594 scrollPosition, mLayoutManager.mPendingScrollPosition); 595 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 596 assertEquals("pending scroll position offset should still be pending", 597 scrollOffset, mLayoutManager.mPendingScrollPositionOffset); 598 } 599 } else { 600 RecyclerView.ViewHolder vh = 601 mRecyclerView.findViewHolderForLayoutPosition(scrollPosition); 602 assertNotNull("scroll to position should work", vh); 603 if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) { 604 assertEquals("scroll offset should be applied properly", 605 mLayoutManager.getPaddingTop() + scrollOffset + 606 ((RecyclerView.LayoutParams) vh.itemView 607 .getLayoutParams()).topMargin, 608 mLayoutManager.getDecoratedTop(vh.itemView)); 609 } 610 } 611 } 612 }; 613 mLayoutManager.expectLayouts(2); 614 runTestOnUiThread(new Runnable() { 615 @Override 616 public void run() { 617 try { 618 mTestAdapter.addAndNotify(0, 1); 619 if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) { 620 mLayoutManager.scrollToPosition(scrollPosition); 621 } else { 622 mLayoutManager.scrollToPositionWithOffset(scrollPosition, 623 scrollOffset); 624 } 625 626 } catch (Throwable throwable) { 627 throwable.printStackTrace(); 628 } 629 630 } 631 }); 632 mLayoutManager.waitForLayout(2); 633 checkForMainThreadException(); 634 } 635 waitForFirstLayout()636 private void waitForFirstLayout() throws Throwable { 637 mLayoutManager.expectLayouts(1); 638 setRecyclerView(mRecyclerView); 639 mLayoutManager.waitForLayout(2); 640 } 641 testRecycleDuringAnimations()642 public void testRecycleDuringAnimations() throws Throwable { 643 final AtomicInteger childCount = new AtomicInteger(0); 644 final TestAdapter adapter = new TestAdapter(300) { 645 @Override 646 public TestViewHolder onCreateViewHolder(ViewGroup parent, 647 int viewType) { 648 final int cnt = childCount.incrementAndGet(); 649 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 650 if (DEBUG) { 651 Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder); 652 } 653 return testViewHolder; 654 } 655 }; 656 setupByConfig(new Config(VERTICAL, false, false).itemCount(300) 657 .adapter(adapter), true); 658 659 final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 660 @Override 661 public void putRecycledView(RecyclerView.ViewHolder scrap) { 662 super.putRecycledView(scrap); 663 int cnt = childCount.decrementAndGet(); 664 if (DEBUG) { 665 Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap); 666 } 667 } 668 669 @Override 670 public RecyclerView.ViewHolder getRecycledView(int viewType) { 671 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType); 672 if (recycledView != null) { 673 final int cnt = childCount.incrementAndGet(); 674 if (DEBUG) { 675 Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView); 676 } 677 } 678 return recycledView; 679 } 680 }; 681 pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500); 682 mRecyclerView.setRecycledViewPool(pool); 683 684 685 // now keep adding children to trigger more children being created etc. 686 for (int i = 0; i < 100; i ++) { 687 adapter.addAndNotify(15, 1); 688 Thread.sleep(15); 689 } 690 getInstrumentation().waitForIdleSync(); 691 waitForAnimations(2); 692 assertEquals("Children count should add up", childCount.get(), 693 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 694 695 // now trigger lots of add again, followed by a scroll to position 696 for (int i = 0; i < 100; i ++) { 697 adapter.addAndNotify(5 + (i % 3) * 3, 1); 698 Thread.sleep(25); 699 } 700 smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20); 701 waitForAnimations(2); 702 getInstrumentation().waitForIdleSync(); 703 assertEquals("Children count should add up", childCount.get(), 704 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 705 } 706 707 testGetFirstLastChildrenTest()708 public void testGetFirstLastChildrenTest() throws Throwable { 709 for (Config config : mBaseVariations) { 710 getFirstLastChildrenTest(config); 711 } 712 } 713 testDontRecycleChildrenOnDetach()714 public void testDontRecycleChildrenOnDetach() throws Throwable { 715 setupByConfig(new Config().recycleChildrenOnDetach(false), true); 716 runTestOnUiThread(new Runnable() { 717 @Override 718 public void run() { 719 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 720 mRecyclerView.setLayoutManager(new TestLayoutManager()); 721 assertEquals("No views are recycled", recyclerSize, 722 mRecyclerView.mRecycler.getRecycledViewPool().size()); 723 } 724 }); 725 } 726 testRecycleChildrenOnDetach()727 public void testRecycleChildrenOnDetach() throws Throwable { 728 setupByConfig(new Config().recycleChildrenOnDetach(true), true); 729 final int childCount = mLayoutManager.getChildCount(); 730 runTestOnUiThread(new Runnable() { 731 @Override 732 public void run() { 733 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 734 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews( 735 mTestAdapter.getItemViewType(0), recyclerSize + childCount); 736 mRecyclerView.setLayoutManager(new TestLayoutManager()); 737 assertEquals("All children should be recycled", childCount + recyclerSize, 738 mRecyclerView.mRecycler.getRecycledViewPool().size()); 739 } 740 }); 741 } 742 getFirstLastChildrenTest(final Config config)743 public void getFirstLastChildrenTest(final Config config) throws Throwable { 744 setupByConfig(config, true); 745 Runnable viewInBoundsTest = new Runnable() { 746 @Override 747 public void run() { 748 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren(); 749 final String boundsLog = mLayoutManager.getBoundsLog(); 750 assertEquals(config + ":\nfirst visible child should match traversal result\n" 751 + boundsLog, visibleChildren.firstVisiblePosition, 752 mLayoutManager.findFirstVisibleItemPosition() 753 ); 754 assertEquals( 755 config + ":\nfirst fully visible child should match traversal result\n" 756 + boundsLog, visibleChildren.firstFullyVisiblePosition, 757 mLayoutManager.findFirstCompletelyVisibleItemPosition() 758 ); 759 760 assertEquals(config + ":\nlast visible child should match traversal result\n" 761 + boundsLog, visibleChildren.lastVisiblePosition, 762 mLayoutManager.findLastVisibleItemPosition() 763 ); 764 assertEquals( 765 config + ":\nlast fully visible child should match traversal result\n" 766 + boundsLog, visibleChildren.lastFullyVisiblePosition, 767 mLayoutManager.findLastCompletelyVisibleItemPosition() 768 ); 769 } 770 }; 771 runTestOnUiThread(viewInBoundsTest); 772 // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching 773 // case 774 final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount(); 775 runTestOnUiThread(new Runnable() { 776 @Override 777 public void run() { 778 mRecyclerView.smoothScrollToPosition(scrollPosition); 779 } 780 }); 781 while (mLayoutManager.isSmoothScrolling() || 782 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 783 runTestOnUiThread(viewInBoundsTest); 784 Thread.sleep(400); 785 } 786 // delete all items 787 mLayoutManager.expectLayouts(2); 788 mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount()); 789 mLayoutManager.waitForLayout(2); 790 // test empty case 791 runTestOnUiThread(viewInBoundsTest); 792 // set a new adapter with huge items to test full bounds check 793 mLayoutManager.expectLayouts(1); 794 final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace(); 795 final TestAdapter newAdapter = new TestAdapter(100) { 796 @Override 797 public void onBindViewHolder(TestViewHolder holder, 798 int position) { 799 super.onBindViewHolder(holder, position); 800 if (config.mOrientation == HORIZONTAL) { 801 holder.itemView.setMinimumWidth(totalSpace + 5); 802 } else { 803 holder.itemView.setMinimumHeight(totalSpace + 5); 804 } 805 } 806 }; 807 runTestOnUiThread(new Runnable() { 808 @Override 809 public void run() { 810 mRecyclerView.setAdapter(newAdapter); 811 } 812 }); 813 mLayoutManager.waitForLayout(2); 814 runTestOnUiThread(viewInBoundsTest); 815 } 816 testSavedState()817 public void testSavedState() throws Throwable { 818 PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{ 819 new PostLayoutRunnable() { 820 @Override 821 public void run() throws Throwable { 822 // do nothing 823 } 824 825 @Override 826 public String describe() { 827 return "doing nothing"; 828 } 829 }, 830 new PostLayoutRunnable() { 831 @Override 832 public void run() throws Throwable { 833 mLayoutManager.expectLayouts(1); 834 scrollToPosition(mTestAdapter.getItemCount() * 3 / 4); 835 mLayoutManager.waitForLayout(2); 836 } 837 838 @Override 839 public String describe() { 840 return "scroll to position"; 841 } 842 }, 843 new PostLayoutRunnable() { 844 @Override 845 public void run() throws Throwable { 846 mLayoutManager.expectLayouts(1); 847 scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3, 848 50); 849 mLayoutManager.waitForLayout(2); 850 } 851 852 @Override 853 public String describe() { 854 return "scroll to position with positive offset"; 855 } 856 }, 857 new PostLayoutRunnable() { 858 @Override 859 public void run() throws Throwable { 860 mLayoutManager.expectLayouts(1); 861 scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3, 862 -10); // Some tests break if this value is below the item height. 863 mLayoutManager.waitForLayout(2); 864 } 865 866 @Override 867 public String describe() { 868 return "scroll to position with negative offset"; 869 } 870 } 871 }; 872 873 PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{ 874 new PostRestoreRunnable() { 875 @Override 876 public String describe() { 877 return "Doing nothing"; 878 } 879 }, 880 new PostRestoreRunnable() { 881 @Override 882 void onAfterRestore(Config config) throws Throwable { 883 // update config as well so that restore assertions will work 884 config.mOrientation = 1 - config.mOrientation; 885 mLayoutManager.setOrientation(config.mOrientation); 886 } 887 888 @Override 889 boolean shouldLayoutMatch(Config config) { 890 return config.mItemCount == 0; 891 } 892 893 @Override 894 public String describe() { 895 return "Changing orientation"; 896 } 897 }, 898 new PostRestoreRunnable() { 899 @Override 900 void onAfterRestore(Config config) throws Throwable { 901 config.mStackFromEnd = !config.mStackFromEnd; 902 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 903 } 904 905 @Override 906 boolean shouldLayoutMatch(Config config) { 907 return true; //stack from end should not move items on change 908 } 909 910 @Override 911 public String describe() { 912 return "Changing stack from end"; 913 } 914 }, 915 new PostRestoreRunnable() { 916 @Override 917 void onAfterRestore(Config config) throws Throwable { 918 config.mReverseLayout = !config.mReverseLayout; 919 mLayoutManager.setReverseLayout(config.mReverseLayout); 920 } 921 922 @Override 923 boolean shouldLayoutMatch(Config config) { 924 return config.mItemCount == 0; 925 } 926 927 @Override 928 public String describe() { 929 return "Changing reverse layout"; 930 } 931 }, 932 new PostRestoreRunnable() { 933 @Override 934 void onAfterRestore(Config config) throws Throwable { 935 config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach; 936 mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach); 937 } 938 939 @Override 940 boolean shouldLayoutMatch(Config config) { 941 return true; 942 } 943 944 @Override 945 String describe() { 946 return "Change should recycle children"; 947 } 948 }, 949 new PostRestoreRunnable() { 950 int position; 951 @Override 952 void onAfterRestore(Config config) throws Throwable { 953 position = mTestAdapter.getItemCount() / 2; 954 mLayoutManager.scrollToPosition(position); 955 } 956 957 @Override 958 boolean shouldLayoutMatch(Config config) { 959 return mTestAdapter.getItemCount() == 0; 960 } 961 962 @Override 963 String describe() { 964 return "Scroll to position " + position ; 965 } 966 967 @Override 968 void onAfterReLayout(Config config) { 969 if (mTestAdapter.getItemCount() > 0) { 970 assertEquals(config + ":scrolled view should be last completely visible", 971 position, 972 config.mStackFromEnd ? 973 mLayoutManager.findLastCompletelyVisibleItemPosition() 974 : mLayoutManager.findFirstCompletelyVisibleItemPosition()); 975 } 976 } 977 } 978 }; 979 boolean[] waitForLayoutOptions = new boolean[]{true, false}; 980 boolean[] loadDataAfterRestoreOptions = new boolean[]{true, false}; 981 List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300); 982 variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true); 983 for (Config config : variations) { 984 for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) { 985 for (boolean waitForLayout : waitForLayoutOptions) { 986 for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) { 987 for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) { 988 savedStateTest((Config) config.clone(), waitForLayout, 989 loadDataAfterRestore, postLayoutRunnable, postRestoreRunnable); 990 removeRecyclerView(); 991 } 992 } 993 994 } 995 } 996 } 997 } 998 savedStateTest(Config config, boolean waitForLayout, boolean loadDataAfterRestore, PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)999 public void savedStateTest(Config config, boolean waitForLayout, boolean loadDataAfterRestore, 1000 PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation) 1001 throws Throwable { 1002 if (DEBUG) { 1003 Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " + 1004 config + " post layout action " + postLayoutOperation.describe() + 1005 "post restore action " + postRestoreOperation.describe()); 1006 } 1007 setupByConfig(config, false); 1008 if (waitForLayout) { 1009 waitForFirstLayout(); 1010 postLayoutOperation.run(); 1011 } 1012 Map<Item, Rect> before = mLayoutManager.collectChildCoordinates(); 1013 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 1014 // we append a suffix to the parcelable to test out of bounds 1015 String parcelSuffix = UUID.randomUUID().toString(); 1016 Parcel parcel = Parcel.obtain(); 1017 savedState.writeToParcel(parcel, 0); 1018 parcel.writeString(parcelSuffix); 1019 removeRecyclerView(); 1020 // reset for reading 1021 parcel.setDataPosition(0); 1022 // re-create 1023 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 1024 removeRecyclerView(); 1025 1026 final int itemCount = mTestAdapter.getItemCount(); 1027 if (loadDataAfterRestore) { 1028 mTestAdapter.deleteAndNotify(0, itemCount); 1029 } 1030 1031 RecyclerView restored = new RecyclerView(getActivity()); 1032 // this config should be no op. 1033 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), 1034 config.mOrientation, config.mReverseLayout); 1035 mLayoutManager.setStackFromEnd(config.mStackFromEnd); 1036 restored.setLayoutManager(mLayoutManager); 1037 // use the same adapter for Rect matching 1038 restored.setAdapter(mTestAdapter); 1039 restored.onRestoreInstanceState(savedState); 1040 1041 if (loadDataAfterRestore) { 1042 mTestAdapter.addAndNotify(itemCount); 1043 } 1044 1045 postRestoreOperation.onAfterRestore(config); 1046 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 1047 parcel.readString()); 1048 mLayoutManager.expectLayouts(1); 1049 setRecyclerView(restored); 1050 mLayoutManager.waitForLayout(2); 1051 // calculate prefix here instead of above to include post restore changes 1052 final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() + 1053 "\npostRestore:" + postRestoreOperation.describe() + "\n"; 1054 assertEquals(logPrefix + " on saved state, reverse layout should be preserved", 1055 config.mReverseLayout, mLayoutManager.getReverseLayout()); 1056 assertEquals(logPrefix + " on saved state, orientation should be preserved", 1057 config.mOrientation, mLayoutManager.getOrientation()); 1058 assertEquals(logPrefix + " on saved state, stack from end should be preserved", 1059 config.mStackFromEnd, mLayoutManager.getStackFromEnd()); 1060 if (waitForLayout) { 1061 final boolean strictItemEquality = !loadDataAfterRestore; 1062 if (postRestoreOperation.shouldLayoutMatch(config)) { 1063 assertRectSetsEqual( 1064 logPrefix + ": on restore, previous view positions should be preserved", 1065 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 1066 } else { 1067 assertRectSetsNotEqual( 1068 logPrefix 1069 + ": on restore with changes, previous view positions should NOT " 1070 + "be preserved", 1071 before, mLayoutManager.collectChildCoordinates(), strictItemEquality); 1072 } 1073 postRestoreOperation.onAfterReLayout(config); 1074 } 1075 } 1076 testScrollAndClear()1077 public void testScrollAndClear() throws Throwable { 1078 setupByConfig(new Config(), true); 1079 1080 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 1081 1082 mLayoutManager.expectLayouts(1); 1083 runTestOnUiThread(new Runnable() { 1084 @Override 1085 public void run() { 1086 mLayoutManager.scrollToPositionWithOffset(1, 0); 1087 mTestAdapter.clearOnUIThread(); 1088 } 1089 }); 1090 mLayoutManager.waitForLayout(2); 1091 1092 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 1093 } 1094 1095 scrollToPositionWithOffset(final int position, final int offset)1096 void scrollToPositionWithOffset(final int position, final int offset) throws Throwable { 1097 runTestOnUiThread(new Runnable() { 1098 @Override 1099 public void run() { 1100 mLayoutManager.scrollToPositionWithOffset(position, offset); 1101 } 1102 }); 1103 } 1104 assertRectSetsNotEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)1105 public void assertRectSetsNotEqual(String message, Map<Item, Rect> before, 1106 Map<Item, Rect> after, boolean strictItemEquality) { 1107 Throwable throwable = null; 1108 try { 1109 assertRectSetsEqual("NOT " + message, before, after, strictItemEquality); 1110 } catch (Throwable t) { 1111 throwable = t; 1112 } 1113 assertNotNull(message + "\ntwo layout should be different", throwable); 1114 } 1115 assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)1116 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) { 1117 assertRectSetsEqual(message, before, after, true); 1118 } 1119 assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)1120 public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, 1121 boolean strictItemEquality) { 1122 StringBuilder sb = new StringBuilder(); 1123 sb.append("checking rectangle equality.\n"); 1124 sb.append("before:\n"); 1125 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1126 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 1127 } 1128 sb.append("after:\n"); 1129 for (Map.Entry<Item, Rect> entry : after.entrySet()) { 1130 sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n"); 1131 } 1132 message = message + "\n" + sb.toString(); 1133 assertEquals(message + ":\nitem counts should be equal", before.size() 1134 , after.size()); 1135 for (Map.Entry<Item, Rect> entry : before.entrySet()) { 1136 final Item beforeItem = entry.getKey(); 1137 Rect afterRect = null; 1138 if (strictItemEquality) { 1139 afterRect = after.get(beforeItem); 1140 assertNotNull(message + ":\nSame item should be visible after simple re-layout", 1141 afterRect); 1142 } else { 1143 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) { 1144 final Item afterItem = afterEntry.getKey(); 1145 if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) { 1146 afterRect = afterEntry.getValue(); 1147 break; 1148 } 1149 } 1150 assertNotNull(message + ":\nItem with same adapter index should be visible " + 1151 "after simple re-layout", 1152 afterRect); 1153 } 1154 assertEquals(message + ":\nItem should be laid out at the same coordinates", 1155 entry.getValue(), afterRect); 1156 } 1157 } 1158 testAccessibilityPositions()1159 public void testAccessibilityPositions() throws Throwable { 1160 setupByConfig(new Config(VERTICAL, false, false), true); 1161 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 1162 .getCompatAccessibilityDelegate(); 1163 final AccessibilityEvent event = AccessibilityEvent.obtain(); 1164 runTestOnUiThread(new Runnable() { 1165 @Override 1166 public void run() { 1167 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 1168 } 1169 }); 1170 final AccessibilityRecordCompat record = AccessibilityEventCompat 1171 .asRecord(event); 1172 assertEquals("result should have first position", 1173 record.getFromIndex(), 1174 mLayoutManager.findFirstVisibleItemPosition()); 1175 assertEquals("result should have last position", 1176 record.getToIndex(), 1177 mLayoutManager.findLastVisibleItemPosition()); 1178 } 1179 testPrepareForDrop()1180 public void testPrepareForDrop() throws Throwable { 1181 SelectTargetChildren[] selectors = new SelectTargetChildren[] { 1182 new SelectTargetChildren() { 1183 @Override 1184 public int[] selectTargetChildren(int childCount) { 1185 return new int[]{1, 0}; 1186 } 1187 }, 1188 new SelectTargetChildren() { 1189 @Override 1190 public int[] selectTargetChildren(int childCount) { 1191 return new int[]{0, 1}; 1192 } 1193 }, 1194 new SelectTargetChildren() { 1195 @Override 1196 public int[] selectTargetChildren(int childCount) { 1197 return new int[]{childCount - 1, childCount - 2}; 1198 } 1199 }, 1200 new SelectTargetChildren() { 1201 @Override 1202 public int[] selectTargetChildren(int childCount) { 1203 return new int[]{childCount - 2, childCount - 1}; 1204 } 1205 }, 1206 new SelectTargetChildren() { 1207 @Override 1208 public int[] selectTargetChildren(int childCount) { 1209 return new int[]{childCount / 2, childCount / 2 + 1}; 1210 } 1211 }, 1212 new SelectTargetChildren() { 1213 @Override 1214 public int[] selectTargetChildren(int childCount) { 1215 return new int[]{childCount / 2 + 1, childCount / 2}; 1216 } 1217 } 1218 }; 1219 for (SelectTargetChildren selector : selectors) { 1220 for (Config config : mBaseVariations) { 1221 prepareForDropTest(config, selector); 1222 removeRecyclerView(); 1223 } 1224 } 1225 } 1226 prepareForDropTest(final Config config, SelectTargetChildren selectTargetChildren)1227 public void prepareForDropTest(final Config config, SelectTargetChildren selectTargetChildren) 1228 throws Throwable { 1229 config.mTestAdapter = new TestAdapter(100) { 1230 @Override 1231 public void onBindViewHolder(TestViewHolder holder, 1232 int position) { 1233 super.onBindViewHolder(holder, position); 1234 if (config.mOrientation == HORIZONTAL) { 1235 final int base = mRecyclerView.getWidth() / 5; 1236 final int itemRand = holder.mBoundItem.mText.hashCode() % base; 1237 holder.itemView.setMinimumWidth(base + itemRand); 1238 } else { 1239 final int base = mRecyclerView.getHeight() / 5; 1240 final int itemRand = holder.mBoundItem.mText.hashCode() % base; 1241 holder.itemView.setMinimumHeight(base + itemRand); 1242 } 1243 } 1244 }; 1245 setupByConfig(config, true); 1246 mLayoutManager.expectLayouts(1); 1247 scrollToPosition(mTestAdapter.getItemCount() / 2); 1248 mLayoutManager.waitForLayout(1); 1249 int[] positions = selectTargetChildren.selectTargetChildren(mRecyclerView.getChildCount()); 1250 final View fromChild = mLayoutManager.getChildAt(positions[0]); 1251 final int fromPos = mLayoutManager.getPosition(fromChild); 1252 final View onChild = mLayoutManager.getChildAt(positions[1]); 1253 final int toPos = mLayoutManager.getPosition(onChild); 1254 final OrientationHelper helper = mLayoutManager.mOrientationHelper; 1255 final int dragCoordinate; 1256 final boolean towardsHead = toPos < fromPos; 1257 final int referenceLine; 1258 if (config.mReverseLayout == towardsHead) { 1259 referenceLine = helper.getDecoratedEnd(onChild); 1260 dragCoordinate = referenceLine + 3 - 1261 helper.getDecoratedMeasurement(fromChild); 1262 } else { 1263 referenceLine = helper.getDecoratedStart(onChild); 1264 dragCoordinate = referenceLine - 3; 1265 } 1266 mLayoutManager.expectLayouts(2); 1267 1268 final int x,y; 1269 if (config.mOrientation == HORIZONTAL) { 1270 x = dragCoordinate; 1271 y = fromChild.getTop(); 1272 } else { 1273 y = dragCoordinate; 1274 x = fromChild.getLeft(); 1275 } 1276 runTestOnUiThread(new Runnable() { 1277 @Override 1278 public void run() { 1279 mTestAdapter.moveInUIThread(fromPos, toPos); 1280 mTestAdapter.notifyItemMoved(fromPos, toPos); 1281 mLayoutManager.prepareForDrop(fromChild, onChild, x, y); 1282 } 1283 }); 1284 mLayoutManager.waitForLayout(2); 1285 1286 assertSame(fromChild, mRecyclerView.findViewHolderForAdapterPosition(toPos).itemView); 1287 // make sure it has the position we wanted 1288 if (config.mReverseLayout == towardsHead) { 1289 assertEquals(referenceLine, helper.getDecoratedEnd(fromChild)); 1290 } else { 1291 assertEquals(referenceLine, helper.getDecoratedStart(fromChild)); 1292 } 1293 } 1294 1295 static class VisibleChildren { 1296 1297 int firstVisiblePosition = RecyclerView.NO_POSITION; 1298 1299 int firstFullyVisiblePosition = RecyclerView.NO_POSITION; 1300 1301 int lastVisiblePosition = RecyclerView.NO_POSITION; 1302 1303 int lastFullyVisiblePosition = RecyclerView.NO_POSITION; 1304 1305 @Override 1306 public String toString() { 1307 return "VisibleChildren{" + 1308 "firstVisiblePosition=" + firstVisiblePosition + 1309 ", firstFullyVisiblePosition=" + firstFullyVisiblePosition + 1310 ", lastVisiblePosition=" + lastVisiblePosition + 1311 ", lastFullyVisiblePosition=" + lastFullyVisiblePosition + 1312 '}'; 1313 } 1314 } 1315 1316 abstract private class PostLayoutRunnable { 1317 1318 abstract void run() throws Throwable; 1319 1320 abstract String describe(); 1321 } 1322 1323 abstract private class PostRestoreRunnable { 1324 1325 void onAfterRestore(Config config) throws Throwable { 1326 } 1327 1328 abstract String describe(); 1329 1330 boolean shouldLayoutMatch(Config config) { 1331 return true; 1332 } 1333 1334 void onAfterReLayout(Config config) { 1335 1336 }; 1337 } 1338 1339 class WrappedLinearLayoutManager extends LinearLayoutManager { 1340 1341 CountDownLatch layoutLatch; 1342 1343 OrientationHelper mSecondaryOrientation; 1344 1345 OnLayoutListener mOnLayoutListener; 1346 1347 public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 1348 super(context, orientation, reverseLayout); 1349 } 1350 1351 public void expectLayouts(int count) { 1352 layoutLatch = new CountDownLatch(count); 1353 } 1354 1355 public void waitForLayout(long timeout) throws InterruptedException { 1356 waitForLayout(timeout, TimeUnit.SECONDS); 1357 } 1358 1359 @Override 1360 public void setOrientation(int orientation) { 1361 super.setOrientation(orientation); 1362 mSecondaryOrientation = null; 1363 } 1364 1365 @Override 1366 public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) { 1367 if (DEBUG) { 1368 Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child)); 1369 } 1370 super.removeAndRecycleView(child, recycler); 1371 } 1372 1373 @Override 1374 public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) { 1375 if (DEBUG) { 1376 Log.d(TAG, "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index))); 1377 } 1378 super.removeAndRecycleViewAt(index, recycler); 1379 } 1380 1381 @Override 1382 void ensureLayoutState() { 1383 super.ensureLayoutState(); 1384 if (mSecondaryOrientation == null) { 1385 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this, 1386 1 - getOrientation()); 1387 } 1388 } 1389 1390 private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException { 1391 layoutLatch.await(timeout * (DEBUG ? 100 : 1), timeUnit); 1392 assertEquals("all expected layouts should be executed at the expected time", 1393 0, layoutLatch.getCount()); 1394 getInstrumentation().waitForIdleSync(); 1395 } 1396 1397 @Override 1398 LayoutState createLayoutState() { 1399 return new LayoutState() { 1400 @Override 1401 View next(RecyclerView.Recycler recycler) { 1402 final boolean hadMore = hasMore(mRecyclerView.mState); 1403 final int position = mCurrentPosition; 1404 View next = super.next(recycler); 1405 assertEquals("if has more, should return a view", hadMore, next != null); 1406 assertEquals("position of the returned view must match current position", 1407 position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition()); 1408 return next; 1409 } 1410 }; 1411 } 1412 1413 public String getBoundsLog() { 1414 StringBuilder sb = new StringBuilder(); 1415 sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding()) 1416 .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding()); 1417 sb.append("\nchildren bounds\n"); 1418 final int childCount = getChildCount(); 1419 for (int i = 0; i < childCount; i++) { 1420 View child = getChildAt(i); 1421 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child)) 1422 .append("[").append("start:").append( 1423 mOrientationHelper.getDecoratedStart(child)).append(", end:") 1424 .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n"); 1425 } 1426 return sb.toString(); 1427 } 1428 1429 public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException { 1430 RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); 1431 if (itemAnimator == null) { 1432 return; 1433 } 1434 final CountDownLatch latch = new CountDownLatch(1); 1435 final boolean running = itemAnimator.isRunning( 1436 new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 1437 @Override 1438 public void onAnimationsFinished() { 1439 latch.countDown(); 1440 } 1441 } 1442 ); 1443 if (running) { 1444 latch.await(timeoutInSeconds, TimeUnit.SECONDS); 1445 } 1446 } 1447 1448 public VisibleChildren traverseAndFindVisibleChildren() { 1449 int childCount = getChildCount(); 1450 final VisibleChildren visibleChildren = new VisibleChildren(); 1451 final int start = mOrientationHelper.getStartAfterPadding(); 1452 final int end = mOrientationHelper.getEndAfterPadding(); 1453 for (int i = 0; i < childCount; i++) { 1454 View child = getChildAt(i); 1455 final int childStart = mOrientationHelper.getDecoratedStart(child); 1456 final int childEnd = mOrientationHelper.getDecoratedEnd(child); 1457 final boolean fullyVisible = childStart >= start && childEnd <= end; 1458 final boolean hidden = childEnd <= start || childStart >= end; 1459 if (hidden) { 1460 continue; 1461 } 1462 final int position = getPosition(child); 1463 if (fullyVisible) { 1464 if (position < visibleChildren.firstFullyVisiblePosition || 1465 visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) { 1466 visibleChildren.firstFullyVisiblePosition = position; 1467 } 1468 1469 if (position > visibleChildren.lastFullyVisiblePosition) { 1470 visibleChildren.lastFullyVisiblePosition = position; 1471 } 1472 } 1473 1474 if (position < visibleChildren.firstVisiblePosition || 1475 visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) { 1476 visibleChildren.firstVisiblePosition = position; 1477 } 1478 1479 if (position > visibleChildren.lastVisiblePosition) { 1480 visibleChildren.lastVisiblePosition = position; 1481 } 1482 1483 } 1484 return visibleChildren; 1485 } 1486 1487 Rect getViewBounds(View view) { 1488 if (getOrientation() == HORIZONTAL) { 1489 return new Rect( 1490 mOrientationHelper.getDecoratedStart(view), 1491 mSecondaryOrientation.getDecoratedStart(view), 1492 mOrientationHelper.getDecoratedEnd(view), 1493 mSecondaryOrientation.getDecoratedEnd(view)); 1494 } else { 1495 return new Rect( 1496 mSecondaryOrientation.getDecoratedStart(view), 1497 mOrientationHelper.getDecoratedStart(view), 1498 mSecondaryOrientation.getDecoratedEnd(view), 1499 mOrientationHelper.getDecoratedEnd(view)); 1500 } 1501 1502 } 1503 1504 Map<Item, Rect> collectChildCoordinates() throws Throwable { 1505 final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>(); 1506 runTestOnUiThread(new Runnable() { 1507 @Override 1508 public void run() { 1509 final int childCount = getChildCount(); 1510 Rect layoutBounds = new Rect(0, 0, 1511 mLayoutManager.getWidth(), mLayoutManager.getHeight()); 1512 for (int i = 0; i < childCount; i++) { 1513 View child = getChildAt(i); 1514 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child 1515 .getLayoutParams(); 1516 TestViewHolder vh = (TestViewHolder) lp.mViewHolder; 1517 Rect childBounds = getViewBounds(child); 1518 if (new Rect(childBounds).intersect(layoutBounds)) { 1519 items.put(vh.mBoundItem, childBounds); 1520 } 1521 } 1522 } 1523 }); 1524 return items; 1525 } 1526 1527 @Override 1528 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 1529 try { 1530 if (mOnLayoutListener != null) { 1531 mOnLayoutListener.before(recycler, state); 1532 } 1533 super.onLayoutChildren(recycler, state); 1534 if (mOnLayoutListener != null) { 1535 mOnLayoutListener.after(recycler, state); 1536 } 1537 } catch (Throwable t) { 1538 postExceptionToInstrumentation(t); 1539 } 1540 layoutLatch.countDown(); 1541 } 1542 1543 1544 } 1545 1546 static class OnLayoutListener { 1547 void before(RecyclerView.Recycler recycler, RecyclerView.State state){} 1548 void after(RecyclerView.Recycler recycler, RecyclerView.State state){} 1549 } 1550 1551 static class Config implements Cloneable { 1552 1553 private static final int DEFAULT_ITEM_COUNT = 100; 1554 1555 private boolean mStackFromEnd; 1556 1557 int mOrientation = VERTICAL; 1558 1559 boolean mReverseLayout = false; 1560 1561 boolean mRecycleChildrenOnDetach = false; 1562 1563 int mItemCount = DEFAULT_ITEM_COUNT; 1564 1565 TestAdapter mTestAdapter; 1566 1567 Config(int orientation, boolean reverseLayout, boolean stackFromEnd) { 1568 mOrientation = orientation; 1569 mReverseLayout = reverseLayout; 1570 mStackFromEnd = stackFromEnd; 1571 } 1572 1573 public Config() { 1574 1575 } 1576 1577 Config adapter(TestAdapter adapter) { 1578 mTestAdapter = adapter; 1579 return this; 1580 } 1581 1582 Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) { 1583 mRecycleChildrenOnDetach = recycleChildrenOnDetach; 1584 return this; 1585 } 1586 1587 Config orientation(int orientation) { 1588 mOrientation = orientation; 1589 return this; 1590 } 1591 1592 Config stackFromBottom(boolean stackFromBottom) { 1593 mStackFromEnd = stackFromBottom; 1594 return this; 1595 } 1596 1597 Config reverseLayout(boolean reverseLayout) { 1598 mReverseLayout = reverseLayout; 1599 return this; 1600 } 1601 1602 public Config itemCount(int itemCount) { 1603 mItemCount = itemCount; 1604 return this; 1605 } 1606 1607 // required by convention 1608 @Override 1609 public Object clone() throws CloneNotSupportedException { 1610 return super.clone(); 1611 } 1612 1613 @Override 1614 public String toString() { 1615 return "Config{" + 1616 "mStackFromEnd=" + mStackFromEnd + 1617 ", mOrientation=" + mOrientation + 1618 ", mReverseLayout=" + mReverseLayout + 1619 ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach + 1620 ", mItemCount=" + mItemCount + 1621 '}'; 1622 } 1623 } 1624 1625 private interface SelectTargetChildren { 1626 int[] selectTargetChildren(int childCount); 1627 } 1628 } 1629