1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.recyclerview.widget; 18 19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 21 22 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL; 23 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL; 24 25 import static org.hamcrest.CoreMatchers.is; 26 import static org.junit.Assert.assertEquals; 27 import static org.junit.Assert.assertFalse; 28 import static org.junit.Assert.assertNotNull; 29 import static org.junit.Assert.assertNull; 30 import static org.junit.Assert.assertSame; 31 import static org.junit.Assert.assertThat; 32 import static org.junit.Assert.assertTrue; 33 34 import android.graphics.Color; 35 import android.graphics.drawable.ColorDrawable; 36 import android.graphics.drawable.StateListDrawable; 37 import android.os.Build; 38 import android.support.test.filters.LargeTest; 39 import android.support.test.filters.SdkSuppress; 40 import android.util.Log; 41 import android.util.StateSet; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.view.accessibility.AccessibilityEvent; 45 46 import androidx.annotation.NonNull; 47 import androidx.core.view.AccessibilityDelegateCompat; 48 import androidx.core.view.ViewCompat; 49 50 import org.junit.Test; 51 52 import java.util.ArrayList; 53 import java.util.List; 54 import java.util.concurrent.CountDownLatch; 55 import java.util.concurrent.TimeUnit; 56 import java.util.concurrent.atomic.AtomicInteger; 57 58 59 /** 60 * Includes tests for {@link LinearLayoutManager}. 61 * <p> 62 * Since most UI tests are not practical, these tests are focused on internal data representation 63 * and stability of LinearLayoutManager in response to different events (state change, scrolling 64 * etc) where it is very hard to do manual testing. 65 */ 66 @LargeTest 67 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest { 68 69 /** 70 * Tests that the LinearLayoutManager retains the focused element after multiple measure 71 * calls to the RecyclerView. There was a bug where the focused view was lost when the soft 72 * keyboard opened. This test simulates the measure/layout events triggered by the opening 73 * of the soft keyboard by making two calls to measure. A simulation was done because using 74 * the soft keyboard in the test caused many issues on API levels 15, 17 and 19. 75 */ 76 @Test 77 public void focusedChildStaysInViewWhenRecyclerViewShrinks() throws Throwable { 78 79 // Arrange. 80 81 final RecyclerView recyclerView = inflateWrappedRV(); 82 ViewGroup.LayoutParams lp = recyclerView.getLayoutParams(); 83 lp.height = WRAP_CONTENT; 84 lp.width = MATCH_PARENT; 85 recyclerView.setHasFixedSize(true); 86 87 final FocusableAdapter focusableAdapter = 88 new FocusableAdapter(50); 89 recyclerView.setAdapter(focusableAdapter); 90 91 mLayoutManager = new WrappedLinearLayoutManager(getActivity(), VERTICAL, false); 92 recyclerView.setLayoutManager(mLayoutManager); 93 94 mLayoutManager.expectLayouts(1); 95 mActivityRule.runOnUiThread(new Runnable() { 96 @Override 97 public void run() { 98 getActivity().getContainer().addView(recyclerView); 99 } 100 }); 101 mLayoutManager.waitForLayout(3); 102 103 int width = recyclerView.getWidth(); 104 int height = recyclerView.getHeight(); 105 final int widthMeasureSpec = 106 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); 107 final int fullHeightMeasureSpec = 108 View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST); 109 // "MinusOne" so that a measure call will appropriately trigger onMeasure after RecyclerView 110 // was previously laid out with the full height version. 111 final int fullHeightMinusOneMeasureSpec = 112 View.MeasureSpec.makeMeasureSpec(height - 1, View.MeasureSpec.AT_MOST); 113 final int halfHeightMeasureSpec = 114 View.MeasureSpec.makeMeasureSpec(height / 2, View.MeasureSpec.AT_MOST); 115 116 // Act 1. 117 118 View toFocus = findLastFullyVisibleChild(recyclerView); 119 int focusIndex = recyclerView.getChildAdapterPosition(toFocus); 120 121 requestFocus(toFocus, false); 122 123 mLayoutManager.expectLayouts(1); 124 mActivityRule.runOnUiThread(new Runnable() { 125 @Override 126 public void run() { 127 recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec); 128 recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec); 129 recyclerView.layout( 130 0, 131 0, 132 recyclerView.getMeasuredWidth(), 133 recyclerView.getMeasuredHeight()); 134 } 135 }); 136 mLayoutManager.waitForLayout(3); 137 138 // Verify 1. 139 140 assertThat("Child at position " + focusIndex + " should be focused", 141 toFocus.hasFocus(), is(true)); 142 // Testing for partial visibility instead of full visibility since TextView calls 143 // requestRectangleOnScreen (inside bringPointIntoView) for the focused view with a rect 144 // containing the content area. This rect is guaranteed to be fully visible whereas a 145 // portion of TextView could be out of bounds. 146 assertThat("Child view at adapter pos " + focusIndex + " should be fully visible.", 147 isViewPartiallyInBound(recyclerView, toFocus), is(true)); 148 149 // Act 2. 150 151 mLayoutManager.expectLayouts(1); 152 mActivityRule.runOnUiThread(new Runnable() { 153 @Override 154 public void run() { 155 recyclerView.measure(widthMeasureSpec, fullHeightMeasureSpec); 156 recyclerView.layout( 157 0, 158 0, 159 recyclerView.getMeasuredWidth(), 160 recyclerView.getMeasuredHeight()); 161 } 162 }); 163 mLayoutManager.waitForLayout(3); 164 165 // Verify 2. 166 167 assertThat("Child at position " + focusIndex + " should be focused", 168 toFocus.hasFocus(), is(true)); 169 assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.", 170 isViewPartiallyInBound(recyclerView, toFocus)); 171 172 // Act 3. 173 174 // Now focus on the first fully visible EditText. 175 toFocus = findFirstFullyVisibleChild(recyclerView); 176 focusIndex = recyclerView.getChildAdapterPosition(toFocus); 177 178 requestFocus(toFocus, false); 179 180 mLayoutManager.expectLayouts(1); 181 mActivityRule.runOnUiThread(new Runnable() { 182 @Override 183 public void run() { 184 recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec); 185 recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec); 186 recyclerView.layout( 187 0, 188 0, 189 recyclerView.getMeasuredWidth(), 190 recyclerView.getMeasuredHeight()); 191 } 192 }); 193 mLayoutManager.waitForLayout(3); 194 195 // Assert 3. 196 197 assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.", 198 isViewPartiallyInBound(recyclerView, toFocus)); 199 } 200 201 @Test 202 public void topUnfocusableViewsVisibility() throws Throwable { 203 // The maximum number of child views that can be visible at any time. 204 final int visibleChildCount = 5; 205 final int consecutiveFocusablesCount = 2; 206 final int consecutiveUnFocusablesCount = 18; 207 final TestAdapter adapter = new TestAdapter( 208 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 209 RecyclerView mAttachedRv; 210 211 @Override 212 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 213 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 214 // Good to have colors for debugging 215 StateListDrawable stl = new StateListDrawable(); 216 stl.addState(new int[]{android.R.attr.state_focused}, 217 new ColorDrawable(Color.RED)); 218 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 219 //noinspection deprecation used to support kitkat tests 220 testViewHolder.itemView.setBackgroundDrawable(stl); 221 return testViewHolder; 222 } 223 224 @Override 225 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 226 mAttachedRv = recyclerView; 227 } 228 229 @Override 230 public void onBindViewHolder(@NonNull TestViewHolder holder, 231 int position) { 232 super.onBindViewHolder(holder, position); 233 if (position < consecutiveFocusablesCount) { 234 holder.itemView.setFocusable(true); 235 holder.itemView.setFocusableInTouchMode(true); 236 } else { 237 holder.itemView.setFocusable(false); 238 holder.itemView.setFocusableInTouchMode(false); 239 } 240 // This height ensures that some portion of #visibleChildCount'th child is 241 // off-bounds, creating more interesting test scenario. 242 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 243 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 244 } 245 }; 246 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter).reverseLayout(true), 247 false); 248 waitForFirstLayout(); 249 250 // adapter position of the currently focused item. 251 int focusIndex = 0; 252 View newFocused = mRecyclerView.getChildAt(focusIndex); 253 requestFocus(newFocused, true); 254 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 255 focusIndex); 256 assertThat("Child at position " + focusIndex + " should be focused", 257 toFocus.itemView.hasFocus(), is(true)); 258 259 // adapter position of the item (whether focusable or not) that just becomes fully 260 // visible after focusSearch. 261 int visibleIndex = 0; 262 // The VH of the above adapter position 263 RecyclerView.ViewHolder toVisible = null; 264 265 // Navigate up through the focusable and unfocusable chunks. The focusable items should 266 // become focused one by one until hitting the last focusable item, at which point, 267 // unfocusable items should become visible on the screen until the currently focused item 268 // stays on the screen. 269 for (int i = 0; i < adapter.getItemCount(); i++) { 270 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_UP, true); 271 // adapter position of the currently focused item. 272 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 273 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 274 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 275 (visibleIndex + 1)); 276 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 277 278 assertThat("Child at position " + focusIndex + " should be focused", 279 toFocus.itemView.hasFocus(), is(true)); 280 assertTrue("Focused child should be at least partially visible.", 281 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 282 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 283 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 284 } 285 } 286 287 @Test 288 public void bottomUnfocusableViewsVisibility() throws Throwable { 289 // The maximum number of child views that can be visible at any time. 290 final int visibleChildCount = 5; 291 final int consecutiveFocusablesCount = 2; 292 final int consecutiveUnFocusablesCount = 18; 293 final TestAdapter adapter = new TestAdapter( 294 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 295 RecyclerView mAttachedRv; 296 297 @Override 298 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 299 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 300 // Good to have colors for debugging 301 StateListDrawable stl = new StateListDrawable(); 302 stl.addState(new int[]{android.R.attr.state_focused}, 303 new ColorDrawable(Color.RED)); 304 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 305 //noinspection deprecation used to support kitkat tests 306 testViewHolder.itemView.setBackgroundDrawable(stl); 307 return testViewHolder; 308 } 309 310 @Override 311 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 312 mAttachedRv = recyclerView; 313 } 314 315 @Override 316 public void onBindViewHolder(@NonNull TestViewHolder holder, 317 int position) { 318 super.onBindViewHolder(holder, position); 319 if (position < consecutiveFocusablesCount) { 320 holder.itemView.setFocusable(true); 321 holder.itemView.setFocusableInTouchMode(true); 322 } else { 323 holder.itemView.setFocusable(false); 324 holder.itemView.setFocusableInTouchMode(false); 325 } 326 // This height ensures that some portion of #visibleChildCount'th child is 327 // off-bounds, creating more interesting test scenario. 328 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 329 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 330 } 331 }; 332 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false); 333 waitForFirstLayout(); 334 335 // adapter position of the currently focused item. 336 int focusIndex = 0; 337 View newFocused = mRecyclerView.getChildAt(focusIndex); 338 requestFocus(newFocused, true); 339 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 340 focusIndex); 341 assertThat("Child at position " + focusIndex + " should be focused", 342 toFocus.itemView.hasFocus(), is(true)); 343 344 // adapter position of the item (whether focusable or not) that just becomes fully 345 // visible after focusSearch. 346 int visibleIndex = 0; 347 // The VH of the above adapter position 348 RecyclerView.ViewHolder toVisible = null; 349 350 // Navigate down through the focusable and unfocusable chunks. The focusable items should 351 // become focused one by one until hitting the last focusable item, at which point, 352 // unfocusable items should become visible on the screen until the currently focused item 353 // stays on the screen. 354 for (int i = 0; i < adapter.getItemCount(); i++) { 355 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true); 356 // adapter position of the currently focused item. 357 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 358 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 359 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 360 (visibleIndex + 1)); 361 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 362 363 assertThat("Child at position " + focusIndex + " should be focused", 364 toFocus.itemView.hasFocus(), is(true)); 365 assertTrue("Focused child should be at least partially visible.", 366 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 367 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 368 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 369 } 370 } 371 372 @Test 373 public void leftUnfocusableViewsVisibility() throws Throwable { 374 // The maximum number of child views that can be visible at any time. 375 final int visibleChildCount = 5; 376 final int consecutiveFocusablesCount = 2; 377 final int consecutiveUnFocusablesCount = 18; 378 final TestAdapter adapter = new TestAdapter( 379 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 380 RecyclerView mAttachedRv; 381 382 @Override 383 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 384 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 385 // Good to have colors for debugging 386 StateListDrawable stl = new StateListDrawable(); 387 stl.addState(new int[]{android.R.attr.state_focused}, 388 new ColorDrawable(Color.RED)); 389 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 390 //noinspection deprecation used to support kitkat tests 391 testViewHolder.itemView.setBackgroundDrawable(stl); 392 return testViewHolder; 393 } 394 395 @Override 396 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 397 mAttachedRv = recyclerView; 398 } 399 400 @Override 401 public void onBindViewHolder(@NonNull TestViewHolder holder, 402 int position) { 403 super.onBindViewHolder(holder, position); 404 if (position < consecutiveFocusablesCount) { 405 holder.itemView.setFocusable(true); 406 holder.itemView.setFocusableInTouchMode(true); 407 } else { 408 holder.itemView.setFocusable(false); 409 holder.itemView.setFocusableInTouchMode(false); 410 } 411 // This width ensures that some portion of #visibleChildCount'th child is 412 // off-bounds, creating more interesting test scenario. 413 holder.itemView.setMinimumWidth((mAttachedRv.getWidth() 414 + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount); 415 } 416 }; 417 setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter).reverseLayout(true), 418 false); 419 waitForFirstLayout(); 420 421 // adapter position of the currently focused item. 422 int focusIndex = 0; 423 View newFocused = mRecyclerView.getChildAt(focusIndex); 424 requestFocus(newFocused, true); 425 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 426 focusIndex); 427 assertThat("Child at position " + focusIndex + " should be focused", 428 toFocus.itemView.hasFocus(), is(true)); 429 430 // adapter position of the item (whether focusable or not) that just becomes fully 431 // visible after focusSearch. 432 int visibleIndex = 0; 433 // The VH of the above adapter position 434 RecyclerView.ViewHolder toVisible = null; 435 436 // Navigate left through the focusable and unfocusable chunks. The focusable items should 437 // become focused one by one until hitting the last focusable item, at which point, 438 // unfocusable items should become visible on the screen until the currently focused item 439 // stays on the screen. 440 for (int i = 0; i < adapter.getItemCount(); i++) { 441 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_LEFT, true); 442 // adapter position of the currently focused item. 443 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 444 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 445 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 446 (visibleIndex + 1)); 447 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 448 449 assertThat("Child at position " + focusIndex + " should be focused", 450 toFocus.itemView.hasFocus(), is(true)); 451 assertTrue("Focused child should be at least partially visible.", 452 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 453 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 454 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 455 } 456 } 457 458 @Test 459 public void rightUnfocusableViewsVisibility() throws Throwable { 460 // The maximum number of child views that can be visible at any time. 461 final int visibleChildCount = 5; 462 final int consecutiveFocusablesCount = 2; 463 final int consecutiveUnFocusablesCount = 18; 464 final TestAdapter adapter = new TestAdapter( 465 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 466 RecyclerView mAttachedRv; 467 468 @Override 469 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 470 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 471 // Good to have colors for debugging 472 StateListDrawable stl = new StateListDrawable(); 473 stl.addState(new int[]{android.R.attr.state_focused}, 474 new ColorDrawable(Color.RED)); 475 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 476 //noinspection deprecation used to support kitkat tests 477 testViewHolder.itemView.setBackgroundDrawable(stl); 478 return testViewHolder; 479 } 480 481 @Override 482 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 483 mAttachedRv = recyclerView; 484 } 485 486 @Override 487 public void onBindViewHolder(@NonNull TestViewHolder holder, 488 int position) { 489 super.onBindViewHolder(holder, position); 490 if (position < consecutiveFocusablesCount) { 491 holder.itemView.setFocusable(true); 492 holder.itemView.setFocusableInTouchMode(true); 493 } else { 494 holder.itemView.setFocusable(false); 495 holder.itemView.setFocusableInTouchMode(false); 496 } 497 // This width ensures that some portion of #visibleChildCount'th child is 498 // off-bounds, creating more interesting test scenario. 499 holder.itemView.setMinimumWidth((mAttachedRv.getWidth() 500 + mAttachedRv.getWidth() / (2 * visibleChildCount)) / visibleChildCount); 501 } 502 }; 503 setupByConfig(new Config(HORIZONTAL, false, false).adapter(adapter), false); 504 waitForFirstLayout(); 505 506 // adapter position of the currently focused item. 507 int focusIndex = 0; 508 View newFocused = mRecyclerView.getChildAt(focusIndex); 509 requestFocus(newFocused, true); 510 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 511 focusIndex); 512 assertThat("Child at position " + focusIndex + " should be focused", 513 toFocus.itemView.hasFocus(), is(true)); 514 515 // adapter position of the item (whether focusable or not) that just becomes fully 516 // visible after focusSearch. 517 int visibleIndex = 0; 518 // The VH of the above adapter position 519 RecyclerView.ViewHolder toVisible = null; 520 521 // Navigate right through the focusable and unfocusable chunks. The focusable items should 522 // become focused one by one until hitting the last focusable item, at which point, 523 // unfocusable items should become visible on the screen until the currently focused item 524 // stays on the screen. 525 for (int i = 0; i < adapter.getItemCount(); i++) { 526 focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_RIGHT, true); 527 // adapter position of the currently focused item. 528 focusIndex = Math.min(consecutiveFocusablesCount - 1, (focusIndex + 1)); 529 toFocus = mRecyclerView.findViewHolderForAdapterPosition(focusIndex); 530 visibleIndex = Math.min(consecutiveFocusablesCount + visibleChildCount - 2, 531 (visibleIndex + 1)); 532 toVisible = mRecyclerView.findViewHolderForAdapterPosition(visibleIndex); 533 534 assertThat("Child at position " + focusIndex + " should be focused", 535 toFocus.itemView.hasFocus(), is(true)); 536 assertTrue("Focused child should be at least partially visible.", 537 isViewPartiallyInBound(mRecyclerView, toFocus.itemView)); 538 assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.", 539 isViewFullyInBound(mRecyclerView, toVisible.itemView)); 540 } 541 } 542 543 // Run this test on Jelly Bean and newer because clearFocus on API 15 will call 544 // requestFocus in ViewRootImpl when clearChildFocus is called. Whereas, in API 16 and above, 545 // this call is delayed until after onFocusChange callback is called. Thus on API 16+, there's a 546 // transient state of no child having focus during which onFocusChange is executed. This 547 // transient state does not exist on API 15-. 548 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN) 549 @Test 550 public void unfocusableScrollingWhenFocusCleared() throws Throwable { 551 // The maximum number of child views that can be visible at any time. 552 final int visibleChildCount = 5; 553 final int consecutiveFocusablesCount = 2; 554 final int consecutiveUnFocusablesCount = 18; 555 final TestAdapter adapter = new TestAdapter( 556 consecutiveFocusablesCount + consecutiveUnFocusablesCount) { 557 RecyclerView mAttachedRv; 558 559 @Override 560 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 561 TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 562 // Good to have colors for debugging 563 StateListDrawable stl = new StateListDrawable(); 564 stl.addState(new int[]{android.R.attr.state_focused}, 565 new ColorDrawable(Color.RED)); 566 stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE)); 567 //noinspection deprecation used to support kitkat tests 568 testViewHolder.itemView.setBackgroundDrawable(stl); 569 return testViewHolder; 570 } 571 572 @Override 573 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 574 mAttachedRv = recyclerView; 575 } 576 577 @Override 578 public void onBindViewHolder(@NonNull TestViewHolder holder, 579 int position) { 580 super.onBindViewHolder(holder, position); 581 if (position < consecutiveFocusablesCount) { 582 holder.itemView.setFocusable(true); 583 holder.itemView.setFocusableInTouchMode(true); 584 } else { 585 holder.itemView.setFocusable(false); 586 holder.itemView.setFocusableInTouchMode(false); 587 } 588 // This height ensures that some portion of #visibleChildCount'th child is 589 // off-bounds, creating more interesting test scenario. 590 holder.itemView.setMinimumHeight((mAttachedRv.getHeight() 591 + mAttachedRv.getHeight() / (2 * visibleChildCount)) / visibleChildCount); 592 } 593 }; 594 setupByConfig(new Config(VERTICAL, false, false).adapter(adapter), false); 595 waitForFirstLayout(); 596 597 // adapter position of the currently focused item. 598 int focusIndex = 0; 599 View newFocused = mRecyclerView.getChildAt(focusIndex); 600 requestFocus(newFocused, true); 601 RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition( 602 focusIndex); 603 assertThat("Child at position " + focusIndex + " should be focused", 604 toFocus.itemView.hasFocus(), is(true)); 605 606 final View nextView = focusSearch(mRecyclerView.getFocusedChild(), View.FOCUS_DOWN, true); 607 focusIndex++; 608 assertThat("Child at position " + focusIndex + " should be focused", 609 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(), 610 is(true)); 611 final CountDownLatch focusLatch = new CountDownLatch(1); 612 mActivityRule.runOnUiThread(new Runnable() { 613 @Override 614 public void run() { 615 nextView.setOnFocusChangeListener(new View.OnFocusChangeListener(){ 616 @Override 617 public void onFocusChange(View v, boolean hasFocus) { 618 assertNull("Focus just got cleared and no children should be holding" 619 + " focus now.", mRecyclerView.getFocusedChild()); 620 try { 621 // Calling focusSearch should be a no-op here since even though there 622 // are unfocusable views down to scroll to, none of RV's children hold 623 // focus at this stage. 624 View focusedChild = focusSearch(v, View.FOCUS_DOWN, true); 625 assertNull("Calling focusSearch should be no-op when no children hold" 626 + "focus", focusedChild); 627 // No scrolling should have happened, so any unfocusables that were 628 // invisible should still be invisible. 629 RecyclerView.ViewHolder unforcusablePartiallyVisibleChild = 630 mRecyclerView.findViewHolderForAdapterPosition( 631 visibleChildCount - 1); 632 assertFalse("Child view at adapter pos " + (visibleChildCount - 1) 633 + " should not be fully visible.", 634 isViewFullyInBound(mRecyclerView, 635 unforcusablePartiallyVisibleChild.itemView)); 636 } catch (Throwable t) { 637 postExceptionToInstrumentation(t); 638 } 639 } 640 }); 641 nextView.clearFocus(); 642 focusLatch.countDown(); 643 } 644 }); 645 assertTrue(focusLatch.await(2, TimeUnit.SECONDS)); 646 assertThat("Child at position " + focusIndex + " should no longer be focused", 647 mRecyclerView.findViewHolderForAdapterPosition(focusIndex).itemView.hasFocus(), 648 is(false)); 649 } 650 651 @Test 652 public void removeAnchorItem() throws Throwable { 653 removeAnchorItemTest( 654 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout( 655 false), 100, 0); 656 } 657 658 @Test 659 public void removeAnchorItemReverse() throws Throwable { 660 removeAnchorItemTest( 661 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100, 662 0); 663 } 664 665 @Test 666 public void removeAnchorItemStackFromEnd() throws Throwable { 667 removeAnchorItemTest( 668 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100, 669 99); 670 } 671 672 @Test 673 public void removeAnchorItemStackFromEndAndReverse() throws Throwable { 674 removeAnchorItemTest( 675 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100, 676 99); 677 } 678 679 @Test 680 public void removeAnchorItemHorizontal() throws Throwable { 681 removeAnchorItemTest( 682 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout( 683 false), 100, 0); 684 } 685 686 @Test 687 public void removeAnchorItemReverseHorizontal() throws Throwable { 688 removeAnchorItemTest( 689 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true), 690 100, 0); 691 } 692 693 @Test 694 public void removeAnchorItemStackFromEndHorizontal() throws Throwable { 695 removeAnchorItemTest( 696 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false), 697 100, 99); 698 } 699 700 @Test 701 public void removeAnchorItemStackFromEndAndReverseHorizontal() throws Throwable { 702 removeAnchorItemTest( 703 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100, 704 99); 705 } 706 707 /** 708 * This tests a regression where predictive animations were not working as expected when the 709 * first item is removed and there aren't any more items to add from that direction. 710 * First item refers to the default anchor item. 711 */ 712 public void removeAnchorItemTest(final Config config, int adapterSize, 713 final int removePos) throws Throwable { 714 config.adapter(new TestAdapter(adapterSize) { 715 @Override 716 public void onBindViewHolder(@NonNull TestViewHolder holder, 717 int position) { 718 super.onBindViewHolder(holder, position); 719 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 720 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 721 lp = new ViewGroup.MarginLayoutParams(0, 0); 722 holder.itemView.setLayoutParams(lp); 723 } 724 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 725 final int maxSize; 726 if (config.mOrientation == HORIZONTAL) { 727 maxSize = mRecyclerView.getWidth(); 728 mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 729 } else { 730 maxSize = mRecyclerView.getHeight(); 731 mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT; 732 } 733 734 final int desiredSize; 735 if (position == removePos) { 736 // make it large 737 desiredSize = maxSize / 4; 738 } else { 739 // make it small 740 desiredSize = maxSize / 8; 741 } 742 if (config.mOrientation == HORIZONTAL) { 743 mlp.width = desiredSize; 744 } else { 745 mlp.height = desiredSize; 746 } 747 } 748 }); 749 setupByConfig(config, true); 750 final int childCount = mLayoutManager.getChildCount(); 751 RecyclerView.ViewHolder toBeRemoved = null; 752 List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>(); 753 for (int i = 0; i < childCount; i++) { 754 View child = mLayoutManager.getChildAt(i); 755 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 756 if (holder.getAdapterPosition() == removePos) { 757 toBeRemoved = holder; 758 } else { 759 toBeMoved.add(holder); 760 } 761 } 762 assertNotNull("test sanity", toBeRemoved); 763 assertEquals("test sanity", childCount - 1, toBeMoved.size()); 764 LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator(); 765 mRecyclerView.setItemAnimator(loggingItemAnimator); 766 loggingItemAnimator.reset(); 767 loggingItemAnimator.expectRunPendingAnimationsCall(1); 768 mLayoutManager.expectLayouts(2); 769 mTestAdapter.deleteAndNotify(removePos, 1); 770 mLayoutManager.waitForLayout(1); 771 loggingItemAnimator.waitForPendingAnimationsCall(2); 772 assertTrue("removed child should receive remove animation", 773 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved)); 774 for (RecyclerView.ViewHolder vh : toBeMoved) { 775 assertTrue("view holder should be in moved list", 776 loggingItemAnimator.mMoveVHs.contains(vh)); 777 } 778 List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>(); 779 for (int i = 0; i < mLayoutManager.getChildCount(); i++) { 780 View child = mLayoutManager.getChildAt(i); 781 RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child); 782 if (toBeRemoved != holder && !toBeMoved.contains(holder)) { 783 newHolders.add(holder); 784 } 785 } 786 assertTrue("some new children should show up for the new space", newHolders.size() > 0); 787 assertEquals("no items should receive animate add since they are not new", 0, 788 loggingItemAnimator.mAddVHs.size()); 789 for (RecyclerView.ViewHolder holder : newHolders) { 790 assertTrue("new holder should receive a move animation", 791 loggingItemAnimator.mMoveVHs.contains(holder)); 792 } 793 assertTrue("control against adding too many children due to bad layout state preparation." 794 + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(), 795 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/); 796 } 797 798 void waitOneCycle() throws Throwable { 799 mActivityRule.runOnUiThread(new Runnable() { 800 @Override 801 public void run() { 802 } 803 }); 804 } 805 806 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT) 807 @Test 808 public void hiddenNoneRemoveViewAccessibility() throws Throwable { 809 final Config config = new Config(); 810 int adapterSize = 1000; 811 final boolean[] firstItemSpecialSize = new boolean[] {false}; 812 TestAdapter adapter = new TestAdapter(adapterSize) { 813 @Override 814 public void onBindViewHolder(@NonNull TestViewHolder holder, 815 int position) { 816 super.onBindViewHolder(holder, position); 817 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); 818 if (!(lp instanceof ViewGroup.MarginLayoutParams)) { 819 lp = new ViewGroup.MarginLayoutParams(0, 0); 820 holder.itemView.setLayoutParams(lp); 821 } 822 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; 823 final int maxSize; 824 if (config.mOrientation == HORIZONTAL) { 825 maxSize = mRecyclerView.getWidth(); 826 mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT; 827 } else { 828 maxSize = mRecyclerView.getHeight(); 829 mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT; 830 } 831 832 final int desiredSize; 833 if (position == 0 && firstItemSpecialSize[0]) { 834 desiredSize = maxSize / 3; 835 } else { 836 desiredSize = maxSize / 8; 837 } 838 if (config.mOrientation == HORIZONTAL) { 839 mlp.width = desiredSize; 840 } else { 841 mlp.height = desiredSize; 842 } 843 } 844 845 @Override 846 public void onBindViewHolder(TestViewHolder holder, 847 int position, List<Object> payloads) { 848 onBindViewHolder(holder, position); 849 } 850 }; 851 adapter.setHasStableIds(false); 852 config.adapter(adapter); 853 setupByConfig(config, true); 854 final DummyItemAnimator itemAnimator = new DummyItemAnimator(); 855 mRecyclerView.setItemAnimator(itemAnimator); 856 857 // push last item out by increasing first item's size 858 final int childBeingPushOut = mLayoutManager.getChildCount() - 1; 859 RecyclerView.ViewHolder itemViewHolder = mRecyclerView 860 .findViewHolderForAdapterPosition(childBeingPushOut); 861 final int originalAccessibility = ViewCompat.getImportantForAccessibility( 862 itemViewHolder.itemView); 863 assertTrue(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO == originalAccessibility 864 || ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES == originalAccessibility); 865 866 itemAnimator.expect(DummyItemAnimator.MOVE_START, 1); 867 mActivityRule.runOnUiThread(new Runnable() { 868 @Override 869 public void run() { 870 firstItemSpecialSize[0] = true; 871 mTestAdapter.notifyItemChanged(0, "XXX"); 872 } 873 }); 874 // wait till itemAnimator starts which will block itemView's accessibility 875 itemAnimator.waitFor(DummyItemAnimator.MOVE_START); 876 // RV Changes accessiblity after onMoveStart, so wait one more cycle. 877 waitOneCycle(); 878 assertTrue(itemAnimator.getMovesAnimations().contains(itemViewHolder)); 879 assertEquals(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, 880 ViewCompat.getImportantForAccessibility(itemViewHolder.itemView)); 881 882 // notify Change again to run predictive animation. 883 mLayoutManager.expectLayouts(2); 884 mActivityRule.runOnUiThread(new Runnable() { 885 @Override 886 public void run() { 887 mTestAdapter.notifyItemChanged(0, "XXX"); 888 } 889 }); 890 mLayoutManager.waitForLayout(1); 891 mActivityRule.runOnUiThread(new Runnable() { 892 @Override 893 public void run() { 894 itemAnimator.endAnimations(); 895 } 896 }); 897 // scroll to the view being pushed out, it should get same view from cache as the item 898 // in adapter does not change. 899 smoothScrollToPosition(childBeingPushOut); 900 RecyclerView.ViewHolder itemViewHolder2 = mRecyclerView 901 .findViewHolderForAdapterPosition(childBeingPushOut); 902 assertSame(itemViewHolder, itemViewHolder2); 903 // the important for accessibility should be reset to YES/AUTO: 904 final int newAccessibility = ViewCompat.getImportantForAccessibility( 905 itemViewHolder.itemView); 906 assertTrue(ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO == newAccessibility 907 || ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES == newAccessibility); 908 } 909 910 @Test 911 public void layoutFrozenBug70402422() throws Throwable { 912 final Config config = new Config(); 913 TestAdapter adapter = new TestAdapter(2); 914 adapter.setHasStableIds(false); 915 config.adapter(adapter); 916 setupByConfig(config, true); 917 final DummyItemAnimator itemAnimator = new DummyItemAnimator(); 918 mRecyclerView.setItemAnimator(itemAnimator); 919 920 final View firstItemView = mRecyclerView 921 .findViewHolderForAdapterPosition(0).itemView; 922 923 itemAnimator.expect(DummyItemAnimator.REMOVE_START, 1); 924 mTestAdapter.deleteAndNotify(1, 1); 925 itemAnimator.waitFor(DummyItemAnimator.REMOVE_START); 926 927 mActivityRule.runOnUiThread(new Runnable() { 928 @Override 929 public void run() { 930 mRecyclerView.setLayoutFrozen(true); 931 } 932 }); 933 // requestLayout during item animation, which should be eaten by setLayoutFrozen(true) 934 mActivityRule.runOnUiThread(new Runnable() { 935 @Override 936 public void run() { 937 firstItemView.requestLayout(); 938 } 939 }); 940 assertTrue(firstItemView.isLayoutRequested()); 941 assertFalse(mRecyclerView.isLayoutRequested()); 942 mActivityRule.runOnUiThread(new Runnable() { 943 @Override 944 public void run() { 945 itemAnimator.endAnimations(); 946 } 947 }); 948 // When setLayoutFrozen(false), the firstItemView should run a layout pass and clear 949 // isLayoutRequested() flag. 950 mLayoutManager.expectLayouts(1); 951 mActivityRule.runOnUiThread(new Runnable() { 952 @Override 953 public void run() { 954 mRecyclerView.setLayoutFrozen(false); 955 } 956 }); 957 mLayoutManager.waitForLayout(1); 958 assertFalse(firstItemView.isLayoutRequested()); 959 assertFalse(mRecyclerView.isLayoutRequested()); 960 } 961 962 @Test 963 public void keepFocusOnRelayout() throws Throwable { 964 setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true); 965 int center = (mLayoutManager.findLastVisibleItemPosition() 966 - mLayoutManager.findFirstVisibleItemPosition()) / 2; 967 final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center); 968 final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView); 969 requestFocus(vh.itemView, true); 970 assertTrue("view should have the focus", vh.itemView.hasFocus()); 971 // add a bunch of items right before that view, make sure it keeps its position 972 mLayoutManager.expectLayouts(2); 973 final int childCountToAdd = mRecyclerView.getChildCount() * 2; 974 mTestAdapter.addAndNotify(center, childCountToAdd); 975 center += childCountToAdd; // offset item 976 mLayoutManager.waitForLayout(2); 977 mLayoutManager.waitForAnimationsToEnd(20); 978 final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center); 979 assertNotNull("focused child should stay in layout", postVH); 980 assertSame("same view holder should be kept for unchanged child", vh, postVH); 981 assertEquals("focused child's screen position should stay unchanged", top, 982 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView)); 983 } 984 985 @Test 986 public void keepFullFocusOnResize() throws Throwable { 987 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true); 988 } 989 990 @Test 991 public void keepPartialFocusOnResize() throws Throwable { 992 keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false); 993 } 994 995 @Test 996 public void keepReverseFullFocusOnResize() throws Throwable { 997 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true); 998 } 999 1000 @Test 1001 public void keepReversePartialFocusOnResize() throws Throwable { 1002 keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false); 1003 } 1004 1005 @Test 1006 public void keepStackFromEndFullFocusOnResize() throws Throwable { 1007 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true); 1008 } 1009 1010 @Test 1011 public void keepStackFromEndPartialFocusOnResize() throws Throwable { 1012 keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false); 1013 } 1014 1015 public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable { 1016 setupByConfig(config, true); 1017 final int targetPosition; 1018 if (config.mStackFromEnd) { 1019 targetPosition = mLayoutManager.findFirstVisibleItemPosition(); 1020 } else { 1021 targetPosition = mLayoutManager.findLastVisibleItemPosition(); 1022 } 1023 final OrientationHelper helper = mLayoutManager.mOrientationHelper; 1024 final RecyclerView.ViewHolder vh = mRecyclerView 1025 .findViewHolderForLayoutPosition(targetPosition); 1026 1027 // scroll enough to offset the child 1028 int startMargin = helper.getDecoratedStart(vh.itemView) - 1029 helper.getStartAfterPadding(); 1030 int endMargin = helper.getEndAfterPadding() - 1031 helper.getDecoratedEnd(vh.itemView); 1032 Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin); 1033 requestFocus(vh.itemView, true); 1034 assertTrue("view should gain the focus", vh.itemView.hasFocus()); 1035 // scroll enough to offset the child 1036 startMargin = helper.getDecoratedStart(vh.itemView) - 1037 helper.getStartAfterPadding(); 1038 endMargin = helper.getEndAfterPadding() - 1039 helper.getDecoratedEnd(vh.itemView); 1040 1041 Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin); 1042 assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0); 1043 1044 int expectedOffset = 0; 1045 boolean offsetAtStart = false; 1046 if (!fullyVisible) { 1047 // move it a bit such that it is no more fully visible 1048 final int childSize = helper 1049 .getDecoratedMeasurement(vh.itemView); 1050 expectedOffset = childSize / 3; 1051 if (startMargin < endMargin) { 1052 scrollBy(expectedOffset); 1053 offsetAtStart = true; 1054 } else { 1055 scrollBy(-expectedOffset); 1056 offsetAtStart = false; 1057 } 1058 startMargin = helper.getDecoratedStart(vh.itemView) - 1059 helper.getStartAfterPadding(); 1060 endMargin = helper.getEndAfterPadding() - 1061 helper.getDecoratedEnd(vh.itemView); 1062 assertTrue("test sanity, view should not be fully visible", startMargin < 0 1063 || endMargin < 0); 1064 } 1065 1066 mLayoutManager.expectLayouts(1); 1067 mActivityRule.runOnUiThread(new Runnable() { 1068 @Override 1069 public void run() { 1070 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams(); 1071 if (config.mOrientation == HORIZONTAL) { 1072 layoutParams.width = mRecyclerView.getWidth() / 2; 1073 } else { 1074 layoutParams.height = mRecyclerView.getHeight() / 2; 1075 } 1076 mRecyclerView.setLayoutParams(layoutParams); 1077 } 1078 }); 1079 Thread.sleep(100); 1080 // add a bunch of items right before that view, make sure it keeps its position 1081 mLayoutManager.waitForLayout(2); 1082 mLayoutManager.waitForAnimationsToEnd(20); 1083 assertTrue("view should preserve the focus", vh.itemView.hasFocus()); 1084 final RecyclerView.ViewHolder postVH = mRecyclerView 1085 .findViewHolderForLayoutPosition(targetPosition); 1086 assertNotNull("focused child should stay in layout", postVH); 1087 assertSame("same view holder should be kept for unchanged child", vh, postVH); 1088 View focused = postVH.itemView; 1089 1090 startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding(); 1091 endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused); 1092 1093 assertTrue("focused child should be somewhat visible", 1094 helper.getDecoratedStart(focused) < helper.getEndAfterPadding() 1095 && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding()); 1096 if (fullyVisible) { 1097 assertTrue("focused child end should stay fully visible", 1098 endMargin >= 0); 1099 assertTrue("focused child start should stay fully visible", 1100 startMargin >= 0); 1101 } else { 1102 if (offsetAtStart) { 1103 assertTrue("start should preserve its offset", startMargin < 0); 1104 assertTrue("end should be visible", endMargin >= 0); 1105 } else { 1106 assertTrue("end should preserve its offset", endMargin < 0); 1107 assertTrue("start should be visible", startMargin >= 0); 1108 } 1109 } 1110 } 1111 1112 @Test 1113 public void scrollToPositionWithPredictive() throws Throwable { 1114 scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET); 1115 removeRecyclerView(); 1116 scrollToPositionWithPredictive(3, 20); 1117 removeRecyclerView(); 1118 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 1119 LinearLayoutManager.INVALID_OFFSET); 1120 removeRecyclerView(); 1121 scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10); 1122 } 1123 1124 @Test 1125 public void recycleDuringAnimations() throws Throwable { 1126 final AtomicInteger childCount = new AtomicInteger(0); 1127 final TestAdapter adapter = new TestAdapter(300) { 1128 @Override 1129 public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 1130 int viewType) { 1131 final int cnt = childCount.incrementAndGet(); 1132 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType); 1133 if (DEBUG) { 1134 Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder); 1135 } 1136 return testViewHolder; 1137 } 1138 }; 1139 setupByConfig(new Config(VERTICAL, false, false).itemCount(300) 1140 .adapter(adapter), true); 1141 1142 final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 1143 @Override 1144 public void putRecycledView(RecyclerView.ViewHolder scrap) { 1145 super.putRecycledView(scrap); 1146 int cnt = childCount.decrementAndGet(); 1147 if (DEBUG) { 1148 Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap); 1149 } 1150 } 1151 1152 @Override 1153 public RecyclerView.ViewHolder getRecycledView(int viewType) { 1154 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType); 1155 if (recycledView != null) { 1156 final int cnt = childCount.incrementAndGet(); 1157 if (DEBUG) { 1158 Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView); 1159 } 1160 } 1161 return recycledView; 1162 } 1163 }; 1164 pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500); 1165 mRecyclerView.setRecycledViewPool(pool); 1166 1167 1168 // now keep adding children to trigger more children being created etc. 1169 for (int i = 0; i < 100; i ++) { 1170 adapter.addAndNotify(15, 1); 1171 Thread.sleep(15); 1172 } 1173 getInstrumentation().waitForIdleSync(); 1174 waitForAnimations(2); 1175 assertEquals("Children count should add up", childCount.get(), 1176 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 1177 1178 // now trigger lots of add again, followed by a scroll to position 1179 for (int i = 0; i < 100; i ++) { 1180 adapter.addAndNotify(5 + (i % 3) * 3, 1); 1181 Thread.sleep(25); 1182 } 1183 1184 final AtomicInteger lastVisiblePosition = new AtomicInteger(); 1185 mActivityRule.runOnUiThread(new Runnable() { 1186 @Override 1187 public void run() { 1188 lastVisiblePosition.set(mLayoutManager.findLastVisibleItemPosition()); 1189 } 1190 }); 1191 1192 smoothScrollToPosition(lastVisiblePosition.get() + 20); 1193 waitForAnimations(2); 1194 getInstrumentation().waitForIdleSync(); 1195 assertEquals("Children count should add up", childCount.get(), 1196 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size()); 1197 } 1198 1199 1200 @Test 1201 public void dontRecycleChildrenOnDetach() throws Throwable { 1202 setupByConfig(new Config().recycleChildrenOnDetach(false), true); 1203 mActivityRule.runOnUiThread(new Runnable() { 1204 @Override 1205 public void run() { 1206 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 1207 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView); 1208 assertEquals("No views are recycled", recyclerSize, 1209 mRecyclerView.mRecycler.getRecycledViewPool().size()); 1210 } 1211 }); 1212 } 1213 1214 @Test 1215 public void recycleChildrenOnDetach() throws Throwable { 1216 setupByConfig(new Config().recycleChildrenOnDetach(true), true); 1217 final int childCount = mLayoutManager.getChildCount(); 1218 mActivityRule.runOnUiThread(new Runnable() { 1219 @Override 1220 public void run() { 1221 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size(); 1222 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews( 1223 mTestAdapter.getItemViewType(0), recyclerSize + childCount); 1224 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView); 1225 assertEquals("All children should be recycled", childCount + recyclerSize, 1226 mRecyclerView.mRecycler.getRecycledViewPool().size()); 1227 } 1228 }); 1229 } 1230 1231 @Test 1232 public void scrollAndClear() throws Throwable { 1233 setupByConfig(new Config(), true); 1234 1235 assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0); 1236 1237 mLayoutManager.expectLayouts(1); 1238 mActivityRule.runOnUiThread(new Runnable() { 1239 @Override 1240 public void run() { 1241 mLayoutManager.scrollToPositionWithOffset(1, 0); 1242 mTestAdapter.clearOnUIThread(); 1243 } 1244 }); 1245 mLayoutManager.waitForLayout(2); 1246 1247 assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size()); 1248 } 1249 1250 1251 @Test 1252 public void accessibilityPositions() throws Throwable { 1253 setupByConfig(new Config(VERTICAL, false, false), true); 1254 final AccessibilityDelegateCompat delegateCompat = mRecyclerView 1255 .getCompatAccessibilityDelegate(); 1256 final AccessibilityEvent event = AccessibilityEvent.obtain(); 1257 mActivityRule.runOnUiThread(new Runnable() { 1258 @Override 1259 public void run() { 1260 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event); 1261 } 1262 }); 1263 assertEquals("result should have first position", 1264 event.getFromIndex(), 1265 mLayoutManager.findFirstVisibleItemPosition()); 1266 assertEquals("result should have last position", 1267 event.getToIndex(), 1268 mLayoutManager.findLastVisibleItemPosition()); 1269 } 1270 } 1271