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