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