[go: nahoru, domu]

1/*
2 * Copyright (C) 2015 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
19
20import org.junit.Test;
21import org.junit.runner.RunWith;
22import org.junit.runners.Parameterized;
23
24import android.graphics.Rect;
25import android.os.Looper;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.support.v4.view.ViewCompat;
29import android.test.suitebuilder.annotation.MediumTest;
30import android.util.Log;
31import android.view.View;
32import android.view.ViewParent;
33
34import java.util.Arrays;
35import java.util.BitSet;
36import java.util.List;
37import java.util.Map;
38import java.util.UUID;
39
40import static android.support.v7.widget.LayoutState.LAYOUT_START;
41import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
42import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
43
44import static org.hamcrest.CoreMatchers.hasItem;
45import static org.hamcrest.CoreMatchers.is;
46import static org.hamcrest.CoreMatchers.not;
47import static org.hamcrest.CoreMatchers.sameInstance;
48import static org.junit.Assert.assertEquals;
49import static org.junit.Assert.assertNotNull;
50import static org.junit.Assert.assertThat;
51import static org.junit.Assert.assertTrue;
52
53@RunWith(Parameterized.class)
54@MediumTest
55public class StaggeredGridLayoutManagerBaseConfigSetTest
56        extends BaseStaggeredGridLayoutManagerTest {
57
58    @Parameterized.Parameters(name = "{0}")
59    public static List<Config> getParams() {
60        return createBaseVariations();
61    }
62
63    private final Config mConfig;
64
65    public StaggeredGridLayoutManagerBaseConfigSetTest(Config config)
66            throws CloneNotSupportedException {
67        mConfig = (Config) config.clone();
68    }
69
70    @Test
71    public void rTL() throws Throwable {
72        rtlTest(false, false);
73    }
74
75    @Test
76    public void rTLChangeAfter() throws Throwable {
77        rtlTest(true, false);
78    }
79
80    @Test
81    public void rTLItemWrapContent() throws Throwable {
82        rtlTest(false, true);
83    }
84
85    @Test
86    public void rTLChangeAfterItemWrapContent() throws Throwable {
87        rtlTest(true, true);
88    }
89
90    void rtlTest(boolean changeRtlAfter, final boolean wrapContent) throws Throwable {
91        if (mConfig.mSpanCount == 1) {
92            mConfig.mSpanCount = 2;
93        }
94        String logPrefix = mConfig + ", changeRtlAfterLayout:" + changeRtlAfter;
95        setupByConfig(mConfig.itemCount(5),
96                new GridTestAdapter(mConfig.mItemCount, mConfig.mOrientation) {
97                    @Override
98                    public void onBindViewHolder(TestViewHolder holder,
99                            int position) {
100                        super.onBindViewHolder(holder, position);
101                        if (wrapContent) {
102                            if (mOrientation == HORIZONTAL) {
103                                holder.itemView.getLayoutParams().height
104                                        = RecyclerView.LayoutParams.WRAP_CONTENT;
105                            } else {
106                                holder.itemView.getLayoutParams().width
107                                        = RecyclerView.LayoutParams.MATCH_PARENT;
108                            }
109                        }
110                    }
111                });
112        if (changeRtlAfter) {
113            waitFirstLayout();
114            mLayoutManager.expectLayouts(1);
115            mLayoutManager.setFakeRtl(true);
116            mLayoutManager.waitForLayout(2);
117        } else {
118            mLayoutManager.mFakeRTL = true;
119            waitFirstLayout();
120        }
121
122        assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
123        OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
124        View child0 = mLayoutManager.findViewByPosition(0);
125        View child1 = mLayoutManager.findViewByPosition(mConfig.mOrientation == VERTICAL ? 1
126                : mConfig.mSpanCount);
127        assertNotNull(logPrefix + " child position 0 should be laid out", child0);
128        assertNotNull(logPrefix + " child position 0 should be laid out", child1);
129        logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1);
130        if (mConfig.mOrientation == VERTICAL || !mConfig.mReverseLayout) {
131            assertTrue(logPrefix + " second child should be to the left of first child",
132                    helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1));
133            assertEquals(logPrefix + " first child should be right aligned",
134                    helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
135        } else {
136            assertTrue(logPrefix + " first child should be to the left of second child",
137                    helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0));
138            assertEquals(logPrefix + " first child should be left aligned",
139                    helper.getDecoratedStart(child0), helper.getStartAfterPadding());
140        }
141        checkForMainThreadException();
142    }
143
144    @Test
145    public void scrollBackAndPreservePositions() throws Throwable {
146        scrollBackAndPreservePositionsTest(false);
147    }
148
149    @Test
150    public void scrollBackAndPreservePositionsWithRestore() throws Throwable {
151        scrollBackAndPreservePositionsTest(true);
152    }
153
154    public void scrollBackAndPreservePositionsTest(final boolean saveRestoreInBetween)
155            throws Throwable {
156        setupByConfig(mConfig);
157        mAdapter.mOnBindCallback = new OnBindCallback() {
158            @Override
159            public void onBoundItem(TestViewHolder vh, int position) {
160                StaggeredGridLayoutManager.LayoutParams
161                        lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
162                        .getLayoutParams();
163                lp.setFullSpan((position * 7) % (mConfig.mSpanCount + 1) == 0);
164            }
165        };
166        waitFirstLayout();
167        final int[] globalPositions = new int[mAdapter.getItemCount()];
168        Arrays.fill(globalPositions, Integer.MIN_VALUE);
169        final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
170                * (mConfig.mReverseLayout ? -1 : 1);
171
172        final int[] globalPos = new int[1];
173        runTestOnUiThread(new Runnable() {
174            @Override
175            public void run() {
176                int globalScrollPosition = 0;
177                while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
178                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
179                        View child = mRecyclerView.getChildAt(i);
180                        final int pos = mRecyclerView.getChildLayoutPosition(child);
181                        if (globalPositions[pos] != Integer.MIN_VALUE) {
182                            continue;
183                        }
184                        if (mConfig.mReverseLayout) {
185                            globalPositions[pos] = globalScrollPosition +
186                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
187                        } else {
188                            globalPositions[pos] = globalScrollPosition +
189                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
190                        }
191                    }
192                    globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
193                            mRecyclerView.mRecycler, mRecyclerView.mState);
194                }
195                if (DEBUG) {
196                    Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
197                }
198                globalPos[0] = globalScrollPosition;
199            }
200        });
201        checkForMainThreadException();
202
203        if (saveRestoreInBetween) {
204            saveRestore(mConfig);
205        }
206
207        checkForMainThreadException();
208        runTestOnUiThread(new Runnable() {
209            @Override
210            public void run() {
211                int globalScrollPosition = globalPos[0];
212                // now scroll back and make sure global positions match
213                BitSet shouldTest = new BitSet(mAdapter.getItemCount());
214                shouldTest.set(0, mAdapter.getItemCount() - 1, true);
215                String assertPrefix = mConfig + ", restored in between:" + saveRestoreInBetween
216                        + " global pos must match when scrolling in reverse for position ";
217                int scrollAmount = Integer.MAX_VALUE;
218                while (!shouldTest.isEmpty() && scrollAmount != 0) {
219                    for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
220                        View child = mRecyclerView.getChildAt(i);
221                        int pos = mRecyclerView.getChildLayoutPosition(child);
222                        if (!shouldTest.get(pos)) {
223                            continue;
224                        }
225                        shouldTest.clear(pos);
226                        int globalPos;
227                        if (mConfig.mReverseLayout) {
228                            globalPos = globalScrollPosition +
229                                    mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
230                        } else {
231                            globalPos = globalScrollPosition +
232                                    mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
233                        }
234                        assertEquals(assertPrefix + pos,
235                                globalPositions[pos], globalPos);
236                    }
237                    scrollAmount = mLayoutManager.scrollBy(-scrollStep,
238                            mRecyclerView.mRecycler, mRecyclerView.mState);
239                    globalScrollPosition += scrollAmount;
240                }
241                assertTrue("all views should be seen", shouldTest.isEmpty());
242            }
243        });
244        checkForMainThreadException();
245    }
246
247    private void saveRestore(final Config config) throws Throwable {
248        runTestOnUiThread(new Runnable() {
249            @Override
250            public void run() {
251                try {
252                    Parcelable savedState = mRecyclerView.onSaveInstanceState();
253                    // we append a suffix to the parcelable to test out of bounds
254                    String parcelSuffix = UUID.randomUUID().toString();
255                    Parcel parcel = Parcel.obtain();
256                    savedState.writeToParcel(parcel, 0);
257                    parcel.writeString(parcelSuffix);
258                    removeRecyclerView();
259                    // reset for reading
260                    parcel.setDataPosition(0);
261                    // re-create
262                    savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
263                    RecyclerView restored = new RecyclerView(getActivity());
264                    mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
265                            config.mOrientation);
266                    mLayoutManager.setGapStrategy(config.mGapStrategy);
267                    restored.setLayoutManager(mLayoutManager);
268                    // use the same adapter for Rect matching
269                    restored.setAdapter(mAdapter);
270                    restored.onRestoreInstanceState(savedState);
271                    if (Looper.myLooper() == Looper.getMainLooper()) {
272                        mLayoutManager.expectLayouts(1);
273                        setRecyclerView(restored);
274                    } else {
275                        mLayoutManager.expectLayouts(1);
276                        setRecyclerView(restored);
277                        mLayoutManager.waitForLayout(2);
278                    }
279                } catch (Throwable t) {
280                    postExceptionToInstrumentation(t);
281                }
282            }
283        });
284        checkForMainThreadException();
285    }
286
287    @Test
288    public void getFirstLastChildrenTest() throws Throwable {
289        getFirstLastChildrenTest(false);
290    }
291
292    @Test
293    public void getFirstLastChildrenTestProvideArray() throws Throwable {
294        getFirstLastChildrenTest(true);
295    }
296
297    public void getFirstLastChildrenTest(final boolean provideArr) throws Throwable {
298        setupByConfig(mConfig);
299        waitFirstLayout();
300        Runnable viewInBoundsTest = new Runnable() {
301            @Override
302            public void run() {
303                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
304                final String boundsLog = mLayoutManager.getBoundsLog();
305                VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
306                queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager
307                        .findFirstVisibleItemClosestToStart(false, true);
308                queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager
309                        .findFirstVisibleItemClosestToEnd(false, true);
310                queryResult.firstFullyVisiblePositions = mLayoutManager
311                        .findFirstCompletelyVisibleItemPositions(
312                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
313                queryResult.firstVisiblePositions = mLayoutManager
314                        .findFirstVisibleItemPositions(
315                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
316                queryResult.lastFullyVisiblePositions = mLayoutManager
317                        .findLastCompletelyVisibleItemPositions(
318                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
319                queryResult.lastVisiblePositions = mLayoutManager
320                        .findLastVisibleItemPositions(
321                                provideArr ? new int[mLayoutManager.getSpanCount()] : null);
322                assertEquals(mConfig + ":\nfirst visible child should match traversal result\n"
323                        + "traversed:" + visibleChildren + "\n"
324                        + "queried:" + queryResult + "\n"
325                        + boundsLog, visibleChildren, queryResult
326                );
327            }
328        };
329        runTestOnUiThread(viewInBoundsTest);
330        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
331        // case
332        final int scrollPosition = mAdapter.getItemCount();
333        runTestOnUiThread(new Runnable() {
334            @Override
335            public void run() {
336                mRecyclerView.smoothScrollToPosition(scrollPosition);
337            }
338        });
339        while (mLayoutManager.isSmoothScrolling() ||
340                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
341            runTestOnUiThread(viewInBoundsTest);
342            checkForMainThreadException();
343            Thread.sleep(400);
344        }
345        // delete all items
346        mLayoutManager.expectLayouts(2);
347        mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
348        mLayoutManager.waitForLayout(2);
349        // test empty case
350        runTestOnUiThread(viewInBoundsTest);
351        // set a new adapter with huge items to test full bounds check
352        mLayoutManager.expectLayouts(1);
353        final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
354        final TestAdapter newAdapter = new TestAdapter(100) {
355            @Override
356            public void onBindViewHolder(TestViewHolder holder,
357                    int position) {
358                super.onBindViewHolder(holder, position);
359                if (mConfig.mOrientation == LinearLayoutManager.HORIZONTAL) {
360                    holder.itemView.setMinimumWidth(totalSpace + 100);
361                } else {
362                    holder.itemView.setMinimumHeight(totalSpace + 100);
363                }
364            }
365        };
366        runTestOnUiThread(new Runnable() {
367            @Override
368            public void run() {
369                mRecyclerView.setAdapter(newAdapter);
370            }
371        });
372        mLayoutManager.waitForLayout(2);
373        runTestOnUiThread(viewInBoundsTest);
374        checkForMainThreadException();
375
376        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
377        // case
378        runTestOnUiThread(new Runnable() {
379            @Override
380            public void run() {
381                final int diff;
382                if (mConfig.mReverseLayout) {
383                    diff = -1;
384                } else {
385                    diff = 1;
386                }
387                final int distance = diff * 10;
388                if (mConfig.mOrientation == HORIZONTAL) {
389                    mRecyclerView.scrollBy(distance, 0);
390                } else {
391                    mRecyclerView.scrollBy(0, distance);
392                }
393            }
394        });
395        runTestOnUiThread(viewInBoundsTest);
396        checkForMainThreadException();
397    }
398
399    @Test
400    public void viewSnapTest() throws Throwable {
401        final Config config = ((Config) mConfig.clone()).itemCount(mConfig.mSpanCount + 1);
402        setupByConfig(config);
403        mAdapter.mOnBindCallback = new OnBindCallback() {
404            @Override
405            void onBoundItem(TestViewHolder vh, int position) {
406                StaggeredGridLayoutManager.LayoutParams
407                        lp = (StaggeredGridLayoutManager.LayoutParams) vh.itemView
408                        .getLayoutParams();
409                if (config.mOrientation == HORIZONTAL) {
410                    lp.width = mRecyclerView.getWidth() / 3;
411                } else {
412                    lp.height = mRecyclerView.getHeight() / 3;
413                }
414            }
415
416            @Override
417            boolean assignRandomSize() {
418                return false;
419            }
420        };
421        waitFirstLayout();
422        // run these tests twice. once initial layout, once after scroll
423        String logSuffix = "";
424        for (int i = 0; i < 2; i++) {
425            Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
426            Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
427            // workaround for SGLM's span distribution issue. Right now, it may leave gaps so we
428            // avoid it by setting its layout params directly
429            if (config.mOrientation == HORIZONTAL) {
430                recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount;
431            } else {
432                recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount;
433            }
434
435            Rect usedLayoutBounds = new Rect();
436            for (Rect rect : itemRectMap.values()) {
437                usedLayoutBounds.union(rect);
438            }
439
440            if (DEBUG) {
441                Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
442            }
443            if (config.mOrientation == VERTICAL) {
444                assertEquals(config + " there should be no gap on left" + logSuffix,
445                        usedLayoutBounds.left, recyclerViewBounds.left);
446                assertEquals(config + " there should be no gap on right" + logSuffix,
447                        usedLayoutBounds.right, recyclerViewBounds.right);
448                if (config.mReverseLayout) {
449                    assertEquals(config + " there should be no gap on bottom" + logSuffix,
450                            usedLayoutBounds.bottom, recyclerViewBounds.bottom);
451                    assertTrue(config + " there should be some gap on top" + logSuffix,
452                            usedLayoutBounds.top > recyclerViewBounds.top);
453                } else {
454                    assertEquals(config + " there should be no gap on top" + logSuffix,
455                            usedLayoutBounds.top, recyclerViewBounds.top);
456                    assertTrue(config + " there should be some gap at the bottom" + logSuffix,
457                            usedLayoutBounds.bottom < recyclerViewBounds.bottom);
458                }
459            } else {
460                assertEquals(config + " there should be no gap on top" + logSuffix,
461                        usedLayoutBounds.top, recyclerViewBounds.top);
462                assertEquals(config + " there should be no gap at the bottom" + logSuffix,
463                        usedLayoutBounds.bottom, recyclerViewBounds.bottom);
464                if (config.mReverseLayout) {
465                    assertEquals(config + " there should be no on right" + logSuffix,
466                            usedLayoutBounds.right, recyclerViewBounds.right);
467                    assertTrue(config + " there should be some gap on left" + logSuffix,
468                            usedLayoutBounds.left > recyclerViewBounds.left);
469                } else {
470                    assertEquals(config + " there should be no gap on left" + logSuffix,
471                            usedLayoutBounds.left, recyclerViewBounds.left);
472                    assertTrue(config + " there should be some gap on right" + logSuffix,
473                            usedLayoutBounds.right < recyclerViewBounds.right);
474                }
475            }
476            final int scroll = config.mReverseLayout ? -500 : 500;
477            scrollBy(scroll);
478            logSuffix = " scrolled " + scroll;
479        }
480    }
481
482    @Test
483    public void scrollToPositionWithOffsetTest() throws Throwable {
484        setupByConfig(mConfig);
485        waitFirstLayout();
486        OrientationHelper orientationHelper = OrientationHelper
487                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
488        Rect layoutBounds = getDecoratedRecyclerViewBounds();
489        // try scrolling towards head, should not affect anything
490        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
491        scrollToPositionWithOffset(0, 20);
492        assertRectSetsEqual(mConfig + " trying to over scroll with offset should be no-op",
493                before, mLayoutManager.collectChildCoordinates());
494        // try offsetting some visible children
495        int testCount = 10;
496        while (testCount-- > 0) {
497            // get middle child
498            final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
499            final int position = mRecyclerView.getChildLayoutPosition(child);
500            final int startOffset = mConfig.mReverseLayout ?
501                    orientationHelper.getEndAfterPadding() - orientationHelper
502                            .getDecoratedEnd(child)
503                    : orientationHelper.getDecoratedStart(child) - orientationHelper
504                            .getStartAfterPadding();
505            final int scrollOffset = startOffset / 2;
506            mLayoutManager.expectLayouts(1);
507            scrollToPositionWithOffset(position, scrollOffset);
508            mLayoutManager.waitForLayout(2);
509            final int finalOffset = mConfig.mReverseLayout ?
510                    orientationHelper.getEndAfterPadding() - orientationHelper
511                            .getDecoratedEnd(child)
512                    : orientationHelper.getDecoratedStart(child) - orientationHelper
513                            .getStartAfterPadding();
514            assertEquals(mConfig + " scroll with offset on a visible child should work fine",
515                    scrollOffset, finalOffset);
516        }
517
518        // try scrolling to invisible children
519        testCount = 10;
520        // we test above and below, one by one
521        int offsetMultiplier = -1;
522        while (testCount-- > 0) {
523            final TargetTuple target = findInvisibleTarget(mConfig);
524            mLayoutManager.expectLayouts(1);
525            final int offset = offsetMultiplier
526                    * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
527            scrollToPositionWithOffset(target.mPosition, offset);
528            mLayoutManager.waitForLayout(2);
529            final View child = mLayoutManager.findViewByPosition(target.mPosition);
530            assertNotNull(mConfig + " scrolling to a mPosition with offset " + offset
531                    + " should layout it", child);
532            final Rect bounds = mLayoutManager.getViewBounds(child);
533            if (DEBUG) {
534                Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
535                        + layoutBounds + " with offset " + offset);
536            }
537
538            if (mConfig.mReverseLayout) {
539                assertEquals(mConfig + " when scrolling with offset to an invisible in reverse "
540                                + "layout, its end should align with recycler view's end - offset",
541                        orientationHelper.getEndAfterPadding() - offset,
542                        orientationHelper.getDecoratedEnd(child)
543                );
544            } else {
545                assertEquals(mConfig + " when scrolling with offset to an invisible child in normal"
546                                + " layout its start should align with recycler view's start + "
547                                + "offset",
548                        orientationHelper.getStartAfterPadding() + offset,
549                        orientationHelper.getDecoratedStart(child)
550                );
551            }
552            offsetMultiplier *= -1;
553        }
554    }
555
556    @Test
557    public void scrollToPositionTest() throws Throwable {
558        setupByConfig(mConfig);
559        waitFirstLayout();
560        OrientationHelper orientationHelper = OrientationHelper
561                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
562        Rect layoutBounds = getDecoratedRecyclerViewBounds();
563        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
564            View view = mLayoutManager.getChildAt(i);
565            Rect bounds = mLayoutManager.getViewBounds(view);
566            if (layoutBounds.contains(bounds)) {
567                Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
568                final int position = mRecyclerView.getChildLayoutPosition(view);
569                StaggeredGridLayoutManager.LayoutParams layoutParams
570                        = (StaggeredGridLayoutManager.LayoutParams) (view.getLayoutParams());
571                TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
572                assertEquals("recycler view mPosition should match adapter mPosition", position,
573                        vh.mBoundItem.mAdapterIndex);
574                if (DEBUG) {
575                    Log.d(TAG, "testing scroll to visible mPosition at " + position
576                            + " " + bounds + " inside " + layoutBounds);
577                }
578                mLayoutManager.expectLayouts(1);
579                scrollToPosition(position);
580                mLayoutManager.waitForLayout(2);
581                if (DEBUG) {
582                    view = mLayoutManager.findViewByPosition(position);
583                    Rect newBounds = mLayoutManager.getViewBounds(view);
584                    Log.d(TAG, "after scrolling to visible mPosition " +
585                            bounds + " equals " + newBounds);
586                }
587
588                assertRectSetsEqual(
589                        mConfig + "scroll to mPosition on fully visible child should be no-op",
590                        initialBounds, mLayoutManager.collectChildCoordinates());
591            } else {
592                final int position = mRecyclerView.getChildLayoutPosition(view);
593                if (DEBUG) {
594                    Log.d(TAG,
595                            "child(" + position + ") not fully visible " + bounds + " not inside "
596                                    + layoutBounds
597                                    + mRecyclerView.getChildLayoutPosition(view)
598                    );
599                }
600                mLayoutManager.expectLayouts(1);
601                runTestOnUiThread(new Runnable() {
602                    @Override
603                    public void run() {
604                        mLayoutManager.scrollToPosition(position);
605                    }
606                });
607                mLayoutManager.waitForLayout(2);
608                view = mLayoutManager.findViewByPosition(position);
609                bounds = mLayoutManager.getViewBounds(view);
610                if (DEBUG) {
611                    Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
612                            + layoutBounds);
613                }
614                assertTrue(mConfig
615                                + " after scrolling to a partially visible child, it should become fully "
616                                + " visible. " + bounds + " not inside " + layoutBounds,
617                        layoutBounds.contains(bounds)
618                );
619                assertTrue(
620                        mConfig + " when scrolling to a partially visible item, one of its edges "
621                                + "should be on the boundaries",
622                        orientationHelper.getStartAfterPadding() ==
623                                orientationHelper.getDecoratedStart(view)
624                                || orientationHelper.getEndAfterPadding() ==
625                                orientationHelper.getDecoratedEnd(view));
626            }
627        }
628
629        // try scrolling to invisible children
630        int testCount = 10;
631        while (testCount-- > 0) {
632            final TargetTuple target = findInvisibleTarget(mConfig);
633            mLayoutManager.expectLayouts(1);
634            scrollToPosition(target.mPosition);
635            mLayoutManager.waitForLayout(2);
636            final View child = mLayoutManager.findViewByPosition(target.mPosition);
637            assertNotNull(mConfig + " scrolling to a mPosition should lay it out", child);
638            final Rect bounds = mLayoutManager.getViewBounds(child);
639            if (DEBUG) {
640                Log.d(TAG, mConfig + " post scroll to invisible mPosition " + bounds + " in "
641                        + layoutBounds);
642            }
643            assertTrue(mConfig + " scrolling to a mPosition should make it fully visible",
644                    layoutBounds.contains(bounds));
645            if (target.mLayoutDirection == LAYOUT_START) {
646                assertEquals(
647                        mConfig + " when scrolling to an invisible child above, its start should"
648                                + " align with recycler view's start",
649                        orientationHelper.getStartAfterPadding(),
650                        orientationHelper.getDecoratedStart(child)
651                );
652            } else {
653                assertEquals(mConfig + " when scrolling to an invisible child below, its end "
654                                + "should align with recycler view's end",
655                        orientationHelper.getEndAfterPadding(),
656                        orientationHelper.getDecoratedEnd(child)
657                );
658            }
659        }
660    }
661
662    @Test
663    public void scollByTest() throws Throwable {
664        setupByConfig(mConfig);
665        waitFirstLayout();
666        // try invalid scroll. should not happen
667        final View first = mLayoutManager.getChildAt(0);
668        OrientationHelper primaryOrientation = OrientationHelper
669                .createOrientationHelper(mLayoutManager, mConfig.mOrientation);
670        int scrollDist;
671        if (mConfig.mReverseLayout) {
672            scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
673        } else {
674            scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
675        }
676        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
677        scrollBy(scrollDist);
678        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
679        assertRectSetsEqual(
680                mConfig + " if there are no more items, scroll should not happen (dt:" + scrollDist
681                        + ")",
682                before, after
683        );
684
685        scrollDist = -scrollDist * 3;
686        before = mLayoutManager.collectChildCoordinates();
687        scrollBy(scrollDist);
688        after = mLayoutManager.collectChildCoordinates();
689        int layoutStart = primaryOrientation.getStartAfterPadding();
690        int layoutEnd = primaryOrientation.getEndAfterPadding();
691        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
692            Rect afterRect = after.get(entry.getKey());
693            // offset rect
694            if (mConfig.mOrientation == VERTICAL) {
695                entry.getValue().offset(0, -scrollDist);
696            } else {
697                entry.getValue().offset(-scrollDist, 0);
698            }
699            if (afterRect == null || afterRect.isEmpty()) {
700                // assert item is out of bounds
701                int start, end;
702                if (mConfig.mOrientation == VERTICAL) {
703                    start = entry.getValue().top;
704                    end = entry.getValue().bottom;
705                } else {
706                    start = entry.getValue().left;
707                    end = entry.getValue().right;
708                }
709                assertTrue(
710                        mConfig + " if item is missing after relayout, it should be out of bounds."
711                                + "item start: " + start + ", end:" + end + " layout start:"
712                                + layoutStart +
713                                ", layout end:" + layoutEnd,
714                        start <= layoutStart && end <= layoutEnd ||
715                                start >= layoutEnd && end >= layoutEnd
716                );
717            } else {
718                assertEquals(mConfig + " Item should be laid out at the scroll offset coordinates",
719                        entry.getValue(),
720                        afterRect);
721            }
722        }
723        assertViewPositions(mConfig);
724    }
725
726    @Test
727    public void layoutOrderTest() throws Throwable {
728        setupByConfig(mConfig);
729        assertViewPositions(mConfig);
730    }
731
732    @Test
733    public void consistentRelayout() throws Throwable {
734        consistentRelayoutTest(mConfig, false);
735    }
736
737    @Test
738    public void consistentRelayoutWithFullSpanFirstChild() throws Throwable {
739        consistentRelayoutTest(mConfig, true);
740    }
741
742    @Test
743    public void dontRecycleViewsTranslatedOutOfBoundsFromStart() throws Throwable {
744        final Config config = ((Config) mConfig.clone()).itemCount(1000);
745        setupByConfig(config);
746        waitFirstLayout();
747        // pick position from child count so that it is not too far away
748        int pos = mRecyclerView.getChildCount() * 2;
749        smoothScrollToPosition(pos, true);
750        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
751        OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
752        int gap = helper.getDecoratedStart(vh.itemView);
753        scrollBy(gap);
754        gap = helper.getDecoratedStart(vh.itemView);
755        assertThat("test sanity", gap, is(0));
756
757        final int size = helper.getDecoratedMeasurement(vh.itemView);
758        AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
759        runTestOnUiThread(new Runnable() {
760            @Override
761            public void run() {
762                if (mConfig.mOrientation == HORIZONTAL) {
763                    ViewCompat.setTranslationX(vh.itemView, size * 2);
764                } else {
765                    ViewCompat.setTranslationY(vh.itemView, size * 2);
766                }
767            }
768        });
769        scrollBy(size * 2);
770        assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
771        assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
772        assertThat(vh.getAdapterPosition(), is(pos));
773        scrollBy(size * 2);
774        assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
775    }
776
777    @Test
778    public void dontRecycleViewsTranslatedOutOfBoundsFromEnd() throws Throwable {
779        final Config config = ((Config) mConfig.clone()).itemCount(1000);
780        setupByConfig(config);
781        waitFirstLayout();
782        // pick position from child count so that it is not too far away
783        int pos = mRecyclerView.getChildCount() * 2;
784        mLayoutManager.expectLayouts(1);
785        scrollToPosition(pos);
786        mLayoutManager.waitForLayout(2);
787        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForAdapterPosition(pos);
788        OrientationHelper helper = mLayoutManager.mPrimaryOrientation;
789        int gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
790        scrollBy(-gap);
791        gap = helper.getEnd() - helper.getDecoratedEnd(vh.itemView);
792        assertThat("test sanity", gap, is(0));
793
794        final int size = helper.getDecoratedMeasurement(vh.itemView);
795        AttachDetachCollector collector = new AttachDetachCollector(mRecyclerView);
796        runTestOnUiThread(new Runnable() {
797            @Override
798            public void run() {
799                if (mConfig.mOrientation == HORIZONTAL) {
800                    ViewCompat.setTranslationX(vh.itemView, -size * 2);
801                } else {
802                    ViewCompat.setTranslationY(vh.itemView, -size * 2);
803                }
804            }
805        });
806        scrollBy(-size * 2);
807        assertThat(collector.getDetached(), not(hasItem(sameInstance(vh.itemView))));
808        assertThat(vh.itemView.getParent(), is((ViewParent) mRecyclerView));
809        assertThat(vh.getAdapterPosition(), is(pos));
810        scrollBy(-size * 2);
811        assertThat(collector.getDetached(), hasItem(sameInstance(vh.itemView)));
812    }
813
814    public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
815            throws Throwable {
816        setupByConfig(config);
817        if (firstChildMultiSpan) {
818            mAdapter.mFullSpanItems.add(0);
819        }
820        waitFirstLayout();
821        // record all child positions
822        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
823        requestLayoutOnUIThread(mRecyclerView);
824        Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
825        assertRectSetsEqual(
826                config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
827                after);
828        // scroll some to create inconsistency
829        View firstChild = mLayoutManager.getChildAt(0);
830        final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
831                .getDecoratedStart(firstChild);
832        int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
833        if (config.mReverseLayout) {
834            distance *= -1;
835        }
836        scrollBy(distance);
837        waitForMainThread(2);
838        assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
839                mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
840        before = mLayoutManager.collectChildCoordinates();
841        mLayoutManager.expectLayouts(1);
842        requestLayoutOnUIThread(mRecyclerView);
843        mLayoutManager.waitForLayout(2);
844        after = mLayoutManager.collectChildCoordinates();
845        assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
846    }
847}
848