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 17 package android.support.v7.widget; 18 19 import android.app.Instrumentation; 20 import android.graphics.Rect; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.support.v4.view.ViewCompat; 24 import android.test.ActivityInstrumentationTestCase2; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.widget.FrameLayout; 30 import android.widget.TextView; 31 32 import java.lang.reflect.InvocationTargetException; 33 import java.lang.reflect.Method; 34 import java.util.ArrayList; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Set; 38 import java.util.concurrent.CountDownLatch; 39 import java.util.concurrent.TimeUnit; 40 import java.util.concurrent.atomic.AtomicInteger; 41 import java.util.concurrent.locks.ReentrantLock; 42 import android.support.v7.recyclerview.test.R; 43 44 abstract 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.getWidth() - mRecyclerView.getPaddingRight(), 176 mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() 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.countDown(); 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, assignDummyPool, 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 void freezeLayout(final boolean freeze) throws Throwable { 359 runTestOnUiThread(new Runnable() { 360 @Override 361 public void run() { 362 mRecyclerView.setLayoutFrozen(freeze); 363 } 364 }); 365 } 366 367 class TestViewHolder extends RecyclerView.ViewHolder { 368 369 Item mBoundItem; 370 371 public TestViewHolder(View itemView) { 372 super(itemView); 373 itemView.setFocusable(true); 374 } 375 376 @Override 377 public String toString() { 378 return super.toString() + " item:" + mBoundItem; 379 } 380 } 381 class DumbLayoutManager extends TestLayoutManager { 382 ReentrantLock mLayoutLock = new ReentrantLock(); 383 public void blockLayout() { 384 mLayoutLock.lock(); 385 } 386 387 public void unblockLayout() { 388 mLayoutLock.unlock(); 389 } 390 @Override 391 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 392 mLayoutLock.lock(); 393 detachAndScrapAttachedViews(recycler); 394 layoutRange(recycler, 0, state.getItemCount()); 395 if (layoutLatch != null) { 396 layoutLatch.countDown(); 397 } 398 mLayoutLock.unlock(); 399 } 400 } 401 public class TestLayoutManager extends RecyclerView.LayoutManager { 402 int mScrollVerticallyAmount; 403 int mScrollHorizontallyAmount; 404 protected CountDownLatch layoutLatch; 405 406 public void expectLayouts(int count) { 407 layoutLatch = new CountDownLatch(count); 408 } 409 410 public void waitForLayout(long timeout, TimeUnit timeUnit, boolean waitForIdle) 411 throws Throwable { 412 layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit); 413 assertEquals("all expected layouts should be executed at the expected time", 414 0, layoutLatch.getCount()); 415 if (waitForIdle) { 416 getInstrumentation().waitForIdleSync(); 417 } 418 } 419 420 public void waitForLayout(long timeout, TimeUnit timeUnit) 421 throws Throwable { 422 waitForLayout(timeout, timeUnit, true); 423 } 424 425 public void assertLayoutCount(int count, String msg, long timeout) throws Throwable { 426 layoutLatch.await(timeout, TimeUnit.SECONDS); 427 assertEquals(msg, count, layoutLatch.getCount()); 428 } 429 430 public void assertNoLayout(String msg, long timeout) throws Throwable { 431 layoutLatch.await(timeout, TimeUnit.SECONDS); 432 assertFalse(msg, layoutLatch.getCount() == 0); 433 } 434 435 public void waitForLayout(long timeout) throws Throwable { 436 waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS, true); 437 } 438 439 public void waitForLayout(long timeout, boolean waitForIdle) throws Throwable { 440 waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS, waitForIdle); 441 } 442 443 @Override 444 public RecyclerView.LayoutParams generateDefaultLayoutParams() { 445 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 446 ViewGroup.LayoutParams.WRAP_CONTENT); 447 } 448 449 void assertVisibleItemPositions() { 450 int i = getChildCount(); 451 TestAdapter testAdapter = (TestAdapter) mRecyclerView.getAdapter(); 452 while (i-- > 0) { 453 View view = getChildAt(i); 454 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams(); 455 Item item = ((TestViewHolder) lp.mViewHolder).mBoundItem; 456 if (mDebug) { 457 Log.d(TAG, "testing item " + i); 458 } 459 if (!lp.isItemRemoved()) { 460 RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view); 461 assertSame("item position in LP should match adapter value :" + vh, 462 testAdapter.mItems.get(vh.mPosition), item); 463 } 464 } 465 } 466 467 RecyclerView.LayoutParams getLp(View v) { 468 return (RecyclerView.LayoutParams) v.getLayoutParams(); 469 } 470 471 protected void layoutRange(RecyclerView.Recycler recycler, int start, int end) { 472 assertScrap(recycler); 473 if (mDebug) { 474 Log.d(TAG, "will layout items from " + start + " to " + end); 475 } 476 int diff = end > start ? 1 : -1; 477 int top = 0; 478 for (int i = start; i != end; i+=diff) { 479 if (mDebug) { 480 Log.d(TAG, "laying out item " + i); 481 } 482 View view = recycler.getViewForPosition(i); 483 assertNotNull("view should not be null for valid position. " 484 + "got null view at position " + i, view); 485 if (!mRecyclerView.mState.isPreLayout()) { 486 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view 487 .getLayoutParams(); 488 assertFalse("In post layout, getViewForPosition should never return a view " 489 + "that is removed", layoutParams != null 490 && layoutParams.isItemRemoved()); 491 492 } 493 assertEquals("getViewForPosition should return correct position", 494 i, getPosition(view)); 495 addView(view); 496 497 measureChildWithMargins(view, 0, 0); 498 if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) { 499 layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top, 500 getWidth(), top + getDecoratedMeasuredHeight(view)); 501 } else { 502 layoutDecorated(view, 0, top, getDecoratedMeasuredWidth(view) 503 , top + getDecoratedMeasuredHeight(view)); 504 } 505 506 top += view.getMeasuredHeight(); 507 } 508 } 509 510 private void assertScrap(RecyclerView.Recycler recycler) { 511 if (mRecyclerView.getAdapter() != null && 512 !mRecyclerView.getAdapter().hasStableIds()) { 513 for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) { 514 assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid()); 515 } 516 } 517 } 518 519 @Override 520 public boolean canScrollHorizontally() { 521 return true; 522 } 523 524 @Override 525 public boolean canScrollVertically() { 526 return true; 527 } 528 529 @Override 530 public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, 531 RecyclerView.State state) { 532 mScrollHorizontallyAmount += dx; 533 return dx; 534 } 535 536 @Override 537 public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, 538 RecyclerView.State state) { 539 mScrollVerticallyAmount += dy; 540 return dy; 541 } 542 } 543 544 static class Item { 545 final static AtomicInteger idCounter = new AtomicInteger(0); 546 final public int mId = idCounter.incrementAndGet(); 547 548 int mAdapterIndex; 549 550 final String mText; 551 552 Item(int adapterIndex, String text) { 553 mAdapterIndex = adapterIndex; 554 mText = text; 555 } 556 557 @Override 558 public String toString() { 559 return "Item{" + 560 "mId=" + mId + 561 ", originalIndex=" + mAdapterIndex + 562 ", text='" + mText + '\'' + 563 '}'; 564 } 565 } 566 567 public class TestAdapter extends RecyclerView.Adapter<TestViewHolder> 568 implements AttachDetachCountingAdapter { 569 570 public static final String DEFAULT_ITEM_PREFIX = "Item "; 571 572 ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter(); 573 List<Item> mItems; 574 575 public TestAdapter(int count) { 576 mItems = new ArrayList<Item>(count); 577 addItems(0, count, DEFAULT_ITEM_PREFIX); 578 } 579 580 private void addItems(int pos, int count, String prefix) { 581 for (int i = 0; i < count; i++, pos++) { 582 mItems.add(pos, new Item(pos, prefix)); 583 } 584 } 585 586 @Override 587 public void onViewAttachedToWindow(TestViewHolder holder) { 588 super.onViewAttachedToWindow(holder); 589 mAttachmentCounter.onViewAttached(holder); 590 } 591 592 @Override 593 public void onViewDetachedFromWindow(TestViewHolder holder) { 594 super.onViewDetachedFromWindow(holder); 595 mAttachmentCounter.onViewDetached(holder); 596 } 597 598 @Override 599 public void onAttachedToRecyclerView(RecyclerView recyclerView) { 600 super.onAttachedToRecyclerView(recyclerView); 601 mAttachmentCounter.onAttached(recyclerView); 602 } 603 604 @Override 605 public void onDetachedFromRecyclerView(RecyclerView recyclerView) { 606 super.onDetachedFromRecyclerView(recyclerView); 607 mAttachmentCounter.onDetached(recyclerView); 608 } 609 610 @Override 611 public TestViewHolder onCreateViewHolder(ViewGroup parent, 612 int viewType) { 613 return new TestViewHolder(new TextView(parent.getContext())); 614 } 615 616 @Override 617 public void onBindViewHolder(TestViewHolder holder, int position) { 618 assertNotNull(holder.mOwnerRecyclerView); 619 assertEquals(position, holder.getAdapterPosition()); 620 final Item item = mItems.get(position); 621 ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")"); 622 holder.mBoundItem = item; 623 } 624 625 public Item getItemAt(int position) { 626 return mItems.get(position); 627 } 628 629 @Override 630 public void onViewRecycled(TestViewHolder holder) { 631 super.onViewRecycled(holder); 632 final int adapterPosition = holder.getAdapterPosition(); 633 final boolean shouldHavePosition = !holder.isRemoved() && holder.isBound() && 634 !holder.isAdapterPositionUnknown() && !holder.isInvalid(); 635 String log = "Position check for " + holder.toString(); 636 assertEquals(log, shouldHavePosition, adapterPosition != RecyclerView.NO_POSITION); 637 if (shouldHavePosition) { 638 assertTrue(log, mItems.size() > adapterPosition); 639 assertSame(log, holder.mBoundItem, mItems.get(adapterPosition)); 640 } 641 } 642 643 public void deleteAndNotify(final int start, final int count) throws Throwable { 644 deleteAndNotify(new int[]{start, count}); 645 } 646 647 /** 648 * Deletes items in the given ranges. 649 * <p> 650 * Note that each operation affects the one after so you should offset them properly. 651 * <p> 652 * For example, if adapter has 5 items (A,B,C,D,E), and then you call this method with 653 * <code>[1, 2],[2, 1]</code>, it will first delete items B,C and the new adapter will be 654 * A D E. Then it will delete 2,1 which means it will delete E. 655 */ 656 public void deleteAndNotify(final int[]... startCountTuples) throws Throwable { 657 for (int[] tuple : startCountTuples) { 658 tuple[1] = -tuple[1]; 659 } 660 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 661 } 662 663 @Override 664 public long getItemId(int position) { 665 return hasStableIds() ? mItems.get(position).mId : super.getItemId(position); 666 } 667 668 public void offsetOriginalIndices(int start, int offset) { 669 for (int i = start; i < mItems.size(); i++) { 670 mItems.get(i).mAdapterIndex += offset; 671 } 672 } 673 674 /** 675 * @param start inclusive 676 * @param end exclusive 677 * @param offset 678 */ 679 public void offsetOriginalIndicesBetween(int start, int end, int offset) { 680 for (int i = start; i < end && i < mItems.size(); i++) { 681 mItems.get(i).mAdapterIndex += offset; 682 } 683 } 684 685 public void addAndNotify(final int count) throws Throwable { 686 assertEquals(0, mItems.size()); 687 new AddRemoveRunnable(DEFAULT_ITEM_PREFIX, new int[]{0, count}).runOnMainThread(); 688 } 689 690 public void addAndNotify(final int start, final int count) throws Throwable { 691 addAndNotify(new int[]{start, count}); 692 } 693 694 public void addAndNotify(final int[]... startCountTuples) throws Throwable { 695 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 696 } 697 698 public void dispatchDataSetChanged() throws Throwable { 699 runTestOnUiThread(new Runnable() { 700 @Override 701 public void run() { 702 notifyDataSetChanged(); 703 } 704 }); 705 } 706 707 public void changeAndNotify(final int start, final int count) throws Throwable { 708 runTestOnUiThread(new Runnable() { 709 @Override 710 public void run() { 711 notifyItemRangeChanged(start, count); 712 } 713 }); 714 } 715 716 public void changePositionsAndNotify(final int... positions) throws Throwable { 717 runTestOnUiThread(new Runnable() { 718 @Override 719 public void run() { 720 for (int i = 0; i < positions.length; i += 1) { 721 TestAdapter.super.notifyItemRangeChanged(positions[i], 1); 722 } 723 } 724 }); 725 } 726 727 /** 728 * Similar to other methods but negative count means delete and position count means add. 729 * <p> 730 * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an 731 * item to index 1, then remove an item from index 2 (updated index 2) 732 */ 733 public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable { 734 new AddRemoveRunnable(startCountTuples).runOnMainThread(); 735 } 736 737 @Override 738 public int getItemCount() { 739 return mItems.size(); 740 } 741 742 /** 743 * Uses notifyDataSetChanged 744 */ 745 public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable { 746 for (int i = 0; i < fromToTuples.length; i += 1) { 747 int[] tuple = fromToTuples[i]; 748 moveItem(tuple[0], tuple[1], false); 749 } 750 if (notifyChange) { 751 dispatchDataSetChanged(); 752 } 753 } 754 755 /** 756 * Uses notifyDataSetChanged 757 */ 758 public void moveItem(final int from, final int to, final boolean notifyChange) 759 throws Throwable { 760 runTestOnUiThread(new Runnable() { 761 @Override 762 public void run() { 763 moveInUIThread(from, to); 764 if (notifyChange) { 765 notifyDataSetChanged(); 766 } 767 } 768 }); 769 } 770 771 /** 772 * Uses notifyItemMoved 773 */ 774 public void moveAndNotify(final int from, final int to) throws Throwable { 775 runTestOnUiThread(new Runnable() { 776 @Override 777 public void run() { 778 moveInUIThread(from, to); 779 notifyItemMoved(from, to); 780 } 781 }); 782 } 783 784 public void clearOnUIThread() { 785 assertEquals("clearOnUIThread called from a wrong thread", 786 Looper.getMainLooper(), Looper.myLooper()); 787 mItems = new ArrayList<Item>(); 788 notifyDataSetChanged(); 789 } 790 791 protected void moveInUIThread(int from, int to) { 792 Item item = mItems.remove(from); 793 offsetOriginalIndices(from, -1); 794 mItems.add(to, item); 795 offsetOriginalIndices(to + 1, 1); 796 item.mAdapterIndex = to; 797 } 798 799 800 @Override 801 public ViewAttachDetachCounter getCounter() { 802 return mAttachmentCounter; 803 } 804 805 806 private class AddRemoveRunnable implements Runnable { 807 final String mNewItemPrefix; 808 final int[][] mStartCountTuples; 809 810 public AddRemoveRunnable(String newItemPrefix, int[]... startCountTuples) { 811 mNewItemPrefix = newItemPrefix; 812 mStartCountTuples = startCountTuples; 813 } 814 815 public AddRemoveRunnable(int[][] startCountTuples) { 816 this("new item ", startCountTuples); 817 } 818 819 public void runOnMainThread() throws Throwable { 820 if (Looper.myLooper() == Looper.getMainLooper()) { 821 run(); 822 } else { 823 runTestOnUiThread(this); 824 } 825 } 826 827 @Override 828 public void run() { 829 for (int[] tuple : mStartCountTuples) { 830 if (tuple[1] < 0) { 831 delete(tuple); 832 } else { 833 add(tuple); 834 } 835 } 836 } 837 838 private void add(int[] tuple) { 839 // offset others 840 offsetOriginalIndices(tuple[0], tuple[1]); 841 addItems(tuple[0], tuple[1], mNewItemPrefix); 842 notifyItemRangeInserted(tuple[0], tuple[1]); 843 } 844 845 private void delete(int[] tuple) { 846 final int count = -tuple[1]; 847 offsetOriginalIndices(tuple[0] + count, tuple[1]); 848 for (int i = 0; i < count; i++) { 849 mItems.remove(tuple[0]); 850 } 851 notifyItemRangeRemoved(tuple[0], count); 852 } 853 } 854 } 855 856 public boolean isMainThread() { 857 return Looper.myLooper() == Looper.getMainLooper(); 858 } 859 860 @Override 861 public void runTestOnUiThread(Runnable r) throws Throwable { 862 if (Looper.myLooper() == Looper.getMainLooper()) { 863 r.run(); 864 } else { 865 super.runTestOnUiThread(r); 866 } 867 } 868 869 static class TargetTuple { 870 871 final int mPosition; 872 873 final int mLayoutDirection; 874 875 TargetTuple(int position, int layoutDirection) { 876 this.mPosition = position; 877 this.mLayoutDirection = layoutDirection; 878 } 879 880 @Override 881 public String toString() { 882 return "TargetTuple{" + 883 "mPosition=" + mPosition + 884 ", mLayoutDirection=" + mLayoutDirection + 885 '}'; 886 } 887 } 888 889 public interface AttachDetachCountingAdapter { 890 891 ViewAttachDetachCounter getCounter(); 892 } 893 894 public class ViewAttachDetachCounter { 895 896 Set<RecyclerView.ViewHolder> mAttachedSet = new HashSet<RecyclerView.ViewHolder>(); 897 898 public void validateRemaining(RecyclerView recyclerView) { 899 final int childCount = recyclerView.getChildCount(); 900 for (int i = 0; i < childCount; i++) { 901 View view = recyclerView.getChildAt(i); 902 RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view); 903 assertTrue("remaining view should be in attached set " + vh, 904 mAttachedSet.contains(vh)); 905 } 906 assertEquals("there should not be any views left in attached set", 907 childCount, mAttachedSet.size()); 908 } 909 910 public void onViewDetached(RecyclerView.ViewHolder viewHolder) { 911 try { 912 assertTrue("view holder should be in attached set", 913 mAttachedSet.remove(viewHolder)); 914 } catch (Throwable t) { 915 postExceptionToInstrumentation(t); 916 } 917 } 918 919 public void onViewAttached(RecyclerView.ViewHolder viewHolder) { 920 try { 921 assertTrue("view holder should not be in attached set", 922 mAttachedSet.add(viewHolder)); 923 } catch (Throwable t) { 924 postExceptionToInstrumentation(t); 925 } 926 } 927 928 public void onAttached(RecyclerView recyclerView) { 929 // when a new RV is attached, clear the set and add all view holders 930 mAttachedSet.clear(); 931 final int childCount = recyclerView.getChildCount(); 932 for (int i = 0; i < childCount; i ++) { 933 View view = recyclerView.getChildAt(i); 934 mAttachedSet.add(recyclerView.getChildViewHolder(view)); 935 } 936 } 937 938 public void onDetached(RecyclerView recyclerView) { 939 validateRemaining(recyclerView); 940 } 941 } 942 } 943