Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 
     18 package android.support.v7.widget;
     19 
     20 
     21 import android.graphics.Rect;
     22 import android.os.Looper;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     25 import android.support.annotation.Nullable;
     26 import android.support.v4.view.AccessibilityDelegateCompat;
     27 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     28 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     29 import android.util.Log;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.accessibility.AccessibilityEvent;
     33 
     34 import java.util.ArrayList;
     35 import java.util.Arrays;
     36 import java.util.BitSet;
     37 import java.util.HashMap;
     38 import java.util.HashSet;
     39 import java.util.LinkedHashMap;
     40 import java.util.List;
     41 import java.util.Map;
     42 import java.util.UUID;
     43 import java.util.concurrent.CountDownLatch;
     44 import java.util.concurrent.TimeUnit;
     45 import java.util.concurrent.atomic.AtomicInteger;
     46 
     47 import static android.support.v7.widget.LayoutState.*;
     48 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     49 import static android.support.v7.widget.StaggeredGridLayoutManager.*;
     50 
     51 public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
     52 
     53     private static final boolean DEBUG = false;
     54 
     55     private static final int AVG_ITEM_PER_VIEW = 3;
     56 
     57     private static final String TAG = "StaggeredGridLayoutManagerTest";
     58 
     59     volatile WrappedLayoutManager mLayoutManager;
     60 
     61     GridTestAdapter mAdapter;
     62 
     63     final List<Config> mBaseVariations = new ArrayList<Config>();
     64 
     65     @Override
     66     protected void setUp() throws Exception {
     67         super.setUp();
     68         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
     69             for (boolean reverseLayout : new boolean[]{false, true}) {
     70                 for (int spanCount : new int[]{1, 3}) {
     71                     for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
     72                             GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
     73                         mBaseVariations.add(new Config(orientation, reverseLayout, spanCount,
     74                                 gapStrategy));
     75                     }
     76                 }
     77             }
     78         }
     79     }
     80 
     81     void setupByConfig(Config config) throws Throwable {
     82         mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation);
     83         mRecyclerView = new RecyclerView(getActivity());
     84         mRecyclerView.setAdapter(mAdapter);
     85         mRecyclerView.setHasFixedSize(true);
     86         mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
     87                 config.mOrientation);
     88         mLayoutManager.setGapStrategy(config.mGapStrategy);
     89         mLayoutManager.setReverseLayout(config.mReverseLayout);
     90         mRecyclerView.setLayoutManager(mLayoutManager);
     91         mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
     92             @Override
     93             public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
     94                     RecyclerView.State state) {
     95                 try {
     96                     LayoutParams lp = (LayoutParams) view.getLayoutParams();
     97                     assertNotNull("view should have layout params assigned", lp);
     98                     assertNotNull("when item offsets are requested, view should have a valid span",
     99                             lp.mSpan);
    100                 } catch (Throwable t) {
    101                     postExceptionToInstrumentation(t);
    102                 }
    103             }
    104         });
    105     }
    106 
    107     public void testAreAllStartsTheSame() throws Throwable {
    108         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
    109         waitFirstLayout();
    110         smoothScrollToPosition(100);
    111         mLayoutManager.expectLayouts(1);
    112         mAdapter.deleteAndNotify(0, 2);
    113         mLayoutManager.waitForLayout(2);
    114         smoothScrollToPosition(0);
    115         assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
    116     }
    117 
    118     public void testAreAllEndsTheSame() throws Throwable {
    119         setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
    120         waitFirstLayout();
    121         smoothScrollToPosition(100);
    122         mLayoutManager.expectLayouts(1);
    123         mAdapter.deleteAndNotify(0, 2);
    124         mLayoutManager.waitForLayout(2);
    125         smoothScrollToPosition(0);
    126         assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
    127     }
    128 
    129     public void testFindLastInUnevenDistribution() throws Throwable {
    130         setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
    131                 .itemCount(5));
    132         mAdapter.mOnBindCallback = new OnBindCallback() {
    133             @Override
    134             void onBoundItem(TestViewHolder vh, int position) {
    135                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
    136                 if (position == 1) {
    137                     lp.height = mRecyclerView.getHeight() - 10;
    138                 } else {
    139                     lp.height = 5;
    140                 }
    141                 vh.itemView.setMinimumHeight(0);
    142             }
    143         };
    144         waitFirstLayout();
    145         int[] into = new int[2];
    146         mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
    147         assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
    148         assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
    149         mLayoutManager.findLastCompletelyVisibleItemPositions(into);
    150         assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
    151         assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
    152         assertEquals("first fully visible child should be at position",
    153                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
    154                         findFirstVisibleItemClosestToStart(true, true)).getPosition());
    155         assertEquals("last fully visible child should be at position",
    156                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
    157                         findFirstVisibleItemClosestToEnd(true, true)).getPosition());
    158 
    159         assertEquals("first visible child should be at position",
    160                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
    161                         findFirstVisibleItemClosestToStart(false, true)).getPosition());
    162         assertEquals("last visible child should be at position",
    163                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
    164                         findFirstVisibleItemClosestToEnd(false, true)).getPosition());
    165 
    166     }
    167 
    168     public void testCustomWidthInHorizontal() throws Throwable {
    169         customSizeInScrollDirectionTest(
    170                 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    171     }
    172 
    173     public void testCustomHeightInVertical() throws Throwable {
    174         customSizeInScrollDirectionTest(
    175                 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    176     }
    177 
    178     public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
    179         setupByConfig(config);
    180         final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
    181         mAdapter.mOnBindCallback = new OnBindCallback() {
    182             @Override
    183             void onBoundItem(TestViewHolder vh, int position) {
    184                 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
    185                 final int size = 1 + position * 5;
    186                 if (config.mOrientation == HORIZONTAL) {
    187                     layoutParams.width = size;
    188                 } else {
    189                     layoutParams.height = size;
    190                 }
    191                 sizeMap.put(vh.itemView, size);
    192                 if (position == 3) {
    193                     getLp(vh.itemView).setFullSpan(true);
    194                 }
    195             }
    196 
    197             @Override
    198             boolean assignRandomSize() {
    199                 return false;
    200             }
    201         };
    202         waitFirstLayout();
    203         assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
    204         for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    205             View child = mRecyclerView.getChildAt(i);
    206             final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
    207                     : child.getHeight();
    208             assertEquals("child " + i + " should have the size specified in its layout params",
    209                     sizeMap.get(child).intValue(), size);
    210         }
    211         checkForMainThreadException();
    212     }
    213 
    214     public void testRTL() throws Throwable {
    215         for (boolean changeRtlAfter : new boolean[]{false, true}) {
    216             for (Config config : mBaseVariations) {
    217                 rtlTest(config, changeRtlAfter);
    218                 removeRecyclerView();
    219             }
    220         }
    221     }
    222 
    223     void rtlTest(Config config, boolean changeRtlAfter) throws Throwable {
    224         if (config.mSpanCount == 1) {
    225             config.mSpanCount = 2;
    226         }
    227         String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter;
    228         setupByConfig(config.itemCount(5));
    229         if (changeRtlAfter) {
    230             waitFirstLayout();
    231             mLayoutManager.expectLayouts(1);
    232             mLayoutManager.setFakeRtl(true);
    233             mLayoutManager.waitForLayout(2);
    234         } else {
    235             mLayoutManager.mFakeRTL = true;
    236             waitFirstLayout();
    237         }
    238 
    239         assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
    240         OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
    241         View child0 = mLayoutManager.findViewByPosition(0);
    242         View child1 = mLayoutManager.findViewByPosition(config.mOrientation == VERTICAL ? 1
    243                 : config.mSpanCount);
    244         assertNotNull(logPrefix + " child position 0 should be laid out", child0);
    245         assertNotNull(logPrefix + " child position 0 should be laid out", child1);
    246         logPrefix += " child1 pos:" + mLayoutManager.getPosition(child1);
    247         if (config.mOrientation == VERTICAL || !config.mReverseLayout) {
    248             assertTrue(logPrefix + " second child should be to the left of first child",
    249                     helper.getDecoratedEnd(child0) > helper.getDecoratedEnd(child1));
    250             assertEquals(logPrefix + " first child should be right aligned",
    251                     helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
    252         } else {
    253             assertTrue(logPrefix + " first child should be to the left of second child",
    254                     helper.getDecoratedStart(child1) >= helper.getDecoratedStart(child0));
    255             assertEquals(logPrefix + " first child should be left aligned",
    256                     helper.getDecoratedStart(child0), helper.getStartAfterPadding());
    257         }
    258         checkForMainThreadException();
    259     }
    260 
    261     public void testGapHandlingWhenItemMovesToTop() throws Throwable {
    262         gapHandlingWhenItemMovesToTopTest();
    263     }
    264 
    265     public void testGapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
    266         gapHandlingWhenItemMovesToTopTest(0);
    267     }
    268 
    269     public void testGapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
    270         gapHandlingWhenItemMovesToTopTest(1);
    271     }
    272 
    273     public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
    274         Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    275         config.itemCount(3);
    276         setupByConfig(config);
    277         mAdapter.mOnBindCallback = new OnBindCallback() {
    278             @Override
    279             void onBoundItem(TestViewHolder vh, int position) {
    280             }
    281 
    282             @Override
    283             boolean assignRandomSize() {
    284                 return false;
    285             }
    286         };
    287         for (int i : fullSpanIndices) {
    288             mAdapter.mFullSpanItems.add(i);
    289         }
    290         waitFirstLayout();
    291         mLayoutManager.expectLayouts(1);
    292         mAdapter.moveItem(1, 0, true);
    293         mLayoutManager.waitForLayout(2);
    294         final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
    295         // move back.
    296         mLayoutManager.expectLayouts(1);
    297         mAdapter.moveItem(0, 1, true);
    298         mLayoutManager.waitForLayout(2);
    299         mLayoutManager.expectLayouts(2);
    300         mAdapter.moveAndNotify(1, 0);
    301         mLayoutManager.waitForLayout(2);
    302         Thread.sleep(1000);
    303         getInstrumentation().waitForIdleSync();
    304         checkForMainThreadException();
    305         // item should be positioned properly
    306         assertRectSetsEqual("final position after a move", desiredPositions,
    307                 mLayoutManager.collectChildCoordinates());
    308 
    309     }
    310 
    311 
    312     public void testScrollBackAndPreservePositions() throws Throwable {
    313         for (boolean saveRestore : new boolean[]{false, true}) {
    314             for (Config config : mBaseVariations) {
    315                 scrollBackAndPreservePositionsTest(config, saveRestore);
    316                 removeRecyclerView();
    317             }
    318         }
    319     }
    320 
    321     public void scrollBackAndPreservePositionsTest(final Config config,
    322             final boolean saveRestoreInBetween)
    323             throws Throwable {
    324         setupByConfig(config);
    325         mAdapter.mOnBindCallback = new OnBindCallback() {
    326             @Override
    327             public void onBoundItem(TestViewHolder vh, int position) {
    328                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
    329                 lp.setFullSpan((position * 7) % (config.mSpanCount + 1) == 0);
    330             }
    331         };
    332         waitFirstLayout();
    333         final int[] globalPositions = new int[mAdapter.getItemCount()];
    334         Arrays.fill(globalPositions, Integer.MIN_VALUE);
    335         final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
    336                 * (config.mReverseLayout ? -1 : 1);
    337 
    338         final int[] globalPos = new int[1];
    339         runTestOnUiThread(new Runnable() {
    340             @Override
    341             public void run() {
    342                 int globalScrollPosition = 0;
    343                 while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
    344                     for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    345                         View child = mRecyclerView.getChildAt(i);
    346                         final int pos = mRecyclerView.getChildLayoutPosition(child);
    347                         if (globalPositions[pos] != Integer.MIN_VALUE) {
    348                             continue;
    349                         }
    350                         if (config.mReverseLayout) {
    351                             globalPositions[pos] = globalScrollPosition +
    352                                     mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
    353                         } else {
    354                             globalPositions[pos] = globalScrollPosition +
    355                                     mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
    356                         }
    357                     }
    358                     globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
    359                             mRecyclerView.mRecycler, mRecyclerView.mState);
    360                 }
    361                 if (DEBUG) {
    362                     Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
    363                 }
    364                 globalPos[0] = globalScrollPosition;
    365             }
    366         });
    367         checkForMainThreadException();
    368 
    369         if (saveRestoreInBetween) {
    370             saveRestore(config);
    371         }
    372 
    373         checkForMainThreadException();
    374         runTestOnUiThread(new Runnable() {
    375             @Override
    376             public void run() {
    377                 int globalScrollPosition = globalPos[0];
    378                 // now scroll back and make sure global positions match
    379                 BitSet shouldTest = new BitSet(mAdapter.getItemCount());
    380                 shouldTest.set(0, mAdapter.getItemCount() - 1, true);
    381                 String assertPrefix = config + ", restored in between:" + saveRestoreInBetween
    382                         + " global pos must match when scrolling in reverse for position ";
    383                 int scrollAmount = Integer.MAX_VALUE;
    384                 while (!shouldTest.isEmpty() && scrollAmount != 0) {
    385                     for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    386                         View child = mRecyclerView.getChildAt(i);
    387                         int pos = mRecyclerView.getChildLayoutPosition(child);
    388                         if (!shouldTest.get(pos)) {
    389                             continue;
    390                         }
    391                         shouldTest.clear(pos);
    392                         int globalPos;
    393                         if (config.mReverseLayout) {
    394                             globalPos = globalScrollPosition +
    395                                     mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
    396                         } else {
    397                             globalPos = globalScrollPosition +
    398                                     mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
    399                         }
    400                         assertEquals(assertPrefix + pos,
    401                                 globalPositions[pos], globalPos);
    402                     }
    403                     scrollAmount = mLayoutManager.scrollBy(-scrollStep,
    404                             mRecyclerView.mRecycler, mRecyclerView.mState);
    405                     globalScrollPosition += scrollAmount;
    406                 }
    407                 assertTrue("all views should be seen", shouldTest.isEmpty());
    408             }
    409         });
    410         checkForMainThreadException();
    411     }
    412 
    413     public void testScrollToPositionWithPredictive() throws Throwable {
    414         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
    415         removeRecyclerView();
    416         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
    417                 LinearLayoutManager.INVALID_OFFSET);
    418         removeRecyclerView();
    419         scrollToPositionWithPredictive(9, 20);
    420         removeRecyclerView();
    421         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
    422 
    423     }
    424 
    425     public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
    426             throws Throwable {
    427         setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
    428                 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
    429         waitFirstLayout();
    430         mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
    431             @Override
    432             void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
    433                 RecyclerView rv = mLayoutManager.mRecyclerView;
    434                 if (state.isPreLayout()) {
    435                     assertEquals("pending scroll position should still be pending",
    436                             scrollPosition, mLayoutManager.mPendingScrollPosition);
    437                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    438                         assertEquals("pending scroll position offset should still be pending",
    439                                 scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
    440                     }
    441                 } else {
    442                     RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
    443                     assertNotNull("scroll to position should work", vh);
    444                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    445                         assertEquals("scroll offset should be applied properly",
    446                                 mLayoutManager.getPaddingTop() + scrollOffset
    447                                         + ((RecyclerView.LayoutParams) vh.itemView
    448                                         .getLayoutParams()).topMargin,
    449                                 mLayoutManager.getDecoratedTop(vh.itemView));
    450                     }
    451                 }
    452             }
    453         };
    454         mLayoutManager.expectLayouts(2);
    455         runTestOnUiThread(new Runnable() {
    456             @Override
    457             public void run() {
    458                 try {
    459                     mAdapter.addAndNotify(0, 1);
    460                     if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
    461                         mLayoutManager.scrollToPosition(scrollPosition);
    462                     } else {
    463                         mLayoutManager.scrollToPositionWithOffset(scrollPosition,
    464                                 scrollOffset);
    465                     }
    466 
    467                 } catch (Throwable throwable) {
    468                     throwable.printStackTrace();
    469                 }
    470 
    471             }
    472         });
    473         mLayoutManager.waitForLayout(2);
    474         checkForMainThreadException();
    475     }
    476 
    477     LayoutParams getLp(View view) {
    478         return (LayoutParams) view.getLayoutParams();
    479     }
    480 
    481     public void testGetFirstLastChildrenTest() throws Throwable {
    482         for (boolean provideArr : new boolean[]{true, false}) {
    483             for (Config config : mBaseVariations) {
    484                 getFirstLastChildrenTest(config, provideArr);
    485                 removeRecyclerView();
    486             }
    487         }
    488     }
    489 
    490     public void getFirstLastChildrenTest(final Config config, final boolean provideArr)
    491             throws Throwable {
    492         setupByConfig(config);
    493         waitFirstLayout();
    494         Runnable viewInBoundsTest = new Runnable() {
    495             @Override
    496             public void run() {
    497                 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
    498                 final String boundsLog = mLayoutManager.getBoundsLog();
    499                 VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
    500                 queryResult.findFirstPartialVisibleClosestToStart = mLayoutManager
    501                         .findFirstVisibleItemClosestToStart(false, true);
    502                 queryResult.findFirstPartialVisibleClosestToEnd = mLayoutManager
    503                         .findFirstVisibleItemClosestToEnd(false, true);
    504                 queryResult.firstFullyVisiblePositions = mLayoutManager
    505                         .findFirstCompletelyVisibleItemPositions(
    506                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    507                 queryResult.firstVisiblePositions = mLayoutManager
    508                         .findFirstVisibleItemPositions(
    509                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    510                 queryResult.lastFullyVisiblePositions = mLayoutManager
    511                         .findLastCompletelyVisibleItemPositions(
    512                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    513                 queryResult.lastVisiblePositions = mLayoutManager
    514                         .findLastVisibleItemPositions(
    515                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    516                 assertEquals(config + ":\nfirst visible child should match traversal result\n"
    517                                 + "traversed:" + visibleChildren + "\n"
    518                                 + "queried:" + queryResult + "\n"
    519                                 + boundsLog, visibleChildren, queryResult
    520                 );
    521             }
    522         };
    523         runTestOnUiThread(viewInBoundsTest);
    524         // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
    525         // case
    526         final int scrollPosition = mAdapter.getItemCount();
    527         runTestOnUiThread(new Runnable() {
    528             @Override
    529             public void run() {
    530                 mRecyclerView.smoothScrollToPosition(scrollPosition);
    531             }
    532         });
    533         while (mLayoutManager.isSmoothScrolling() ||
    534                 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
    535             runTestOnUiThread(viewInBoundsTest);
    536             checkForMainThreadException();
    537             Thread.sleep(400);
    538         }
    539         // delete all items
    540         mLayoutManager.expectLayouts(2);
    541         mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
    542         mLayoutManager.waitForLayout(2);
    543         // test empty case
    544         runTestOnUiThread(viewInBoundsTest);
    545         // set a new adapter with huge items to test full bounds check
    546         mLayoutManager.expectLayouts(1);
    547         final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
    548         final TestAdapter newAdapter = new TestAdapter(100) {
    549             @Override
    550             public void onBindViewHolder(TestViewHolder holder,
    551                     int position) {
    552                 super.onBindViewHolder(holder, position);
    553                 if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
    554                     holder.itemView.setMinimumWidth(totalSpace + 100);
    555                 } else {
    556                     holder.itemView.setMinimumHeight(totalSpace + 100);
    557                 }
    558             }
    559         };
    560         runTestOnUiThread(new Runnable() {
    561             @Override
    562             public void run() {
    563                 mRecyclerView.setAdapter(newAdapter);
    564             }
    565         });
    566         mLayoutManager.waitForLayout(2);
    567         runTestOnUiThread(viewInBoundsTest);
    568         checkForMainThreadException();
    569 
    570         // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
    571         // case
    572         runTestOnUiThread(new Runnable() {
    573             @Override
    574             public void run() {
    575                 final int diff;
    576                 if (config.mReverseLayout) {
    577                     diff = -1;
    578                 } else {
    579                     diff = 1;
    580                 }
    581                 final int distance = diff * 10;
    582                 if (config.mOrientation == HORIZONTAL) {
    583                     mRecyclerView.scrollBy(distance, 0);
    584                 } else {
    585                     mRecyclerView.scrollBy(0, distance);
    586                 }
    587             }
    588         });
    589         runTestOnUiThread(viewInBoundsTest);
    590         checkForMainThreadException();
    591     }
    592 
    593     public void testMoveGapHandling() throws Throwable {
    594         Config config = new Config().spanCount(2).itemCount(40);
    595         setupByConfig(config);
    596         waitFirstLayout();
    597         mLayoutManager.expectLayouts(2);
    598         mAdapter.moveAndNotify(4, 1);
    599         mLayoutManager.waitForLayout(2);
    600         assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
    601     }
    602 
    603     public void testUpdateAfterFullSpan() throws Throwable {
    604         updateAfterFullSpanGapHandlingTest(0);
    605     }
    606 
    607     public void testUpdateAfterFullSpan2() throws Throwable {
    608         updateAfterFullSpanGapHandlingTest(20);
    609     }
    610 
    611     public void testTemporaryGapHandling() throws Throwable {
    612         int fullSpanIndex = 200;
    613         setupByConfig(new Config().spanCount(2).itemCount(500));
    614         mAdapter.mFullSpanItems.add(fullSpanIndex);
    615         waitFirstLayout();
    616         smoothScrollToPosition(fullSpanIndex + 200);// go far away
    617         assertNull("test sanity. full span item should not be visible",
    618                 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
    619         mLayoutManager.expectLayouts(1);
    620         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
    621         mLayoutManager.waitForLayout(1);
    622         smoothScrollToPosition(0);
    623         mLayoutManager.expectLayouts(1);
    624         smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
    625         String log = mLayoutManager.layoutToString("post gap");
    626         mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
    627                 + "relayout " + log, 2);
    628         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
    629         assertNotNull("full span item should be there:\n" + log, fullSpan);
    630         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
    631         assertNotNull("next view should be there\n" + log, view1);
    632         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
    633         assertNotNull("+2 view should be there\n" + log, view2);
    634 
    635         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
    636         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
    637         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
    638         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
    639         assertEquals("no gap between span and view 1",
    640                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    641                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
    642         assertEquals("no gap between span and view 2",
    643                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    644                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    645     }
    646 
    647     public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
    648         setupByConfig(new Config().spanCount(2).itemCount(100));
    649         mAdapter.mFullSpanItems.add(fullSpanIndex);
    650         waitFirstLayout();
    651         smoothScrollToPosition(fullSpanIndex + 30);
    652         mLayoutManager.expectLayouts(1);
    653         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
    654         mLayoutManager.waitForLayout(1);
    655         smoothScrollToPosition(fullSpanIndex);
    656         // give it some time to fix the gap
    657         Thread.sleep(500);
    658         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
    659 
    660         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
    661         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
    662 
    663         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
    664         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
    665         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
    666         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
    667         assertEquals("no gap between span and view 1",
    668                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    669                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
    670         assertEquals("no gap between span and view 2",
    671                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    672                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    673     }
    674 
    675     public void testInnerGapHandling() throws Throwable {
    676         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
    677         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    678     }
    679 
    680     public void innerGapHandlingTest(int strategy) throws Throwable {
    681         Config config = new Config().spanCount(3).itemCount(500);
    682         setupByConfig(config);
    683         mLayoutManager.setGapStrategy(strategy);
    684         mAdapter.mFullSpanItems.add(100);
    685         mAdapter.mFullSpanItems.add(104);
    686         mAdapter.mViewsHaveEqualSize = true;
    687         mAdapter.mOnBindCallback = new OnBindCallback() {
    688             @Override
    689             void onBoundItem(TestViewHolder vh, int position) {
    690 
    691             }
    692 
    693             @Override
    694             void onCreatedViewHolder(TestViewHolder vh) {
    695                 super.onCreatedViewHolder(vh);
    696                 //make sure we have enough views
    697                 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
    698             }
    699         };
    700         waitFirstLayout();
    701         mLayoutManager.expectLayouts(1);
    702         scrollToPosition(400);
    703         mLayoutManager.waitForLayout(2);
    704         View view400 = mLayoutManager.findViewByPosition(400);
    705         assertNotNull("test sanity, scrollToPos should succeed", view400);
    706         assertTrue("test sanity, view should be visible top",
    707                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
    708                         mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
    709         assertTrue("test sanity, view should be visible bottom",
    710                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
    711                         mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
    712         mLayoutManager.expectLayouts(2);
    713         mAdapter.addAndNotify(101, 1);
    714         mLayoutManager.waitForLayout(2);
    715         checkForMainThreadException();
    716         if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
    717             mLayoutManager.expectLayouts(1);
    718         }
    719         // state
    720         // now smooth scroll to 99 to trigger a layout around 100
    721         mLayoutManager.validateChildren();
    722         smoothScrollToPosition(99);
    723         switch (strategy) {
    724             case GAP_HANDLING_NONE:
    725                 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
    726                         new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
    727                         new int[]{105, 0});
    728                 break;
    729             case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
    730                 mLayoutManager.waitForLayout(2);
    731                 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
    732                         new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
    733                 break;
    734         }
    735 
    736     }
    737 
    738     public void testFullSizeSpans() throws Throwable {
    739         Config config = new Config().spanCount(5).itemCount(30);
    740         setupByConfig(config);
    741         mAdapter.mFullSpanItems.add(3);
    742         waitFirstLayout();
    743         assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
    744                 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
    745                 new int[]{7, 3}, new int[]{8, 4});
    746     }
    747 
    748     void assertSpans(String msg, int[]... childSpanTuples) {
    749         msg = msg + mLayoutManager.layoutToString("\n\n");
    750         for (int i = 0; i < childSpanTuples.length; i++) {
    751             assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
    752         }
    753     }
    754 
    755     void assertSpan(String msg, int childPosition, int expectedSpan) {
    756         View view = mLayoutManager.findViewByPosition(childPosition);
    757         assertNotNull(msg + " view at position " + childPosition + " should exists", view);
    758         assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
    759                 getLp(view).mSpan.mIndex);
    760     }
    761 
    762     public void testGapAtTheBeginning() throws Throwable {
    763         for (Config config : mBaseVariations) {
    764             for (int deleteCount = 1; deleteCount < config.mSpanCount * 2; deleteCount++) {
    765                 for (int deletePosition = config.mSpanCount - 1;
    766                         deletePosition < config.mSpanCount + 2; deletePosition++) {
    767                     gapAtTheBeginningOfTheListTest(config, deletePosition, deleteCount);
    768                     removeRecyclerView();
    769                 }
    770             }
    771         }
    772     }
    773 
    774     public void gapAtTheBeginningOfTheListTest(final Config config, int deletePosition,
    775             int deleteCount) throws Throwable {
    776         if (config.mSpanCount < 2 || config.mGapStrategy == GAP_HANDLING_NONE) {
    777             return;
    778         }
    779         if (config.mItemCount < 100) {
    780             config.itemCount(100);
    781         }
    782         final String logPrefix = config + ", deletePos:" + deletePosition + ", deleteCount:"
    783                 + deleteCount;
    784         setupByConfig(config);
    785         final RecyclerView.Adapter adapter = mAdapter;
    786         waitFirstLayout();
    787         // scroll far away
    788         smoothScrollToPosition(config.mItemCount / 2);
    789         checkForMainThreadException();
    790         // assert to be deleted child is not visible
    791         assertNull(logPrefix + " test sanity, to be deleted child should be invisible",
    792                 mRecyclerView.findViewHolderForLayoutPosition(deletePosition));
    793         // delete the child and notify
    794         mAdapter.deleteAndNotify(deletePosition, deleteCount);
    795         getInstrumentation().waitForIdleSync();
    796         mLayoutManager.expectLayouts(1);
    797         smoothScrollToPosition(0);
    798         mLayoutManager.waitForLayout(2);
    799         checkForMainThreadException();
    800         // due to data changes, first item may become visible before others which will cause
    801         // smooth scrolling to stop. Triggering it twice more is a naive hack.
    802         // Until we have time to consider it as a bug, this is the only workaround.
    803         smoothScrollToPosition(0);
    804         checkForMainThreadException();
    805         Thread.sleep(300);
    806         smoothScrollToPosition(0);
    807         checkForMainThreadException();
    808         Thread.sleep(500);
    809         // some animations should happen and we should recover layout
    810         final Map<Item, Rect> actualCoords = mLayoutManager.collectChildCoordinates();
    811 
    812         // now layout another RV with same adapter
    813         removeRecyclerView();
    814         setupByConfig(config);
    815         mRecyclerView.setAdapter(adapter);// use same adapter so that items can be matched
    816         waitFirstLayout();
    817         final Map<Item, Rect> desiredCoords = mLayoutManager.collectChildCoordinates();
    818         assertRectSetsEqual(logPrefix + " when an item from the start of the list is deleted, "
    819                         + "layout should recover the state once scrolling is stopped",
    820                 desiredCoords, actualCoords);
    821         checkForMainThreadException();
    822     }
    823 
    824     public void testPartialSpanInvalidation() throws Throwable {
    825         Config config = new Config().spanCount(5).itemCount(100);
    826         setupByConfig(config);
    827         for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
    828             mAdapter.mFullSpanItems.add(i);
    829         }
    830         waitFirstLayout();
    831         smoothScrollToPosition(50);
    832         int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
    833         mAdapter.changeAndNotify(15, 2);
    834         Thread.sleep(200);
    835         assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
    836                 mLayoutManager.mLazySpanLookup.mData[30]);
    837         assertEquals("item in invalidated range should have clear span id",
    838                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    839         smoothScrollToPosition(85);
    840         int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
    841         mAdapter.deleteAndNotify(55, 2);
    842         Thread.sleep(200);
    843         assertEquals("item in invalidated range should have clear span id",
    844                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    845         int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
    846         assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
    847                 newSpans, 0, 0, newSpans.length);
    848     }
    849 
    850     // Same as Arrays.copyOfRange but for API 7
    851     private int[] copyOfRange(int[] original, int from, int to) {
    852         int newLength = to - from;
    853         if (newLength < 0) {
    854             throw new IllegalArgumentException(from + " > " + to);
    855         }
    856         int[] copy = new int[newLength];
    857         System.arraycopy(original, from, copy, 0,
    858                 Math.min(original.length - from, newLength));
    859         return copy;
    860     }
    861 
    862     public void testSpanReassignmentsOnItemChange() throws Throwable {
    863         Config config = new Config().spanCount(5);
    864         setupByConfig(config);
    865         waitFirstLayout();
    866         smoothScrollToPosition(mAdapter.getItemCount() / 2);
    867         final int changePosition = mAdapter.getItemCount() / 4;
    868         mLayoutManager.expectLayouts(1);
    869         mAdapter.changeAndNotify(changePosition, 1);
    870         mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
    871                 1);
    872         // delete an item before visible area
    873         int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
    874         assertTrue("test sanity", deletedPosition >= 0);
    875         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
    876         if (DEBUG) {
    877             Log.d(TAG, "before:");
    878             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
    879                 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
    880             }
    881         }
    882         mLayoutManager.expectLayouts(1);
    883         mAdapter.deleteAndNotify(deletedPosition, 1);
    884         mLayoutManager.waitForLayout(2);
    885         assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
    886                         + "should not affect the layout if it is not visible", before,
    887                 mLayoutManager.collectChildCoordinates()
    888         );
    889         deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
    890         mLayoutManager.expectLayouts(1);
    891         mAdapter.deleteAndNotify(deletedPosition, 1);
    892         mLayoutManager.waitForLayout(2);
    893         assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
    894                 + "layout", before, mLayoutManager.collectChildCoordinates());
    895     }
    896 
    897     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start, int end) {
    898         for (int i = start; i < end; i++) {
    899             assertEquals(msg + " ind:" + i, set1[i], set2[i]);
    900         }
    901     }
    902 
    903     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
    904             int length) {
    905         for (int i = 0; i < length; i++) {
    906             assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
    907                     set2[start2 + i]);
    908         }
    909     }
    910 
    911     public void testViewSnapping() throws Throwable {
    912         for (Config config : mBaseVariations) {
    913             viewSnapTest(config.itemCount(config.mSpanCount + 1));
    914             removeRecyclerView();
    915         }
    916     }
    917 
    918     public void viewSnapTest(final Config config) throws Throwable {
    919         setupByConfig(config);
    920         mAdapter.mOnBindCallback = new OnBindCallback() {
    921             @Override
    922             void onBoundItem(TestViewHolder vh, int position) {
    923                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
    924                 if (config.mOrientation == HORIZONTAL) {
    925                     lp.width = mRecyclerView.getWidth() / 3;
    926                 } else {
    927                     lp.height = mRecyclerView.getHeight() / 3;
    928                 }
    929             }
    930             @Override
    931             boolean assignRandomSize() {
    932                 return false;
    933             }
    934         };
    935         waitFirstLayout();
    936         // run these tests twice. once initial layout, once after scroll
    937         String logSuffix = "";
    938         for (int i = 0; i < 2; i++) {
    939             Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
    940             Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
    941             // workaround for SGLM's span distribution issue. Right now, it may leave gaps so we
    942             // avoid it by setting its layout params directly
    943             if(config.mOrientation == HORIZONTAL) {
    944                 recyclerViewBounds.bottom -= recyclerViewBounds.height() % config.mSpanCount;
    945             } else {
    946                 recyclerViewBounds.right -= recyclerViewBounds.width() % config.mSpanCount;
    947             }
    948 
    949             Rect usedLayoutBounds = new Rect();
    950             for (Rect rect : itemRectMap.values()) {
    951                 usedLayoutBounds.union(rect);
    952             }
    953 
    954             if (DEBUG) {
    955                 Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
    956             }
    957             if (config.mOrientation == VERTICAL) {
    958                 assertEquals(config + " there should be no gap on left" + logSuffix,
    959                         usedLayoutBounds.left, recyclerViewBounds.left);
    960                 assertEquals(config + " there should be no gap on right" + logSuffix,
    961                         usedLayoutBounds.right, recyclerViewBounds.right);
    962                 if (config.mReverseLayout) {
    963                     assertEquals(config + " there should be no gap on bottom" + logSuffix,
    964                             usedLayoutBounds.bottom, recyclerViewBounds.bottom);
    965                     assertTrue(config + " there should be some gap on top" + logSuffix,
    966                             usedLayoutBounds.top > recyclerViewBounds.top);
    967                 } else {
    968                     assertEquals(config + " there should be no gap on top" + logSuffix,
    969                             usedLayoutBounds.top, recyclerViewBounds.top);
    970                     assertTrue(config + " there should be some gap at the bottom" + logSuffix,
    971                             usedLayoutBounds.bottom < recyclerViewBounds.bottom);
    972                 }
    973             } else {
    974                 assertEquals(config + " there should be no gap on top" + logSuffix,
    975                         usedLayoutBounds.top, recyclerViewBounds.top);
    976                 assertEquals(config + " there should be no gap at the bottom" + logSuffix,
    977                         usedLayoutBounds.bottom, recyclerViewBounds.bottom);
    978                 if (config.mReverseLayout) {
    979                     assertEquals(config + " there should be no on right" + logSuffix,
    980                             usedLayoutBounds.right, recyclerViewBounds.right);
    981                     assertTrue(config + " there should be some gap on left" + logSuffix,
    982                             usedLayoutBounds.left > recyclerViewBounds.left);
    983                 } else {
    984                     assertEquals(config + " there should be no gap on left" + logSuffix,
    985                             usedLayoutBounds.left, recyclerViewBounds.left);
    986                     assertTrue(config + " there should be some gap on right" + logSuffix,
    987                             usedLayoutBounds.right < recyclerViewBounds.right);
    988                 }
    989             }
    990             final int scroll = config.mReverseLayout ? -500 : 500;
    991             scrollBy(scroll);
    992             logSuffix = " scrolled " + scroll;
    993         }
    994 
    995     }
    996 
    997     public void testSpanCountChangeOnRestoreSavedState() throws Throwable {
    998         Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
    999         setupByConfig(config);
   1000         waitFirstLayout();
   1001 
   1002         int beforeChildCount = mLayoutManager.getChildCount();
   1003         Parcelable savedState = mRecyclerView.onSaveInstanceState();
   1004         // we append a suffix to the parcelable to test out of bounds
   1005         String parcelSuffix = UUID.randomUUID().toString();
   1006         Parcel parcel = Parcel.obtain();
   1007         savedState.writeToParcel(parcel, 0);
   1008         parcel.writeString(parcelSuffix);
   1009         removeRecyclerView();
   1010         // reset for reading
   1011         parcel.setDataPosition(0);
   1012         // re-create
   1013         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
   1014         removeRecyclerView();
   1015 
   1016         RecyclerView restored = new RecyclerView(getActivity());
   1017         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
   1018         mLayoutManager.setReverseLayout(config.mReverseLayout);
   1019         mLayoutManager.setGapStrategy(config.mGapStrategy);
   1020         restored.setLayoutManager(mLayoutManager);
   1021         // use the same adapter for Rect matching
   1022         restored.setAdapter(mAdapter);
   1023         restored.onRestoreInstanceState(savedState);
   1024         mLayoutManager.setSpanCount(1);
   1025         mLayoutManager.expectLayouts(1);
   1026         setRecyclerView(restored);
   1027         mLayoutManager.waitForLayout(2);
   1028         assertEquals("on saved state, reverse layout should be preserved",
   1029                 config.mReverseLayout, mLayoutManager.getReverseLayout());
   1030         assertEquals("on saved state, orientation should be preserved",
   1031                 config.mOrientation, mLayoutManager.getOrientation());
   1032         assertEquals("after setting new span count, layout manager should keep new value",
   1033                 1, mLayoutManager.getSpanCount());
   1034         assertEquals("on saved state, gap strategy should be preserved",
   1035                 config.mGapStrategy, mLayoutManager.getGapStrategy());
   1036         assertTrue("when span count is dramatically changed after restore, # of child views "
   1037                 + "should change", beforeChildCount > mLayoutManager.getChildCount());
   1038         // make sure LLM can layout all children. is some span info is leaked, this would crash
   1039         smoothScrollToPosition(mAdapter.getItemCount() - 1);
   1040     }
   1041 
   1042     public void testSavedState() throws Throwable {
   1043         PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
   1044                 new PostLayoutRunnable() {
   1045                     @Override
   1046                     public void run() throws Throwable {
   1047                         // do nothing
   1048                     }
   1049 
   1050                     @Override
   1051                     public String describe() {
   1052                         return "doing nothing";
   1053                     }
   1054                 },
   1055                 new PostLayoutRunnable() {
   1056                     @Override
   1057                     public void run() throws Throwable {
   1058                         mLayoutManager.expectLayouts(1);
   1059                         scrollToPosition(mAdapter.getItemCount() * 3 / 4);
   1060                         mLayoutManager.waitForLayout(2);
   1061                     }
   1062 
   1063                     @Override
   1064                     public String describe() {
   1065                         return "scroll to position " + (mAdapter == null ? "" :
   1066                                 mAdapter.getItemCount() * 3 / 4);
   1067                     }
   1068                 },
   1069                 new PostLayoutRunnable() {
   1070                     @Override
   1071                     public void run() throws Throwable {
   1072                         mLayoutManager.expectLayouts(1);
   1073                         scrollToPositionWithOffset(mAdapter.getItemCount() / 3,
   1074                                 50);
   1075                         mLayoutManager.waitForLayout(2);
   1076                     }
   1077 
   1078                     @Override
   1079                     public String describe() {
   1080                         return "scroll to position " + (mAdapter == null ? "" :
   1081                                 mAdapter.getItemCount() / 3) + "with positive offset";
   1082                     }
   1083                 },
   1084                 new PostLayoutRunnable() {
   1085                     @Override
   1086                     public void run() throws Throwable {
   1087                         mLayoutManager.expectLayouts(1);
   1088                         scrollToPositionWithOffset(mAdapter.getItemCount() * 2 / 3,
   1089                                 -50);
   1090                         mLayoutManager.waitForLayout(2);
   1091                     }
   1092 
   1093                     @Override
   1094                     public String describe() {
   1095                         return "scroll to position with negative offset";
   1096                     }
   1097                 }
   1098         };
   1099         boolean[] waitForLayoutOptions = new boolean[]{false, true};
   1100         boolean[] loadDataAfterRestoreOptions = new boolean[]{false, true};
   1101         List<Config> testVariations = new ArrayList<Config>();
   1102         testVariations.addAll(mBaseVariations);
   1103         for (Config config : mBaseVariations) {
   1104             if (config.mSpanCount < 2) {
   1105                 continue;
   1106             }
   1107             final Config clone = (Config) config.clone();
   1108             clone.mItemCount = clone.mSpanCount - 1;
   1109             testVariations.add(clone);
   1110         }
   1111         for (Config config : testVariations) {
   1112             for (PostLayoutRunnable runnable : postLayoutOptions) {
   1113                 for (boolean waitForLayout : waitForLayoutOptions) {
   1114                     for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) {
   1115                         savedStateTest(config, waitForLayout, loadDataAfterRestore, runnable);
   1116                         removeRecyclerView();
   1117                         checkForMainThreadException();
   1118                     }
   1119                 }
   1120             }
   1121         }
   1122     }
   1123 
   1124     private void saveRestore(final Config config) throws Throwable {
   1125         runTestOnUiThread(new Runnable() {
   1126             @Override
   1127             public void run() {
   1128                 try {
   1129                     Parcelable savedState = mRecyclerView.onSaveInstanceState();
   1130                     // we append a suffix to the parcelable to test out of bounds
   1131                     String parcelSuffix = UUID.randomUUID().toString();
   1132                     Parcel parcel = Parcel.obtain();
   1133                     savedState.writeToParcel(parcel, 0);
   1134                     parcel.writeString(parcelSuffix);
   1135                     removeRecyclerView();
   1136                     // reset for reading
   1137                     parcel.setDataPosition(0);
   1138                     // re-create
   1139                     savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
   1140                     RecyclerView restored = new RecyclerView(getActivity());
   1141                     mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
   1142                             config.mOrientation);
   1143                     mLayoutManager.setGapStrategy(config.mGapStrategy);
   1144                     restored.setLayoutManager(mLayoutManager);
   1145                     // use the same adapter for Rect matching
   1146                     restored.setAdapter(mAdapter);
   1147                     restored.onRestoreInstanceState(savedState);
   1148                     if (Looper.myLooper() == Looper.getMainLooper()) {
   1149                         mLayoutManager.expectLayouts(1);
   1150                         setRecyclerView(restored);
   1151                     } else {
   1152                         mLayoutManager.expectLayouts(1);
   1153                         setRecyclerView(restored);
   1154                         mLayoutManager.waitForLayout(2);
   1155                     }
   1156                 } catch (Throwable t) {
   1157                     postExceptionToInstrumentation(t);
   1158                 }
   1159             }
   1160         });
   1161         checkForMainThreadException();
   1162     }
   1163 
   1164     public void savedStateTest(Config config, boolean waitForLayout, boolean loadDataAfterRestore,
   1165             PostLayoutRunnable postLayoutOperations)
   1166             throws Throwable {
   1167         if (DEBUG) {
   1168             Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config "
   1169                     + config + " post layout action " + postLayoutOperations.describe());
   1170         }
   1171         setupByConfig(config);
   1172         if (loadDataAfterRestore) {
   1173             // We are going to re-create items, force non-random item size.
   1174             mAdapter.mOnBindCallback = new OnBindCallback() {
   1175                 @Override
   1176                 void onBoundItem(TestViewHolder vh, int position) {
   1177                 }
   1178 
   1179                 boolean assignRandomSize() {
   1180                     return false;
   1181                 }
   1182             };
   1183         }
   1184         waitFirstLayout();
   1185         if (waitForLayout) {
   1186             postLayoutOperations.run();
   1187             // ugly thread sleep but since post op is anything, we need to give it time to settle.
   1188             Thread.sleep(500);
   1189         }
   1190         getInstrumentation().waitForIdleSync();
   1191         final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt();
   1192         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1193         Parcelable savedState = mRecyclerView.onSaveInstanceState();
   1194         // we append a suffix to the parcelable to test out of bounds
   1195         String parcelSuffix = UUID.randomUUID().toString();
   1196         Parcel parcel = Parcel.obtain();
   1197         savedState.writeToParcel(parcel, 0);
   1198         parcel.writeString(parcelSuffix);
   1199         removeRecyclerView();
   1200         // reset for reading
   1201         parcel.setDataPosition(0);
   1202         // re-create
   1203         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
   1204         removeRecyclerView();
   1205 
   1206         final int itemCount = mAdapter.getItemCount();
   1207         if (loadDataAfterRestore) {
   1208             mAdapter.deleteAndNotify(0, itemCount);
   1209         }
   1210 
   1211         RecyclerView restored = new RecyclerView(getActivity());
   1212         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
   1213         mLayoutManager.setGapStrategy(config.mGapStrategy);
   1214         restored.setLayoutManager(mLayoutManager);
   1215         // use the same adapter for Rect matching
   1216         restored.setAdapter(mAdapter);
   1217         restored.onRestoreInstanceState(savedState);
   1218 
   1219         if (loadDataAfterRestore) {
   1220             mAdapter.addAndNotify(itemCount);
   1221         }
   1222 
   1223         assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
   1224                 parcel.readString());
   1225         mLayoutManager.expectLayouts(1);
   1226         setRecyclerView(restored);
   1227         mLayoutManager.waitForLayout(2);
   1228         assertEquals(config + " on saved state, reverse layout should be preserved",
   1229                 config.mReverseLayout, mLayoutManager.getReverseLayout());
   1230         assertEquals(config + " on saved state, orientation should be preserved",
   1231                 config.mOrientation, mLayoutManager.getOrientation());
   1232         assertEquals(config + " on saved state, span count should be preserved",
   1233                 config.mSpanCount, mLayoutManager.getSpanCount());
   1234         assertEquals(config + " on saved state, gap strategy should be preserved",
   1235                 config.mGapStrategy, mLayoutManager.getGapStrategy());
   1236         assertEquals(config + " on saved state, first completely visible child position should"
   1237                         + " be preserved", firstCompletelyVisiblePosition,
   1238                 mLayoutManager.findFirstVisibleItemPositionInt());
   1239         if (waitForLayout) {
   1240             final boolean strictItemEquality = !loadDataAfterRestore;
   1241             assertRectSetsEqual(config + "\npost layout op:" + postLayoutOperations.describe()
   1242                             + ": on restore, previous view positions should be preserved",
   1243                     before, mLayoutManager.collectChildCoordinates(), strictItemEquality);
   1244         }
   1245         // TODO add tests for changing values after restore before layout
   1246     }
   1247 
   1248     public void testScrollAndClear() throws Throwable {
   1249         setupByConfig(new Config());
   1250         waitFirstLayout();
   1251 
   1252         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
   1253 
   1254         mLayoutManager.expectLayouts(1);
   1255         runTestOnUiThread(new Runnable() {
   1256             @Override
   1257             public void run() {
   1258                 mLayoutManager.scrollToPositionWithOffset(1, 0);
   1259                 mAdapter.clearOnUIThread();
   1260             }
   1261         });
   1262         mLayoutManager.waitForLayout(2);
   1263 
   1264         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
   1265     }
   1266 
   1267     public void testScrollToPositionWithOffset() throws Throwable {
   1268         for (Config config : mBaseVariations) {
   1269             scrollToPositionWithOffsetTest(config);
   1270             removeRecyclerView();
   1271         }
   1272     }
   1273 
   1274     public void scrollToPositionWithOffsetTest(Config config) throws Throwable {
   1275         setupByConfig(config);
   1276         waitFirstLayout();
   1277         OrientationHelper orientationHelper = OrientationHelper
   1278                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1279         Rect layoutBounds = getDecoratedRecyclerViewBounds();
   1280         // try scrolling towards head, should not affect anything
   1281         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1282         scrollToPositionWithOffset(0, 20);
   1283         assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
   1284                 before, mLayoutManager.collectChildCoordinates());
   1285         // try offsetting some visible children
   1286         int testCount = 10;
   1287         while (testCount-- > 0) {
   1288             // get middle child
   1289             final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
   1290             final int position = mRecyclerView.getChildLayoutPosition(child);
   1291             final int startOffset = config.mReverseLayout ?
   1292                     orientationHelper.getEndAfterPadding() - orientationHelper
   1293                             .getDecoratedEnd(child)
   1294                     : orientationHelper.getDecoratedStart(child) - orientationHelper
   1295                             .getStartAfterPadding();
   1296             final int scrollOffset = startOffset / 2;
   1297             mLayoutManager.expectLayouts(1);
   1298             scrollToPositionWithOffset(position, scrollOffset);
   1299             mLayoutManager.waitForLayout(2);
   1300             final int finalOffset = config.mReverseLayout ?
   1301                     orientationHelper.getEndAfterPadding() - orientationHelper
   1302                             .getDecoratedEnd(child)
   1303                     : orientationHelper.getDecoratedStart(child) - orientationHelper
   1304                             .getStartAfterPadding();
   1305             assertEquals(config + " scroll with offset on a visible child should work fine",
   1306                     scrollOffset, finalOffset);
   1307         }
   1308 
   1309         // try scrolling to invisible children
   1310         testCount = 10;
   1311         // we test above and below, one by one
   1312         int offsetMultiplier = -1;
   1313         while (testCount-- > 0) {
   1314             final TargetTuple target = findInvisibleTarget(config);
   1315             mLayoutManager.expectLayouts(1);
   1316             final int offset = offsetMultiplier
   1317                     * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
   1318             scrollToPositionWithOffset(target.mPosition, offset);
   1319             mLayoutManager.waitForLayout(2);
   1320             final View child = mLayoutManager.findViewByPosition(target.mPosition);
   1321             assertNotNull(config + " scrolling to a mPosition with offset " + offset
   1322                     + " should layout it", child);
   1323             final Rect bounds = mLayoutManager.getViewBounds(child);
   1324             if (DEBUG) {
   1325                 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
   1326                         + layoutBounds + " with offset " + offset);
   1327             }
   1328 
   1329             if (config.mReverseLayout) {
   1330                 assertEquals(config + " when scrolling with offset to an invisible in reverse "
   1331                                 + "layout, its end should align with recycler view's end - offset",
   1332                         orientationHelper.getEndAfterPadding() - offset,
   1333                         orientationHelper.getDecoratedEnd(child)
   1334                 );
   1335             } else {
   1336                 assertEquals(config + " when scrolling with offset to an invisible child in normal"
   1337                                 + " layout its start should align with recycler view's start + "
   1338                                 + "offset",
   1339                         orientationHelper.getStartAfterPadding() + offset,
   1340                         orientationHelper.getDecoratedStart(child)
   1341                 );
   1342             }
   1343             offsetMultiplier *= -1;
   1344         }
   1345     }
   1346 
   1347     public void testScrollToPosition() throws Throwable {
   1348         for (Config config : mBaseVariations) {
   1349             scrollToPositionTest(config);
   1350             removeRecyclerView();
   1351         }
   1352     }
   1353 
   1354     private TargetTuple findInvisibleTarget(Config config) {
   1355         int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
   1356         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
   1357             View child = mLayoutManager.getChildAt(i);
   1358             int position = mRecyclerView.getChildLayoutPosition(child);
   1359             if (position < minPosition) {
   1360                 minPosition = position;
   1361             }
   1362             if (position > maxPosition) {
   1363                 maxPosition = position;
   1364             }
   1365         }
   1366         final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
   1367         final int headTarget = minPosition / 2;
   1368         final int target;
   1369         // where will the child come from ?
   1370         final int itemLayoutDirection;
   1371         if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
   1372             target = tailTarget;
   1373             itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
   1374         } else {
   1375             target = headTarget;
   1376             itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
   1377         }
   1378         if (DEBUG) {
   1379             Log.d(TAG,
   1380                     config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
   1381         }
   1382         return new TargetTuple(target, itemLayoutDirection);
   1383     }
   1384 
   1385     public void scrollToPositionTest(Config config) throws Throwable {
   1386         setupByConfig(config);
   1387         waitFirstLayout();
   1388         OrientationHelper orientationHelper = OrientationHelper
   1389                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1390         Rect layoutBounds = getDecoratedRecyclerViewBounds();
   1391         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
   1392             View view = mLayoutManager.getChildAt(i);
   1393             Rect bounds = mLayoutManager.getViewBounds(view);
   1394             if (layoutBounds.contains(bounds)) {
   1395                 Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
   1396                 final int position = mRecyclerView.getChildLayoutPosition(view);
   1397                 LayoutParams layoutParams
   1398                         = (LayoutParams) (view.getLayoutParams());
   1399                 TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
   1400                 assertEquals("recycler view mPosition should match adapter mPosition", position,
   1401                         vh.mBoundItem.mAdapterIndex);
   1402                 if (DEBUG) {
   1403                     Log.d(TAG, "testing scroll to visible mPosition at " + position
   1404                             + " " + bounds + " inside " + layoutBounds);
   1405                 }
   1406                 mLayoutManager.expectLayouts(1);
   1407                 scrollToPosition(position);
   1408                 mLayoutManager.waitForLayout(2);
   1409                 if (DEBUG) {
   1410                     view = mLayoutManager.findViewByPosition(position);
   1411                     Rect newBounds = mLayoutManager.getViewBounds(view);
   1412                     Log.d(TAG, "after scrolling to visible mPosition " +
   1413                             bounds + " equals " + newBounds);
   1414                 }
   1415 
   1416                 assertRectSetsEqual(
   1417                         config + "scroll to mPosition on fully visible child should be no-op",
   1418                         initialBounds, mLayoutManager.collectChildCoordinates());
   1419             } else {
   1420                 final int position = mRecyclerView.getChildLayoutPosition(view);
   1421                 if (DEBUG) {
   1422                     Log.d(TAG,
   1423                             "child(" + position + ") not fully visible " + bounds + " not inside "
   1424                                     + layoutBounds
   1425                                     + mRecyclerView.getChildLayoutPosition(view)
   1426                     );
   1427                 }
   1428                 mLayoutManager.expectLayouts(1);
   1429                 runTestOnUiThread(new Runnable() {
   1430                     @Override
   1431                     public void run() {
   1432                         mLayoutManager.scrollToPosition(position);
   1433                     }
   1434                 });
   1435                 mLayoutManager.waitForLayout(2);
   1436                 view = mLayoutManager.findViewByPosition(position);
   1437                 bounds = mLayoutManager.getViewBounds(view);
   1438                 if (DEBUG) {
   1439                     Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
   1440                             + layoutBounds);
   1441                 }
   1442                 assertTrue(config
   1443                                 + " after scrolling to a partially visible child, it should become fully "
   1444                                 + " visible. " + bounds + " not inside " + layoutBounds,
   1445                         layoutBounds.contains(bounds)
   1446                 );
   1447                 assertTrue(config + " when scrolling to a partially visible item, one of its edges "
   1448                         + "should be on the boundaries", orientationHelper.getStartAfterPadding() ==
   1449                         orientationHelper.getDecoratedStart(view)
   1450                         || orientationHelper.getEndAfterPadding() ==
   1451                         orientationHelper.getDecoratedEnd(view));
   1452             }
   1453         }
   1454 
   1455         // try scrolling to invisible children
   1456         int testCount = 10;
   1457         while (testCount-- > 0) {
   1458             final TargetTuple target = findInvisibleTarget(config);
   1459             mLayoutManager.expectLayouts(1);
   1460             scrollToPosition(target.mPosition);
   1461             mLayoutManager.waitForLayout(2);
   1462             final View child = mLayoutManager.findViewByPosition(target.mPosition);
   1463             assertNotNull(config + " scrolling to a mPosition should lay it out", child);
   1464             final Rect bounds = mLayoutManager.getViewBounds(child);
   1465             if (DEBUG) {
   1466                 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
   1467                         + layoutBounds);
   1468             }
   1469             assertTrue(config + " scrolling to a mPosition should make it fully visible",
   1470                     layoutBounds.contains(bounds));
   1471             if (target.mLayoutDirection == LAYOUT_START) {
   1472                 assertEquals(
   1473                         config + " when scrolling to an invisible child above, its start should"
   1474                                 + " align with recycler view's start",
   1475                         orientationHelper.getStartAfterPadding(),
   1476                         orientationHelper.getDecoratedStart(child)
   1477                 );
   1478             } else {
   1479                 assertEquals(config + " when scrolling to an invisible child below, its end "
   1480                                 + "should align with recycler view's end",
   1481                         orientationHelper.getEndAfterPadding(),
   1482                         orientationHelper.getDecoratedEnd(child)
   1483                 );
   1484             }
   1485         }
   1486     }
   1487 
   1488     private void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
   1489         runTestOnUiThread(new Runnable() {
   1490             @Override
   1491             public void run() {
   1492                 mLayoutManager.scrollToPositionWithOffset(position, offset);
   1493             }
   1494         });
   1495     }
   1496 
   1497     public void testLayoutOrder() throws Throwable {
   1498         for (Config config : mBaseVariations) {
   1499             layoutOrderTest(config);
   1500             removeRecyclerView();
   1501         }
   1502     }
   1503 
   1504     public void layoutOrderTest(Config config) throws Throwable {
   1505         setupByConfig(config);
   1506         assertViewPositions(config);
   1507     }
   1508 
   1509     void assertViewPositions(Config config) {
   1510         ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
   1511         OrientationHelper orientationHelper = OrientationHelper
   1512                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1513         for (ArrayList<View> span : viewsBySpan) {
   1514             // validate all children's order. first child should have min start mPosition
   1515             final int count = span.size();
   1516             for (int i = 0, j = 1; j < count; i++, j++) {
   1517                 View prev = span.get(i);
   1518                 View next = span.get(j);
   1519                 assertTrue(config + " prev item should be above next item",
   1520                         orientationHelper.getDecoratedEnd(prev) <= orientationHelper
   1521                                 .getDecoratedStart(next)
   1522                 );
   1523 
   1524             }
   1525         }
   1526     }
   1527 
   1528     public void testScrollBy() throws Throwable {
   1529         for (Config config : mBaseVariations) {
   1530             scollByTest(config);
   1531             removeRecyclerView();
   1532         }
   1533     }
   1534 
   1535     void waitFirstLayout() throws Throwable {
   1536         mLayoutManager.expectLayouts(1);
   1537         setRecyclerView(mRecyclerView);
   1538         mLayoutManager.waitForLayout(2);
   1539         getInstrumentation().waitForIdleSync();
   1540     }
   1541 
   1542     public void scollByTest(Config config) throws Throwable {
   1543         setupByConfig(config);
   1544         waitFirstLayout();
   1545         // try invalid scroll. should not happen
   1546         final View first = mLayoutManager.getChildAt(0);
   1547         OrientationHelper primaryOrientation = OrientationHelper
   1548                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1549         int scrollDist;
   1550         if (config.mReverseLayout) {
   1551             scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
   1552         } else {
   1553             scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
   1554         }
   1555         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1556         scrollBy(scrollDist);
   1557         Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
   1558         assertRectSetsEqual(
   1559                 config + " if there are no more items, scroll should not happen (dt:" + scrollDist
   1560                         + ")",
   1561                 before, after
   1562         );
   1563 
   1564         scrollDist = -scrollDist * 3;
   1565         before = mLayoutManager.collectChildCoordinates();
   1566         scrollBy(scrollDist);
   1567         after = mLayoutManager.collectChildCoordinates();
   1568         int layoutStart = primaryOrientation.getStartAfterPadding();
   1569         int layoutEnd = primaryOrientation.getEndAfterPadding();
   1570         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1571             Rect afterRect = after.get(entry.getKey());
   1572             // offset rect
   1573             if (config.mOrientation == VERTICAL) {
   1574                 entry.getValue().offset(0, -scrollDist);
   1575             } else {
   1576                 entry.getValue().offset(-scrollDist, 0);
   1577             }
   1578             if (afterRect == null || afterRect.isEmpty()) {
   1579                 // assert item is out of bounds
   1580                 int start, end;
   1581                 if (config.mOrientation == VERTICAL) {
   1582                     start = entry.getValue().top;
   1583                     end = entry.getValue().bottom;
   1584                 } else {
   1585                     start = entry.getValue().left;
   1586                     end = entry.getValue().right;
   1587                 }
   1588                 assertTrue(
   1589                         config + " if item is missing after relayout, it should be out of bounds."
   1590                                 + "item start: " + start + ", end:" + end + " layout start:"
   1591                                 + layoutStart +
   1592                                 ", layout end:" + layoutEnd,
   1593                         start <= layoutStart && end <= layoutEnd ||
   1594                                 start >= layoutEnd && end >= layoutEnd
   1595                 );
   1596             } else {
   1597                 assertEquals(config + " Item should be laid out at the scroll offset coordinates",
   1598                         entry.getValue(),
   1599                         afterRect);
   1600             }
   1601         }
   1602         assertViewPositions(config);
   1603     }
   1604 
   1605     public void testAccessibilityPositions() throws Throwable {
   1606         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
   1607         waitFirstLayout();
   1608         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
   1609                 .getCompatAccessibilityDelegate();
   1610         final AccessibilityEvent event = AccessibilityEvent.obtain();
   1611         runTestOnUiThread(new Runnable() {
   1612             @Override
   1613             public void run() {
   1614                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
   1615             }
   1616         });
   1617         final AccessibilityRecordCompat record = AccessibilityEventCompat
   1618                 .asRecord(event);
   1619         final int start = mRecyclerView
   1620                 .getChildLayoutPosition(
   1621                         mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
   1622         final int end = mRecyclerView
   1623                 .getChildLayoutPosition(
   1624                         mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
   1625         assertEquals("first item position should match",
   1626                 Math.min(start, end), record.getFromIndex());
   1627         assertEquals("last item position should match",
   1628                 Math.max(start, end), record.getToIndex());
   1629 
   1630     }
   1631 
   1632     public void testConsistentRelayout() throws Throwable {
   1633         for (Config config : mBaseVariations) {
   1634             for (boolean firstChildMultiSpan : new boolean[]{false, true}) {
   1635                 consistentRelayoutTest(config, firstChildMultiSpan);
   1636             }
   1637             removeRecyclerView();
   1638         }
   1639     }
   1640 
   1641     public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
   1642             throws Throwable {
   1643         setupByConfig(config);
   1644         if (firstChildMultiSpan) {
   1645             mAdapter.mFullSpanItems.add(0);
   1646         }
   1647         waitFirstLayout();
   1648         // record all child positions
   1649         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1650         requestLayoutOnUIThread(mRecyclerView);
   1651         Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
   1652         assertRectSetsEqual(
   1653                 config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
   1654                 after);
   1655         // scroll some to create inconsistency
   1656         View firstChild = mLayoutManager.getChildAt(0);
   1657         final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
   1658                 .getDecoratedStart(firstChild);
   1659         int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
   1660         if (config.mReverseLayout) {
   1661             distance *= -1;
   1662         }
   1663         scrollBy(distance);
   1664         waitForMainThread(2);
   1665         assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
   1666                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
   1667         before = mLayoutManager.collectChildCoordinates();
   1668         mLayoutManager.expectLayouts(1);
   1669         requestLayoutOnUIThread(mRecyclerView);
   1670         mLayoutManager.waitForLayout(2);
   1671         after = mLayoutManager.collectChildCoordinates();
   1672         assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
   1673     }
   1674 
   1675     /**
   1676      * enqueues an empty runnable to main thread so that we can be assured it did run
   1677      *
   1678      * @param count Number of times to run
   1679      */
   1680     private void waitForMainThread(int count) throws Throwable {
   1681         final AtomicInteger i = new AtomicInteger(count);
   1682         while (i.get() > 0) {
   1683             runTestOnUiThread(new Runnable() {
   1684                 @Override
   1685                 public void run() {
   1686                     i.decrementAndGet();
   1687                 }
   1688             });
   1689         }
   1690     }
   1691 
   1692     public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
   1693             Map<Item, Rect> after) {
   1694         Throwable throwable = null;
   1695         try {
   1696             assertRectSetsEqual("NOT " + message, before, after);
   1697         } catch (Throwable t) {
   1698             throwable = t;
   1699         }
   1700         assertNotNull(message + " two layout should be different", throwable);
   1701     }
   1702 
   1703     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
   1704         assertRectSetsEqual(message, before, after, true);
   1705     }
   1706 
   1707     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
   1708             boolean strictItemEquality) {
   1709         StringBuilder log = new StringBuilder();
   1710         if (DEBUG) {
   1711             log.append("checking rectangle equality.\n");
   1712             log.append("total space:" + mLayoutManager.mPrimaryOrientation.getTotalSpace());
   1713             log.append("before:");
   1714             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1715                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
   1716                         .append(entry.getValue());
   1717             }
   1718             log.append("\nafter:");
   1719             for (Map.Entry<Item, Rect> entry : after.entrySet()) {
   1720                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
   1721                         .append(entry.getValue());
   1722             }
   1723             message += "\n\n" + log.toString();
   1724         }
   1725         assertEquals(message + ": item counts should be equal", before.size()
   1726                 , after.size());
   1727         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1728             final Item beforeItem = entry.getKey();
   1729             Rect afterRect = null;
   1730             if (strictItemEquality) {
   1731                 afterRect = after.get(beforeItem);
   1732                 assertNotNull(message + ": Same item should be visible after simple re-layout",
   1733                         afterRect);
   1734             } else {
   1735                 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) {
   1736                     final Item afterItem = afterEntry.getKey();
   1737                     if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
   1738                         afterRect = afterEntry.getValue();
   1739                         break;
   1740                     }
   1741                 }
   1742                 assertNotNull(message + ": Item with same adapter index should be visible " +
   1743                                 "after simple re-layout",
   1744                         afterRect);
   1745             }
   1746             assertEquals(message + ": Item should be laid out at the same coordinates",
   1747                     entry.getValue(),
   1748                     afterRect);
   1749         }
   1750     }
   1751 
   1752     // test layout params assignment
   1753 
   1754     static class OnLayoutListener {
   1755 
   1756         void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
   1757         }
   1758 
   1759         void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
   1760         }
   1761     }
   1762 
   1763     class WrappedLayoutManager extends StaggeredGridLayoutManager {
   1764 
   1765         CountDownLatch layoutLatch;
   1766         OnLayoutListener mOnLayoutListener;
   1767         // gradle does not yet let us customize manifest for tests which is necessary to test RTL.
   1768         // until bug is fixed, we'll fake it.
   1769         // public issue id: 57819
   1770         Boolean mFakeRTL;
   1771 
   1772         @Override
   1773         boolean isLayoutRTL() {
   1774             return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
   1775         }
   1776 
   1777         public void expectLayouts(int count) {
   1778             layoutLatch = new CountDownLatch(count);
   1779         }
   1780 
   1781         public void waitForLayout(long timeout) throws InterruptedException {
   1782             waitForLayout(timeout * (DEBUG ? 1000 : 1), TimeUnit.SECONDS);
   1783         }
   1784 
   1785         public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
   1786             layoutLatch.await(timeout, timeUnit);
   1787             assertEquals("all expected layouts should be executed at the expected time",
   1788                     0, layoutLatch.getCount());
   1789         }
   1790 
   1791         public void assertNoLayout(String msg, long timeout) throws Throwable {
   1792             layoutLatch.await(timeout, TimeUnit.SECONDS);
   1793             assertFalse(msg, layoutLatch.getCount() == 0);
   1794         }
   1795 
   1796         @Override
   1797         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
   1798             String before;
   1799             if (DEBUG) {
   1800                 before = layoutToString("before");
   1801             } else {
   1802                 before = "enable DEBUG";
   1803             }
   1804             try {
   1805                 if (mOnLayoutListener != null) {
   1806                     mOnLayoutListener.before(recycler, state);
   1807                 }
   1808                 super.onLayoutChildren(recycler, state);
   1809                 if (mOnLayoutListener != null) {
   1810                     mOnLayoutListener.after(recycler, state);
   1811                 }
   1812                 validateChildren(before);
   1813             } catch (Throwable t) {
   1814                 postExceptionToInstrumentation(t);
   1815             }
   1816 
   1817             layoutLatch.countDown();
   1818         }
   1819 
   1820         @Override
   1821         int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
   1822             try {
   1823                 int result = super.scrollBy(dt, recycler, state);
   1824                 validateChildren();
   1825                 return result;
   1826             } catch (Throwable t) {
   1827                 postExceptionToInstrumentation(t);
   1828             }
   1829 
   1830             return 0;
   1831         }
   1832 
   1833         public WrappedLayoutManager(int spanCount, int orientation) {
   1834             super(spanCount, orientation);
   1835         }
   1836 
   1837         ArrayList<ArrayList<View>> collectChildrenBySpan() {
   1838             ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
   1839             for (int i = 0; i < getSpanCount(); i++) {
   1840                 viewsBySpan.add(new ArrayList<View>());
   1841             }
   1842             for (int i = 0; i < getChildCount(); i++) {
   1843                 View view = getChildAt(i);
   1844                 LayoutParams lp
   1845                         = (LayoutParams) view
   1846                         .getLayoutParams();
   1847                 viewsBySpan.get(lp.mSpan.mIndex).add(view);
   1848             }
   1849             return viewsBySpan;
   1850         }
   1851 
   1852         @Nullable
   1853         @Override
   1854         public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
   1855                 RecyclerView.State state) {
   1856             View result = null;
   1857             try {
   1858                 result = super.onFocusSearchFailed(focused, direction, recycler, state);
   1859                 validateChildren();
   1860             } catch (Throwable t) {
   1861                 postExceptionToInstrumentation(t);
   1862             }
   1863             return result;
   1864         }
   1865 
   1866         Rect getViewBounds(View view) {
   1867             if (getOrientation() == HORIZONTAL) {
   1868                 return new Rect(
   1869                         mPrimaryOrientation.getDecoratedStart(view),
   1870                         mSecondaryOrientation.getDecoratedStart(view),
   1871                         mPrimaryOrientation.getDecoratedEnd(view),
   1872                         mSecondaryOrientation.getDecoratedEnd(view));
   1873             } else {
   1874                 return new Rect(
   1875                         mSecondaryOrientation.getDecoratedStart(view),
   1876                         mPrimaryOrientation.getDecoratedStart(view),
   1877                         mSecondaryOrientation.getDecoratedEnd(view),
   1878                         mPrimaryOrientation.getDecoratedEnd(view));
   1879             }
   1880         }
   1881 
   1882         public String getBoundsLog() {
   1883             StringBuilder sb = new StringBuilder();
   1884             sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding())
   1885                     .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding());
   1886             sb.append("\nchildren bounds\n");
   1887             final int childCount = getChildCount();
   1888             for (int i = 0; i < childCount; i++) {
   1889                 View child = getChildAt(i);
   1890                 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
   1891                         .append("[").append("start:").append(
   1892                         mPrimaryOrientation.getDecoratedStart(child)).append(", end:")
   1893                         .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n");
   1894             }
   1895             return sb.toString();
   1896         }
   1897 
   1898         public VisibleChildren traverseAndFindVisibleChildren() {
   1899             int childCount = getChildCount();
   1900             final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount());
   1901             final int start = mPrimaryOrientation.getStartAfterPadding();
   1902             final int end = mPrimaryOrientation.getEndAfterPadding();
   1903             for (int i = 0; i < childCount; i++) {
   1904                 View child = getChildAt(i);
   1905                 final int childStart = mPrimaryOrientation.getDecoratedStart(child);
   1906                 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
   1907                 final boolean fullyVisible = childStart >= start && childEnd <= end;
   1908                 final boolean hidden = childEnd <= start || childStart >= end;
   1909                 if (hidden) {
   1910                     continue;
   1911                 }
   1912                 final int position = getPosition(child);
   1913                 final int span = getLp(child).getSpanIndex();
   1914                 if (fullyVisible) {
   1915                     if (position < visibleChildren.firstFullyVisiblePositions[span] ||
   1916                             visibleChildren.firstFullyVisiblePositions[span]
   1917                                     == RecyclerView.NO_POSITION) {
   1918                         visibleChildren.firstFullyVisiblePositions[span] = position;
   1919                     }
   1920 
   1921                     if (position > visibleChildren.lastFullyVisiblePositions[span]) {
   1922                         visibleChildren.lastFullyVisiblePositions[span] = position;
   1923                     }
   1924                 }
   1925 
   1926                 if (position < visibleChildren.firstVisiblePositions[span] ||
   1927                         visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) {
   1928                     visibleChildren.firstVisiblePositions[span] = position;
   1929                 }
   1930 
   1931                 if (position > visibleChildren.lastVisiblePositions[span]) {
   1932                     visibleChildren.lastVisiblePositions[span] = position;
   1933                 }
   1934                 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) {
   1935                     visibleChildren.findFirstPartialVisibleClosestToStart = child;
   1936                 }
   1937                 visibleChildren.findFirstPartialVisibleClosestToEnd = child;
   1938             }
   1939             return visibleChildren;
   1940         }
   1941 
   1942         Map<Item, Rect> collectChildCoordinates() throws Throwable {
   1943             final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
   1944             runTestOnUiThread(new Runnable() {
   1945                 @Override
   1946                 public void run() {
   1947                     final int childCount = getChildCount();
   1948                     for (int i = 0; i < childCount; i++) {
   1949                         View child = getChildAt(i);
   1950                         // do it if and only if child is visible
   1951                         if (child.getRight() < 0 || child.getBottom() < 0 ||
   1952                                 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
   1953                             // invisible children may be drawn in cases like scrolling so we should
   1954                             // ignore them
   1955                             continue;
   1956                         }
   1957                         LayoutParams lp = (LayoutParams) child
   1958                                 .getLayoutParams();
   1959                         TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
   1960                         items.put(vh.mBoundItem, getViewBounds(child));
   1961                     }
   1962                 }
   1963             });
   1964             return items;
   1965         }
   1966 
   1967 
   1968         public void setFakeRtl(Boolean fakeRtl) {
   1969             mFakeRTL = fakeRtl;
   1970             try {
   1971                 requestLayoutOnUIThread(mRecyclerView);
   1972             } catch (Throwable throwable) {
   1973                 postExceptionToInstrumentation(throwable);
   1974             }
   1975         }
   1976 
   1977         private String layoutToString(String hint) {
   1978             StringBuilder sb = new StringBuilder();
   1979             sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n");
   1980             for (int i = 0; i < getChildCount(); i++) {
   1981                 final View view = getChildAt(i);
   1982                 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
   1983                 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s",
   1984                         i, getPosition(view),
   1985                         mPrimaryOrientation.getDecoratedStart(view),
   1986                         mPrimaryOrientation.getDecoratedEnd(view),
   1987                         layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n");
   1988             }
   1989             return sb.toString();
   1990         }
   1991 
   1992         private void validateChildren() {
   1993             validateChildren(null);
   1994         }
   1995 
   1996         private void validateChildren(String msg) {
   1997             if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) {
   1998                 return;
   1999             }
   2000             final int dir = mShouldReverseLayout ? -1 : 1;
   2001             int i = 0;
   2002             int pos = -1;
   2003             while (i < getChildCount()) {
   2004                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
   2005                 if (lp.isItemRemoved()) {
   2006                     i++;
   2007                     continue;
   2008                 }
   2009                 pos = getPosition(getChildAt(i));
   2010                 break;
   2011             }
   2012             if (pos == -1) {
   2013                 return;
   2014             }
   2015             while (++i < getChildCount()) {
   2016                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
   2017                 if (lp.isItemRemoved()) {
   2018                     continue;
   2019                 }
   2020                 pos += dir;
   2021                 if (getPosition(getChildAt(i)) != pos) {
   2022                     throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" +
   2023                             layoutToString("ERROR") + "\n msg:" + msg);
   2024                 }
   2025             }
   2026         }
   2027     }
   2028 
   2029     static class VisibleChildren {
   2030 
   2031         int[] firstVisiblePositions;
   2032 
   2033         int[] firstFullyVisiblePositions;
   2034 
   2035         int[] lastVisiblePositions;
   2036 
   2037         int[] lastFullyVisiblePositions;
   2038 
   2039         View findFirstPartialVisibleClosestToStart;
   2040         View findFirstPartialVisibleClosestToEnd;
   2041 
   2042         VisibleChildren(int spanCount) {
   2043             firstFullyVisiblePositions = new int[spanCount];
   2044             firstVisiblePositions = new int[spanCount];
   2045             lastVisiblePositions = new int[spanCount];
   2046             lastFullyVisiblePositions = new int[spanCount];
   2047             for (int i = 0; i < spanCount; i++) {
   2048                 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
   2049                 firstVisiblePositions[i] = RecyclerView.NO_POSITION;
   2050                 lastVisiblePositions[i] = RecyclerView.NO_POSITION;
   2051                 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
   2052             }
   2053         }
   2054 
   2055         @Override
   2056         public boolean equals(Object o) {
   2057             if (this == o) {
   2058                 return true;
   2059             }
   2060             if (o == null || getClass() != o.getClass()) {
   2061                 return false;
   2062             }
   2063 
   2064             VisibleChildren that = (VisibleChildren) o;
   2065 
   2066             if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) {
   2067                 return false;
   2068             }
   2069             if (findFirstPartialVisibleClosestToStart
   2070                     != null ? !findFirstPartialVisibleClosestToStart
   2071                     .equals(that.findFirstPartialVisibleClosestToStart)
   2072                     : that.findFirstPartialVisibleClosestToStart != null) {
   2073                 return false;
   2074             }
   2075             if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) {
   2076                 return false;
   2077             }
   2078             if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) {
   2079                 return false;
   2080             }
   2081             if (findFirstPartialVisibleClosestToEnd != null ? !findFirstPartialVisibleClosestToEnd
   2082                     .equals(that.findFirstPartialVisibleClosestToEnd)
   2083                     : that.findFirstPartialVisibleClosestToEnd
   2084                             != null) {
   2085                 return false;
   2086             }
   2087             if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) {
   2088                 return false;
   2089             }
   2090 
   2091             return true;
   2092         }
   2093 
   2094         @Override
   2095         public int hashCode() {
   2096             int result = Arrays.hashCode(firstVisiblePositions);
   2097             result = 31 * result + Arrays.hashCode(firstFullyVisiblePositions);
   2098             result = 31 * result + Arrays.hashCode(lastVisiblePositions);
   2099             result = 31 * result + Arrays.hashCode(lastFullyVisiblePositions);
   2100             result = 31 * result + (findFirstPartialVisibleClosestToStart != null
   2101                     ? findFirstPartialVisibleClosestToStart
   2102                     .hashCode() : 0);
   2103             result = 31 * result + (findFirstPartialVisibleClosestToEnd != null
   2104                     ? findFirstPartialVisibleClosestToEnd
   2105                     .hashCode()
   2106                     : 0);
   2107             return result;
   2108         }
   2109 
   2110         @Override
   2111         public String toString() {
   2112             return "VisibleChildren{" +
   2113                     "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) +
   2114                     ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) +
   2115                     ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) +
   2116                     ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) +
   2117                     ", findFirstPartialVisibleClosestToStart=" +
   2118                     viewToString(findFirstPartialVisibleClosestToStart) +
   2119                     ", findFirstPartialVisibleClosestToEnd=" +
   2120                     viewToString(findFirstPartialVisibleClosestToEnd) +
   2121                     '}';
   2122         }
   2123 
   2124         private String viewToString(View view) {
   2125             if (view == null) {
   2126                 return null;
   2127             }
   2128             ViewGroup.LayoutParams lp = view.getLayoutParams();
   2129             if (lp instanceof RecyclerView.LayoutParams == false) {
   2130                 return System.identityHashCode(view) + "(?)";
   2131             }
   2132             RecyclerView.LayoutParams rvlp = (RecyclerView.LayoutParams) lp;
   2133             return System.identityHashCode(view) + "(" + rvlp.getViewAdapterPosition() + ")";
   2134         }
   2135     }
   2136 
   2137     class GridTestAdapter extends TestAdapter {
   2138 
   2139         int mOrientation;
   2140         int mRecyclerViewWidth;
   2141         int mRecyclerViewHeight;
   2142         Integer mSizeReference = null;
   2143 
   2144         // original ids of items that should be full span
   2145         HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
   2146 
   2147         private boolean mViewsHaveEqualSize = false; // size in the scrollable direction
   2148 
   2149         private OnBindCallback mOnBindCallback;
   2150 
   2151         GridTestAdapter(int count, int orientation) {
   2152             super(count);
   2153             mOrientation = orientation;
   2154         }
   2155 
   2156         @Override
   2157         public TestViewHolder onCreateViewHolder(ViewGroup parent,
   2158                 int viewType) {
   2159             mRecyclerViewWidth = parent.getWidth();
   2160             mRecyclerViewHeight = parent.getHeight();
   2161             TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
   2162             if (mOnBindCallback != null) {
   2163                 mOnBindCallback.onCreatedViewHolder(vh);
   2164             }
   2165             return vh;
   2166         }
   2167 
   2168         @Override
   2169         public void offsetOriginalIndices(int start, int offset) {
   2170             if (mFullSpanItems.size() > 0) {
   2171                 HashSet<Integer> old = mFullSpanItems;
   2172                 mFullSpanItems = new HashSet<Integer>();
   2173                 for (Integer i : old) {
   2174                     if (i < start) {
   2175                         mFullSpanItems.add(i);
   2176                     } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
   2177                         mFullSpanItems.add(i + offset);
   2178                     } else if (DEBUG) {
   2179                         Log.d(TAG, "removed full span item " + i);
   2180                     }
   2181                 }
   2182             }
   2183             super.offsetOriginalIndices(start, offset);
   2184         }
   2185 
   2186         @Override
   2187         protected void moveInUIThread(int from, int to) {
   2188             boolean setAsFullSpanAgain = mFullSpanItems.contains(from);
   2189             super.moveInUIThread(from, to);
   2190             if (setAsFullSpanAgain) {
   2191                 mFullSpanItems.add(to);
   2192             }
   2193         }
   2194 
   2195         @Override
   2196         public void onBindViewHolder(TestViewHolder holder,
   2197                 int position) {
   2198             if (mSizeReference == null) {
   2199                 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth
   2200                         / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW;
   2201             }
   2202             super.onBindViewHolder(holder, position);
   2203             Item item = mItems.get(position);
   2204             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
   2205                     .getLayoutParams();
   2206             if (lp instanceof LayoutParams) {
   2207                 ((LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
   2208             } else {
   2209                 LayoutParams slp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   2210                         ViewGroup.LayoutParams.WRAP_CONTENT);
   2211                 holder.itemView.setLayoutParams(slp);
   2212                 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
   2213                 lp = slp;
   2214             }
   2215 
   2216             if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) {
   2217                 final int minSize = mViewsHaveEqualSize ? mSizeReference :
   2218                         mSizeReference + 20 * (item.mId % 10);
   2219                 if (mOrientation == OrientationHelper.HORIZONTAL) {
   2220                     holder.itemView.setMinimumWidth(minSize);
   2221                 } else {
   2222                     holder.itemView.setMinimumHeight(minSize);
   2223                 }
   2224                 lp.topMargin = 3;
   2225                 lp.leftMargin = 5;
   2226                 lp.rightMargin = 7;
   2227                 lp.bottomMargin = 9;
   2228             }
   2229 
   2230             if (mOnBindCallback != null) {
   2231                 mOnBindCallback.onBoundItem(holder, position);
   2232             }
   2233         }
   2234     }
   2235 
   2236     abstract static class OnBindCallback {
   2237 
   2238         abstract void onBoundItem(TestViewHolder vh, int position);
   2239 
   2240         boolean assignRandomSize() {
   2241             return true;
   2242         }
   2243 
   2244         void onCreatedViewHolder(TestViewHolder vh) {
   2245         }
   2246     }
   2247 
   2248     static class Config implements Cloneable {
   2249 
   2250         private static final int DEFAULT_ITEM_COUNT = 300;
   2251 
   2252         int mOrientation = OrientationHelper.VERTICAL;
   2253 
   2254         boolean mReverseLayout = false;
   2255 
   2256         int mSpanCount = 3;
   2257 
   2258         int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
   2259 
   2260         int mItemCount = DEFAULT_ITEM_COUNT;
   2261 
   2262         Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
   2263             mOrientation = orientation;
   2264             mReverseLayout = reverseLayout;
   2265             mSpanCount = spanCount;
   2266             mGapStrategy = gapStrategy;
   2267         }
   2268 
   2269         public Config() {
   2270 
   2271         }
   2272 
   2273         Config orientation(int orientation) {
   2274             mOrientation = orientation;
   2275             return this;
   2276         }
   2277 
   2278         Config reverseLayout(boolean reverseLayout) {
   2279             mReverseLayout = reverseLayout;
   2280             return this;
   2281         }
   2282 
   2283         Config spanCount(int spanCount) {
   2284             mSpanCount = spanCount;
   2285             return this;
   2286         }
   2287 
   2288         Config gapStrategy(int gapStrategy) {
   2289             mGapStrategy = gapStrategy;
   2290             return this;
   2291         }
   2292 
   2293         public Config itemCount(int itemCount) {
   2294             mItemCount = itemCount;
   2295             return this;
   2296         }
   2297 
   2298         @Override
   2299         public String toString() {
   2300             return "[CONFIG:" +
   2301                     " span:" + mSpanCount + "," +
   2302                     " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
   2303                     " reverse:" + (mReverseLayout ? "T" : "F") +
   2304                     " itemCount:" + mItemCount +
   2305                     " gap strategy: " + gapStrategyName(mGapStrategy);
   2306         }
   2307 
   2308         private static String gapStrategyName(int gapStrategy) {
   2309             switch (gapStrategy) {
   2310                 case GAP_HANDLING_NONE:
   2311                     return "none";
   2312                 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
   2313                     return "move spans";
   2314             }
   2315             return "gap strategy: unknown";
   2316         }
   2317 
   2318         @Override
   2319         public Object clone() throws CloneNotSupportedException {
   2320             return super.clone();
   2321         }
   2322     }
   2323 
   2324     private interface PostLayoutRunnable {
   2325 
   2326         void run() throws Throwable;
   2327 
   2328         String describe();
   2329     }
   2330 
   2331 }
   2332