Home | History | Annotate | Download | only in widget
      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 
     17 package android.support.v7.widget;
     18 
     19 
     20 import org.junit.Test;
     21 import org.junit.runner.RunWith;
     22 import org.junit.runners.Parameterized;
     23 
     24 import android.graphics.Rect;
     25 import android.os.Looper;
     26 import android.os.Parcel;
     27 import android.os.Parcelable;
     28 import android.support.v4.view.ViewCompat;
     29 import android.test.suitebuilder.annotation.MediumTest;
     30 import android.util.Log;
     31 import android.view.View;
     32 import android.view.ViewParent;
     33 
     34 import java.util.Arrays;
     35 import java.util.BitSet;
     36 import java.util.List;
     37 import java.util.Map;
     38 import java.util.UUID;
     39 
     40 import static android.support.v7.widget.LayoutState.LAYOUT_START;
     41 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     42 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
     43 
     44 import static org.hamcrest.CoreMatchers.hasItem;
     45 import static org.hamcrest.CoreMatchers.is;
     46 import static org.hamcrest.CoreMatchers.not;
     47 import static org.hamcrest.CoreMatchers.sameInstance;
     48 import static org.junit.Assert.assertEquals;
     49 import static org.junit.Assert.assertNotNull;
     50 import static org.junit.Assert.assertThat;
     51 import static org.junit.Assert.assertTrue;
     52 
     53 @RunWith(Parameterized.class)
     54 @MediumTest
     55 public 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