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