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