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