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