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