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