1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.recyclerview.widget; 17 18 import static org.hamcrest.CoreMatchers.instanceOf; 19 import static org.hamcrest.CoreMatchers.is; 20 import static org.hamcrest.CoreMatchers.not; 21 import static org.hamcrest.CoreMatchers.notNullValue; 22 import static org.hamcrest.CoreMatchers.sameInstance; 23 import static org.hamcrest.MatcherAssert.assertThat; 24 25 import android.support.test.filters.MediumTest; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.TextView; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.recyclerview.test.R; 34 35 import org.junit.Test; 36 import org.junit.runner.RunWith; 37 import org.junit.runners.Parameterized; 38 39 import java.util.Arrays; 40 import java.util.List; 41 import java.util.concurrent.atomic.AtomicLong; 42 43 /** 44 * This class only tests the RV's focus recovery logic as focus moves between two views that 45 * represent the same item in the adapter. Keeping a focused view visible is up-to-the 46 * LayoutManager and all FW LayoutManagers already have tests for it. 47 */ 48 @MediumTest 49 @RunWith(Parameterized.class) 50 public class RecyclerViewFocusRecoveryTest extends BaseRecyclerViewInstrumentationTest { 51 TestLayoutManager mLayoutManager; 52 TestAdapter mAdapter; 53 int mChildCount = 10; 54 55 // Parameter indicating whether RV's children are simple views (false) or ViewGroups (true). 56 private final boolean mFocusOnChild; 57 // Parameter indicating whether RV recovers focus after layout is finished. 58 private final boolean mDisableRecovery; 59 // Parameter indicating whether animation is enabled for the ViewHolder items. 60 private final boolean mDisableAnimation; 61 62 @Parameterized.Parameters(name = "focusSubChild:{0},disableRecovery:{1}," 63 + "disableAnimation:{2}") getParams()64 public static List<Object[]> getParams() { 65 return Arrays.asList( 66 new Object[]{false, false, true}, 67 new Object[]{true, false, true}, 68 new Object[]{false, true, true}, 69 new Object[]{true, true, true}, 70 new Object[]{false, false, false}, 71 new Object[]{true, false, false}, 72 new Object[]{false, true, false}, 73 new Object[]{true, true, false} 74 ); 75 } 76 RecyclerViewFocusRecoveryTest(boolean focusOnChild, boolean disableRecovery, boolean disableAnimation)77 public RecyclerViewFocusRecoveryTest(boolean focusOnChild, boolean disableRecovery, 78 boolean disableAnimation) { 79 super(false); 80 mFocusOnChild = focusOnChild; 81 mDisableRecovery = disableRecovery; 82 mDisableAnimation = disableAnimation; 83 } 84 setupBasic()85 void setupBasic() throws Throwable { 86 setupBasic(false); 87 } 88 setupBasic(boolean hasStableIds)89 void setupBasic(boolean hasStableIds) throws Throwable { 90 TestAdapter adapter = new FocusTestAdapter(mChildCount); 91 adapter.setHasStableIds(hasStableIds); 92 setupBasic(adapter, null); 93 } 94 setupBasic(TestLayoutManager layoutManager)95 void setupBasic(TestLayoutManager layoutManager) throws Throwable { 96 setupBasic(null, layoutManager); 97 } 98 setupBasic(TestAdapter adapter)99 void setupBasic(TestAdapter adapter) throws Throwable { 100 setupBasic(adapter, null); 101 } 102 setupBasic(@ullable TestAdapter adapter, @Nullable TestLayoutManager layoutManager)103 void setupBasic(@Nullable TestAdapter adapter, @Nullable TestLayoutManager layoutManager) 104 throws Throwable { 105 RecyclerView recyclerView = new RecyclerView(getActivity()); 106 if (layoutManager == null) { 107 layoutManager = new FocusLayoutManager(); 108 } 109 110 if (adapter == null) { 111 adapter = new FocusTestAdapter(mChildCount); 112 } 113 mLayoutManager = layoutManager; 114 mAdapter = adapter; 115 recyclerView.setAdapter(adapter); 116 recyclerView.setLayoutManager(mLayoutManager); 117 recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery); 118 if (mDisableAnimation) { 119 recyclerView.setItemAnimator(null); 120 } 121 mLayoutManager.expectLayouts(1); 122 setRecyclerView(recyclerView); 123 mLayoutManager.waitForLayout(1); 124 } 125 126 @Test testFocusRecoveryInChange()127 public void testFocusRecoveryInChange() throws Throwable { 128 setupBasic(); 129 mLayoutManager.setSupportsPredictive(true); 130 final RecyclerView.ViewHolder oldVh = focusVh(3); 131 132 mLayoutManager.expectLayouts(mDisableAnimation ? 1 : 2); 133 mAdapter.changeAndNotify(3, 1); 134 mLayoutManager.waitForLayout(2); 135 if (!mDisableAnimation) { 136 // waiting for RV's ItemAnimator to finish the animation of the removed item 137 waitForAnimations(2); 138 } 139 140 mActivityRule.runOnUiThread(new Runnable() { 141 @Override 142 public void run() { 143 RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3); 144 assertFocusTransition(oldVh, newVh, false); 145 146 } 147 }); 148 } 149 150 @Test testFocusRecoveryAfterRemovingFocusedChild()151 public void testFocusRecoveryAfterRemovingFocusedChild() throws Throwable { 152 setupBasic(true); 153 FocusViewHolder fvh = cast(focusVh(4)); 154 155 assertThat("test sanity", fvh, notNullValue()); 156 assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true)); 157 158 assertThat("RV should pass the focus down to its children", 159 mRecyclerView.isFocused(), is(false)); 160 assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(), 161 is(true)); 162 assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(), 163 is(true)); 164 165 mLayoutManager.expectLayouts(1); 166 mActivityRule.runOnUiThread(new Runnable() { 167 @Override 168 public void run() { 169 // removing focused child 170 mAdapter.mItems.remove(4); 171 mAdapter.notifyItemRemoved(4); 172 } 173 }); 174 mLayoutManager.waitForLayout(1); 175 if (!mDisableAnimation) { 176 // waiting for RV's ItemAnimator to finish the animation of the removed item 177 waitForAnimations(2); 178 } 179 assertThat("RV should have " + (mChildCount - 1) + " instead of " 180 + mRecyclerView.getChildCount() + " children", 181 mChildCount - 1, is(mRecyclerView.getChildCount())); 182 assertFocusAfterLayout(4, 0); 183 } 184 185 @Test testFocusRecoveryAfterMovingFocusedChild()186 public void testFocusRecoveryAfterMovingFocusedChild() throws Throwable { 187 setupBasic(true); 188 FocusViewHolder fvh = cast(focusVh(3)); 189 190 assertThat("test sanity", fvh, notNullValue()); 191 assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true)); 192 193 assertThat("RV should pass the focus down to its children", 194 mRecyclerView.isFocused(), is(false)); 195 assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(), 196 is(true)); 197 assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(), 198 is(true)); 199 200 mLayoutManager.expectLayouts(1); 201 mAdapter.moveAndNotify(3, 1); 202 mLayoutManager.waitForLayout(1); 203 if (!mDisableAnimation) { 204 // waiting for RV's ItemAnimator to finish the animation of the removed item 205 waitForAnimations(2); 206 } 207 assertFocusAfterLayout(1, 1); 208 } 209 210 @Test testFocusRecoveryAfterRemovingLastChild()211 public void testFocusRecoveryAfterRemovingLastChild() throws Throwable { 212 mChildCount = 1; 213 setupBasic(true); 214 FocusViewHolder fvh = cast(focusVh(0)); 215 216 assertThat("test sanity", fvh, notNullValue()); 217 assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true)); 218 219 assertThat("RV should pass the focus down to its children", 220 mRecyclerView.isFocused(), is(false)); 221 assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(), 222 is(true)); 223 assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(), 224 is(true)); 225 226 mLayoutManager.expectLayouts(1); 227 mActivityRule.runOnUiThread(new Runnable() { 228 @Override 229 public void run() { 230 // removing focused child 231 mAdapter.mItems.remove(0); 232 mAdapter.notifyDataSetChanged(); 233 } 234 }); 235 mLayoutManager.waitForLayout(1); 236 if (!mDisableAnimation) { 237 // waiting for RV's ItemAnimator to finish the animation of the removed item 238 waitForAnimations(2); 239 } 240 assertThat("RV should have " + (mChildCount - 1) + " instead of " 241 + mRecyclerView.getChildCount() + " children", 242 mChildCount - 1, is(mRecyclerView.getChildCount())); 243 assertFocusAfterLayout(-1, -1); 244 } 245 246 @Test testFocusRecoveryAfterAddingFirstChild()247 public void testFocusRecoveryAfterAddingFirstChild() throws Throwable { 248 mChildCount = 0; 249 setupBasic(true); 250 mActivityRule.runOnUiThread(new Runnable() { 251 @Override 252 public void run() { 253 requestFocusOnRV(); 254 } 255 }); 256 257 mLayoutManager.expectLayouts(1); 258 mActivityRule.runOnUiThread(new Runnable() { 259 @Override 260 public void run() { 261 // adding first child 262 mAdapter.mItems.add(0, new Item(0, TestAdapter.DEFAULT_ITEM_PREFIX)); 263 mAdapter.notifyDataSetChanged(); 264 } 265 }); 266 mLayoutManager.waitForLayout(1); 267 if (!mDisableAnimation) { 268 // waiting for RV's ItemAnimator to finish the animation of the removed item 269 waitForAnimations(2); 270 } 271 assertFocusAfterLayout(0, -1); 272 } 273 274 @Test testFocusRecoveryAfterChangingFocusableFlag()275 public void testFocusRecoveryAfterChangingFocusableFlag() throws Throwable { 276 setupBasic(true); 277 FocusViewHolder fvh = cast(focusVh(6)); 278 279 assertThat("test sanity", fvh, notNullValue()); 280 assertThat("RV should have focus", mRecyclerView.hasFocus(), is(true)); 281 282 assertThat("RV should pass the focus down to its children", 283 mRecyclerView.isFocused(), is(false)); 284 assertThat("Viewholder did not receive focus", fvh.itemView.hasFocus(), 285 is(true)); 286 assertThat("Viewholder is not focused", fvh.getViewToFocus().isFocused(), 287 is(true)); 288 289 mLayoutManager.expectLayouts(1); 290 mActivityRule.runOnUiThread(new Runnable() { 291 @Override 292 public void run() { 293 Item item = mAdapter.mItems.get(6); 294 item.setFocusable(false); 295 mAdapter.notifyItemChanged(6); 296 } 297 }); 298 mLayoutManager.waitForLayout(1); 299 if (!mDisableAnimation) { 300 waitForAnimations(2); 301 } 302 FocusViewHolder newVh = cast(mRecyclerView.findViewHolderForAdapterPosition(6)); 303 assertThat("VH should no longer be focusable", newVh.getViewToFocus().isFocusable(), 304 is(false)); 305 assertFocusAfterLayout(7, 0); 306 } 307 308 @Test testFocusRecoveryBeforeLayoutWithFocusBefore()309 public void testFocusRecoveryBeforeLayoutWithFocusBefore() throws Throwable { 310 testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 311 } 312 313 @Test testFocusRecoveryBeforeLayoutWithFocusAfter()314 public void testFocusRecoveryBeforeLayoutWithFocusAfter() throws Throwable { 315 testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS); 316 } 317 318 @Test testFocusRecoveryBeforeLayoutWithFocusBlocked()319 public void testFocusRecoveryBeforeLayoutWithFocusBlocked() throws Throwable { 320 testFocusRecoveryBeforeLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 321 } 322 323 @Test testFocusRecoveryDuringLayoutWithFocusBefore()324 public void testFocusRecoveryDuringLayoutWithFocusBefore() throws Throwable { 325 testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 326 } 327 328 @Test testFocusRecoveryDuringLayoutWithFocusAfter()329 public void testFocusRecoveryDuringLayoutWithFocusAfter() throws Throwable { 330 testFocusRecoveryDuringLayout(ViewGroup.FOCUS_AFTER_DESCENDANTS); 331 } 332 333 @Test testFocusRecoveryDuringLayoutWithFocusBlocked()334 public void testFocusRecoveryDuringLayoutWithFocusBlocked() throws Throwable { 335 testFocusRecoveryDuringLayout(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 336 } 337 338 /** 339 * Tests whether the focus is correctly recovered when requestFocus on RV is called before 340 * laying out the children. 341 * @throws Throwable 342 */ testFocusRecoveryBeforeLayout(int descendantFocusability)343 private void testFocusRecoveryBeforeLayout(int descendantFocusability) throws Throwable { 344 RecyclerView recyclerView = new RecyclerView(getActivity()); 345 recyclerView.setDescendantFocusability(descendantFocusability); 346 mLayoutManager = new FocusLayoutManager(); 347 mAdapter = new FocusTestAdapter(10); 348 recyclerView.setLayoutManager(mLayoutManager); 349 recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery); 350 if (mDisableAnimation) { 351 recyclerView.setItemAnimator(null); 352 } 353 setRecyclerView(recyclerView); 354 assertThat("RV should always be focusable", mRecyclerView.isFocusable(), is(true)); 355 356 mLayoutManager.expectLayouts(1); 357 mActivityRule.runOnUiThread(new Runnable() { 358 @Override 359 public void run() { 360 requestFocusOnRV(); 361 mRecyclerView.setAdapter(mAdapter); 362 } 363 }); 364 mLayoutManager.waitForLayout(1); 365 assertFocusAfterLayout(0, -1); 366 } 367 368 /** 369 * Tests whether the focus is correctly recovered when requestFocus on RV is called during 370 * laying out the children. 371 * @throws Throwable 372 */ testFocusRecoveryDuringLayout(int descendantFocusability)373 private void testFocusRecoveryDuringLayout(int descendantFocusability) throws Throwable { 374 RecyclerView recyclerView = new RecyclerView(getActivity()); 375 recyclerView.setDescendantFocusability(descendantFocusability); 376 mLayoutManager = new FocusLayoutManager() { 377 @Override 378 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 379 super.onLayoutChildren(recycler, state); 380 requestFocusOnRV(); 381 } 382 }; 383 mAdapter = new FocusTestAdapter(10); 384 recyclerView.setAdapter(mAdapter); 385 recyclerView.setLayoutManager(mLayoutManager); 386 if (mDisableAnimation) { 387 recyclerView.setItemAnimator(null); 388 } 389 recyclerView.setPreserveFocusAfterLayout(!mDisableRecovery); 390 mLayoutManager.expectLayouts(1); 391 setRecyclerView(recyclerView); 392 mLayoutManager.waitForLayout(1); 393 assertFocusAfterLayout(0, -1); 394 } 395 requestFocusOnRV()396 private void requestFocusOnRV() { 397 assertThat("RV initially has no focus", mRecyclerView.hasFocus(), is(false)); 398 assertThat("RV initially is not focused", mRecyclerView.isFocused(), is(false)); 399 mRecyclerView.requestFocus(); 400 String msg = !mRecyclerView.isComputingLayout() ? " before laying out the children" 401 : " during laying out the children"; 402 assertThat("RV should have focus after calling requestFocus()" + msg, 403 mRecyclerView.hasFocus(), is(true)); 404 assertThat("RV after calling requestFocus() should become focused" + msg, 405 mRecyclerView.isFocused(), is(true)); 406 } 407 408 /** 409 * Asserts whether RV and one of its children have the correct focus flags after the layout is 410 * complete. This is normally called once the RV layout is complete after initiating 411 * notifyItemChanged. 412 * @param focusedChildIndexWhenRecoveryEnabled 413 * This index is relevant when mDisableRecovery is false. In that case, it refers to the index 414 * of the child that should have focus if the ancestors allow passing down the focus. -1 415 * indicates none of the children can receive focus even if the ancestors don't block focus, in 416 * which case RV holds and becomes focused. 417 * @param focusedChildIndexWhenRecoveryDisabled 418 * This index is relevant when mDisableRecovery is true. In that case, it refers to the index 419 * of the child that should have focus if the ancestors allow passing down the focus. -1 420 * indicates none of the children can receive focus even if the ancestors don't block focus, in 421 * which case RV holds and becomes focused. 422 */ assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled, int focusedChildIndexWhenRecoveryDisabled)423 private void assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled, 424 int focusedChildIndexWhenRecoveryDisabled) { 425 if (mDisableAnimation && mDisableRecovery) { 426 // This case is not quite handled properly at the moment. For now, RV may become focused 427 // without re-delivering the focus down to the children. Skip the checks for now. 428 return; 429 } 430 if (mRecyclerView.getChildCount() == 0) { 431 assertThat("RV should have focus when it has no children", 432 mRecyclerView.hasFocus(), is(true)); 433 assertThat("RV should be focused when it has no children", 434 mRecyclerView.isFocused(), is(true)); 435 return; 436 } 437 438 assertThat("RV should still have focus after layout", mRecyclerView.hasFocus(), is(true)); 439 if ((mDisableRecovery && focusedChildIndexWhenRecoveryDisabled == -1) 440 || (!mDisableRecovery && focusedChildIndexWhenRecoveryEnabled == -1) 441 || mRecyclerView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS 442 || mRecyclerView.getDescendantFocusability() 443 == ViewGroup.FOCUS_BEFORE_DESCENDANTS) { 444 FocusViewHolder fvh = cast(mRecyclerView.findViewHolderForAdapterPosition(0)); 445 String msg1 = " when focus recovery is disabled"; 446 String msg2 = " when descendant focusability is FOCUS_BLOCK_DESCENDANTS"; 447 String msg3 = " when descendant focusability is FOCUS_BEFORE_DESCENDANTS"; 448 449 assertThat("RV should not pass the focus down to its children" 450 + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability() 451 == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)), 452 mRecyclerView.isFocused(), is(true)); 453 assertThat("RV's first child should not have focus" 454 + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability() 455 == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)), 456 fvh.itemView.hasFocus(), is(false)); 457 assertThat("RV's first child should not be focused" 458 + (mDisableRecovery ? msg1 : (mRecyclerView.getDescendantFocusability() 459 == ViewGroup.FOCUS_BLOCK_DESCENDANTS ? msg2 : msg3)), 460 fvh.getViewToFocus().isFocused(), is(false)); 461 } else { 462 FocusViewHolder fvh = mDisableRecovery 463 ? cast(mRecyclerView.findViewHolderForAdapterPosition( 464 focusedChildIndexWhenRecoveryDisabled)) : 465 (focusedChildIndexWhenRecoveryEnabled != -1 466 ? cast(mRecyclerView.findViewHolderForAdapterPosition( 467 focusedChildIndexWhenRecoveryEnabled)) : 468 cast(mRecyclerView.findViewHolderForAdapterPosition(0))); 469 470 assertThat("test sanity", fvh, notNullValue()); 471 assertThat("RV's first child should be focusable", fvh.getViewToFocus().isFocusable(), 472 is(true)); 473 String msg = " when descendant focusability is FOCUS_AFTER_DESCENDANTS"; 474 assertThat("RV should pass the focus down to its children after layout" + msg, 475 mRecyclerView.isFocused(), is(false)); 476 assertThat("RV's child #" + focusedChildIndexWhenRecoveryEnabled + " should have focus" 477 + " after layout" + msg, 478 fvh.itemView.hasFocus(), is(true)); 479 if (mFocusOnChild) { 480 assertThat("Either the ViewGroup or the TextView within the first child of RV" 481 + "should be focused after layout" + msg, 482 fvh.itemView.isFocused() || fvh.getViewToFocus().isFocused(), is(true)); 483 } else { 484 assertThat("RV's first child should be focused after layout" + msg, 485 fvh.getViewToFocus().isFocused(), is(true)); 486 } 487 488 } 489 } 490 assertFocusTransition(RecyclerView.ViewHolder oldVh, RecyclerView.ViewHolder newVh, boolean typeChanged)491 private void assertFocusTransition(RecyclerView.ViewHolder oldVh, 492 RecyclerView.ViewHolder newVh, boolean typeChanged) { 493 if (mDisableRecovery) { 494 if (mDisableAnimation) { 495 return; 496 } 497 assertFocus(newVh, false); 498 return; 499 } 500 assertThat("test sanity", newVh, notNullValue()); 501 if (!typeChanged && mDisableAnimation) { 502 assertThat(oldVh, sameInstance(newVh)); 503 } else { 504 assertThat(oldVh, not(sameInstance(newVh))); 505 assertFocus(oldVh, false); 506 } 507 assertFocus(newVh, true); 508 } 509 510 @Test testFocusRecoveryInTypeChangeWithPredictive()511 public void testFocusRecoveryInTypeChangeWithPredictive() throws Throwable { 512 testFocusRecoveryInTypeChange(true); 513 } 514 515 @Test testFocusRecoveryInTypeChangeWithoutPredictive()516 public void testFocusRecoveryInTypeChangeWithoutPredictive() throws Throwable { 517 testFocusRecoveryInTypeChange(false); 518 } 519 testFocusRecoveryInTypeChange(boolean withAnimation)520 private void testFocusRecoveryInTypeChange(boolean withAnimation) throws Throwable { 521 setupBasic(); 522 if (!mDisableAnimation) { 523 ((SimpleItemAnimator) (mRecyclerView.getItemAnimator())) 524 .setSupportsChangeAnimations(true); 525 } 526 mLayoutManager.setSupportsPredictive(withAnimation); 527 final RecyclerView.ViewHolder oldVh = focusVh(3); 528 mLayoutManager.expectLayouts(!mDisableAnimation && withAnimation ? 2 : 1); 529 mActivityRule.runOnUiThread(new Runnable() { 530 @Override 531 public void run() { 532 Item item = mAdapter.mItems.get(3); 533 item.mType += 2; 534 mAdapter.notifyItemChanged(3); 535 } 536 }); 537 mLayoutManager.waitForLayout(2); 538 539 RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForAdapterPosition(3); 540 assertFocusTransition(oldVh, newVh, true); 541 assertThat("test sanity", oldVh.getItemViewType(), not(newVh.getItemViewType())); 542 } 543 544 @Test testRecoverAdapterChangeViaStableIdOnDataSetChanged()545 public void testRecoverAdapterChangeViaStableIdOnDataSetChanged() throws Throwable { 546 recoverAdapterChangeViaStableId(false, false); 547 } 548 549 @Test testRecoverAdapterChangeViaStableIdOnSwap()550 public void testRecoverAdapterChangeViaStableIdOnSwap() throws Throwable { 551 recoverAdapterChangeViaStableId(true, false); 552 } 553 554 @Test testRecoverAdapterChangeViaStableIdOnDataSetChangedWithTypeChange()555 public void testRecoverAdapterChangeViaStableIdOnDataSetChangedWithTypeChange() 556 throws Throwable { 557 recoverAdapterChangeViaStableId(false, true); 558 } 559 560 @Test testRecoverAdapterChangeViaStableIdOnSwapWithTypeChange()561 public void testRecoverAdapterChangeViaStableIdOnSwapWithTypeChange() throws Throwable { 562 recoverAdapterChangeViaStableId(true, true); 563 } 564 recoverAdapterChangeViaStableId(final boolean swap, final boolean changeType)565 private void recoverAdapterChangeViaStableId(final boolean swap, final boolean changeType) 566 throws Throwable { 567 setupBasic(true); 568 RecyclerView.ViewHolder oldVh = focusVh(4); 569 long itemId = oldVh.getItemId(); 570 571 mLayoutManager.expectLayouts(1); 572 mActivityRule.runOnUiThread(new Runnable() { 573 @Override 574 public void run() { 575 Item item = mAdapter.mItems.get(4); 576 if (changeType) { 577 item.mType += 2; 578 } 579 if (swap) { 580 mAdapter = new FocusTestAdapter(8); 581 mAdapter.setHasStableIds(true); 582 mAdapter.mItems.add(2, item); 583 mRecyclerView.swapAdapter(mAdapter, false); 584 } else { 585 mAdapter.mItems.remove(0); 586 mAdapter.mItems.remove(0); 587 mAdapter.notifyDataSetChanged(); 588 } 589 } 590 }); 591 mLayoutManager.waitForLayout(1); 592 if (!mDisableAnimation) { 593 waitForAnimations(2); 594 } 595 596 RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId); 597 if (changeType) { 598 assertFocusTransition(oldVh, newVh, true); 599 } else { 600 // in this case we should use the same VH because we have stable ids 601 assertThat(oldVh, sameInstance(newVh)); 602 assertFocus(newVh, true); 603 } 604 } 605 606 @Test testDoNotRecoverViaPositionOnSetAdapter()607 public void testDoNotRecoverViaPositionOnSetAdapter() throws Throwable { 608 testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() { 609 @Override 610 public void run(TestAdapter adapter) throws Throwable { 611 mRecyclerView.setAdapter(new FocusTestAdapter(10)); 612 } 613 }); 614 } 615 616 @Test testDoNotRecoverViaPositionOnSwapAdapterWithRecycle()617 public void testDoNotRecoverViaPositionOnSwapAdapterWithRecycle() throws Throwable { 618 testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() { 619 @Override 620 public void run(TestAdapter adapter) throws Throwable { 621 mRecyclerView.swapAdapter(new FocusTestAdapter(10), true); 622 } 623 }); 624 } 625 626 @Test testDoNotRecoverViaPositionOnSwapAdapterWithoutRecycle()627 public void testDoNotRecoverViaPositionOnSwapAdapterWithoutRecycle() throws Throwable { 628 testDoNotRecoverViaPositionOnNewDataSet(new RecyclerViewLayoutTest.AdapterRunnable() { 629 @Override 630 public void run(TestAdapter adapter) throws Throwable { 631 mRecyclerView.swapAdapter(new FocusTestAdapter(10), false); 632 } 633 }); 634 } 635 testDoNotRecoverViaPositionOnNewDataSet( final RecyclerViewLayoutTest.AdapterRunnable runnable)636 public void testDoNotRecoverViaPositionOnNewDataSet( 637 final RecyclerViewLayoutTest.AdapterRunnable runnable) throws Throwable { 638 setupBasic(false); 639 assertThat("test sanity", mAdapter.hasStableIds(), is(false)); 640 focusVh(4); 641 mLayoutManager.expectLayouts(1); 642 mActivityRule.runOnUiThread(new Runnable() { 643 @Override 644 public void run() { 645 try { 646 runnable.run(mAdapter); 647 } catch (Throwable throwable) { 648 postExceptionToInstrumentation(throwable); 649 } 650 } 651 }); 652 653 mLayoutManager.waitForLayout(1); 654 RecyclerView.ViewHolder otherVh = mRecyclerView.findViewHolderForAdapterPosition(4); 655 checkForMainThreadException(); 656 // even if the VH is re-used, it will be removed-reAdded so focus will go away from it. 657 assertFocus("should not recover focus if data set is badly invalid", otherVh, false); 658 659 } 660 661 @Test testDoNotRecoverIfReplacementIsNotFocusable()662 public void testDoNotRecoverIfReplacementIsNotFocusable() throws Throwable { 663 final int TYPE_NO_FOCUS = 1001; 664 TestAdapter adapter = new FocusTestAdapter(10) { 665 @Override 666 public void onBindViewHolder(@NonNull TestViewHolder holder, 667 int position) { 668 super.onBindViewHolder(holder, position); 669 if (holder.getItemViewType() == TYPE_NO_FOCUS) { 670 cast(holder).setFocusable(false); 671 } 672 } 673 }; 674 adapter.setHasStableIds(true); 675 setupBasic(adapter); 676 RecyclerView.ViewHolder oldVh = focusVh(3); 677 final long itemId = oldVh.getItemId(); 678 mLayoutManager.expectLayouts(1); 679 mActivityRule.runOnUiThread(new Runnable() { 680 @Override 681 public void run() { 682 mAdapter.mItems.get(3).mType = TYPE_NO_FOCUS; 683 mAdapter.notifyDataSetChanged(); 684 } 685 }); 686 mLayoutManager.waitForLayout(2); 687 if (!mDisableAnimation) { 688 waitForAnimations(2); 689 } 690 RecyclerView.ViewHolder newVh = mRecyclerView.findViewHolderForItemId(itemId); 691 assertFocus(newVh, false); 692 } 693 694 @NonNull focusVh(int pos)695 private RecyclerView.ViewHolder focusVh(int pos) throws Throwable { 696 final RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForAdapterPosition(pos); 697 assertThat("test sanity", oldVh, notNullValue()); 698 requestFocus(oldVh); 699 assertFocus("test sanity", oldVh, true); 700 getInstrumentation().waitForIdleSync(); 701 return oldVh; 702 } 703 704 @Test testDoNotOverrideAdapterRequestedFocus()705 public void testDoNotOverrideAdapterRequestedFocus() throws Throwable { 706 final AtomicLong toFocusId = new AtomicLong(-1); 707 708 FocusTestAdapter adapter = new FocusTestAdapter(10) { 709 @Override 710 public void onBindViewHolder(@NonNull TestViewHolder holder, 711 int position) { 712 super.onBindViewHolder(holder, position); 713 if (holder.getItemId() == toFocusId.get()) { 714 try { 715 requestFocus(holder); 716 } catch (Throwable throwable) { 717 postExceptionToInstrumentation(throwable); 718 } 719 } 720 } 721 }; 722 adapter.setHasStableIds(true); 723 toFocusId.set(adapter.mItems.get(3).mId); 724 long firstFocusId = toFocusId.get(); 725 setupBasic(adapter); 726 RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get()); 727 assertFocus(oldVh, true); 728 toFocusId.set(mAdapter.mItems.get(5).mId); 729 mLayoutManager.expectLayouts(1); 730 mActivityRule.runOnUiThread(new Runnable() { 731 @Override 732 public void run() { 733 mAdapter.mItems.get(3).mType += 2; 734 mAdapter.mItems.get(5).mType += 2; 735 mAdapter.notifyDataSetChanged(); 736 } 737 }); 738 mLayoutManager.waitForLayout(2); 739 if (!mDisableAnimation) { 740 waitForAnimations(2); 741 } 742 RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get()); 743 assertFocus(oldVh, false); 744 assertFocus(requested, true); 745 RecyclerView.ViewHolder oldReplacement = mRecyclerView 746 .findViewHolderForItemId(firstFocusId); 747 assertFocus(oldReplacement, false); 748 checkForMainThreadException(); 749 } 750 751 @Test testDoNotOverrideLayoutManagerRequestedFocus()752 public void testDoNotOverrideLayoutManagerRequestedFocus() throws Throwable { 753 final AtomicLong toFocusId = new AtomicLong(-1); 754 FocusTestAdapter adapter = new FocusTestAdapter(10); 755 adapter.setHasStableIds(true); 756 757 FocusLayoutManager lm = new FocusLayoutManager() { 758 @Override 759 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 760 detachAndScrapAttachedViews(recycler); 761 layoutRange(recycler, 0, state.getItemCount()); 762 RecyclerView.ViewHolder toFocus = mRecyclerView 763 .findViewHolderForItemId(toFocusId.get()); 764 if (toFocus != null) { 765 try { 766 requestFocus(toFocus); 767 } catch (Throwable throwable) { 768 postExceptionToInstrumentation(throwable); 769 } 770 } 771 layoutLatch.countDown(); 772 } 773 }; 774 775 toFocusId.set(adapter.mItems.get(3).mId); 776 long firstFocusId = toFocusId.get(); 777 setupBasic(adapter, lm); 778 779 RecyclerView.ViewHolder oldVh = mRecyclerView.findViewHolderForItemId(toFocusId.get()); 780 assertFocus(oldVh, true); 781 toFocusId.set(mAdapter.mItems.get(5).mId); 782 mLayoutManager.expectLayouts(1); 783 requestLayoutOnUIThread(mRecyclerView); 784 mLayoutManager.waitForLayout(2); 785 RecyclerView.ViewHolder requested = mRecyclerView.findViewHolderForItemId(toFocusId.get()); 786 assertFocus(oldVh, false); 787 assertFocus(requested, true); 788 RecyclerView.ViewHolder oldReplacement = mRecyclerView 789 .findViewHolderForItemId(firstFocusId); 790 assertFocus(oldReplacement, false); 791 checkForMainThreadException(); 792 } 793 requestFocus(RecyclerView.ViewHolder viewHolder)794 private void requestFocus(RecyclerView.ViewHolder viewHolder) throws Throwable { 795 FocusViewHolder fvh = cast(viewHolder); 796 requestFocus(fvh.getViewToFocus(), false); 797 } 798 assertFocus(RecyclerView.ViewHolder viewHolder, boolean hasFocus)799 private void assertFocus(RecyclerView.ViewHolder viewHolder, boolean hasFocus) { 800 assertFocus("", viewHolder, hasFocus); 801 } 802 assertFocus(String msg, RecyclerView.ViewHolder vh, boolean hasFocus)803 private void assertFocus(String msg, RecyclerView.ViewHolder vh, boolean hasFocus) { 804 FocusViewHolder fvh = cast(vh); 805 assertThat(msg, fvh.getViewToFocus().hasFocus(), is(hasFocus)); 806 } 807 cast(RecyclerView.ViewHolder vh)808 private <T extends FocusViewHolder> T cast(RecyclerView.ViewHolder vh) { 809 assertThat(vh, instanceOf(FocusViewHolder.class)); 810 //noinspection unchecked 811 return (T) vh; 812 } 813 814 private class FocusTestAdapter extends TestAdapter { 815 FocusTestAdapter(int count)816 public FocusTestAdapter(int count) { 817 super(count); 818 } 819 820 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)821 public FocusViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 822 int viewType) { 823 final FocusViewHolder fvh; 824 if (mFocusOnChild) { 825 fvh = new FocusViewHolderWithChildren( 826 LayoutInflater.from(parent.getContext()) 827 .inflate(R.layout.focus_test_item_view, parent, false)); 828 } else { 829 fvh = new SimpleFocusViewHolder(new TextView(parent.getContext())); 830 } 831 fvh.setFocusable(true); 832 return fvh; 833 } 834 835 @Override onBindViewHolder(@onNull TestViewHolder holder, int position)836 public void onBindViewHolder(@NonNull TestViewHolder holder, int position) { 837 cast(holder).bindTo(mItems.get(position)); 838 } 839 } 840 841 private class FocusLayoutManager extends TestLayoutManager { 842 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)843 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 844 detachAndScrapAttachedViews(recycler); 845 layoutRange(recycler, 0, state.getItemCount()); 846 layoutLatch.countDown(); 847 } 848 } 849 850 private class FocusViewHolderWithChildren extends FocusViewHolder { 851 public final ViewGroup root; 852 public final ViewGroup parent1; 853 public final ViewGroup parent2; 854 public final TextView textView; 855 FocusViewHolderWithChildren(View view)856 public FocusViewHolderWithChildren(View view) { 857 super(view); 858 root = (ViewGroup) view; 859 parent1 = (ViewGroup) root.findViewById(R.id.parent1); 860 parent2 = (ViewGroup) root.findViewById(R.id.parent2); 861 textView = (TextView) root.findViewById(R.id.text_view); 862 863 } 864 865 @Override setFocusable(boolean focusable)866 void setFocusable(boolean focusable) { 867 parent1.setFocusableInTouchMode(focusable); 868 parent2.setFocusableInTouchMode(focusable); 869 textView.setFocusableInTouchMode(focusable); 870 root.setFocusableInTouchMode(focusable); 871 872 parent1.setFocusable(focusable); 873 parent2.setFocusable(focusable); 874 textView.setFocusable(focusable); 875 root.setFocusable(focusable); 876 } 877 878 @Override onBind(Item item)879 void onBind(Item item) { 880 textView.setText(getText(item)); 881 } 882 883 @Override getViewToFocus()884 View getViewToFocus() { 885 return textView; 886 } 887 } 888 889 private class SimpleFocusViewHolder extends FocusViewHolder { 890 SimpleFocusViewHolder(View itemView)891 public SimpleFocusViewHolder(View itemView) { 892 super(itemView); 893 } 894 895 @Override setFocusable(boolean focusable)896 void setFocusable(boolean focusable) { 897 itemView.setFocusableInTouchMode(focusable); 898 itemView.setFocusable(focusable); 899 } 900 901 @Override getViewToFocus()902 View getViewToFocus() { 903 return itemView; 904 } 905 906 @Override onBind(Item item)907 void onBind(Item item) { 908 ((TextView) (itemView)).setText(getText(item)); 909 } 910 } 911 912 private abstract class FocusViewHolder extends TestViewHolder { 913 FocusViewHolder(View itemView)914 public FocusViewHolder(View itemView) { 915 super(itemView); 916 } 917 getText(Item item)918 protected String getText(Item item) { 919 return item.mText + "(" + item.mId + ")"; 920 } 921 setFocusable(boolean focusable)922 abstract void setFocusable(boolean focusable); 923 getViewToFocus()924 abstract View getViewToFocus(); 925 onBind(Item item)926 abstract void onBind(Item item); 927 bindTo(Item item)928 final void bindTo(Item item) { 929 mBoundItem = item; 930 setFocusable(item.isFocusable()); 931 onBind(item); 932 } 933 } 934 } 935