LinearLayoutManagerTest.java revision 504c54ea52c1b2aae6f8f4ae128f1dcaac7e3f6a
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.content.Context;
20import android.graphics.Rect;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26
27import java.lang.ref.WeakReference;
28import java.lang.reflect.Field;
29import java.util.ArrayList;
30import java.util.LinkedHashMap;
31import java.util.List;
32import java.util.Map;
33import java.util.UUID;
34import java.util.concurrent.CountDownLatch;
35import java.util.concurrent.TimeUnit;
36import java.util.concurrent.atomic.AtomicInteger;
37
38/**
39 * Includes tests for {@link LinearLayoutManager}.
40 * <p>
41 * Since most UI tests are not practical, these tests are focused on internal data representation
42 * and stability of LinearLayoutManager in response to different events (state change, scrolling
43 * etc) where it is very hard to do manual testing.
44 */
45public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
46
47    private static final boolean DEBUG = false;
48
49    private static final String TAG = "LinearLayoutManagerTest";
50
51    WrappedLinearLayoutManager mLayoutManager;
52
53    TestAdapter mTestAdapter;
54
55    final List<Config> mBaseVariations = new ArrayList<Config>();
56
57    @Override
58    protected void setUp() throws Exception {
59        super.setUp();
60        for (int orientation : new int[]{LinearLayoutManager.VERTICAL,
61                LinearLayoutManager.HORIZONTAL}) {
62            for (boolean reverseLayout : new boolean[]{false, true}) {
63                for (boolean stackFromBottom : new boolean[]{false, true}) {
64                    mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom));
65                }
66            }
67        }
68    }
69
70    protected List<Config> addConfigVariation(List<Config> base, String fieldName,
71            Object... variations)
72            throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
73        List<Config> newConfigs = new ArrayList<Config>();
74        Field field = Config.class.getDeclaredField(fieldName);
75        for (Config config : base) {
76            for (Object variation : variations) {
77                Config newConfig = (Config) config.clone();
78                field.set(newConfig, variation);
79                newConfigs.add(newConfig);
80            }
81        }
82        return newConfigs;
83    }
84
85    void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
86        mRecyclerView = new RecyclerView(getActivity());
87        mRecyclerView.setHasFixedSize(true);
88        mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount)
89                : config.mTestAdapter;
90        mRecyclerView.setAdapter(mTestAdapter);
91        mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
92                config.mReverseLayout);
93        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
94        mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
95        mRecyclerView.setLayoutManager(mLayoutManager);
96        if (waitForFirstLayout) {
97            waitForFirstLayout();
98        }
99    }
100
101    private void waitForFirstLayout() throws Throwable {
102        mLayoutManager.expectLayouts(1);
103        setRecyclerView(mRecyclerView);
104        mLayoutManager.waitForLayout(2);
105    }
106
107    public void testRecycleDuringAnimations() throws Throwable {
108        final AtomicInteger childCount = new AtomicInteger(0);
109        final TestAdapter adapter = new TestAdapter(300) {
110            @Override
111            public TestViewHolder onCreateViewHolder(ViewGroup parent,
112                    int viewType) {
113                final int cnt = childCount.incrementAndGet();
114                final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
115                if (DEBUG) {
116                    Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder);
117                }
118                return testViewHolder;
119            }
120        };
121        setupByConfig(new Config(LinearLayoutManager.VERTICAL, false, false).itemCount(300)
122                .adapter(adapter), true);
123
124        final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
125            @Override
126            public void putRecycledView(RecyclerView.ViewHolder scrap) {
127                super.putRecycledView(scrap);
128                int cnt = childCount.decrementAndGet();
129                if (DEBUG) {
130                    Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap);
131                }
132            }
133
134            @Override
135            public RecyclerView.ViewHolder getRecycledView(int viewType) {
136                final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
137                if (recycledView != null) {
138                    final int cnt = childCount.incrementAndGet();
139                    if (DEBUG) {
140                        Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView);
141                    }
142                }
143                return recycledView;
144            }
145        };
146        pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500);
147        mRecyclerView.setRecycledViewPool(pool);
148
149
150        // now keep adding children to trigger more children being created etc.
151        for (int i = 0; i < 100; i ++) {
152            adapter.addAndNotify(15, 1);
153            Thread.sleep(15);
154        }
155        getInstrumentation().waitForIdleSync();
156        waitForAnimations(2);
157        assertEquals("Children count should add up", childCount.get(),
158                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
159
160        // now trigger lots of add again, followed by a scroll to position
161        for (int i = 0; i < 100; i ++) {
162            adapter.addAndNotify(5 + (i % 3) * 3, 1);
163            Thread.sleep(25);
164        }
165        smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
166        waitForAnimations(2);
167        getInstrumentation().waitForIdleSync();
168        assertEquals("Children count should add up", childCount.get(),
169                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
170    }
171
172
173    public void testGetFirstLastChildrenTest() throws Throwable {
174        for (Config config : mBaseVariations) {
175            getFirstLastChildrenTest(config);
176        }
177    }
178
179    public void testDontRecycleChildrenOnDetach() throws Throwable {
180        setupByConfig(new Config().recycleChildrenOnDetach(false), true);
181        runTestOnUiThread(new Runnable() {
182            @Override
183            public void run() {
184                int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
185                mRecyclerView.setLayoutManager(new TestLayoutManager());
186                assertEquals("No views are recycled", recyclerSize,
187                        mRecyclerView.mRecycler.getRecycledViewPool().size());
188            }
189        });
190    }
191
192    public void testRecycleChildrenOnDetach() throws Throwable {
193        setupByConfig(new Config().recycleChildrenOnDetach(true), true);
194        final int childCount = mLayoutManager.getChildCount();
195        runTestOnUiThread(new Runnable() {
196            @Override
197            public void run() {
198                int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
199                mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
200                        mTestAdapter.getItemViewType(0), recyclerSize + childCount);
201                mRecyclerView.setLayoutManager(new TestLayoutManager());
202                assertEquals("All children should be recycled", childCount + recyclerSize,
203                        mRecyclerView.mRecycler.getRecycledViewPool().size());
204            }
205        });
206    }
207
208    public void getFirstLastChildrenTest(final Config config) throws Throwable {
209        setupByConfig(config, true);
210        Runnable viewInBoundsTest = new Runnable() {
211            @Override
212            public void run() {
213                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
214                final String boundsLog = mLayoutManager.getBoundsLog();
215                assertEquals(config + ":\nfirst visible child should match traversal result\n"
216                                + boundsLog, visibleChildren.firstVisiblePosition,
217                        mLayoutManager.findFirstVisibleItemPosition()
218                );
219                assertEquals(
220                        config + ":\nfirst fully visible child should match traversal result\n"
221                                + boundsLog, visibleChildren.firstFullyVisiblePosition,
222                        mLayoutManager.findFirstCompletelyVisibleItemPosition()
223                );
224
225                assertEquals(config + ":\nlast visible child should match traversal result\n"
226                                + boundsLog, visibleChildren.lastVisiblePosition,
227                        mLayoutManager.findLastVisibleItemPosition()
228                );
229                assertEquals(
230                        config + ":\nlast fully visible child should match traversal result\n"
231                                + boundsLog, visibleChildren.lastFullyVisiblePosition,
232                        mLayoutManager.findLastCompletelyVisibleItemPosition()
233                );
234            }
235        };
236        runTestOnUiThread(viewInBoundsTest);
237        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
238        // case
239        final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount();
240        runTestOnUiThread(new Runnable() {
241            @Override
242            public void run() {
243                mRecyclerView.smoothScrollToPosition(scrollPosition);
244            }
245        });
246        while (mLayoutManager.isSmoothScrolling() ||
247                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
248            runTestOnUiThread(viewInBoundsTest);
249            Thread.sleep(400);
250        }
251        // delete all items
252        mLayoutManager.expectLayouts(2);
253        mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount());
254        mLayoutManager.waitForLayout(2);
255        // test empty case
256        runTestOnUiThread(viewInBoundsTest);
257        // set a new adapter with huge items to test full bounds check
258        mLayoutManager.expectLayouts(1);
259        final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace();
260        final TestAdapter newAdapter = new TestAdapter(100) {
261            @Override
262            public void onBindViewHolder(TestViewHolder holder,
263                    int position) {
264                super.onBindViewHolder(holder, position);
265                if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
266                    holder.itemView.setMinimumWidth(totalSpace + 5);
267                } else {
268                    holder.itemView.setMinimumHeight(totalSpace + 5);
269                }
270            }
271        };
272        runTestOnUiThread(new Runnable() {
273            @Override
274            public void run() {
275                mRecyclerView.setAdapter(newAdapter);
276            }
277        });
278        mLayoutManager.waitForLayout(2);
279        runTestOnUiThread(viewInBoundsTest);
280    }
281
282    public void testSavedState() throws Throwable {
283        Thread.sleep(5000);
284        PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
285                new PostLayoutRunnable() {
286                    @Override
287                    public void run() throws Throwable {
288                        // do nothing
289                    }
290
291                    @Override
292                    public String describe() {
293                        return "doing nothing";
294                    }
295                },
296                new PostLayoutRunnable() {
297                    @Override
298                    public void run() throws Throwable {
299                        mLayoutManager.expectLayouts(1);
300                        scrollToPosition(mTestAdapter.getItemCount() * 3 / 4);
301                        mLayoutManager.waitForLayout(2);
302                    }
303
304                    @Override
305                    public String describe() {
306                        return "scroll to position";
307                    }
308                },
309                new PostLayoutRunnable() {
310                    @Override
311                    public void run() throws Throwable {
312                        mLayoutManager.expectLayouts(1);
313                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3,
314                                50);
315                        mLayoutManager.waitForLayout(2);
316                    }
317
318                    @Override
319                    public String describe() {
320                        return "scroll to position with positive offset";
321                    }
322                },
323                new PostLayoutRunnable() {
324                    @Override
325                    public void run() throws Throwable {
326                        mLayoutManager.expectLayouts(1);
327                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3,
328                                -50);
329                        mLayoutManager.waitForLayout(2);
330                    }
331
332                    @Override
333                    public String describe() {
334                        return "scroll to position with negative offset";
335                    }
336                }
337        };
338
339        PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{
340                new PostRestoreRunnable() {
341                    @Override
342                    public String describe() {
343                        return "Doing nothing";
344                    }
345                },
346                new PostRestoreRunnable() {
347                    @Override
348                    void onAfterRestore(Config config) throws Throwable {
349                        // update config as well so that restore assertions will work
350                        config.mOrientation = 1 - config.mOrientation;
351                        mLayoutManager.setOrientation(config.mOrientation);
352                    }
353
354                    @Override
355                    boolean shouldLayoutMatch(Config config) {
356                        return config.mItemCount == 0;
357                    }
358
359                    @Override
360                    public String describe() {
361                        return "Changing orientation";
362                    }
363                },
364                new PostRestoreRunnable() {
365                    @Override
366                    void onAfterRestore(Config config) throws Throwable {
367                        config.mStackFromEnd = !config.mStackFromEnd;
368                        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
369                    }
370
371                    @Override
372                    boolean shouldLayoutMatch(Config config) {
373                        return true; //stack from end should not move items on change
374                    }
375
376                    @Override
377                    public String describe() {
378                        return "Changing stack from end";
379                    }
380                },
381                new PostRestoreRunnable() {
382                    @Override
383                    void onAfterRestore(Config config) throws Throwable {
384                        config.mReverseLayout = !config.mReverseLayout;
385                        mLayoutManager.setReverseLayout(config.mReverseLayout);
386                    }
387
388                    @Override
389                    boolean shouldLayoutMatch(Config config) {
390                        return config.mItemCount == 0;
391                    }
392
393                    @Override
394                    public String describe() {
395                        return "Changing reverse layout";
396                    }
397                },
398                new PostRestoreRunnable() {
399                    @Override
400                    void onAfterRestore(Config config) throws Throwable {
401                        config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach;
402                        mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
403                    }
404
405                    @Override
406                    boolean shouldLayoutMatch(Config config) {
407                        return true;
408                    }
409
410                    @Override
411                    String describe() {
412                        return "Change shoudl recycle children";
413                    }
414                },
415                new PostRestoreRunnable() {
416                    int position;
417                    @Override
418                    void onAfterRestore(Config config) throws Throwable {
419                        position = mTestAdapter.getItemCount() / 2;
420                        mLayoutManager.scrollToPosition(position);
421                    }
422
423                    @Override
424                    boolean shouldLayoutMatch(Config config) {
425                        return mTestAdapter.getItemCount() == 0;
426                    }
427
428                    @Override
429                    String describe() {
430                        return "Scroll to position " + position ;
431                    }
432
433                    @Override
434                    void onAfterReLayout(Config config) {
435                        if (mTestAdapter.getItemCount() > 0) {
436                            assertEquals(config + ":scrolled view should be last completely visible",
437                                    position,
438                                    config.mStackFromEnd ?
439                                            mLayoutManager.findLastCompletelyVisibleItemPosition()
440                                        : mLayoutManager.findFirstCompletelyVisibleItemPosition());
441                        }
442                    }
443                }
444        };
445        boolean[] waitForLayoutOptions = new boolean[]{true, false};
446        List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300);
447        variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true);
448        for (Config config : variations) {
449            for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) {
450                for (boolean waitForLayout : waitForLayoutOptions) {
451                    for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) {
452                        savedStateTest((Config) config.clone(), waitForLayout, postLayoutRunnable,
453                                postRestoreRunnable);
454                        removeRecyclerView();
455                    }
456
457                }
458            }
459        }
460    }
461
462    public void savedStateTest(Config config, boolean waitForLayout,
463            PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)
464            throws Throwable {
465        if (DEBUG) {
466            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " +
467                    config + " post layout action " + postLayoutOperation.describe() +
468                    "post restore action " + postRestoreOperation.describe());
469        }
470        setupByConfig(config, false);
471        if (waitForLayout) {
472            waitForFirstLayout();
473            postLayoutOperation.run();
474        }
475        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
476        Parcelable savedState = mRecyclerView.onSaveInstanceState();
477        // we append a suffix to the parcelable to test out of bounds
478        String parcelSuffix = UUID.randomUUID().toString();
479        Parcel parcel = Parcel.obtain();
480        savedState.writeToParcel(parcel, 0);
481        parcel.writeString(parcelSuffix);
482        removeRecyclerView();
483        // reset for reading
484        parcel.setDataPosition(0);
485        // re-create
486        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
487        removeRecyclerView();
488
489        RecyclerView restored = new RecyclerView(getActivity());
490        // this config should be no op.
491        mLayoutManager = new WrappedLinearLayoutManager(getActivity(),
492                1 - config.mOrientation, !config.mReverseLayout);
493        mLayoutManager.setStackFromEnd(!config.mStackFromEnd);
494        restored.setLayoutManager(mLayoutManager);
495        // use the same adapter for Rect matching
496        restored.setAdapter(mTestAdapter);
497        restored.onRestoreInstanceState(savedState);
498        postRestoreOperation.onAfterRestore(config);
499        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
500                parcel.readString());
501        mLayoutManager.expectLayouts(1);
502        setRecyclerView(restored);
503        mLayoutManager.waitForLayout(2);
504        // calculate prefix here instead of above to include post restore changes
505        final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() +
506                "\npostRestore:" + postRestoreOperation.describe() + "\n";
507        assertEquals(logPrefix + " on saved state, reverse layout should be preserved",
508                config.mReverseLayout, mLayoutManager.getReverseLayout());
509        assertEquals(logPrefix + " on saved state, orientation should be preserved",
510                config.mOrientation, mLayoutManager.getOrientation());
511        assertEquals(logPrefix + " on saved state, stack from end should be preserved",
512                config.mStackFromEnd, mLayoutManager.getStackFromEnd());
513        assertEquals(logPrefix + " on saved state, mRecycleChildrenOnDetach should be preserved",
514                config.mRecycleChildrenOnDetach, mLayoutManager.getRecycleChildrenOnDetach());
515        if (waitForLayout) {
516            if (postRestoreOperation.shouldLayoutMatch(config)) {
517                assertRectSetsEqual(
518                        logPrefix + ": on restore, previous view positions should be preserved",
519                        before, mLayoutManager.collectChildCoordinates());
520            } else {
521                assertRectSetsNotEqual(
522                        logPrefix
523                                + ": on restore with changes, previous view positions should NOT "
524                                + "be preserved",
525                        before, mLayoutManager.collectChildCoordinates());
526            }
527            postRestoreOperation.onAfterReLayout(config);
528        }
529    }
530
531    void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
532        runTestOnUiThread(new Runnable() {
533            @Override
534            public void run() {
535                mLayoutManager.scrollToPositionWithOffset(position, offset);
536            }
537        });
538    }
539
540    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
541            Map<Item, Rect> after) {
542        Throwable throwable = null;
543        try {
544            assertRectSetsEqual("NOT " + message, before, after);
545        } catch (Throwable t) {
546            throwable = t;
547        }
548        assertNotNull(message + "\ntwo layout should be different", throwable);
549    }
550
551    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
552        StringBuilder sb = new StringBuilder();
553        sb.append("checking rectangle equality.");
554         sb.append("before:\n");
555        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
556            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
557        }
558        sb.append("after:\n");
559        for (Map.Entry<Item, Rect> entry : after.entrySet()) {
560            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
561        }
562        message = message + "\n" + sb.toString();
563        assertEquals(message + ":\nitem counts should be equal", before.size()
564                , after.size());
565        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
566            Rect afterRect = after.get(entry.getKey());
567            assertNotNull(message + ":\nSame item should be visible after simple re-layout",
568                    afterRect);
569            assertEquals(message + ":\nItem should be laid out at the same coordinates",
570                    entry.getValue(), afterRect);
571        }
572    }
573
574    static class VisibleChildren {
575
576        int firstVisiblePosition = RecyclerView.NO_POSITION;
577
578        int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
579
580        int lastVisiblePosition = RecyclerView.NO_POSITION;
581
582        int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
583
584        @Override
585        public String toString() {
586            return "VisibleChildren{" +
587                    "firstVisiblePosition=" + firstVisiblePosition +
588                    ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
589                    ", lastVisiblePosition=" + lastVisiblePosition +
590                    ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
591                    '}';
592        }
593    }
594
595    abstract private class PostLayoutRunnable {
596
597        abstract void run() throws Throwable;
598
599        abstract String describe();
600    }
601
602    abstract private class PostRestoreRunnable {
603
604        void onAfterRestore(Config config) throws Throwable {
605        }
606
607        abstract String describe();
608
609        boolean shouldLayoutMatch(Config config) {
610            return true;
611        }
612
613        void onAfterReLayout(Config config) {
614
615        };
616    }
617
618    class WrappedLinearLayoutManager extends LinearLayoutManager {
619
620        CountDownLatch layoutLatch;
621
622        OrientationHelper mSecondaryOrientation;
623
624        public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
625            super(context, orientation, reverseLayout);
626        }
627
628        public void expectLayouts(int count) {
629            layoutLatch = new CountDownLatch(count);
630        }
631
632        public void waitForLayout(long timeout) throws InterruptedException {
633            waitForLayout(timeout, TimeUnit.SECONDS);
634        }
635
636        @Override
637        public void setOrientation(int orientation) {
638            super.setOrientation(orientation);
639            mSecondaryOrientation = null;
640        }
641
642        @Override
643        public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) {
644            if (DEBUG) {
645                Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child));
646            }
647            super.removeAndRecycleView(child, recycler);
648        }
649
650        @Override
651        public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) {
652            if (DEBUG) {
653                Log.d(TAG, "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index)));
654            }
655            super.removeAndRecycleViewAt(index, recycler);
656        }
657
658        @Override
659        void ensureLayoutState() {
660            super.ensureLayoutState();
661            if (mSecondaryOrientation == null) {
662                mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
663                        1 - getOrientation());
664            }
665        }
666
667        private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
668            layoutLatch.await(timeout, timeUnit);
669            assertEquals("all expected layouts should be executed at the expected time",
670                    0, layoutLatch.getCount());
671        }
672
673        public String getBoundsLog() {
674            StringBuilder sb = new StringBuilder();
675            sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
676                    .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
677            sb.append("\nchildren bounds\n");
678            final int childCount = getChildCount();
679            for (int i = 0; i < childCount; i++) {
680                View child = getChildAt(i);
681                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
682                        .append("[").append("start:").append(
683                        mOrientationHelper.getDecoratedStart(child)).append(", end:")
684                        .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
685            }
686            return sb.toString();
687        }
688
689        public VisibleChildren traverseAndFindVisibleChildren() {
690            int childCount = getChildCount();
691            final VisibleChildren visibleChildren = new VisibleChildren();
692            final int start = mOrientationHelper.getStartAfterPadding();
693            final int end = mOrientationHelper.getEndAfterPadding();
694            for (int i = 0; i < childCount; i++) {
695                View child = getChildAt(i);
696                final int childStart = mOrientationHelper.getDecoratedStart(child);
697                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
698                final boolean fullyVisible = childStart >= start && childEnd <= end;
699                final boolean hidden = childEnd <= start || childStart >= end;
700                if (hidden) {
701                    continue;
702                }
703                final int position = getPosition(child);
704                if (fullyVisible) {
705                    if (position < visibleChildren.firstFullyVisiblePosition ||
706                            visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
707                        visibleChildren.firstFullyVisiblePosition = position;
708                    }
709
710                    if (position > visibleChildren.lastFullyVisiblePosition) {
711                        visibleChildren.lastFullyVisiblePosition = position;
712                    }
713                }
714
715                if (position < visibleChildren.firstVisiblePosition ||
716                        visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
717                    visibleChildren.firstVisiblePosition = position;
718                }
719
720                if (position > visibleChildren.lastVisiblePosition) {
721                    visibleChildren.lastVisiblePosition = position;
722                }
723
724            }
725            return visibleChildren;
726        }
727
728        Rect getViewBounds(View view) {
729            if (getOrientation() == HORIZONTAL) {
730                return new Rect(
731                        mOrientationHelper.getDecoratedStart(view),
732                        mSecondaryOrientation.getDecoratedStart(view),
733                        mOrientationHelper.getDecoratedEnd(view),
734                        mSecondaryOrientation.getDecoratedEnd(view));
735            } else {
736                return new Rect(
737                        mSecondaryOrientation.getDecoratedStart(view),
738                        mOrientationHelper.getDecoratedStart(view),
739                        mSecondaryOrientation.getDecoratedEnd(view),
740                        mOrientationHelper.getDecoratedEnd(view));
741            }
742
743        }
744
745        Map<Item, Rect> collectChildCoordinates() throws Throwable {
746            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
747            runTestOnUiThread(new Runnable() {
748                @Override
749                public void run() {
750                    final int childCount = getChildCount();
751                    for (int i = 0; i < childCount; i++) {
752                        View child = getChildAt(i);
753                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
754                                .getLayoutParams();
755                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
756                        items.put(vh.mBindedItem, getViewBounds(child));
757                    }
758                }
759            });
760            return items;
761        }
762
763        @Override
764        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
765            super.onLayoutChildren(recycler, state);
766            layoutLatch.countDown();
767        }
768    }
769
770    static class Config implements Cloneable {
771
772        private static final int DEFAULT_ITEM_COUNT = 100;
773
774        private boolean mStackFromEnd;
775
776        int mOrientation = LinearLayoutManager.VERTICAL;
777
778        boolean mReverseLayout = false;
779
780        boolean mRecycleChildrenOnDetach = false;
781
782        int mItemCount = DEFAULT_ITEM_COUNT;
783
784        TestAdapter mTestAdapter;
785
786        Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
787            mOrientation = orientation;
788            mReverseLayout = reverseLayout;
789            mStackFromEnd = stackFromEnd;
790        }
791
792        public Config() {
793
794        }
795
796        Config adapter(TestAdapter adapter) {
797            mTestAdapter = adapter;
798            return this;
799        }
800
801        Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
802            mRecycleChildrenOnDetach = recycleChildrenOnDetach;
803            return this;
804        }
805
806        Config orientation(int orientation) {
807            mOrientation = orientation;
808            return this;
809        }
810
811        Config stackFromBottom(boolean stackFromBottom) {
812            mStackFromEnd = stackFromBottom;
813            return this;
814        }
815
816        Config reverseLayout(boolean reverseLayout) {
817            mReverseLayout = reverseLayout;
818            return this;
819        }
820
821        public Config itemCount(int itemCount) {
822            mItemCount = itemCount;
823            return this;
824        }
825
826        // required by convention
827        @Override
828        public Object clone() throws CloneNotSupportedException {
829            return super.clone();
830        }
831
832        @Override
833        public String toString() {
834            return "Config{" +
835                    "mStackFromEnd=" + mStackFromEnd +
836                    ", mOrientation=" + mOrientation +
837                    ", mReverseLayout=" + mReverseLayout +
838                    ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
839                    ", mItemCount=" + mItemCount +
840                    '}';
841        }
842    }
843}
844