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