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 static android.support.v7.widget.LinearLayoutManager.HORIZONTAL; 20 import static android.support.v7.widget.LinearLayoutManager.VERTICAL; 21 22 import static org.hamcrest.CoreMatchers.is; 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertFalse; 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.assertNull; 27 import static org.junit.Assert.assertSame; 28 import static org.junit.Assert.assertThat; 29 import static org.junit.Assert.assertTrue; 30 31 import android.graphics.Color; 32 import android.graphics.drawable.ColorDrawable; 33 import android.graphics.drawable.StateListDrawable; 34 import android.os.Build; 35 import android.support.test.filters.LargeTest; 36 import android.support.test.filters.SdkSuppress; 37 import android.support.v4.view.AccessibilityDelegateCompat; 38 import android.util.Log; 39 import android.util.StateSet; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.accessibility.AccessibilityEvent; 43 44 import org.junit.Test; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.TimeUnit; 50 import java.util.concurrent.atomic.AtomicInteger; 51 52 53 /** 54 * Includes tests for {@link LinearLayoutManager}. 55 * <p> 56 * Since most UI tests are not practical, these tests are focused on internal data representation 57 * and stability of LinearLayoutManager in response to different events (state change, scrolling 58 * etc) where it is very hard to do manual testing. 59 */ 60 @LargeTest 61 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { 62 63 @Test 64 public void topUnfocusableViewsVisibility() throws Throwable { 65 // The maximum number of child views that can be visible at any time. 66 final int visibleChildCount = 5; 67 final int consecutiveFocusablesCount = 2; 68 final int consecutiveUnFocusablesCount = 18; 69 final TestAdapter adapter = new TestAdapter( 70 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 71 RecyclerView mAttachedRv; 72 73 @Override 74 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 75 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 76 // Good to have colors for debugging 77 StateListDrawable stl = new StateListDrawable(); 78 stl.addState(new int[]{android.R.attr.state_focused}, 79 new ColorDrawable(Color.RED)); 80 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 81 //noinspection deprecation used to support kitkat tests 82 testViewHolder.itemView.setBackgroundDrawable(stl); 83 return testViewHolder; 84 } 85 86 @Override 87 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 88 mAttachedRv = recyclerView; 89 } 90 91 @Override 92 public void onBindViewHolder(TestViewHolder holder, 93 int position) { 94 super.onBindViewHolder(holder, position); 95 if (position < consecutiveFocusablesCount) { 96 holder.itemView.setFocusable(true); 97 holder.itemView.setFocusableInTouchMode(true); 98 } else { 99 holder.itemView.setFocusable(false); 100 holder.itemView.setFocusableInTouchMode(false); 101 } 102 // This height ensures that some portion of #visibleChildCount'th child is 103 // off-bounds, creating more interesting test scenario. 104 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 105 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 106 } 107 }; 108 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter).reverseLayout(true), 109 false); 110 waitForFirstLayout(); 111 112 // adapter position of the currently focused item. 113 int focusIndex = 0; 114 View newFocused = mRecyclerView.getChildAt(focusIndex); 115 requestFocus(newFocused, true); 116 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 117 focusIndex); 118 assertThat("Child at position " + focusIndex + " should be focused", 119 toFocus.itemView.hasFocus(), is(true)); 120 121 // adapter position of the item (whether focusable or not) that just becomes fully 122 // visible after focusSearch. 123 int visibleIndex = 0; 124 // The VH of the above adapter position 125 RecyclerView.ViewHolder toVisible = null; 126 127 // Navigate up through the focusable and unfocusable chunks. The focusable items should 128 // become focused one by one until hitting the last focusable item, at which point, 129 // unfocusable items should become visible on the screen until the currently focused item 130 // stays on the screen. 131 for (int i = 0; i < adapter.getItemCount(); i++) { 132 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_UP, true); 133 // adapter position of the currently focused item. 134 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 135 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 136 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 137 (visibleIndex + 1)); 138 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 139 140 assertThat("Child at position " + focusIndex + " should be focused", 141 toFocus.itemView.hasFocus(), is(true)); 142 assertTrue("Focused child should be at least partially visible.", 143 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 144 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 145 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 146 } 147 } 148 149 @Test 150 public void bottomUnfocusableViewsVisibility() throws Throwable { 151 // The maximum number of child views that can be visible at any time. 152 final int visibleChildCount = 5; 153 final int consecutiveFocusablesCount = 2; 154 final int consecutiveUnFocusablesCount = 18; 155 final TestAdapter adapter = new TestAdapter( 156 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 157 RecyclerView mAttachedRv; 158 159 @Override 160 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 161 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 162 // Good to have colors for debugging 163 StateListDrawable stl = new StateListDrawable(); 164 stl.addState(new int[]{android.R.attr.state_focused}, 165 new ColorDrawable(Color.RED)); 166 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 167 //noinspection deprecation used to support kitkat tests 168 testViewHolder.itemView.setBackgroundDrawable(stl); 169 return testViewHolder; 170 } 171 172 @Override 173 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 174 mAttachedRv = recyclerView; 175 } 176 177 @Override 178 public void onBindViewHolder(TestViewHolder holder, 179 int position) { 180 super.onBindViewHolder(holder, position); 181 if (position < consecutiveFocusablesCount) { 182 holder.itemView.setFocusable(true); 183 holder.itemView.setFocusableInTouchMode(true); 184 } else { 185 holder.itemView.setFocusable(false); 186 holder.itemView.setFocusableInTouchMode(false); 187 } 188 // This height ensures that some portion of #visibleChildCount'th child is 189 // off-bounds, creating more interesting test scenario. 190 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 191 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 192 } 193 }; 194 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false); 195 waitForFirstLayout(); 196 197 // adapter position of the currently focused item. 198 int focusIndex = 0; 199 View newFocused = mRecyclerView.getChildAt(focusIndex); 200 requestFocus(newFocused, true); 201 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 202 focusIndex); 203 assertThat("Child at position " + focusIndex + " should be focused", 204 toFocus.itemView.hasFocus(), is(true)); 205 206 // adapter position of the item (whether focusable or not) that just becomes fully 207 // visible after focusSearch. 208 int visibleIndex = 0; 209 // The VH of the above adapter position 210 RecyclerView.ViewHolder toVisible = null; 211 212 // Navigate down through the focusable and unfocusable chunks. The focusable items should 213 // become focused one by one until hitting the last focusable item, at which point, 214 // unfocusable items should become visible on the screen until the currently focused item 215 // stays on the screen. 216 for (int i = 0; i < adapter.getItemCount(); i++) { 217 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true); 218 // adapter position of the currently focused item. 219 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 220 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 221 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 222 (visibleIndex + 1)); 223 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 224 225 assertThat("Child at position " + focusIndex + " should be focused", 226 toFocus.itemView.hasFocus(), is(true)); 227 assertTrue("Focused child should be at least partially visible.", 228 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 229 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 230 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 231 } 232 } 233 234 @Test 235 public void leftUnfocusableViewsVisibility() throws Throwable { 236 // The maximum number of child views that can be visible at any time. 237 final int visibleChildCount = 5; 238 final int consecutiveFocusablesCount = 2; 239 final int consecutiveUnFocusablesCount = 18; 240 final TestAdapter adapter = new TestAdapter( 241 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 242 RecyclerView mAttachedRv; 243 244 @Override 245 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 246 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 247 // Good to have colors for debugging 248 StateListDrawable stl = new StateListDrawable(); 249 stl.addState(new int[]{android.R.attr.state_focused}, 250 new ColorDrawable(Color.RED)); 251 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 252 //noinspection deprecation used to support kitkat tests 253 testViewHolder.itemView.setBackgroundDrawable(stl); 254 return testViewHolder; 255 } 256 257 @Override 258 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 259 mAttachedRv = recyclerView; 260 } 261 262 @Override 263 public void onBindViewHolder(TestViewHolder holder, 264 int position) { 265 super.onBindViewHolder(holder, position); 266 if (position < consecutiveFocusablesCount) { 267 holder.itemView.setFocusable(true); 268 holder.itemView.setFocusableInTouchMode(true); 269 } else { 270 holder.itemView.setFocusable(false); 271 holder.itemView.setFocusableInTouchMode(false); 272 } 273 // This width ensures that some portion of #visibleChildCount'th child is 274 // off-bounds, creating more interesting test scenario. 275 holder.itemView.setMinimumWidth((mAttachedRv.getWidth() 276 + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount); 277 } 278 }; 279 setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter).reverseLayout(true), 280 false); 281 waitForFirstLayout(); 282 283 // adapter position of the currently focused item. 284 int focusIndex = 0; 285 View newFocused = mRecyclerView.getChildAt(focusIndex); 286 requestFocus(newFocused, true); 287 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 288 focusIndex); 289 assertThat("Child at position " + focusIndex + " should be focused", 290 toFocus.itemView.hasFocus(), is(true)); 291 292 // adapter position of the item (whether focusable or not) that just becomes fully 293 // visible after focusSearch. 294 int visibleIndex = 0; 295 // The VH of the above adapter position 296 RecyclerView.ViewHolder toVisible = null; 297 298 // Navigate left through the focusable and unfocusable chunks. The focusable items should 299 // become focused one by one until hitting the last focusable item, at which point, 300 // unfocusable items should become visible on the screen until the currently focused item 301 // stays on the screen. 302 for (int i = 0; i < adapter.getItemCount(); i++) { 303 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_LEFT, true); 304 // adapter position of the currently focused item. 305 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 306 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 307 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 308 (visibleIndex + 1)); 309 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 310 311 assertThat("Child at position " + focusIndex + " should be focused", 312 toFocus.itemView.hasFocus(), is(true)); 313 assertTrue("Focused child should be at least partially visible.", 314 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 315 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 316 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 317 } 318 } 319 320 @Test 321 public void rightUnfocusableViewsVisibility() throws Throwable { 322 // The maximum number of child views that can be visible at any time. 323 final int visibleChildCount = 5; 324 final int consecutiveFocusablesCount = 2; 325 final int consecutiveUnFocusablesCount = 18; 326 final TestAdapter adapter = new TestAdapter( 327 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 328 RecyclerView mAttachedRv; 329 330 @Override 331 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 332 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 333 // Good to have colors for debugging 334 StateListDrawable stl = new StateListDrawable(); 335 stl.addState(new int[]{android.R.attr.state_focused}, 336 new ColorDrawable(Color.RED)); 337 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 338 //noinspection deprecation used to support kitkat tests 339 testViewHolder.itemView.setBackgroundDrawable(stl); 340 return testViewHolder; 341 } 342 343 @Override 344 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 345 mAttachedRv = recyclerView; 346 } 347 348 @Override 349 public void onBindViewHolder(TestViewHolder holder, 350 int position) { 351 super.onBindViewHolder(holder, position); 352 if (position < consecutiveFocusablesCount) { 353 holder.itemView.setFocusable(true); 354 holder.itemView.setFocusableInTouchMode(true); 355 } else { 356 holder.itemView.setFocusable(false); 357 holder.itemView.setFocusableInTouchMode(false); 358 } 359 // This width ensures that some portion of #visibleChildCount'th child is 360 // off-bounds, creating more interesting test scenario. 361 holder.itemView.setMinimumWidth((mAttachedRv.getWidth() 362 + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount); 363 } 364 }; 365 setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter), false); 366 waitForFirstLayout(); 367 368 // adapter position of the currently focused item. 369 int focusIndex = 0; 370 View newFocused = mRecyclerView.getChildAt(focusIndex); 371 requestFocus(newFocused, true); 372 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 373 focusIndex); 374 assertThat("Child at position " + focusIndex + " should be focused", 375 toFocus.itemView.hasFocus(), is(true)); 376 377 // adapter position of the item (whether focusable or not) that just becomes fully 378 // visible after focusSearch. 379 int visibleIndex = 0; 380 // The VH of the above adapter position 381 RecyclerView.ViewHolder toVisible = null; 382 383 // Navigate right through the focusable and unfocusable chunks. The focusable items should 384 // become focused one by one until hitting the last focusable item, at which point, 385 // unfocusable items should become visible on the screen until the currently focused item 386 // stays on the screen. 387 for (int i = 0; i < adapter.getItemCount(); i++) { 388 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_RIGHT, true); 389 // adapter position of the currently focused item. 390 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 391 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 392 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 393 (visibleIndex + 1)); 394 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 395 396 assertThat("Child at position " + focusIndex + " should be focused", 397 toFocus.itemView.hasFocus(), is(true)); 398 assertTrue("Focused child should be at least partially visible.", 399 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 400 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 401 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 402 } 403 } 404 405 // Run this test on Jelly Bean and newer because clearFocus on API 15 will call 406 // requestFocus in ViewRootImpl when clearChildFocus is called. Whereas, in API 16 and above, 407 // this call is delayed until after onFocusChange callback is called. Thus on API 16+, there's a 408 // transient state of no child having focus during which onFocusChange is executed. This 409 // transient state does not exist on API 15-. 410 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) 411 @Test 412 public void unfocusableScrollingWhenFocusCleared() throws Throwable { 413 // The maximum number of child views that can be visible at any time. 414 final int visibleChildCount = 5; 415 final int consecutiveFocusablesCount = 2; 416 final int consecutiveUnFocusablesCount = 18; 417 final TestAdapter adapter = new TestAdapter( 418 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 419 RecyclerView mAttachedRv; 420 421 @Override 422 public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 423 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 424 // Good to have colors for debugging 425 StateListDrawable stl = new StateListDrawable(); 426 stl.addState(new int[]{android.R.attr.state_focused}, 427 new ColorDrawable(Color.RED)); 428 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 429 //noinspection deprecation used to support kitkat tests 430 testViewHolder.itemView.setBackgroundDrawable(stl); 431 return testViewHolder; 432 } 433 434 @Override 435 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 436 mAttachedRv = recyclerView; 437 } 438 439 @Override 440 public void onBindViewHolder(TestViewHolder holder, 441 int position) { 442 super.onBindViewHolder(holder, position); 443 if (position < consecutiveFocusablesCount) { 444 holder.itemView.setFocusable(true); 445 holder.itemView.setFocusableInTouchMode(true); 446 } else { 447 holder.itemView.setFocusable(false); 448 holder.itemView.setFocusableInTouchMode(false); 449 } 450 // This height ensures that some portion of #visibleChildCount'th child is 451 // off-bounds, creating more interesting test scenario. 452 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 453 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 454 } 455 }; 456 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false); 457 waitForFirstLayout(); 458 459 // adapter position of the currently focused item. 460 int focusIndex = 0; 461 View newFocused = mRecyclerView.getChildAt(focusIndex); 462 requestFocus(newFocused, true); 463 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 464 focusIndex); 465 assertThat("Child at position " + focusIndex + " should be focused", 466 toFocus.itemView.hasFocus(), is(true)); 467 468 final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true); 469 focusIndex++; 470 assertThat("Child at position " + focusIndex + " should be focused", 471 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(), 472 is(true)); 473 final CountDownLatch focusLatch = new CountDownLatch(1); 474 mActivityRule.runOnUiThread(new Runnable() { 475 @Override 476 public void run() { 477 nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){ 478 @Override 479 public void onFocusChange(View v, boolean hasFocus) { 480 assertNull("Focus just got cleared and no children should be holding" 481 + " focus now.", mRecyclerView.getFocusedChild()); 482 try { 483 // Calling focusSearch should be a no-op here since even though there 484 // are unfocusable views down to scroll to, none of RV's children hold 485 // focus at this stage. 486 View focusedChild = focusSearch(v, View.FOCUS_DOWN, true); 487 assertNull("Calling focusSearch should be no-op when no children hold" 488 + "focus", focusedChild); 489 // No scrolling should have happened, so any unfocusables that were 490 // invisible should still be invisible. 491 RecyclerView.ViewHolder unforcusablePartiallyVisibleChild = 492 mRecyclerView.findViewHolderForAdapterPosition( 493 visibleChildCount - 1); 494 assertFalse("Child view at adapter pos " + (visibleChildCount - 1) 495 + " should not be fully visible.", 496 isViewFullyInBound(mRecyclerView, 497 unforcusablePartiallyVisibleChild.itemView)); 498 } catch (Throwable t) { 499 postExceptionToInstrumentation(t); 500 } 501 } 502 }); 503 nextView.clearFocus(); 504 focusLatch.countDown(); 505 } 506 }); 507 assertTrue(focusLatch.await(2, TimeUnit.SECONDS)); 508 assertThat("Child at position " + focusIndex + " should no longer be focused", 509 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(), 510 is(false)); 511 } 512 513 @Test 514 public void removeAnchorItem() throws Throwable { 515 removeAnchorItemTest( 516 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout( 517 false), 100, 0); 518 } 519 520 @Test 521 public void removeAnchorItemReverse() throws Throwable { 522 removeAnchorItemTest( 523 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100, 524 0); 525 } 526 527 @Test 528 public void removeAnchorItemStackFromEnd() throws Throwable { 529 removeAnchorItemTest( 530 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100, 531 99); 532 } 533 534 @Test 535 public void removeAnchorItemStackFromEndAndReverse() throws Throwable { 536 removeAnchorItemTest( 537 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100, 538 99); 539 } 540 541 @Test 542 public void removeAnchorItemHorizontal() throws Throwable { 543 removeAnchorItemTest( 544 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout( 545 false), 100, 0); 546 } 547 548 @Test 549 public void removeAnchorItemReverseHorizontal() throws Throwable { 550 removeAnchorItemTest( 551 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true), 552 100, 0); 553 } 554 555 @Test 556 public void removeAnchorItemStackFromEndHorizontal() throws Throwable { 557 removeAnchorItemTest( 558 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false), 559 100, 99); 560 } 561 562 @Test 563 public void removeAnchorItemStackFromEndAndReverseHorizontal() throws Throwable { 564 removeAnchorItemTest( 565 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100, 566 99); 567 } 568 569 /** 570 * This tests a regression where predictive animations were not working as expected when the 571 * first item is removed and there aren't any more items to add from that direction. 572 * First item refers to the default anchor item. 573 */ 574 public void removeAnchorItemTest(final Config config, int adapterSize, 575 final int removePos) throws Throwable { 576 config.adapter(new TestAdapter(adapterSize) { 577 @Override 578 public void onBindViewHolder(TestViewHolder holder, 579 int position) { 580 super.onBindViewHolder(holder, position); 581 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 582 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 583 lp = new ViewGroup.MarginLayoutParams(0, 0); 584 holder.itemView.setLayoutParams(lp); 585 } 586 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 587 final int maxSize; 588 if (config.mOrientation == HORIZONTAL) { 589 maxSize = mRecyclerView.getWidth(); 590 mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 591 } else { 592 maxSize = mRecyclerView.getHeight(); 593 mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT; 594 } 595 596 final int desiredSize; 597 if (position == removePos) { 598 // make it large 599 desiredSize = maxSize / 4; 600 } else { 601 // make it small 602 desiredSize = maxSize / 8; 603 } 604 if (config.mOrientation == HORIZONTAL) { 605 mlp.width = desiredSize; 606 } else { 607 mlp.height = desiredSize; 608 } 609 } 610 }); 611 setupByConfig(config, true); 612 final int childCount = mLayoutManager.getChildCount(); 613 RecyclerView.ViewHolder toBeRemoved = null; 614 List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>(); 615 for (int i = 0; i < childCount; i++) { 616 View child = mLayoutManager.getChildAt(i); 617 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 618 if (holder.getAdapterPosition() == removePos) { 619 toBeRemoved = holder; 620 } else { 621 toBeMoved.add(holder); 622 } 623 } 624 assertNotNull("test sanity", toBeRemoved); 625 assertEquals("test sanity", childCount - 1, toBeMoved.size()); 626 LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator(); 627 mRecyclerView.setItemAnimator(loggingItemAnimator); 628 loggingItemAnimator.reset(); 629 loggingItemAnimator.expectRunPendingAnimationsCall(1); 630 mLayoutManager.expectLayouts(2); 631 mTestAdapter.deleteAndNotify(removePos, 1); 632 mLayoutManager.waitForLayout(1); 633 loggingItemAnimator.waitForPendingAnimationsCall(2); 634 assertTrue("removed child should receive remove animation", 635 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved)); 636 for (RecyclerView.ViewHolder vh : toBeMoved) { 637 assertTrue("view holder should be in moved list", 638 loggingItemAnimator.mMoveVHs.contains(vh)); 639 } 640 List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>(); 641 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 642 View child = mLayoutManager.getChildAt(i); 643 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 644 if (toBeRemoved != holder && !toBeMoved.contains(holder)) { 645 newHolders.add(holder); 646 } 647 } 648 assertTrue("some new children should show up for the new space", newHolders.size() > 0); 649 assertEquals("no items should receive animate add since they are not new", 0, 650 loggingItemAnimator.mAddVHs.size()); 651 for (RecyclerView.ViewHolder holder : newHolders) { 652 assertTrue("new holder should receive a move animation", 653 loggingItemAnimator.mMoveVHs.contains(holder)); 654 } 655 assertTrue("control against adding too many children due to bad layout state preparation." 656 + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(), 657 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/); 658 } 659 660 @Test 661 public void keepFocusOnRelayout() throws Throwable { 662 setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true); 663 int center = (mLayoutManager.findLastVisibleItemPosition() 664 - mLayoutManager.findFirstVisibleItemPosition()) / 2; 665 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center); 666 final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView); 667 requestFocus(vh.itemView, true); 668 assertTrue("view should have the focus", vh.itemView.hasFocus()); 669 // add a bunch of items right before that view, make sure it keeps its position 670 mLayoutManager.expectLayouts(2); 671 final int childCountToAdd = mRecyclerView.getChildCount() * 2; 672 mTestAdapter.addAndNotify(center, childCountToAdd); 673 center += childCountToAdd; // offset item 674 mLayoutManager.waitForLayout(2); 675 mLayoutManager.waitForAnimationsToEnd(20); 676 final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center); 677 assertNotNull("focused child should stay in layout", postVH); 678 assertSame("same view holder should be kept for unchanged child", vh, postVH); 679 assertEquals("focused child's screen position should stay unchanged", top, 680 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView)); 681 } 682 683 @Test 684 public void keepFullFocusOnResize() throws Throwable { 685 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true); 686 } 687 688 @Test 689 public void keepPartialFocusOnResize() throws Throwable { 690 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false); 691 } 692 693 @Test 694 public void keepReverseFullFocusOnResize() throws Throwable { 695 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true); 696 } 697 698 @Test 699 public void keepReversePartialFocusOnResize() throws Throwable { 700 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false); 701 } 702 703 @Test 704 public void keepStackFromEndFullFocusOnResize() throws Throwable { 705 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true); 706 } 707 708 @Test 709 public void keepStackFromEndPartialFocusOnResize() throws Throwable { 710 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false); 711 } 712 713 public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable { 714 setupByConfig(config, true); 715 final int targetPosition; 716 if (config.mStackFromEnd) { 717 targetPosition = mLayoutManager.findFirstVisibleItemPosition(); 718 } else { 719 targetPosition = mLayoutManager.findLastVisibleItemPosition(); 720 } 721 final OrientationHelper helper = mLayoutManager.mOrientationHelper; 722 final RecyclerView.ViewHolder vh = mRecyclerView 723 .findViewHolderForLayoutPosition(targetPosition); 724 725 // scroll enough to offset the child 726 int startMargin = helper.getDecoratedStart(vh.itemView) - 727 helper.getStartAfterPadding(); 728 int endMargin = helper.getEndAfterPadding() - 729 helper.getDecoratedEnd(vh.itemView); 730 Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin); 731 requestFocus(vh.itemView, true); 732 assertTrue("view should gain the focus", vh.itemView.hasFocus()); 733 // scroll enough to offset the child 734 startMargin = helper.getDecoratedStart(vh.itemView) - 735 helper.getStartAfterPadding(); 736 endMargin = helper.getEndAfterPadding() - 737 helper.getDecoratedEnd(vh.itemView); 738 739 Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin); 740 assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0); 741 742 int expectedOffset = 0; 743 boolean offsetAtStart = false; 744 if (!fullyVisible) { 745 // move it a bit such that it is no more fully visible 746 final int childSize = helper 747 .getDecoratedMeasurement(vh.itemView); 748 expectedOffset = childSize / 3; 749 if (startMargin < endMargin) { 750 scrollBy(expectedOffset); 751 offsetAtStart = true; 752 } else { 753 scrollBy(-expectedOffset); 754 offsetAtStart = false; 755 } 756 startMargin = helper.getDecoratedStart(vh.itemView) - 757 helper.getStartAfterPadding(); 758 endMargin = helper.getEndAfterPadding() - 759 helper.getDecoratedEnd(vh.itemView); 760 assertTrue("test sanity, view should not be fully visible", startMargin < 0 761 || endMargin < 0); 762 } 763 764 mLayoutManager.expectLayouts(1); 765 mActivityRule.runOnUiThread(new Runnable() { 766 @Override 767 public void run() { 768 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams(); 769 if (config.mOrientation == HORIZONTAL) { 770 layoutParams.width = mRecyclerView.getWidth() / 2; 771 } else { 772 layoutParams.height = mRecyclerView.getHeight() / 2; 773 } 774 mRecyclerView.setLayoutParams(layoutParams); 775 } 776 }); 777 Thread.sleep(100); 778 // add a bunch of items right before that view, make sure it keeps its position 779 mLayoutManager.waitForLayout(2); 780 mLayoutManager.waitForAnimationsToEnd(20); 781 assertTrue("view should preserve the focus", vh.itemView.hasFocus()); 782 final RecyclerView.ViewHolder postVH = mRecyclerView 783 .findViewHolderForLayoutPosition(targetPosition); 784 assertNotNull("focused child should stay in layout", postVH); 785 assertSame("same view holder should be kept for unchanged child", vh, postVH); 786 View focused = postVH.itemView; 787 788 startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding(); 789 endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused); 790 791 assertTrue("focused child should be somewhat visible", 792 helper.getDecoratedStart(focused) < helper.getEndAfterPadding() 793 && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding()); 794 if (fullyVisible) { 795 assertTrue("focused child end should stay fully visible", 796 endMargin >= 0); 797 assertTrue("focused child start should stay fully visible", 798 startMargin >= 0); 799 } else { 800 if (offsetAtStart) { 801 assertTrue("start should preserve its offset", startMargin < 0); 802 assertTrue("end should be visible", endMargin >= 0); 803 } else { 804 assertTrue("end should preserve its offset", endMargin < 0); 805 assertTrue("start should be visible", startMargin >= 0); 806 } 807 } 808 } 809 810 @Test 811 public void scrollToPositionWithPredictive() throws Throwable { 812 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 813 removeRecyclerView(); 814 scrollToPositionWithPredictive(3, 20); 815 removeRecyclerView(); 816 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 817 LinearLayoutManager.INVALID_OFFSET); 818 removeRecyclerView(); 819 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 820 } 821 822 @Test 823 public void recycleDuringAnimations() throws Throwable { 824 final AtomicInteger childCount = new AtomicInteger(0); 825 final TestAdapter adapter = new TestAdapter(300) { 826 @Override 827 public TestViewHolder onCreateViewHolder(ViewGroup parent, 828 int viewType) { 829 final int cnt = childCount.incrementAndGet(); 830 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 831 if (DEBUG) { 832 Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder); 833 } 834 return testViewHolder; 835 } 836 }; 837 setupByConfig(new Config(VERTICAL, false, false).itemCount(300) 838 .adapter(adapter), true); 839 840 final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 841 @Override 842 public void putRecycledView(RecyclerView.ViewHolder scrap) { 843 super.putRecycledView(scrap); 844 int cnt = childCount.decrementAndGet(); 845 if (DEBUG) { 846 Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap); 847 } 848 } 849 850 @Override 851 public RecyclerView.ViewHolder getRecycledView(int viewType) { 852 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType); 853 if (recycledView != null) { 854 final int cnt = childCount.incrementAndGet(); 855 if (DEBUG) { 856 Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView); 857 } 858 } 859 return recycledView; 860 } 861 }; 862 pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500); 863 mRecyclerView.setRecycledViewPool(pool); 864 865 866 // now keep adding children to trigger more children being created etc. 867 for (int i = 0; i < 100; i ++) { 868 adapter.addAndNotify(15, 1); 869 Thread.sleep(15); 870 } 871 getInstrumentation().waitForIdleSync(); 872 waitForAnimations(2); 873 assertEquals("Children count should add up", childCount.get(), 874 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 875 876 // now trigger lots of add again, followed by a scroll to position 877 for (int i = 0; i < 100; i ++) { 878 adapter.addAndNotify(5 + (i % 3) * 3, 1); 879 Thread.sleep(25); 880 } 881 smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20); 882 waitForAnimations(2); 883 getInstrumentation().waitForIdleSync(); 884 assertEquals("Children count should add up", childCount.get(), 885 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 886 } 887 888 889 @Test 890 public void dontRecycleChildrenOnDetach() throws Throwable { 891 setupByConfig(new Config().recycleChildrenOnDetach(false), true); 892 mActivityRule.runOnUiThread(new Runnable() { 893 @Override 894 public void run() { 895 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 896 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView); 897 assertEquals("No views are recycled", recyclerSize, 898 mRecyclerView.mRecycler.getRecycledViewPool().size()); 899 } 900 }); 901 } 902 903 @Test 904 public void recycleChildrenOnDetach() throws Throwable { 905 setupByConfig(new Config().recycleChildrenOnDetach(true), true); 906 final int childCount = mLayoutManager.getChildCount(); 907 mActivityRule.runOnUiThread(new Runnable() { 908 @Override 909 public void run() { 910 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 911 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews( 912 mTestAdapter.getItemViewType(0), recyclerSize + childCount); 913 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView); 914 assertEquals("All children should be recycled", childCount + recyclerSize, 915 mRecyclerView.mRecycler.getRecycledViewPool().size()); 916 } 917 }); 918 } 919 920 @Test 921 public void scrollAndClear() throws Throwable { 922 setupByConfig(new Config(), true); 923 924 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 925 926 mLayoutManager.expectLayouts(1); 927 mActivityRule.runOnUiThread(new Runnable() { 928 @Override 929 public void run() { 930 mLayoutManager.scrollToPositionWithOffset(1, 0); 931 mTestAdapter.clearOnUIThread(); 932 } 933 }); 934 mLayoutManager.waitForLayout(2); 935 936 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 937 } 938 939 940 @Test 941 public void accessibilityPositions() throws Throwable { 942 setupByConfig(new Config(VERTICAL, false, false), true); 943 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 944 .getCompatAccessibilityDelegate(); 945 final AccessibilityEvent event = AccessibilityEvent.obtain(); 946 mActivityRule.runOnUiThread(new Runnable() { 947 @Override 948 public void run() { 949 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 950 } 951 }); 952 assertEquals("result should have first position", 953 event.getFromIndex(), 954 mLayoutManager.findFirstVisibleItemPosition()); 955 assertEquals("result should have last position", 956 event.getToIndex(), 957 mLayoutManager.findLastVisibleItemPosition()); 958 } 959 } 960