BaseRecyclerViewInstrumentationTest.java revision 3d8453880afb3e32c4c59c52b8b580f91d78b29f
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 android.app.Instrumentation; 20import android.graphics.Rect; 21import android.os.Handler; 22import android.os.Looper; 23import android.support.v4.view.ViewCompat; 24import android.test.ActivityInstrumentationTestCase2; 25import android.util.Log; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.FrameLayout; 30import android.widget.TextView; 31 32import java.lang.reflect.InvocationTargetException; 33import java.lang.reflect.Method; 34import java.util.ArrayList; 35import java.util.HashSet; 36import java.util.List; 37import java.util.Set; 38import java.util.concurrent.CountDownLatch; 39import java.util.concurrent.TimeUnit; 40import java.util.concurrent.atomic.AtomicInteger; 41import java.util.concurrent.locks.ReentrantLock; 42import android.support.v7.recyclerview.test.R; 43 44abstract public class BaseRecyclerViewInstrumentationTest extends 45 ActivityInstrumentationTestCase2<TestActivity> { 46 47 private static final String TAG = "RecyclerViewTest"; 48 49 private boolean mDebug; 50 51 protected RecyclerView mRecyclerView; 52 53 protected AdapterHelper mAdapterHelper; 54 55 Throwable mainThreadException; 56 57 Thread mInstrumentationThread; 58 59 public BaseRecyclerViewInstrumentationTest() { 60 this(false); 61 } 62 63 public BaseRecyclerViewInstrumentationTest(boolean debug) { 64 super("android.support.v7.recyclerview", TestActivity.class); 65 mDebug = debug; 66 } 67 68 void checkForMainThreadException() throws Throwable { 69 if (mainThreadException != null) { 70 throw mainThreadException; 71 } 72 } 73 74 @Override 75 protected void setUp() throws Exception { 76 super.setUp(); 77 mInstrumentationThread = Thread.currentThread(); 78 } 79 80 void setHasTransientState(final View view, final boolean value) { 81 try { 82 runTestOnUiThread(new Runnable() { 83 @Override 84 public void run() { 85 ViewCompat.setHasTransientState(view, value); 86 } 87 }); 88 } catch (Throwable throwable) { 89 Log.e(TAG, "", throwable); 90 } 91 } 92 93 protected void enableAccessibility() 94 throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 95 Method getUIAutomation = Instrumentation.class.getMethod("getUiAutomation"); 96 getUIAutomation.invoke(getInstrumentation()); 97 } 98 99 void setAdapter(final RecyclerView.Adapter adapter) throws Throwable { 100 runTestOnUiThread(new Runnable() { 101 @Override 102 public void run() { 103 mRecyclerView.setAdapter(adapter); 104 } 105 }); 106 } 107 108 protected WrappedRecyclerView inflateWrappedRV() { 109 return (WrappedRecyclerView) 110 LayoutInflater.from(getActivity()).inflate(R.layout.wrapped_test_rv, 111 getRecyclerViewContainer(), false); 112 } 113 114 void swapAdapter(final RecyclerView.Adapter adapter, 115 final boolean removeAndRecycleExistingViews) throws Throwable { 116 runTestOnUiThread(new Runnable() { 117 @Override 118 public void run() { 119 try { 120 mRecyclerView.swapAdapter(adapter, removeAndRecycleExistingViews); 121 } catch (Throwable t) { 122 postExceptionToInstrumentation(t); 123 } 124 } 125 }); 126 checkForMainThreadException(); 127 } 128 129 void postExceptionToInstrumentation(Throwable t) { 130 if (mInstrumentationThread == Thread.currentThread()) { 131 throw new RuntimeException(t); 132 } 133 if (mainThreadException != null) { 134 Log.e(TAG, "receiving another main thread exception. dropping.", t); 135 } else { 136 Log.e(TAG, "captured exception on main thread", t); 137 mainThreadException = t; 138 } 139 140 if (mRecyclerView != null && mRecyclerView 141 .getLayoutManager() instanceof TestLayoutManager) { 142 TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager(); 143 // finish all layouts so that we get the correct exception 144 while (lm.layoutLatch.getCount() > 0) { 145 lm.layoutLatch.countDown(); 146 } 147 } 148 } 149 150 @Override 151 protected void tearDown() throws Exception { 152 if (mRecyclerView != null) { 153 try { 154 removeRecyclerView(); 155 } catch (Throwable throwable) { 156 throwable.printStackTrace(); 157 } 158 } 159 getInstrumentation().waitForIdleSync(); 160 super.tearDown(); 161 162 try { 163 checkForMainThreadException(); 164 } catch (Exception e) { 165 throw e; 166 } catch (Throwable throwable) { 167 throw new Exception(throwable); 168 } 169 } 170 171 public Rect getDecoratedRecyclerViewBounds() { 172 return new Rect( 173 mRecyclerView.getPaddingLeft(), 174 mRecyclerView.getPaddingTop(), 175 mRecyclerView.getPaddingLeft() + mRecyclerView.getWidth(), 176 mRecyclerView.getPaddingTop() + mRecyclerView.getHeight() 177 ); 178 } 179 180 public void removeRecyclerView() throws Throwable { 181 if (mRecyclerView == null) { 182 return; 183 } 184 if (!isMainThread()) { 185 getInstrumentation().waitForIdleSync(); 186 } 187 runTestOnUiThread(new Runnable() { 188 @Override 189 public void run() { 190 try { 191 final RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); 192 if (adapter instanceof AttachDetachCountingAdapter) { 193 ((AttachDetachCountingAdapter) adapter).getCounter() 194 .validateRemaining(mRecyclerView); 195 } 196 getActivity().mContainer.removeAllViews(); 197 } catch (Throwable t) { 198 postExceptionToInstrumentation(t); 199 } 200 } 201 }); 202 mRecyclerView = null; 203 } 204 205 void waitForAnimations(int seconds) throws InterruptedException { 206 final CountDownLatch latch = new CountDownLatch(2); 207 boolean running = mRecyclerView.mItemAnimator 208 .isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 209 @Override 210 public void onAnimationsFinished() { 211 latch.countDown(); 212 } 213 }); 214 if (running) { 215 latch.await(seconds, TimeUnit.SECONDS); 216 } 217 } 218 219 public boolean requestFocus(final View view) { 220 final boolean[] result = new boolean[1]; 221 getActivity().runOnUiThread(new Runnable() { 222 @Override 223 public void run() { 224 result[0] = view.requestFocus(); 225 } 226 }); 227 return result[0]; 228 } 229 230 public void setRecyclerView(final RecyclerView recyclerView) throws Throwable { 231 setRecyclerView(recyclerView, true); 232 } 233 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool) 234 throws Throwable { 235 setRecyclerView(recyclerView, assignDummyPool, true); 236 } 237 public void setRecyclerView(final RecyclerView recyclerView, boolean assignDummyPool, 238 boolean addPositionCheckItemAnimator) 239 throws Throwable { 240 mRecyclerView = recyclerView; 241 if (assignDummyPool) { 242 RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() { 243 @Override 244 public RecyclerView.ViewHolder getRecycledView(int viewType) { 245 RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType); 246 if (viewHolder == null) { 247 return null; 248 } 249 viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND); 250 viewHolder.mPosition = 200; 251 viewHolder.mOldPosition = 300; 252 viewHolder.mPreLayoutPosition = 500; 253 return viewHolder; 254 } 255 256 @Override 257 public void putRecycledView(RecyclerView.ViewHolder scrap) { 258 assertNull(scrap.mOwnerRecyclerView); 259 super.putRecycledView(scrap); 260 } 261 }; 262 mRecyclerView.setRecycledViewPool(pool); 263 } 264 if (addPositionCheckItemAnimator) { 265 mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() { 266 @Override 267 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 268 RecyclerView.State state) { 269 RecyclerView.ViewHolder vh = parent.getChildViewHolder(view); 270 if (!vh.isRemoved()) { 271 assertNotSame("If getItemOffsets is called, child should have a valid" 272 + " adapter position unless it is removed : " + vh, 273 vh.getAdapterPosition(), RecyclerView.NO_POSITION); 274 } 275 } 276 }); 277 } 278 mAdapterHelper = recyclerView.mAdapterHelper; 279 runTestOnUiThread(new Runnable() { 280 @Override 281 public void run() { 282 getActivity().mContainer.addView(recyclerView); 283 } 284 }); 285 } 286 287 protected FrameLayout getRecyclerViewContainer() { 288 return getActivity().mContainer; 289 } 290 291 public void requestLayoutOnUIThread(final View view) { 292 try { 293 runTestOnUiThread(new Runnable() { 294 @Override 295 public void run() { 296 view.requestLayout(); 297 } 298 }); 299 } catch (Throwable throwable) { 300 Log.e(TAG, "", throwable); 301 } 302 } 303 304 public void scrollBy(final int dt) { 305 try { 306 runTestOnUiThread(new Runnable() { 307 @Override 308 public void run() { 309 if (mRecyclerView.getLayoutManager().canScrollHorizontally()) { 310 mRecyclerView.scrollBy(dt, 0); 311 } else { 312 mRecyclerView.scrollBy(0, dt); 313 } 314 315 } 316 }); 317 } catch (Throwable throwable) { 318 Log.e(TAG, "", throwable); 319 } 320 } 321 322 void scrollToPosition(final int position) throws Throwable { 323 runTestOnUiThread(new Runnable() { 324 @Override 325 public void run() { 326 mRecyclerView.getLayoutManager().scrollToPosition(position); 327 } 328 }); 329 } 330 331 void smoothScrollToPosition(final int position) 332 throws Throwable { 333 if (mDebug) { 334 Log.d(TAG, "SMOOTH scrolling to " + position); 335 } 336 runTestOnUiThread(new Runnable() { 337 @Override 338 public void run() { 339 mRecyclerView.smoothScrollToPosition(position); 340 } 341 }); 342 getInstrumentation().waitForIdleSync(); 343 Thread.sleep(200); //give scroller some time so start 344 while (mRecyclerView.getLayoutManager().isSmoothScrolling() || 345 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 346 if (mDebug) { 347 Log.d(TAG, "SMOOTH scrolling step"); 348 } 349 Thread.sleep(200); 350 } 351 if (mDebug) { 352 Log.d(TAG, "SMOOTH scrolling done"); 353 } 354 getInstrumentation().waitForIdleSync(); 355 } 356 357 class TestViewHolder extends RecyclerView.ViewHolder { 358 359 Item mBoundItem; 360 361 public TestViewHolder(View itemView) { 362 super(itemView); 363 itemView.setFocusable(true); 364 } 365 366 @Override 367 public String toString() { 368 return super.toString() + " item:" + mBoundItem; 369 } 370 } 371 class DumbLayoutManager extends TestLayoutManager { 372 ReentrantLock mLayoutLock = new ReentrantLock(); 373 public void blockLayout() { 374 mLayoutLock.lock(); 375 } 376 377 public void unblockLayout() { 378 mLayoutLock.unlock(); 379 } 380 @Override 381 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 382 mLayoutLock.lock(); 383 detachAndScrapAttachedViews(recycler); 384 layoutRange(recycler, 0, state.getItemCount()); 385 if (layoutLatch != null) { 386 layoutLatch.countDown(); 387 } 388 mLayoutLock.unlock(); 389 } 390 } 391 public class TestLayoutManager extends RecyclerView.LayoutManager { 392 int mScrollVerticallyAmount; 393 int mScrollHorizontallyAmount; 394 protected CountDownLatch layoutLatch; 395 396 public void expectLayouts(int count) { 397 layoutLatch = new CountDownLatch(count); 398 } 399 400 public void waitForLayout(long timeout, TimeUnit timeUnit, boolean waitForIdle) 401 throws Throwable { 402 layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit); 403 assertEquals("all expected layouts should be executed at the expected time", 404 0, layoutLatch.getCount()); 405 if (waitForIdle) { 406 getInstrumentation().waitForIdleSync(); 407 } 408 } 409 410 public void waitForLayout(long timeout, TimeUnit timeUnit) 411 throws Throwable { 412 waitForLayout(timeout, timeUnit, true); 413 } 414 415 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 416 layoutLatch.await(timeout, TimeUnit.SECONDS); 417 assertEquals(msg, count, layoutLatch.getCount()); 418 } 419 420 public void assertNoLayout(String msg, long timeout) throws Throwable { 421 layoutLatch.await(timeout, TimeUnit.SECONDS); 422 assertFalse(msg, layoutLatch.getCount() == 0); 423 } 424 425 public void waitForLayout(long timeout) throws Throwable { 426 waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS, true); 427 } 428 429 public void waitForLayout(long timeout, boolean waitForIdle) throws Throwable { 430 waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS, waitForIdle); 431 } 432 433 @Override 434 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 435 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 436 ViewGroup.LayoutParams.WRAP_CONTENT); 437 } 438 439 void assertVisibleItemPositions() { 440 int i = getChildCount(); 441 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 442 while (i-- > 0) { 443 View view = getChildAt(i); 444 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 445 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 446 if (mDebug) { 447 Log.d(TAG, "testing item " + i); 448 } 449 if (!lp.isItemRemoved()) { 450 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 451 assertSame("item position in LP should match adapter value :" + vh, 452 testAdapter.mItems.get(vh.mPosition), item); 453 } 454 } 455 } 456 457 RecyclerView.LayoutParams getLp(View v) { 458 return (RecyclerView.LayoutParams) v.getLayoutParams(); 459 } 460 461 protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 462 assertScrap(recycler); 463 if (mDebug) { 464 Log.d(TAG, "will layout items from " + start + " to " + end); 465 } 466 int diff = end > start ? 1 : -1; 467 int top = 0; 468 for (int i = start; i != end; i+=diff) { 469 if (mDebug) { 470 Log.d(TAG, "laying out item " + i); 471 } 472 View view = recycler.getViewForPosition(i); 473 assertNotNull("view should not be null for valid position. " 474 + "got null view at position " + i, view); 475 if (!mRecyclerView.mState.isPreLayout()) { 476 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 477 .getLayoutParams(); 478 assertFalse("In post layout, getViewForPosition should never return a view " 479 + "that is removed", layoutParams != null 480 && layoutParams.isItemRemoved()); 481 482 } 483 assertEquals("getViewForPosition should return correct position", 484 i, getPosition(view)); 485 addView(view); 486 487 measureChildWithMargins(view, 0, 0); 488 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { 489 layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top, 490 getWidth(), top + getDecoratedMeasuredHeight(view)); 491 } else { 492 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 493 , top + getDecoratedMeasuredHeight(view)); 494 } 495 496 top += view.getMeasuredHeight(); 497 } 498 } 499 500 private void assertScrap(RecyclerView.Recycler recycler) { 501 if (mRecyclerView.getAdapter() != null && 502 !mRecyclerView.getAdapter().hasStableIds()) { 503 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 504 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 505 } 506 } 507 } 508 509 @Override 510 public boolean canScrollHorizontally() { 511 return true; 512 } 513 514 @Override 515 public boolean canScrollVertically() { 516 return true; 517 } 518 519 @Override 520 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 521 RecyclerView.State state) { 522 mScrollHorizontallyAmount += dx; 523 return dx; 524 } 525 526 @Override 527 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 528 RecyclerView.State state) { 529 mScrollVerticallyAmount += dy; 530 return dy; 531 } 532 } 533 534 static class Item { 535 final static AtomicInteger idCounter = new AtomicInteger(0); 536 final public int mId = idCounter.incrementAndGet(); 537 538 int mAdapterIndex; 539 540 final String mText; 541 542 Item(int adapterIndex, String text) { 543 mAdapterIndex = adapterIndex; 544 mText = text; 545 } 546 547 @Override 548 public String toString() { 549 return "Item{" + 550 "mId=" + mId + 551 ", originalIndex=" + mAdapterIndex + 552 ", text='" + mText + '\'' + 553 '}'; 554 } 555 } 556 557 public class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 558 implements AttachDetachCountingAdapter { 559 560 public static final String DEFAULT_ITEM_PREFIX = "Item "; 561 562 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 563 List<Item> mItems; 564 565 public TestAdapter(int count) { 566 mItems = new ArrayList<Item>(count); 567 addItems(0, count, DEFAULT_ITEM_PREFIX); 568 } 569 570 private void addItems(int pos, int count, String prefix) { 571 for (int i = 0; i < count; i++, pos++) { 572 mItems.add(pos, new Item(pos, prefix)); 573 } 574 } 575 576 @Override 577 public void onViewAttachedToWindow(TestViewHolder holder) { 578 super.onViewAttachedToWindow(holder); 579 mAttachmentCounter.onViewAttached(holder); 580 } 581 582 @Override 583 public void onViewDetachedFromWindow(TestViewHolder holder) { 584 super.onViewDetachedFromWindow(holder); 585 mAttachmentCounter.onViewDetached(holder); 586 } 587 588 @Override 589 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 590 super.onAttachedToRecyclerView(recyclerView); 591 mAttachmentCounter.onAttached(recyclerView); 592 } 593 594 @Override 595 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 596 super.onDetachedFromRecyclerView(recyclerView); 597 mAttachmentCounter.onDetached(recyclerView); 598 } 599 600 @Override 601 public TestViewHolder onCreateViewHolder(ViewGroup parent, 602 int viewType) { 603 return new TestViewHolder(new TextView(parent.getContext())); 604 } 605 606 @Override 607 public void onBindViewHolder(TestViewHolder holder, int position) { 608 assertNotNull(holder.mOwnerRecyclerView); 609 assertEquals(position, holder.getAdapterPosition()); 610 final Item item = mItems.get(position); 611 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); 612 holder.mBoundItem = item; 613 } 614 615 public Item getItemAt(int position) { 616 return mItems.get(position); 617 } 618 619 @Override 620 public void onViewRecycled(TestViewHolder holder) { 621 super.onViewRecycled(holder); 622 final int adapterPosition = holder.getAdapterPosition(); 623 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 624 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 625 String log = "Position check for " + holder.toString(); 626 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 627 if (shouldHavePosition) { 628 assertTrue(log, mItems.size() > adapterPosition); 629 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 630 } 631 } 632 633 public void deleteAndNotify(final int start, final int count) throws Throwable { 634 deleteAndNotify(new int[]{start, count}); 635 } 636 637 /** 638 * Deletes items in the given ranges. 639 * <p> 640 * Note that each operation affects the one after so you should offset them properly. 641 * <p> 642 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 643 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 644 * A D E. Then it will delete 2,1 which means it will delete E. 645 */ 646 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 647 for (int[] tuple : startCountTuples) { 648 tuple[1] = -tuple[1]; 649 } 650 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 651 } 652 653 @Override 654 public long getItemId(int position) { 655 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 656 } 657 658 public void offsetOriginalIndices(int start, int offset) { 659 for (int i = start; i < mItems.size(); i++) { 660 mItems.get(i).mAdapterIndex += offset; 661 } 662 } 663 664 /** 665 * @param start inclusive 666 * @param end exclusive 667 * @param offset 668 */ 669 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 670 for (int i = start; i < end && i < mItems.size(); i++) { 671 mItems.get(i).mAdapterIndex += offset; 672 } 673 } 674 675 public void addAndNotify(final int count) throws Throwable { 676 assertEquals(0, mItems.size()); 677 new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}).runOnMainThread(); 678 } 679 680 public void addAndNotify(final int start, final int count) throws Throwable { 681 addAndNotify(new int[]{start, count}); 682 } 683 684 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 685 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 686 } 687 688 public void dispatchDataSetChanged() throws Throwable { 689 runTestOnUiThread(new Runnable() { 690 @Override 691 public void run() { 692 notifyDataSetChanged(); 693 } 694 }); 695 } 696 697 public void changeAndNotify(final int start, final int count) throws Throwable { 698 runTestOnUiThread(new Runnable() { 699 @Override 700 public void run() { 701 notifyItemRangeChanged(start, count); 702 } 703 }); 704 } 705 706 public void changePositionsAndNotify(final int... positions) throws Throwable { 707 runTestOnUiThread(new Runnable() { 708 @Override 709 public void run() { 710 for (int i = 0; i < positions.length; i += 1) { 711 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 712 } 713 } 714 }); 715 } 716 717 /** 718 * Similar to other methods but negative count means delete and position count means add. 719 * <p> 720 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 721 * item to index 1, then remove an item from index 2 (updated index 2) 722 */ 723 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 724 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 725 } 726 727 @Override 728 public int getItemCount() { 729 return mItems.size(); 730 } 731 732 /** 733 * Uses notifyDataSetChanged 734 */ 735 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 736 for (int i = 0; i < fromToTuples.length; i += 1) { 737 int[] tuple = fromToTuples[i]; 738 moveItem(tuple[0], tuple[1], false); 739 } 740 if (notifyChange) { 741 dispatchDataSetChanged(); 742 } 743 } 744 745 /** 746 * Uses notifyDataSetChanged 747 */ 748 public void moveItem(final int from, final int to, final boolean notifyChange) 749 throws Throwable { 750 runTestOnUiThread(new Runnable() { 751 @Override 752 public void run() { 753 moveInUIThread(from, to); 754 if (notifyChange) { 755 notifyDataSetChanged(); 756 } 757 } 758 }); 759 } 760 761 /** 762 * Uses notifyItemMoved 763 */ 764 public void moveAndNotify(final int from, final int to) throws Throwable { 765 runTestOnUiThread(new Runnable() { 766 @Override 767 public void run() { 768 moveInUIThread(from, to); 769 notifyItemMoved(from, to); 770 } 771 }); 772 } 773 774 public void clearOnUIThread() { 775 assertEquals("clearOnUIThread called from a wrong thread", 776 Looper.getMainLooper(), Looper.myLooper()); 777 mItems = new ArrayList<Item>(); 778 notifyDataSetChanged(); 779 } 780 781 protected void moveInUIThread(int from, int to) { 782 Item item = mItems.remove(from); 783 offsetOriginalIndices(from, -1); 784 mItems.add(to, item); 785 offsetOriginalIndices(to + 1, 1); 786 item.mAdapterIndex = to; 787 } 788 789 790 @Override 791 public ViewAttachDetachCounter getCounter() { 792 return mAttachmentCounter; 793 } 794 795 796 private class AddRemoveRunnable implements Runnable { 797 final String mNewItemPrefix; 798 final int[][] mStartCountTuples; 799 800 public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) { 801 mNewItemPrefix = newItemPrefix; 802 mStartCountTuples = startCountTuples; 803 } 804 805 public AddRemoveRunnable(int[][] startCountTuples) { 806 this("new item ", startCountTuples); 807 } 808 809 public void runOnMainThread() throws Throwable { 810 if (Looper.myLooper() == Looper.getMainLooper()) { 811 run(); 812 } else { 813 runTestOnUiThread(this); 814 } 815 } 816 817 @Override 818 public void run() { 819 for (int[] tuple : mStartCountTuples) { 820 if (tuple[1] < 0) { 821 delete(tuple); 822 } else { 823 add(tuple); 824 } 825 } 826 } 827 828 private void add(int[] tuple) { 829 // offset others 830 offsetOriginalIndices(tuple[0], tuple[1]); 831 addItems(tuple[0], tuple[1], mNewItemPrefix); 832 notifyItemRangeInserted(tuple[0], tuple[1]); 833 } 834 835 private void delete(int[] tuple) { 836 final int count = -tuple[1]; 837 offsetOriginalIndices(tuple[0] + count, tuple[1]); 838 for (int i = 0; i < count; i++) { 839 mItems.remove(tuple[0]); 840 } 841 notifyItemRangeRemoved(tuple[0], count); 842 } 843 } 844 } 845 846 public boolean isMainThread() { 847 return Looper.myLooper() == Looper.getMainLooper(); 848 } 849 850 @Override 851 public void runTestOnUiThread(Runnable r) throws Throwable { 852 if (Looper.myLooper() == Looper.getMainLooper()) { 853 r.run(); 854 } else { 855 super.runTestOnUiThread(r); 856 } 857 } 858 859 static class TargetTuple { 860 861 final int mPosition; 862 863 final int mLayoutDirection; 864 865 TargetTuple(int position, int layoutDirection) { 866 this.mPosition = position; 867 this.mLayoutDirection = layoutDirection; 868 } 869 870 @Override 871 public String toString() { 872 return "TargetTuple{" + 873 "mPosition=" + mPosition + 874 ", mLayoutDirection=" + mLayoutDirection + 875 '}'; 876 } 877 } 878 879 public interface AttachDetachCountingAdapter { 880 881 ViewAttachDetachCounter getCounter(); 882 } 883 884 public class ViewAttachDetachCounter { 885 886 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 887 888 public void validateRemaining(RecyclerView recyclerView) { 889 final int childCount = recyclerView.getChildCount(); 890 for (int i = 0; i < childCount; i++) { 891 View view = recyclerView.getChildAt(i); 892 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 893 assertTrue("remaining view should be in attached set " + vh, 894 mAttachedSet.contains(vh)); 895 } 896 assertEquals("there should not be any views left in attached set", 897 childCount, mAttachedSet.size()); 898 } 899 900 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 901 try { 902 assertTrue("view holder should be in attached set", 903 mAttachedSet.remove(viewHolder)); 904 } catch (Throwable t) { 905 postExceptionToInstrumentation(t); 906 } 907 } 908 909 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 910 try { 911 assertTrue("view holder should not be in attached set", 912 mAttachedSet.add(viewHolder)); 913 } catch (Throwable t) { 914 postExceptionToInstrumentation(t); 915 } 916 } 917 918 public void onAttached(RecyclerView recyclerView) { 919 // when a new RV is attached, clear the set and add all view holders 920 mAttachedSet.clear(); 921 final int childCount = recyclerView.getChildCount(); 922 for (int i = 0; i < childCount; i ++) { 923 View view = recyclerView.getChildAt(i); 924 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 925 } 926 } 927 928 public void onDetached(RecyclerView recyclerView) { 929 validateRemaining(recyclerView); 930 } 931 } 932} 933