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 org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotEquals; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertNull; 24 import static org.junit.Assert.assertSame; 25 import static org.junit.Assert.assertTrue; 26 import static org.junit.Assert.fail; 27 import static org.mockito.ArgumentMatchers.anyBoolean; 28 import static org.mockito.ArgumentMatchers.anyInt; 29 import static org.mockito.ArgumentMatchers.eq; 30 import static org.mockito.Mockito.mock; 31 import static org.mockito.Mockito.never; 32 import static org.mockito.Mockito.verify; 33 34 import android.annotation.SuppressLint; 35 import android.content.Context; 36 import android.os.Build; 37 import android.os.Parcel; 38 import android.os.Parcelable; 39 import android.os.SystemClock; 40 import android.util.AttributeSet; 41 import android.util.SparseArray; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.ViewGroup; 46 import android.view.animation.Interpolator; 47 import android.view.animation.LinearInterpolator; 48 import android.widget.FrameLayout; 49 import android.widget.LinearLayout; 50 import android.widget.TextView; 51 52 import androidx.core.util.Pair; 53 import androidx.core.view.InputDeviceCompat; 54 import androidx.core.view.ScrollFeedbackProviderCompat; 55 import androidx.core.view.ViewCompat; 56 import androidx.recyclerview.test.R; 57 import androidx.test.core.app.ApplicationProvider; 58 import androidx.test.ext.junit.runners.AndroidJUnit4; 59 import androidx.test.filters.SdkSuppress; 60 import androidx.test.filters.SmallTest; 61 62 import org.jspecify.annotations.NonNull; 63 import org.junit.Before; 64 import org.junit.Ignore; 65 import org.junit.Test; 66 import org.junit.runner.RunWith; 67 68 import java.lang.ref.WeakReference; 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.UUID; 72 73 @SuppressWarnings("unchecked") 74 @SmallTest 75 @RunWith(AndroidJUnit4.class) 76 public class RecyclerViewBasicTest { 77 78 RecyclerView mRecyclerView; 79 80 ScrollFeedbackProviderCompat mScrollFeedbackProvider; 81 82 @Before setUp()83 public void setUp() throws Exception { 84 mRecyclerView = new RecyclerView(getContext()); 85 } 86 getContext()87 private Context getContext() { 88 return ApplicationProvider.getApplicationContext(); 89 } 90 91 @Test measureWithoutLayoutManager()92 public void measureWithoutLayoutManager() { 93 measure(); 94 } 95 measure()96 private void measure() { 97 mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240); 98 } 99 layout()100 private void layout() { 101 mRecyclerView.layout(0, 0, 320, 320); 102 } 103 focusSearch()104 private void focusSearch() { 105 mRecyclerView.focusSearch(1); 106 } 107 108 @Test layoutWithoutAdapter()109 public void layoutWithoutAdapter() throws InterruptedException { 110 MockLayoutManager layoutManager = new MockLayoutManager(); 111 mRecyclerView.setLayoutManager(layoutManager); 112 layout(); 113 assertEquals("layout manager should not be called if there is no adapter attached", 114 0, layoutManager.mLayoutCount); 115 } 116 117 @Test 118 @Ignore("b/236978861") setScrollContainer()119 public void setScrollContainer() { 120 assertTrue("RecyclerView should announce itself as scroll container for the IME to " 121 + "handle it properly", mRecyclerView.isScrollContainer()); 122 } 123 124 @Test layoutWithoutLayoutManager()125 public void layoutWithoutLayoutManager() throws InterruptedException { 126 mRecyclerView.setAdapter(new MockAdapter(20)); 127 measure(); 128 layout(); 129 } 130 131 @Test focusWithoutLayoutManager()132 public void focusWithoutLayoutManager() throws InterruptedException { 133 mRecyclerView.setAdapter(new MockAdapter(20)); 134 measure(); 135 layout(); 136 focusSearch(); 137 } 138 139 @Test scrollWithoutLayoutManager()140 public void scrollWithoutLayoutManager() throws InterruptedException { 141 mRecyclerView.setAdapter(new MockAdapter(20)); 142 measure(); 143 layout(); 144 mRecyclerView.scrollBy(10, 10); 145 } 146 147 @Test smoothScrollWithoutLayoutManager()148 public void smoothScrollWithoutLayoutManager() throws InterruptedException { 149 mRecyclerView.setAdapter(new MockAdapter(20)); 150 measure(); 151 layout(); 152 mRecyclerView.smoothScrollBy(10, 10); 153 } 154 155 @Test scrollToPositionWithoutLayoutManager()156 public void scrollToPositionWithoutLayoutManager() throws InterruptedException { 157 mRecyclerView.setAdapter(new MockAdapter(20)); 158 measure(); 159 layout(); 160 mRecyclerView.scrollToPosition(5); 161 } 162 163 @Test 164 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) scrollFeedbackCallbacks()165 public void scrollFeedbackCallbacks() { 166 mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class); 167 mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider; 168 mRecyclerView.setAdapter(new MockAdapter(20)); 169 MockLayoutManager layoutManager = new MockLayoutManager(); 170 mRecyclerView.setLayoutManager(layoutManager); 171 measure(); 172 layout(); 173 174 MotionEvent ev = TouchUtils.createMotionEvent( 175 /* inputDeviceId= */ 1, 176 InputDeviceCompat.SOURCE_TOUCHSCREEN, 177 MotionEvent.ACTION_MOVE, 178 List.of( 179 Pair.create(MotionEvent.AXIS_X, 10), 180 Pair.create(MotionEvent.AXIS_Y, -20))); 181 layoutManager.mConsumedHorizontalScroll = 3; 182 layoutManager.mConsumedVerticalScroll = -20; 183 mRecyclerView.scrollByInternal( 184 /* x= */ 10, 185 /* y= */ -20, 186 /* horizontalAxis= */ MotionEvent.AXIS_X, 187 /* verticalAxis= */ MotionEvent.AXIS_Y, 188 ev, 189 ViewCompat.TYPE_TOUCH); 190 191 // Verify onScrollProgress calls equating to the amount of consumed pixels on each axis. 192 verify(mScrollFeedbackProvider).onScrollProgress( 193 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X, 194 /* deltaInPixels= */ 3); 195 verify(mScrollFeedbackProvider).onScrollProgress( 196 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_Y, 197 /* deltaInPixels= */ -20); 198 // Part of the X scroll was not consumed, so expect an onScrollLimit call. 199 verify(mScrollFeedbackProvider).onScrollLimit( 200 /* inputDeviceId= */ 1, InputDeviceCompat.SOURCE_TOUCHSCREEN, MotionEvent.AXIS_X, 201 /* isStart= */ false); 202 // All of the Y scroll was consumed. So expect no onScrollLimit call. 203 verify(mScrollFeedbackProvider, never()).onScrollLimit( 204 /* inputDeviceId= */ anyInt(), anyInt(), eq(MotionEvent.AXIS_Y), 205 /* isStart= */ eq(false)); 206 } 207 208 @Test 209 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) scrollFeedbackCallbacks_motionEventUnavailable()210 public void scrollFeedbackCallbacks_motionEventUnavailable() { 211 mScrollFeedbackProvider = mock(ScrollFeedbackProviderCompat.class); 212 mRecyclerView.mScrollFeedbackProvider = mScrollFeedbackProvider; 213 mRecyclerView.setAdapter(new MockAdapter(20)); 214 MockLayoutManager layoutManager = new MockLayoutManager(); 215 mRecyclerView.setLayoutManager(layoutManager); 216 measure(); 217 layout(); 218 219 mRecyclerView.scrollByInternal( 220 /* x= */ 10, 221 /* y= */ -20, 222 /* horizontalAxis= */ -1, 223 /* verticalAxis= */ -1, 224 null, 225 ViewCompat.TYPE_TOUCH); 226 227 verify(mScrollFeedbackProvider, never()).onScrollProgress( 228 anyInt(), anyInt(), anyInt(), anyInt()); 229 verify(mScrollFeedbackProvider, never()).onScrollLimit( 230 anyInt(), anyInt(), anyInt(), anyBoolean()); 231 } 232 233 @Test smoothScrollToPositionWithoutLayoutManager()234 public void smoothScrollToPositionWithoutLayoutManager() throws InterruptedException { 235 mRecyclerView.setAdapter(new MockAdapter(20)); 236 measure(); 237 layout(); 238 mRecyclerView.smoothScrollToPosition(5); 239 } 240 241 @Test interceptTouchWithoutLayoutManager()242 public void interceptTouchWithoutLayoutManager() { 243 mRecyclerView.setAdapter(new MockAdapter(20)); 244 measure(); 245 layout(); 246 assertFalse(mRecyclerView.onInterceptTouchEvent( 247 MotionEvent.obtain(SystemClock.uptimeMillis(), 248 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0))); 249 } 250 251 @Test onTouchWithoutLayoutManager()252 public void onTouchWithoutLayoutManager() { 253 mRecyclerView.setAdapter(new MockAdapter(20)); 254 measure(); 255 layout(); 256 assertFalse(mRecyclerView.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 257 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0))); 258 } 259 260 @Test layoutSimple()261 public void layoutSimple() throws InterruptedException { 262 MockLayoutManager layoutManager = new MockLayoutManager(); 263 mRecyclerView.setLayoutManager(layoutManager); 264 mRecyclerView.setAdapter(new MockAdapter(3)); 265 layout(); 266 assertEquals("when both layout manager and activity is set, recycler view should call" 267 + " layout manager's layout method", 1, layoutManager.mLayoutCount); 268 } 269 270 @Test observingAdapters()271 public void observingAdapters() { 272 MockAdapter adapterOld = new MockAdapter(1); 273 mRecyclerView.setAdapter(adapterOld); 274 assertTrue("attached adapter should have observables", adapterOld.hasObservers()); 275 276 MockAdapter adapterNew = new MockAdapter(2); 277 mRecyclerView.setAdapter(adapterNew); 278 assertFalse("detached adapter should lose observable", adapterOld.hasObservers()); 279 assertTrue("new adapter should have observers", adapterNew.hasObservers()); 280 281 mRecyclerView.setAdapter(null); 282 assertNull("adapter should be removed successfully", mRecyclerView.getAdapter()); 283 assertFalse("when adapter is removed, observables should be removed too", 284 adapterNew.hasObservers()); 285 } 286 287 @Test adapterChangeCallbacks()288 public void adapterChangeCallbacks() { 289 MockLayoutManager layoutManager = new MockLayoutManager(); 290 mRecyclerView.setLayoutManager(layoutManager); 291 MockAdapter adapterOld = new MockAdapter(1); 292 mRecyclerView.setAdapter(adapterOld); 293 layoutManager.assertPrevNextAdapters(null, adapterOld); 294 295 MockAdapter adapterNew = new MockAdapter(2); 296 mRecyclerView.setAdapter(adapterNew); 297 layoutManager.assertPrevNextAdapters("switching adapters should trigger correct callbacks" 298 , adapterOld, adapterNew); 299 300 mRecyclerView.setAdapter(null); 301 layoutManager.assertPrevNextAdapters( 302 "Setting adapter null should trigger correct callbacks", 303 adapterNew, null); 304 } 305 306 @Test recyclerOffsetsOnMove()307 public void recyclerOffsetsOnMove() { 308 MockLayoutManager layoutManager = new MockLayoutManager(); 309 final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>(); 310 mRecyclerView.setLayoutManager(layoutManager); 311 MockAdapter adapter = new MockAdapter(100) { 312 @Override 313 public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) { 314 super.onViewRecycled(holder); 315 recycledVhs.add(holder); 316 } 317 }; 318 MockViewHolder mvh = new MockViewHolder(new TextView(getContext())); 319 mRecyclerView.setAdapter(adapter); 320 adapter.bindViewHolder(mvh, 20); 321 mRecyclerView.mRecycler.mCachedViews.add(mvh); 322 mRecyclerView.offsetPositionRecordsForRemove(10, 9, false); 323 324 mRecyclerView.offsetPositionRecordsForRemove(11, 1, false); 325 assertEquals(1, recycledVhs.size()); 326 assertSame(mvh, recycledVhs.get(0)); 327 } 328 329 @Test recyclerOffsetsOnAdd()330 public void recyclerOffsetsOnAdd() { 331 MockLayoutManager layoutManager = new MockLayoutManager(); 332 final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>(); 333 mRecyclerView.setLayoutManager(layoutManager); 334 MockAdapter adapter = new MockAdapter(100) { 335 @Override 336 public void onViewRecycled(RecyclerView.@NonNull ViewHolder holder) { 337 super.onViewRecycled(holder); 338 recycledVhs.add(holder); 339 } 340 }; 341 MockViewHolder mvh = new MockViewHolder(new TextView(getContext())); 342 mRecyclerView.setAdapter(adapter); 343 adapter.bindViewHolder(mvh, 20); 344 mRecyclerView.mRecycler.mCachedViews.add(mvh); 345 mRecyclerView.offsetPositionRecordsForRemove(10, 9, false); 346 347 mRecyclerView.offsetPositionRecordsForInsert(15, 10); 348 assertEquals(11, mvh.mPosition); 349 } 350 351 @Test savedStateWithStatelessLayoutManager()352 public void savedStateWithStatelessLayoutManager() throws InterruptedException { 353 mRecyclerView.setLayoutManager(new MockLayoutManager() { 354 @Override 355 public Parcelable onSaveInstanceState() { 356 return null; 357 } 358 }); 359 mRecyclerView.setAdapter(new MockAdapter(3)); 360 Parcel parcel = Parcel.obtain(); 361 String parcelSuffix = UUID.randomUUID().toString(); 362 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 363 savedState.writeToParcel(parcel, 0); 364 parcel.writeString(parcelSuffix); 365 366 // reset position for reading 367 parcel.setDataPosition(0); 368 RecyclerView restored = new RecyclerView(getContext()); 369 restored.setLayoutManager(new MockLayoutManager()); 370 mRecyclerView.setAdapter(new MockAdapter(3)); 371 // restore 372 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 373 restored.onRestoreInstanceState(savedState); 374 375 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 376 parcel.readString()); 377 assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail()); 378 379 } 380 381 @Test savedState()382 public void savedState() throws InterruptedException { 383 MockLayoutManager mlm = new MockLayoutManager(); 384 mRecyclerView.setLayoutManager(mlm); 385 mRecyclerView.setAdapter(new MockAdapter(3)); 386 layout(); 387 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 388 // we append a suffix to the parcelable to test out of bounds 389 String parcelSuffix = UUID.randomUUID().toString(); 390 Parcel parcel = Parcel.obtain(); 391 savedState.writeToParcel(parcel, 0); 392 parcel.writeString(parcelSuffix); 393 394 // reset for reading 395 parcel.setDataPosition(0); 396 // re-create 397 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 398 399 RecyclerView restored = new RecyclerView(getContext()); 400 mRecyclerView = restored; 401 MockLayoutManager mlmRestored = new MockLayoutManager(); 402 restored.setLayoutManager(mlmRestored); 403 restored.setAdapter(new MockAdapter(3)); 404 restored.onRestoreInstanceState(savedState); 405 layout(); 406 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 407 parcel.readString()); 408 assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail()); 409 assertEquals("uuid in layout manager should be preserved properly", mlm.mUuid, 410 mlmRestored.mUuid); 411 } 412 413 @Test dontSaveChildrenState()414 public void dontSaveChildrenState() throws InterruptedException { 415 MockLayoutManager mlm = new MockLayoutManager() { 416 @Override 417 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 418 super.onLayoutChildren(recycler, state); 419 View view = recycler.getViewForPosition(0); 420 addView(view); 421 measureChildWithMargins(view, 0, 0); 422 view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 423 } 424 }; 425 mRecyclerView.setLayoutManager(mlm); 426 mRecyclerView.setAdapter(new MockAdapter(3) { 427 @Override 428 public RecyclerView.@NonNull ViewHolder onCreateViewHolder( 429 @NonNull ViewGroup parent, int viewType) { 430 final LoggingView itemView = new LoggingView(parent.getContext()); 431 //noinspection ResourceType 432 itemView.setId(3); 433 return new MockViewHolder(itemView); 434 } 435 }); 436 measure(); 437 layout(); 438 View view = mRecyclerView.getChildAt(0); 439 assertNotNull("Assumption check", view); 440 LoggingView loggingView = (LoggingView) view; 441 SparseArray<Parcelable> container = new SparseArray<Parcelable>(); 442 mRecyclerView.saveHierarchyState(container); 443 assertEquals("children's save state method should not be called", 0, 444 loggingView.getOnSavedInstanceCnt()); 445 } 446 447 @Test smoothScrollBy_withCustomInterpolator_usesCustomInterpolator()448 public void smoothScrollBy_withCustomInterpolator_usesCustomInterpolator() { 449 mRecyclerView.setLayoutManager(new MockLayoutManager()); 450 mRecyclerView.setAdapter(new MockAdapter(20)); 451 Interpolator interpolator = new LinearInterpolator(); 452 mRecyclerView.smoothScrollBy(0, 100, interpolator); 453 assertSame(interpolator, mRecyclerView.mViewFlinger.mInterpolator); 454 } 455 456 @Test smoothScrollBy_withoutCustomInterpolator_resetsToDefaultInterpolator()457 public void smoothScrollBy_withoutCustomInterpolator_resetsToDefaultInterpolator() { 458 mRecyclerView.setLayoutManager(new MockLayoutManager()); 459 mRecyclerView.setAdapter(new MockAdapter(20)); 460 Interpolator interpolator = new LinearInterpolator(); 461 mRecyclerView.smoothScrollBy(0, 100, interpolator); 462 463 mRecyclerView.smoothScrollBy(0, -100); 464 465 assertSame(RecyclerView.sQuinticInterpolator, mRecyclerView.mViewFlinger.mInterpolator); 466 } 467 468 @Test fling_resetsInterpolatorToDefault()469 public void fling_resetsInterpolatorToDefault() { 470 mRecyclerView.setLayoutManager(new MockLayoutManager()); 471 mRecyclerView.setAdapter(new MockAdapter(20)); 472 Interpolator interpolator = new LinearInterpolator(); 473 mRecyclerView.smoothScrollBy(0, 100, interpolator); 474 475 mRecyclerView.fling(0, -1000); 476 477 assertSame(RecyclerView.sQuinticInterpolator, mRecyclerView.mViewFlinger.mInterpolator); 478 } 479 480 @Test createAttachedException()481 public void createAttachedException() { 482 mRecyclerView.setAdapter(new RecyclerView.Adapter() { 483 @Override 484 public RecyclerView.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 485 int viewType) { 486 View view = LayoutInflater.from(parent.getContext()) 487 .inflate(R.layout.item_view, parent, true) 488 .findViewById(R.id.item_view); // find child, since parent is returned 489 return new RecyclerView.ViewHolder(view) {}; 490 } 491 492 @Override 493 public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) { 494 fail("shouldn't get here, should throw during create"); 495 } 496 497 @Override 498 public int getItemCount() { 499 return 1; 500 } 501 }); 502 mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 503 504 try { 505 measure(); 506 //layout(); 507 fail("IllegalStateException expected"); 508 } catch (IllegalStateException e) { 509 // expected 510 } 511 } 512 513 @Test prefetchChangesCacheSize()514 public void prefetchChangesCacheSize() { 515 mRecyclerView.setAdapter(new MockAdapter(20)); 516 MockLayoutManager mlm = new MockLayoutManager() { 517 @Override 518 public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, 519 RecyclerView.LayoutManager.LayoutPrefetchRegistry prefetchManager) { 520 prefetchManager.addPosition(0, 0); 521 prefetchManager.addPosition(1, 0); 522 prefetchManager.addPosition(2, 0); 523 } 524 }; 525 526 RecyclerView.Recycler recycler = mRecyclerView.mRecycler; 527 assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax); 528 mRecyclerView.setLayoutManager(mlm); 529 assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax); 530 531 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 532 // layout, so prefetches can occur 533 mRecyclerView.measure(View.MeasureSpec.EXACTLY | 100, View.MeasureSpec.EXACTLY | 100); 534 mRecyclerView.layout(0, 0, 100, 100); 535 536 // prefetch gets 3 items, so expands cache by 3 537 mRecyclerView.mPrefetchRegistry.collectPrefetchPositionsFromView(mRecyclerView, false); 538 assertEquals(3, mRecyclerView.mPrefetchRegistry.mCount); 539 assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax); 540 541 // Reset to default by removing layout 542 mRecyclerView.setLayoutManager(null); 543 assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax); 544 545 // And restore by restoring layout 546 mRecyclerView.setLayoutManager(mlm); 547 assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax); 548 } 549 } 550 551 @Test getNanoTime()552 public void getNanoTime() throws InterruptedException { 553 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 554 // check that it looks vaguely time-ish 555 long time = mRecyclerView.getNanoTime(); 556 assertNotEquals(0, time); 557 558 // Sleep for 1 nano to ensure next call won't have the same measurement. 559 Thread.sleep(0, 1); 560 assertNotEquals(time, mRecyclerView.getNanoTime()); 561 } else { 562 // expect to avoid cost of system.nanoTime on older platforms that don't do prefetch 563 assertEquals(0, mRecyclerView.getNanoTime()); 564 } 565 } 566 567 @Test findNestedRecyclerView()568 public void findNestedRecyclerView() { 569 RecyclerView recyclerView = new RecyclerView(getContext()); 570 assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(recyclerView)); 571 572 ViewGroup parent = new FrameLayout(getContext()); 573 assertEquals(null, RecyclerView.findNestedRecyclerView(parent)); 574 parent.addView(recyclerView); 575 assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(parent)); 576 577 ViewGroup grandParent = new FrameLayout(getContext()); 578 assertEquals(null, RecyclerView.findNestedRecyclerView(grandParent)); 579 grandParent.addView(parent); 580 assertEquals(recyclerView, RecyclerView.findNestedRecyclerView(grandParent)); 581 } 582 583 @Test clearNestedRecyclerViewIfNotNested()584 public void clearNestedRecyclerViewIfNotNested() { 585 RecyclerView recyclerView = new RecyclerView(getContext()); 586 ViewGroup parent = new FrameLayout(getContext()); 587 parent.addView(recyclerView); 588 ViewGroup grandParent = new FrameLayout(getContext()); 589 grandParent.addView(parent); 590 591 // verify trivial noop case 592 RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(recyclerView) {}; 593 holder.mNestedRecyclerView = new WeakReference<>(recyclerView); 594 RecyclerView.clearNestedRecyclerViewIfNotNested(holder); 595 assertEquals(recyclerView, holder.mNestedRecyclerView.get()); 596 597 // verify clear case 598 holder = new RecyclerView.ViewHolder(new View(getContext())) {}; 599 holder.mNestedRecyclerView = new WeakReference<>(recyclerView); 600 RecyclerView.clearNestedRecyclerViewIfNotNested(holder); 601 assertNull(holder.mNestedRecyclerView); 602 603 // verify more deeply nested case 604 holder = new RecyclerView.ViewHolder(grandParent) {}; 605 holder.mNestedRecyclerView = new WeakReference<>(recyclerView); 606 RecyclerView.clearNestedRecyclerViewIfNotNested(holder); 607 assertEquals(recyclerView, holder.mNestedRecyclerView.get()); 608 } 609 610 @Test exceptionContainsClasses()611 public void exceptionContainsClasses() { 612 RecyclerView first = new RecyclerView(getContext()); 613 first.setLayoutManager(new LinearLayoutManager(getContext())); 614 first.setAdapter(new MockAdapter(10)); 615 616 RecyclerView second = new RecyclerView(getContext()); 617 try { 618 second.setLayoutManager(first.getLayoutManager()); 619 fail("exception expected"); 620 } catch (IllegalArgumentException e) { 621 // Note: exception contains first RV 622 String m = e.getMessage(); 623 assertTrue("must contain RV class", m.contains(RecyclerView.class.getName())); 624 assertTrue("must contain Adapter class", m.contains(MockAdapter.class.getName())); 625 assertTrue("must contain LM class", m.contains(LinearLayoutManager.class.getName())); 626 assertTrue("must contain ctx class", m.contains(getContext().getClass().getName())); 627 } 628 } 629 630 @Test focusOrderTest()631 public void focusOrderTest() { 632 FocusOrderAdapter focusAdapter = new FocusOrderAdapter(getContext()); 633 mRecyclerView.setAdapter(focusAdapter); 634 mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); 635 measure(); 636 layout(); 637 638 View expected = focusAdapter.mBottomLeft; 639 assertEquals(expected, focusAdapter.mTopRight.focusSearch(View.FOCUS_FORWARD)); 640 641 expected = focusAdapter.mBottomRight; 642 assertSame(expected, focusAdapter.mBottomLeft.focusSearch(View.FOCUS_FORWARD)); 643 644 // we don't want looping within RecyclerView 645 assertNull(focusAdapter.mBottomRight.focusSearch(View.FOCUS_FORWARD)); 646 assertNull(focusAdapter.mTopLeft.focusSearch(View.FOCUS_BACKWARD)); 647 } 648 649 @Test setAdapter_callsCorrectLmMethods()650 public void setAdapter_callsCorrectLmMethods() throws Throwable { 651 MockLayoutManager mockLayoutManager = new MockLayoutManager(); 652 MockAdapter mockAdapter = new MockAdapter(1); 653 mRecyclerView.setLayoutManager(mockLayoutManager); 654 655 mRecyclerView.setAdapter(mockAdapter); 656 layout(); 657 658 assertEquals(1, mockLayoutManager.mAdapterChangedCount); 659 assertEquals(0, mockLayoutManager.mItemsChangedCount); 660 } 661 662 @Test swapAdapter_callsCorrectLmMethods()663 public void swapAdapter_callsCorrectLmMethods() throws Throwable { 664 MockLayoutManager mockLayoutManager = new MockLayoutManager(); 665 MockAdapter mockAdapter = new MockAdapter(1); 666 mRecyclerView.setLayoutManager(mockLayoutManager); 667 668 mRecyclerView.swapAdapter(mockAdapter, true); 669 layout(); 670 671 assertEquals(1, mockLayoutManager.mAdapterChangedCount); 672 assertEquals(1, mockLayoutManager.mItemsChangedCount); 673 } 674 675 @Test notifyDataSetChanged_callsCorrectLmMethods()676 public void notifyDataSetChanged_callsCorrectLmMethods() throws Throwable { 677 MockLayoutManager mockLayoutManager = new MockLayoutManager(); 678 MockAdapter mockAdapter = new MockAdapter(1); 679 mRecyclerView.setLayoutManager(mockLayoutManager); 680 mRecyclerView.setAdapter(mockAdapter); 681 mockLayoutManager.mAdapterChangedCount = 0; 682 mockLayoutManager.mItemsChangedCount = 0; 683 684 mockAdapter.notifyDataSetChanged(); 685 layout(); 686 687 assertEquals(0, mockLayoutManager.mAdapterChangedCount); 688 assertEquals(1, mockLayoutManager.mItemsChangedCount); 689 } 690 691 static class MockLayoutManager extends RecyclerView.LayoutManager { 692 693 int mLayoutCount = 0; 694 695 int mAdapterChangedCount = 0; 696 int mItemsChangedCount = 0; 697 698 int mConsumedHorizontalScroll = Integer.MIN_VALUE; 699 int mConsumedVerticalScroll = Integer.MIN_VALUE; 700 701 RecyclerView.Adapter mPrevAdapter; 702 703 RecyclerView.Adapter mNextAdapter; 704 705 String mUuid = UUID.randomUUID().toString(); 706 707 @Override onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)708 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 709 RecyclerView.Adapter newAdapter) { 710 super.onAdapterChanged(oldAdapter, newAdapter); 711 mPrevAdapter = oldAdapter; 712 mNextAdapter = newAdapter; 713 mAdapterChangedCount++; 714 } 715 716 @Override onItemsChanged(RecyclerView recyclerView)717 public void onItemsChanged(RecyclerView recyclerView) { 718 mItemsChangedCount++; 719 } 720 721 @Override onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)722 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 723 mLayoutCount += 1; 724 } 725 726 @Override onSaveInstanceState()727 public Parcelable onSaveInstanceState() { 728 LayoutManagerSavedState lss = new LayoutManagerSavedState(); 729 lss.mUuid = mUuid; 730 return lss; 731 } 732 733 @Override onRestoreInstanceState(Parcelable state)734 public void onRestoreInstanceState(Parcelable state) { 735 super.onRestoreInstanceState(state); 736 if (state instanceof LayoutManagerSavedState) { 737 mUuid = ((LayoutManagerSavedState) state).mUuid; 738 } 739 } 740 741 @Override generateDefaultLayoutParams()742 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 743 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 744 ViewGroup.LayoutParams.WRAP_CONTENT); 745 } 746 assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter, RecyclerView.Adapter nextAdapter)747 public void assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter, 748 RecyclerView.Adapter nextAdapter) { 749 assertSame(message, prevAdapter, mPrevAdapter); 750 assertSame(message, nextAdapter, mNextAdapter); 751 } 752 assertPrevNextAdapters(RecyclerView.Adapter prevAdapter, RecyclerView.Adapter nextAdapter)753 public void assertPrevNextAdapters(RecyclerView.Adapter prevAdapter, 754 RecyclerView.Adapter nextAdapter) { 755 assertPrevNextAdapters("Adapters from onAdapterChanged callback should match", 756 prevAdapter, nextAdapter); 757 } 758 759 @Override scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)760 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 761 RecyclerView.State state) { 762 return mConsumedHorizontalScroll != Integer.MIN_VALUE ? mConsumedHorizontalScroll : dx; 763 } 764 765 @Override scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)766 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 767 RecyclerView.State state) { 768 return mConsumedVerticalScroll != Integer.MIN_VALUE ? mConsumedVerticalScroll : dy; 769 } 770 771 @Override canScrollHorizontally()772 public boolean canScrollHorizontally() { 773 return true; 774 } 775 776 @Override canScrollVertically()777 public boolean canScrollVertically() { 778 return true; 779 } 780 } 781 782 @SuppressLint("BanParcelableUsage") 783 static class LayoutManagerSavedState implements Parcelable { 784 785 String mUuid; 786 LayoutManagerSavedState(Parcel in)787 public LayoutManagerSavedState(Parcel in) { 788 mUuid = in.readString(); 789 } 790 LayoutManagerSavedState()791 public LayoutManagerSavedState() { 792 793 } 794 795 @Override describeContents()796 public int describeContents() { 797 return 0; 798 } 799 800 @Override writeToParcel(Parcel dest, int flags)801 public void writeToParcel(Parcel dest, int flags) { 802 dest.writeString(mUuid); 803 } 804 805 public static final Parcelable.Creator<LayoutManagerSavedState> CREATOR 806 = new Parcelable.Creator<LayoutManagerSavedState>() { 807 @Override 808 public LayoutManagerSavedState createFromParcel(Parcel in) { 809 return new LayoutManagerSavedState(in); 810 } 811 812 @Override 813 public LayoutManagerSavedState[] newArray(int size) { 814 return new LayoutManagerSavedState[size]; 815 } 816 }; 817 } 818 819 static class MockAdapter extends RecyclerView.Adapter { 820 821 private int mCount = 0; 822 823 MockAdapter(int count)824 MockAdapter(int count) { 825 this.mCount = count; 826 } 827 828 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)829 public RecyclerView.@NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, 830 int viewType) { 831 return new MockViewHolder(new TextView(parent.getContext())); 832 } 833 834 @Override onBindViewHolder(RecyclerView.@onNull ViewHolder holder, int position)835 public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) { 836 837 } 838 839 @Override getItemCount()840 public int getItemCount() { 841 return mCount; 842 } 843 removeItems(int start, int count)844 void removeItems(int start, int count) { 845 mCount -= count; 846 notifyItemRangeRemoved(start, count); 847 } 848 addItems(int start, int count)849 void addItems(int start, int count) { 850 mCount += count; 851 notifyItemRangeInserted(start, count); 852 } 853 } 854 855 static class MockViewHolder extends RecyclerView.ViewHolder { 856 public Object mItem; MockViewHolder(View itemView)857 public MockViewHolder(View itemView) { 858 super(itemView); 859 } 860 } 861 862 static class LoggingView extends TextView { 863 private int mOnSavedInstanceCnt = 0; 864 LoggingView(Context context)865 public LoggingView(Context context) { 866 super(context); 867 } 868 LoggingView(Context context, AttributeSet attrs)869 public LoggingView(Context context, AttributeSet attrs) { 870 super(context, attrs); 871 } 872 LoggingView(Context context, AttributeSet attrs, int defStyleAttr)873 public LoggingView(Context context, AttributeSet attrs, int defStyleAttr) { 874 super(context, attrs, defStyleAttr); 875 } 876 877 @Override onSaveInstanceState()878 public Parcelable onSaveInstanceState() { 879 mOnSavedInstanceCnt ++; 880 return super.onSaveInstanceState(); 881 } 882 getOnSavedInstanceCnt()883 public int getOnSavedInstanceCnt() { 884 return mOnSavedInstanceCnt; 885 } 886 } 887 888 static class FocusOrderAdapter extends RecyclerView.Adapter { 889 TextView mTopLeft; 890 TextView mTopRight; 891 TextView mBottomLeft; 892 TextView mBottomRight; 893 FocusOrderAdapter(Context context)894 FocusOrderAdapter(Context context) { 895 mTopLeft = new TextView(context); 896 mTopRight = new TextView(context); 897 mBottomLeft = new TextView(context); 898 mBottomRight = new TextView(context); 899 for (TextView tv : new TextView[]{mTopLeft, mTopRight, mBottomLeft, mBottomRight}) { 900 tv.setFocusableInTouchMode(true); 901 tv.setLayoutParams(new LinearLayout.LayoutParams(100, 100)); 902 } 903 // create a scenario where the "first" focusable is to the right of the last one 904 mTopLeft.setFocusable(false); 905 mTopRight.getLayoutParams().width = 101; 906 mTopLeft.getLayoutParams().width = 101; 907 } 908 909 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)910 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 911 LinearLayout holder = new LinearLayout(parent.getContext()); 912 holder.setOrientation(LinearLayout.HORIZONTAL); 913 return new MockViewHolder(holder); 914 } 915 916 @Override onBindViewHolder(RecyclerView.@onNull ViewHolder holder, int position)917 public void onBindViewHolder(RecyclerView.@NonNull ViewHolder holder, int position) { 918 LinearLayout l = (LinearLayout) holder.itemView; 919 l.removeAllViews(); 920 if (position == 0) { 921 l.addView(mTopLeft); 922 l.addView(mTopRight); 923 } else { 924 l.addView(mBottomLeft); 925 l.addView(mBottomRight); 926 } 927 } 928 929 @Override getItemCount()930 public int getItemCount() { 931 return 2; 932 } 933 removeItems(int start, int count)934 void removeItems(int start, int count) { 935 } 936 addItems(int start, int count)937 void addItems(int start, int count) { 938 } 939 } 940 } 941