1/* 2 * Copyright (C) 2014 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 17package android.support.v7.widget; 18 19import org.junit.Before; 20import org.junit.Test; 21import org.junit.runner.RunWith; 22 23import android.app.Instrumentation; 24import android.content.Context; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.os.SystemClock; 28import android.support.test.InstrumentationRegistry; 29import android.support.test.runner.AndroidJUnit4; 30import android.test.AndroidTestCase; 31import android.test.suitebuilder.annotation.SmallTest; 32import android.util.AttributeSet; 33import android.util.SparseArray; 34import android.view.MotionEvent; 35import android.view.View; 36import android.view.ViewGroup; 37import android.widget.TextView; 38 39import java.util.ArrayList; 40import java.util.List; 41import java.util.UUID; 42import static org.junit.Assert.*; 43 44@SmallTest 45@RunWith(AndroidJUnit4.class) 46public class RecyclerViewBasicTest { 47 48 RecyclerView mRecyclerView; 49 50 51 @Before 52 public void setUp() throws Exception { 53 mRecyclerView = new RecyclerView(getContext()); 54 } 55 56 private Context getContext() { 57 return InstrumentationRegistry.getContext(); 58 } 59 60 @Test 61 public void measureWithoutLayoutManager() { 62 measure(); 63 } 64 65 private void measure() { 66 mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240); 67 } 68 69 private void layout() { 70 mRecyclerView.layout(0, 0, 320, 320); 71 } 72 73 private void focusSearch() { 74 mRecyclerView.focusSearch(1); 75 } 76 77 @Test 78 public void layoutWithoutAdapter() throws InterruptedException { 79 MockLayoutManager layoutManager = new MockLayoutManager(); 80 mRecyclerView.setLayoutManager(layoutManager); 81 layout(); 82 assertEquals("layout manager should not be called if there is no adapter attached", 83 0, layoutManager.mLayoutCount); 84 } 85 86 public void setScrollContainer() { 87 assertEquals("RecyclerView should announce itself as scroll container for the IME to " 88 + "handle it properly", true, mRecyclerView.isScrollContainer()); 89 } 90 91 @Test 92 public void layoutWithoutLayoutManager() throws InterruptedException { 93 mRecyclerView.setAdapter(new MockAdapter(20)); 94 measure(); 95 layout(); 96 } 97 98 @Test 99 public void focusWithoutLayoutManager() throws InterruptedException { 100 mRecyclerView.setAdapter(new MockAdapter(20)); 101 measure(); 102 layout(); 103 focusSearch(); 104 } 105 106 @Test 107 public void scrollWithoutLayoutManager() throws InterruptedException { 108 mRecyclerView.setAdapter(new MockAdapter(20)); 109 measure(); 110 layout(); 111 mRecyclerView.scrollBy(10, 10); 112 } 113 114 @Test 115 public void smoothScrollWithoutLayoutManager() throws InterruptedException { 116 mRecyclerView.setAdapter(new MockAdapter(20)); 117 measure(); 118 layout(); 119 mRecyclerView.smoothScrollBy(10, 10); 120 } 121 122 @Test 123 public void scrollToPositionWithoutLayoutManager() throws InterruptedException { 124 mRecyclerView.setAdapter(new MockAdapter(20)); 125 measure(); 126 layout(); 127 mRecyclerView.scrollToPosition(5); 128 } 129 130 @Test 131 public void smoothScrollToPositionWithoutLayoutManager() throws InterruptedException { 132 mRecyclerView.setAdapter(new MockAdapter(20)); 133 measure(); 134 layout(); 135 mRecyclerView.smoothScrollToPosition(5); 136 } 137 138 @Test 139 public void interceptTouchWithoutLayoutManager() { 140 mRecyclerView.setAdapter(new MockAdapter(20)); 141 measure(); 142 layout(); 143 assertFalse(mRecyclerView.onInterceptTouchEvent( 144 MotionEvent.obtain(SystemClock.uptimeMillis(), 145 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0))); 146 } 147 148 @Test 149 public void onTouchWithoutLayoutManager() { 150 mRecyclerView.setAdapter(new MockAdapter(20)); 151 measure(); 152 layout(); 153 assertFalse(mRecyclerView.onTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), 154 SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 10, 10, 0))); 155 } 156 157 @Test 158 public void layoutSimple() throws InterruptedException { 159 MockLayoutManager layoutManager = new MockLayoutManager(); 160 mRecyclerView.setLayoutManager(layoutManager); 161 mRecyclerView.setAdapter(new MockAdapter(3)); 162 layout(); 163 assertEquals("when both layout manager and activity is set, recycler view should call" 164 + " layout manager's layout method", 1, layoutManager.mLayoutCount); 165 } 166 167 @Test 168 public void observingAdapters() { 169 MockAdapter adapterOld = new MockAdapter(1); 170 mRecyclerView.setAdapter(adapterOld); 171 assertTrue("attached adapter should have observables", adapterOld.hasObservers()); 172 173 MockAdapter adapterNew = new MockAdapter(2); 174 mRecyclerView.setAdapter(adapterNew); 175 assertFalse("detached adapter should lose observable", adapterOld.hasObservers()); 176 assertTrue("new adapter should have observers", adapterNew.hasObservers()); 177 178 mRecyclerView.setAdapter(null); 179 assertNull("adapter should be removed successfully", mRecyclerView.getAdapter()); 180 assertFalse("when adapter is removed, observables should be removed too", 181 adapterNew.hasObservers()); 182 } 183 184 @Test 185 public void adapterChangeCallbacks() { 186 MockLayoutManager layoutManager = new MockLayoutManager(); 187 mRecyclerView.setLayoutManager(layoutManager); 188 MockAdapter adapterOld = new MockAdapter(1); 189 mRecyclerView.setAdapter(adapterOld); 190 layoutManager.assertPrevNextAdapters(null, adapterOld); 191 192 MockAdapter adapterNew = new MockAdapter(2); 193 mRecyclerView.setAdapter(adapterNew); 194 layoutManager.assertPrevNextAdapters("switching adapters should trigger correct callbacks" 195 , adapterOld, adapterNew); 196 197 mRecyclerView.setAdapter(null); 198 layoutManager.assertPrevNextAdapters( 199 "Setting adapter null should trigger correct callbacks", 200 adapterNew, null); 201 } 202 203 @Test 204 public void recyclerOffsetsOnMove() { 205 MockLayoutManager layoutManager = new MockLayoutManager(); 206 final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>(); 207 mRecyclerView.setLayoutManager(layoutManager); 208 MockAdapter adapter = new MockAdapter(100) { 209 @Override 210 public void onViewRecycled(RecyclerView.ViewHolder holder) { 211 super.onViewRecycled(holder); 212 recycledVhs.add(holder); 213 } 214 }; 215 MockViewHolder mvh = new MockViewHolder(new TextView(getContext())); 216 mRecyclerView.setAdapter(adapter); 217 adapter.bindViewHolder(mvh, 20); 218 mRecyclerView.mRecycler.mCachedViews.add(mvh); 219 mRecyclerView.offsetPositionRecordsForRemove(10, 9, false); 220 221 mRecyclerView.offsetPositionRecordsForRemove(11, 1, false); 222 assertEquals(1, recycledVhs.size()); 223 assertSame(mvh, recycledVhs.get(0)); 224 } 225 226 @Test 227 public void recyclerOffsetsOnAdd() { 228 MockLayoutManager layoutManager = new MockLayoutManager(); 229 final List<RecyclerView.ViewHolder> recycledVhs = new ArrayList<>(); 230 mRecyclerView.setLayoutManager(layoutManager); 231 MockAdapter adapter = new MockAdapter(100) { 232 @Override 233 public void onViewRecycled(RecyclerView.ViewHolder holder) { 234 super.onViewRecycled(holder); 235 recycledVhs.add(holder); 236 } 237 }; 238 MockViewHolder mvh = new MockViewHolder(new TextView(getContext())); 239 mRecyclerView.setAdapter(adapter); 240 adapter.bindViewHolder(mvh, 20); 241 mRecyclerView.mRecycler.mCachedViews.add(mvh); 242 mRecyclerView.offsetPositionRecordsForRemove(10, 9, false); 243 244 mRecyclerView.offsetPositionRecordsForInsert(15, 10); 245 assertEquals(11, mvh.mPosition); 246 } 247 248 @Test 249 public void savedStateWithStatelessLayoutManager() throws InterruptedException { 250 mRecyclerView.setLayoutManager(new MockLayoutManager() { 251 @Override 252 public Parcelable onSaveInstanceState() { 253 return null; 254 } 255 }); 256 mRecyclerView.setAdapter(new MockAdapter(3)); 257 Parcel parcel = Parcel.obtain(); 258 String parcelSuffix = UUID.randomUUID().toString(); 259 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 260 savedState.writeToParcel(parcel, 0); 261 parcel.writeString(parcelSuffix); 262 263 // reset position for reading 264 parcel.setDataPosition(0); 265 RecyclerView restored = new RecyclerView(getContext()); 266 restored.setLayoutManager(new MockLayoutManager()); 267 mRecyclerView.setAdapter(new MockAdapter(3)); 268 // restore 269 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 270 restored.onRestoreInstanceState(savedState); 271 272 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 273 parcel.readString()); 274 assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail()); 275 276 } 277 278 @Test 279 public void savedState() throws InterruptedException { 280 MockLayoutManager mlm = new MockLayoutManager(); 281 mRecyclerView.setLayoutManager(mlm); 282 mRecyclerView.setAdapter(new MockAdapter(3)); 283 layout(); 284 Parcelable savedState = mRecyclerView.onSaveInstanceState(); 285 // we append a suffix to the parcelable to test out of bounds 286 String parcelSuffix = UUID.randomUUID().toString(); 287 Parcel parcel = Parcel.obtain(); 288 savedState.writeToParcel(parcel, 0); 289 parcel.writeString(parcelSuffix); 290 291 // reset for reading 292 parcel.setDataPosition(0); 293 // re-create 294 savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel); 295 296 RecyclerView restored = new RecyclerView(getContext()); 297 MockLayoutManager mlmRestored = new MockLayoutManager(); 298 restored.setLayoutManager(mlmRestored); 299 restored.setAdapter(new MockAdapter(3)); 300 restored.onRestoreInstanceState(savedState); 301 302 assertEquals("Parcel reading should not go out of bounds", parcelSuffix, 303 parcel.readString()); 304 assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail()); 305 assertEquals("uuid in layout manager should be preserved properly", mlm.mUuid, 306 mlmRestored.mUuid); 307 assertNotSame("stateless parameter should not be preserved", mlm.mLayoutCount, 308 mlmRestored.mLayoutCount); 309 layout(); 310 } 311 312 313 @Test 314 public void dontSaveChildrenState() throws InterruptedException { 315 MockLayoutManager mlm = new MockLayoutManager() { 316 @Override 317 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 318 super.onLayoutChildren(recycler, state); 319 View view = recycler.getViewForPosition(0); 320 addView(view); 321 measureChildWithMargins(view, 0, 0); 322 view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 323 } 324 }; 325 mRecyclerView.setLayoutManager(mlm); 326 mRecyclerView.setAdapter(new MockAdapter(3) { 327 @Override 328 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 329 final LoggingView itemView = new LoggingView(parent.getContext()); 330 //noinspection ResourceType 331 itemView.setId(3); 332 return new MockViewHolder(itemView); 333 } 334 }); 335 measure(); 336 layout(); 337 View view = mRecyclerView.getChildAt(0); 338 assertNotNull("test sanity", view); 339 LoggingView loggingView = (LoggingView) view; 340 SparseArray<Parcelable> container = new SparseArray<Parcelable>(); 341 mRecyclerView.saveHierarchyState(container); 342 assertEquals("children's save state method should not be called", 0, 343 loggingView.getOnSavedInstanceCnt()); 344 } 345 346 static class MockLayoutManager extends RecyclerView.LayoutManager { 347 348 int mLayoutCount = 0; 349 350 int mAdapterChangedCount = 0; 351 352 RecyclerView.Adapter mPrevAdapter; 353 354 RecyclerView.Adapter mNextAdapter; 355 356 String mUuid = UUID.randomUUID().toString(); 357 358 @Override 359 public void onAdapterChanged(RecyclerView.Adapter oldAdapter, 360 RecyclerView.Adapter newAdapter) { 361 super.onAdapterChanged(oldAdapter, newAdapter); 362 mPrevAdapter = oldAdapter; 363 mNextAdapter = newAdapter; 364 mAdapterChangedCount++; 365 } 366 367 @Override 368 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 369 mLayoutCount += 1; 370 } 371 372 @Override 373 public Parcelable onSaveInstanceState() { 374 LayoutManagerSavedState lss = new LayoutManagerSavedState(); 375 lss.mUuid = mUuid; 376 return lss; 377 } 378 379 @Override 380 public void onRestoreInstanceState(Parcelable state) { 381 super.onRestoreInstanceState(state); 382 if (state instanceof LayoutManagerSavedState) { 383 mUuid = ((LayoutManagerSavedState) state).mUuid; 384 } 385 } 386 387 @Override 388 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 389 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 390 ViewGroup.LayoutParams.WRAP_CONTENT); 391 } 392 393 public void assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter, 394 RecyclerView.Adapter nextAdapter) { 395 assertSame(message, prevAdapter, mPrevAdapter); 396 assertSame(message, nextAdapter, mNextAdapter); 397 } 398 399 public void assertPrevNextAdapters(RecyclerView.Adapter prevAdapter, 400 RecyclerView.Adapter nextAdapter) { 401 assertPrevNextAdapters("Adapters from onAdapterChanged callback should match", 402 prevAdapter, nextAdapter); 403 } 404 405 @Override 406 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 407 RecyclerView.State state) { 408 return dx; 409 } 410 411 @Override 412 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 413 RecyclerView.State state) { 414 return dy; 415 } 416 417 @Override 418 public boolean canScrollHorizontally() { 419 return true; 420 } 421 422 @Override 423 public boolean canScrollVertically() { 424 return true; 425 } 426 } 427 428 static class LayoutManagerSavedState implements Parcelable { 429 430 String mUuid; 431 432 public LayoutManagerSavedState(Parcel in) { 433 mUuid = in.readString(); 434 } 435 436 public LayoutManagerSavedState() { 437 438 } 439 440 @Override 441 public int describeContents() { 442 return 0; 443 } 444 445 @Override 446 public void writeToParcel(Parcel dest, int flags) { 447 dest.writeString(mUuid); 448 } 449 450 public static final Parcelable.Creator<LayoutManagerSavedState> CREATOR 451 = new Parcelable.Creator<LayoutManagerSavedState>() { 452 @Override 453 public LayoutManagerSavedState createFromParcel(Parcel in) { 454 return new LayoutManagerSavedState(in); 455 } 456 457 @Override 458 public LayoutManagerSavedState[] newArray(int size) { 459 return new LayoutManagerSavedState[size]; 460 } 461 }; 462 } 463 464 static class MockAdapter extends RecyclerView.Adapter { 465 466 private int mCount = 0; 467 468 469 MockAdapter(int count) { 470 this.mCount = count; 471 } 472 473 @Override 474 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 475 return new MockViewHolder(new TextView(parent.getContext())); 476 } 477 478 @Override 479 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 480 481 } 482 483 @Override 484 public int getItemCount() { 485 return mCount; 486 } 487 488 void removeItems(int start, int count) { 489 mCount -= count; 490 notifyItemRangeRemoved(start, count); 491 } 492 493 void addItems(int start, int count) { 494 mCount += count; 495 notifyItemRangeInserted(start, count); 496 } 497 } 498 499 static class MockViewHolder extends RecyclerView.ViewHolder { 500 public Object mItem; 501 public MockViewHolder(View itemView) { 502 super(itemView); 503 } 504 } 505 506 static class LoggingView extends TextView { 507 private int mOnSavedInstanceCnt = 0; 508 509 public LoggingView(Context context) { 510 super(context); 511 } 512 513 public LoggingView(Context context, AttributeSet attrs) { 514 super(context, attrs); 515 } 516 517 public LoggingView(Context context, AttributeSet attrs, int defStyleAttr) { 518 super(context, attrs, defStyleAttr); 519 } 520 521 @Override 522 public Parcelable onSaveInstanceState() { 523 mOnSavedInstanceCnt ++; 524 return super.onSaveInstanceState(); 525 } 526 527 public int getOnSavedInstanceCnt() { 528 return mOnSavedInstanceCnt; 529 } 530 } 531}