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.v4.view.AccessibilityDelegateCompat;
     26 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     27 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     28 import android.util.Log;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.accessibility.AccessibilityEvent;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Arrays;
     35 import java.util.BitSet;
     36 import java.util.HashSet;
     37 import java.util.LinkedHashMap;
     38 import java.util.List;
     39 import java.util.Map;
     40 import java.util.UUID;
     41 import java.util.concurrent.CountDownLatch;
     42 import java.util.concurrent.TimeUnit;
     43 import java.util.concurrent.atomic.AtomicInteger;
     44 
     45 import static android.support.v7.widget.LayoutState.*;
     46 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     47 import static android.support.v7.widget.StaggeredGridLayoutManager.*;
     48 
     49 public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
     50 
     51     private static final boolean DEBUG = false;
     52 
     53     private static final String TAG = "StaggeredGridLayoutManagerTest";
     54 
     55     volatile WrappedLayoutManager mLayoutManager;
     56 
     57     GridTestAdapter mAdapter;
     58 
     59     final List<Config> mBaseVariations = new ArrayList<Config>();
     60 
     61     @Override
     62     protected void setUp() throws Exception {
     63         super.setUp();
     64         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
     65             for (boolean reverseLayout : new boolean[]{false, true}) {
     66                 for (int spanCount : new int[]{1, 3}) {
     67                     for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
     68                             GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
     69                         mBaseVariations.add(new Config(orientation, reverseLayout, spanCount,
     70                                 gapStrategy));
     71                     }
     72                 }
     73             }
     74         }
     75     }
     76 
     77     void setupByConfig(Config config) throws Throwable {
     78         mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation);
     79         mRecyclerView = new RecyclerView(getActivity());
     80         mRecyclerView.setAdapter(mAdapter);
     81         mRecyclerView.setHasFixedSize(true);
     82         mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
     83                 config.mOrientation);
     84         mLayoutManager.setGapStrategy(config.mGapStrategy);
     85         mLayoutManager.setReverseLayout(config.mReverseLayout);
     86         mRecyclerView.setLayoutManager(mLayoutManager);
     87     }
     88 
     89     public void testRTL() throws Throwable {
     90         for (boolean changeRtlAfter : new boolean[]{false, true}) {
     91             for (Config config : mBaseVariations) {
     92                 rtlTest(config, changeRtlAfter);
     93                 removeRecyclerView();
     94             }
     95         }
     96     }
     97 
     98     void rtlTest(Config config, boolean changeRtlAfter) throws Throwable {
     99         if (config.mSpanCount == 1) {
    100             config.mSpanCount = 2;
    101         }
    102         String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter;
    103         setupByConfig(config.itemCount(5));
    104         if (changeRtlAfter) {
    105             waitFirstLayout();
    106             mLayoutManager.expectLayouts(1);
    107             mLayoutManager.setFakeRtl(true);
    108             mLayoutManager.waitForLayout(2);
    109         } else {
    110             mLayoutManager.mFakeRTL = true;
    111             waitFirstLayout();
    112         }
    113 
    114         assertEquals("view should become rtl", true, mLayoutManager.isLayoutRTL());
    115         OrientationHelper helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
    116         View child0 = mLayoutManager.findViewByPosition(0);
    117         View child1 = mLayoutManager.findViewByPosition(config.mOrientation == VERTICAL ? 1
    118             : config.mSpanCount);
    119         assertNotNull(logPrefix + " child position 0 should be laid out", child0);
    120         assertNotNull(logPrefix + " child position 0 should be laid out", child1);
    121         if (config.mOrientation == VERTICAL || !config.mReverseLayout) {
    122             assertTrue(logPrefix + " second child should be to the left of first child",
    123                     helper.getDecoratedStart(child0) >= helper.getDecoratedEnd(child1));
    124             assertEquals(logPrefix + " first child should be right aligned",
    125                     helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
    126         } else {
    127             assertTrue(logPrefix + " first child should be to the left of second child",
    128                     helper.getDecoratedStart(child1) >= helper.getDecoratedEnd(child0));
    129             assertEquals(logPrefix + " first child should be left aligned",
    130                     helper.getDecoratedStart(child0), helper.getStartAfterPadding());
    131         }
    132         checkForMainThreadException();
    133     }
    134 
    135     public void testScrollBackAndPreservePositions() throws Throwable {
    136         for (boolean saveRestore : new boolean[]{false, true}) {
    137             for (Config config : mBaseVariations) {
    138                 scrollBackAndPreservePositionsTest(config, saveRestore);
    139                 removeRecyclerView();
    140             }
    141         }
    142     }
    143 
    144     public void scrollBackAndPreservePositionsTest(final Config config, final boolean saveRestoreInBetween)
    145             throws Throwable {
    146         setupByConfig(config);
    147         mAdapter.mOnBindHandler = new OnBindHandler() {
    148             @Override
    149             public void onBoundItem(TestViewHolder vh, int postion) {
    150                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
    151                 lp.setFullSpan((postion * 7) % (config.mSpanCount + 1) == 0);
    152             }
    153         };
    154         waitFirstLayout();
    155         final int[] globalPositions = new int[mAdapter.getItemCount()];
    156         Arrays.fill(globalPositions, Integer.MIN_VALUE);
    157         final int scrollStep = (mLayoutManager.mPrimaryOrientation.getTotalSpace() / 10)
    158                 * (config.mReverseLayout ? -1 : 1);
    159 
    160 
    161         final int[] globalPos = new int[1];
    162         runTestOnUiThread(new Runnable() {
    163             @Override
    164             public void run() {
    165                 int globalScrollPosition = 0;
    166                 while (globalPositions[mAdapter.getItemCount() - 1] == Integer.MIN_VALUE) {
    167                     for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    168                         View child = mRecyclerView.getChildAt(i);
    169                         final int pos = mRecyclerView.getChildPosition(child);
    170                         if (globalPositions[pos] != Integer.MIN_VALUE) {
    171                             continue;
    172                         }
    173                         if (config.mReverseLayout) {
    174                             globalPositions[pos] = globalScrollPosition +
    175                                     mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
    176                         } else {
    177                             globalPositions[pos] = globalScrollPosition +
    178                                     mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
    179                         }
    180                     }
    181                     globalScrollPosition += mLayoutManager.scrollBy(scrollStep,
    182                             mRecyclerView.mRecycler, mRecyclerView.mState);
    183                 }
    184                 if (DEBUG) {
    185                     Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
    186                 }
    187                 globalPos[0] = globalScrollPosition;
    188             }
    189         });
    190         checkForMainThreadException();
    191 
    192         if (saveRestoreInBetween) {
    193             saveRestore(config);
    194         }
    195 
    196         checkForMainThreadException();
    197         runTestOnUiThread(new Runnable() {
    198             @Override
    199             public void run() {
    200                 int globalScrollPosition = globalPos[0];
    201                 // now scroll back and make sure global positions match
    202                 BitSet shouldTest = new BitSet(mAdapter.getItemCount());
    203                 shouldTest.set(0, mAdapter.getItemCount() - 1, true);
    204                 String assertPrefix = config + ", restored in between:" + saveRestoreInBetween
    205                         + " global pos must match when scrolling in reverse for position ";
    206                 int scrollAmount = Integer.MAX_VALUE;
    207                 while (!shouldTest.isEmpty() && scrollAmount != 0) {
    208                     for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    209                         View child = mRecyclerView.getChildAt(i);
    210                         int pos = mRecyclerView.getChildPosition(child);
    211                         if (!shouldTest.get(pos)) {
    212                             continue;
    213                         }
    214                         shouldTest.clear(pos);
    215                         int globalPos;
    216                         if (config.mReverseLayout) {
    217                             globalPos = globalScrollPosition +
    218                                     mLayoutManager.mPrimaryOrientation.getDecoratedEnd(child);
    219                         } else {
    220                             globalPos = globalScrollPosition +
    221                                     mLayoutManager.mPrimaryOrientation.getDecoratedStart(child);
    222                         }
    223                         assertEquals(assertPrefix + pos,
    224                                 globalPositions[pos], globalPos);
    225                     }
    226                     scrollAmount = mLayoutManager.scrollBy(-scrollStep,
    227                             mRecyclerView.mRecycler, mRecyclerView.mState);
    228                     globalScrollPosition += scrollAmount;
    229                 }
    230                 assertTrue("all views should be seen", shouldTest.isEmpty());
    231             }
    232         });
    233         checkForMainThreadException();
    234     }
    235 
    236     public void testScrollToPositionWithPredictive() throws Throwable {
    237         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
    238         removeRecyclerView();
    239         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
    240                 LinearLayoutManager.INVALID_OFFSET);
    241         removeRecyclerView();
    242         scrollToPositionWithPredictive(9, 20);
    243         removeRecyclerView();
    244         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
    245 
    246     }
    247 
    248     public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
    249             throws Throwable {
    250         setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
    251                 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
    252         waitFirstLayout();
    253         mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
    254             @Override
    255             void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
    256                 RecyclerView rv = mLayoutManager.mRecyclerView;
    257                 if (state.isPreLayout()) {
    258                     assertEquals("pending scroll position should still be pending",
    259                             scrollPosition, mLayoutManager.mPendingScrollPosition);
    260                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    261                         assertEquals("pending scroll position offset should still be pending",
    262                                 scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
    263                     }
    264                 } else {
    265                     RecyclerView.ViewHolder vh = rv.findViewHolderForPosition(scrollPosition);
    266                     assertNotNull("scroll to position should work", vh);
    267                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    268                         assertEquals("scroll offset should be applied properly",
    269                                 mLayoutManager.getPaddingTop() + scrollOffset
    270                                         + ((RecyclerView.LayoutParams) vh.itemView
    271                                             .getLayoutParams()).topMargin,
    272                                 mLayoutManager.getDecoratedTop(vh.itemView));
    273                     }
    274                 }
    275             }
    276         };
    277         mLayoutManager.expectLayouts(2);
    278         runTestOnUiThread(new Runnable() {
    279             @Override
    280             public void run() {
    281                 try {
    282                     mAdapter.addAndNotify(0, 1);
    283                     if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
    284                         mLayoutManager.scrollToPosition(scrollPosition);
    285                     } else {
    286                         mLayoutManager.scrollToPositionWithOffset(scrollPosition,
    287                                 scrollOffset);
    288                     }
    289 
    290                 } catch (Throwable throwable) {
    291                     throwable.printStackTrace();
    292                 }
    293 
    294             }
    295         });
    296         mLayoutManager.waitForLayout(2);
    297         checkForMainThreadException();
    298     }
    299 
    300     LayoutParams getLp(View view) {
    301         return (LayoutParams) view.getLayoutParams();
    302     }
    303 
    304     public void testGetFirstLastChildrenTest() throws Throwable {
    305         for (boolean provideArr : new boolean[]{true, false}) {
    306             for (Config config : mBaseVariations) {
    307                 getFirstLastChildrenTest(config, provideArr);
    308                 removeRecyclerView();
    309             }
    310         }
    311     }
    312 
    313     public void getFirstLastChildrenTest(final Config config, final boolean provideArr)
    314             throws Throwable {
    315         setupByConfig(config);
    316         waitFirstLayout();
    317         Runnable viewInBoundsTest = new Runnable() {
    318             @Override
    319             public void run() {
    320                 VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
    321                 final String boundsLog = mLayoutManager.getBoundsLog();
    322                 VisibleChildren queryResult = new VisibleChildren(mLayoutManager.getSpanCount());
    323                 queryResult.firstFullyVisiblePositions = mLayoutManager
    324                         .findFirstCompletelyVisibleItemPositions(
    325                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    326                 queryResult.firstVisiblePositions = mLayoutManager
    327                         .findFirstVisibleItemPositions(
    328                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    329                 queryResult.lastFullyVisiblePositions = mLayoutManager
    330                         .findLastCompletelyVisibleItemPositions(
    331                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    332                 queryResult.lastVisiblePositions = mLayoutManager
    333                         .findLastVisibleItemPositions(
    334                                 provideArr ? new int[mLayoutManager.getSpanCount()] : null);
    335                 assertEquals(config + ":\nfirst visible child should match traversal result\n"
    336                                 + "traversed:" + visibleChildren + "\n"
    337                                 + "queried:" + queryResult + "\n"
    338                                 + boundsLog, visibleChildren, queryResult
    339                 );
    340             }
    341         };
    342         runTestOnUiThread(viewInBoundsTest);
    343         // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
    344         // case
    345         final int scrollPosition = mAdapter.getItemCount();
    346         runTestOnUiThread(new Runnable() {
    347             @Override
    348             public void run() {
    349                 mRecyclerView.smoothScrollToPosition(scrollPosition);
    350             }
    351         });
    352         while (mLayoutManager.isSmoothScrolling() ||
    353                 mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
    354             runTestOnUiThread(viewInBoundsTest);
    355             Thread.sleep(400);
    356         }
    357         // delete all items
    358         mLayoutManager.expectLayouts(2);
    359         mAdapter.deleteAndNotify(0, mAdapter.getItemCount());
    360         mLayoutManager.waitForLayout(2);
    361         // test empty case
    362         runTestOnUiThread(viewInBoundsTest);
    363         // set a new adapter with huge items to test full bounds check
    364         mLayoutManager.expectLayouts(1);
    365         final int totalSpace = mLayoutManager.mPrimaryOrientation.getTotalSpace();
    366         final TestAdapter newAdapter = new TestAdapter(100) {
    367             @Override
    368             public void onBindViewHolder(TestViewHolder holder,
    369                     int position) {
    370                 super.onBindViewHolder(holder, position);
    371                 if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
    372                     holder.itemView.setMinimumWidth(totalSpace + 5);
    373                 } else {
    374                     holder.itemView.setMinimumHeight(totalSpace + 5);
    375                 }
    376             }
    377         };
    378         runTestOnUiThread(new Runnable() {
    379             @Override
    380             public void run() {
    381                 mRecyclerView.setAdapter(newAdapter);
    382             }
    383         });
    384         mLayoutManager.waitForLayout(2);
    385         runTestOnUiThread(viewInBoundsTest);
    386     }
    387 
    388     public void testInnerGapHandling() throws Throwable {
    389         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
    390         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    391     }
    392 
    393     public void innerGapHandlingTest(int strategy) throws Throwable {
    394         Config config = new Config().spanCount(3).itemCount(500);
    395         setupByConfig(config);
    396         mLayoutManager.setGapStrategy(strategy);
    397         mAdapter.mFullSpanItems.add(100);
    398         mAdapter.mFullSpanItems.add(104);
    399         mAdapter.mViewsHaveEqualSize = true;
    400         waitFirstLayout();
    401         mLayoutManager.expectLayouts(1);
    402         scrollToPosition(400);
    403         mLayoutManager.waitForLayout(2);
    404         mLayoutManager.expectLayouts(2);
    405         mAdapter.addAndNotify(101, 1);
    406         mLayoutManager.waitForLayout(2);
    407         if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
    408             mLayoutManager.expectLayouts(1);
    409         }
    410         // state
    411         // now smooth scroll to 99 to trigger a layout around 100
    412         smoothScrollToPosition(99);
    413         switch (strategy) {
    414             case GAP_HANDLING_NONE:
    415                 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
    416                         new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
    417                         new int[]{105, 0});
    418                 break;
    419             case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
    420                 mLayoutManager.waitForLayout(2);
    421                 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
    422                         new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
    423                 break;
    424         }
    425 
    426     }
    427 
    428     public void testFullSizeSpans() throws Throwable {
    429         Config config = new Config().spanCount(5).itemCount(30);
    430         setupByConfig(config);
    431         mAdapter.mFullSpanItems.add(3);
    432         waitFirstLayout();
    433         assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
    434                 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
    435                 new int[]{7, 3}, new int[]{8, 4});
    436     }
    437 
    438     void assertSpans(String msg, int[]... childSpanTuples) {
    439         for (int i = 0; i < childSpanTuples.length; i++) {
    440             assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
    441         }
    442     }
    443 
    444     void assertSpan(String msg, int childPosition, int expectedSpan) {
    445         View view = mLayoutManager.findViewByPosition(childPosition);
    446         assertNotNull(msg + "view at position " + childPosition + " should exists", view);
    447         assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
    448                 getLp(view).mSpan.mIndex);
    449     }
    450 
    451     public void gapInTheMiddle(Config config) throws Throwable {
    452 
    453     }
    454 
    455     public void testGapAtTheBeginning() throws Throwable {
    456         for (Config config : mBaseVariations) {
    457             for (int deleteCount = 1; deleteCount < config.mSpanCount * 2; deleteCount ++) {
    458                 for (int deletePosition = config.mSpanCount - 1;
    459                         deletePosition < config.mSpanCount + 2; deletePosition ++) {
    460                     gapAtTheBeginningOfTheListTest(config, deletePosition, deleteCount);
    461                     removeRecyclerView();
    462                 }
    463             }
    464         }
    465     }
    466 
    467     public void gapAtTheBeginningOfTheListTest(final Config config, int deletePosition,
    468             int deleteCount) throws Throwable {
    469         if (config.mSpanCount < 2 || config.mGapStrategy == GAP_HANDLING_NONE) {
    470             return;
    471         }
    472         if (config.mItemCount < 100) {
    473             config.itemCount(100);
    474         }
    475         final String logPrefix = config + ", deletePos:" + deletePosition + ", deleteCount:"
    476                 + deleteCount;
    477         setupByConfig(config);
    478         final RecyclerView.Adapter adapter = mAdapter;
    479         waitFirstLayout();
    480         // scroll far away
    481         smoothScrollToPosition(config.mItemCount / 2);
    482         // assert to be deleted child is not visible
    483         assertNull(logPrefix + " test sanity, to be deleted child should be invisible",
    484                 mRecyclerView.findViewHolderForPosition(deletePosition));
    485         // delete the child and notify
    486         mAdapter.deleteAndNotify(deletePosition, deleteCount);
    487         getInstrumentation().waitForIdleSync();
    488         mLayoutManager.expectLayouts(1);
    489         smoothScrollToPosition(0);
    490         mLayoutManager.waitForLayout(2);
    491         // due to data changes, first item may become visible before others which will cause
    492         // smooth scrolling to stop. Triggering it twice more is a naive hack.
    493         // Until we have time to consider it as a bug, this is the only workaround.
    494         smoothScrollToPosition(0);
    495         Thread.sleep(300);
    496         smoothScrollToPosition(0);
    497         Thread.sleep(500);
    498         // some animations should happen and we should recover layout
    499         final Map<Item, Rect> actualCoords = mLayoutManager.collectChildCoordinates();
    500         // now layout another RV with same adapter
    501         removeRecyclerView();
    502         setupByConfig(config);
    503         mRecyclerView.setAdapter(adapter);// use same adapter so that items can be matched
    504         waitFirstLayout();
    505         final Map<Item, Rect> desiredCoords = mLayoutManager.collectChildCoordinates();
    506         assertRectSetsEqual(logPrefix + " when an item from the start of the list is deleted, "
    507                         + "layout should recover the state once scrolling is stopped",
    508                 desiredCoords, actualCoords);
    509     }
    510 
    511     public void testPartialSpanInvalidation() throws Throwable {
    512         Config config = new Config().spanCount(5).itemCount(100);
    513         setupByConfig(config);
    514         for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
    515             mAdapter.mFullSpanItems.add(i);
    516         }
    517         waitFirstLayout();
    518         smoothScrollToPosition(50);
    519         int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
    520         mAdapter.changeAndNotify(15, 2);
    521         Thread.sleep(200);
    522         assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
    523                 mLayoutManager.mLazySpanLookup.mData[30]);
    524         assertEquals("item in invalidated range should have clear span id",
    525                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    526         smoothScrollToPosition(85);
    527         int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
    528         mAdapter.deleteAndNotify(55, 2);
    529         Thread.sleep(200);
    530         assertEquals("item in invalidated range should have clear span id",
    531                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    532         int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
    533         assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
    534                 newSpans, 0, 0, newSpans.length);
    535     }
    536 
    537     // Same as Arrays.copyOfRange but for API 7
    538     private int[] copyOfRange(int[] original, int from, int to) {
    539         int newLength = to - from;
    540         if (newLength < 0)
    541             throw new IllegalArgumentException(from + " > " + to);
    542         int[] copy = new int[newLength];
    543         System.arraycopy(original, from, copy, 0,
    544                 Math.min(original.length - from, newLength));
    545         return copy;
    546     }
    547 
    548     public void testSpanReassignmentsOnItemChange() throws Throwable {
    549         Config config = new Config().spanCount(5);
    550         setupByConfig(config);
    551         waitFirstLayout();
    552         smoothScrollToPosition(mAdapter.getItemCount() / 2);
    553         final int changePosition = mAdapter.getItemCount() / 4;
    554         mLayoutManager.expectLayouts(1);
    555         mAdapter.changeAndNotify(changePosition, 1);
    556         mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
    557                 1);
    558         // delete an item before visible area
    559         int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
    560         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
    561         if (DEBUG) {
    562             Log.d(TAG, "before:");
    563             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
    564                 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
    565             }
    566         }
    567         mLayoutManager.expectLayouts(1);
    568         mAdapter.deleteAndNotify(deletedPosition, 1);
    569         mLayoutManager.waitForLayout(2);
    570         assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
    571                         + "should not affect the layout if it is not visible", before,
    572                 mLayoutManager.collectChildCoordinates()
    573         );
    574         deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
    575         mLayoutManager.expectLayouts(1);
    576         mAdapter.deleteAndNotify(deletedPosition, 1);
    577         mLayoutManager.waitForLayout(2);
    578         assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
    579                 + "layout", before, mLayoutManager.collectChildCoordinates());
    580     }
    581 
    582     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start, int end) {
    583         for (int i = start; i < end; i++) {
    584             assertEquals(msg + " ind:" + i, set1[i], set2[i]);
    585         }
    586     }
    587 
    588     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
    589             int length) {
    590         for (int i = 0; i < length; i++) {
    591             assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
    592                     set2[start2 + i]);
    593         }
    594     }
    595 
    596     public void testViewSnapping() throws Throwable {
    597         for (Config config : mBaseVariations) {
    598             viewSnapTest(config.itemCount(config.mSpanCount + 1));
    599             removeRecyclerView();
    600         }
    601     }
    602 
    603     public void viewSnapTest(Config config) throws Throwable {
    604         setupByConfig(config);
    605         waitFirstLayout();
    606         // run these tests twice. once initial layout, once after scroll
    607         String logSuffix = "";
    608         for (int i = 0; i < 2; i++) {
    609             Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
    610             Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
    611             Rect usedLayoutBounds = new Rect();
    612             for (Rect rect : itemRectMap.values()) {
    613                 usedLayoutBounds.union(rect);
    614             }
    615             if (DEBUG) {
    616                 Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
    617             }
    618             if (config.mOrientation == VERTICAL) {
    619                 assertEquals(config + " there should be no gap on left" + logSuffix,
    620                         usedLayoutBounds.left, recyclerViewBounds.left);
    621                 assertEquals(config + " there should be no gap on right" + logSuffix,
    622                         usedLayoutBounds.right, recyclerViewBounds.right);
    623                 if (config.mReverseLayout) {
    624                     assertEquals(config + " there should be no gap on bottom" + logSuffix,
    625                             usedLayoutBounds.bottom, recyclerViewBounds.bottom);
    626                     assertTrue(config + " there should be some gap on top" + logSuffix,
    627                             usedLayoutBounds.top > recyclerViewBounds.top);
    628                 } else {
    629                     assertEquals(config + " there should be no gap on top" + logSuffix,
    630                             usedLayoutBounds.top, recyclerViewBounds.top);
    631                     assertTrue(config + " there should be some gap at the bottom" + logSuffix,
    632                             usedLayoutBounds.bottom < recyclerViewBounds.bottom);
    633                 }
    634             } else {
    635                 assertEquals(config + " there should be no gap on top" + logSuffix,
    636                         usedLayoutBounds.top, recyclerViewBounds.top);
    637                 assertEquals(config + " there should be no gap at the bottom" + logSuffix,
    638                         usedLayoutBounds.bottom, recyclerViewBounds.bottom);
    639                 if (config.mReverseLayout) {
    640                     assertEquals(config + " there should be no on right" + logSuffix,
    641                             usedLayoutBounds.right, recyclerViewBounds.right);
    642                     assertTrue(config + " there should be some gap on left" + logSuffix,
    643                             usedLayoutBounds.left > recyclerViewBounds.left);
    644                 } else {
    645                     assertEquals(config + " there should be no gap on left" + logSuffix,
    646                             usedLayoutBounds.left, recyclerViewBounds.left);
    647                     assertTrue(config + " there should be some gap on right" + logSuffix,
    648                             usedLayoutBounds.right < recyclerViewBounds.right);
    649                 }
    650             }
    651             final int scroll = config.mReverseLayout ? -500 : 500;
    652             scrollBy(scroll);
    653             logSuffix = " scrolled " + scroll;
    654         }
    655 
    656     }
    657 
    658     public void testSpanCountChangeOnRestoreSavedState() throws Throwable {
    659         Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
    660         setupByConfig(config);
    661         waitFirstLayout();
    662 
    663         int beforeChildCount = mLayoutManager.getChildCount();
    664         Parcelable savedState = mRecyclerView.onSaveInstanceState();
    665         // we append a suffix to the parcelable to test out of bounds
    666         String parcelSuffix = UUID.randomUUID().toString();
    667         Parcel parcel = Parcel.obtain();
    668         savedState.writeToParcel(parcel, 0);
    669         parcel.writeString(parcelSuffix);
    670         removeRecyclerView();
    671         // reset for reading
    672         parcel.setDataPosition(0);
    673         // re-create
    674         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
    675         removeRecyclerView();
    676 
    677         RecyclerView restored = new RecyclerView(getActivity());
    678         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
    679         mLayoutManager.setReverseLayout(config.mReverseLayout);
    680         mLayoutManager.setGapStrategy(config.mGapStrategy);
    681         restored.setLayoutManager(mLayoutManager);
    682         // use the same adapter for Rect matching
    683         restored.setAdapter(mAdapter);
    684         restored.onRestoreInstanceState(savedState);
    685         mLayoutManager.setSpanCount(1);
    686         mLayoutManager.expectLayouts(1);
    687         setRecyclerView(restored);
    688         mLayoutManager.waitForLayout(2);
    689         assertEquals("on saved state, reverse layout should be preserved",
    690                 config.mReverseLayout, mLayoutManager.getReverseLayout());
    691         assertEquals("on saved state, orientation should be preserved",
    692                 config.mOrientation, mLayoutManager.getOrientation());
    693         assertEquals("after setting new span count, layout manager should keep new value",
    694                 1, mLayoutManager.getSpanCount());
    695         assertEquals("on saved state, gap strategy should be preserved",
    696                 config.mGapStrategy, mLayoutManager.getGapStrategy());
    697         assertTrue("when span count is dramatically changed after restore, # of child views "
    698                 + "should change", beforeChildCount > mLayoutManager.getChildCount());
    699         // make sure LLM can layout all children. is some span info is leaked, this would crash
    700         smoothScrollToPosition(mAdapter.getItemCount() - 1);
    701     }
    702 
    703     public void testSavedState() throws Throwable {
    704         PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
    705                 new PostLayoutRunnable() {
    706                     @Override
    707                     public void run() throws Throwable {
    708                         // do nothing
    709                     }
    710 
    711                     @Override
    712                     public String describe() {
    713                         return "doing nothing";
    714                     }
    715                 },
    716                 new PostLayoutRunnable() {
    717                     @Override
    718                     public void run() throws Throwable {
    719                         mLayoutManager.expectLayouts(1);
    720                         scrollToPosition(mAdapter.getItemCount() * 3 / 4);
    721                         mLayoutManager.waitForLayout(2);
    722                     }
    723 
    724                     @Override
    725                     public String describe() {
    726                         return "scroll to position " + (mAdapter == null ? "" :
    727                                 mAdapter.getItemCount() * 3 / 4);
    728                     }
    729                 },
    730                 new PostLayoutRunnable() {
    731                     @Override
    732                     public void run() throws Throwable {
    733                         mLayoutManager.expectLayouts(1);
    734                         scrollToPositionWithOffset(mAdapter.getItemCount() / 3,
    735                                 50);
    736                         mLayoutManager.waitForLayout(2);
    737                     }
    738 
    739                     @Override
    740                     public String describe() {
    741                         return "scroll to position " + (mAdapter == null ? "" :
    742                                 mAdapter.getItemCount() / 3) + "with positive offset";
    743                     }
    744                 },
    745                 new PostLayoutRunnable() {
    746                     @Override
    747                     public void run() throws Throwable {
    748                         mLayoutManager.expectLayouts(1);
    749                         scrollToPositionWithOffset(mAdapter.getItemCount() * 2 / 3,
    750                                 -50);
    751                         mLayoutManager.waitForLayout(2);
    752                     }
    753 
    754                     @Override
    755                     public String describe() {
    756                         return "scroll to position with negative offset";
    757                     }
    758                 }
    759         };
    760         boolean[] waitForLayoutOptions = new boolean[]{false, true};
    761         List<Config> testVariations = new ArrayList<Config>();
    762         testVariations.addAll(mBaseVariations);
    763         for (Config config : mBaseVariations) {
    764             if (config.mSpanCount < 2) {
    765                 continue;
    766             }
    767             final Config clone = (Config) config.clone();
    768             clone.mItemCount = clone.mSpanCount - 1;
    769             testVariations.add(clone);
    770         }
    771 
    772         for (Config config : testVariations) {
    773             for (PostLayoutRunnable runnable : postLayoutOptions) {
    774                 for (boolean waitForLayout : waitForLayoutOptions) {
    775                     savedStateTest(config, waitForLayout, runnable);
    776                     removeRecyclerView();
    777                 }
    778             }
    779         }
    780     }
    781 
    782     private void saveRestore(Config config) throws Throwable {
    783         Parcelable savedState = mRecyclerView.onSaveInstanceState();
    784         // we append a suffix to the parcelable to test out of bounds
    785         String parcelSuffix = UUID.randomUUID().toString();
    786         Parcel parcel = Parcel.obtain();
    787         savedState.writeToParcel(parcel, 0);
    788         parcel.writeString(parcelSuffix);
    789         removeRecyclerView();
    790         // reset for reading
    791         parcel.setDataPosition(0);
    792         // re-create
    793         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
    794         RecyclerView restored = new RecyclerView(getActivity());
    795         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
    796         mLayoutManager.setGapStrategy(config.mGapStrategy);
    797         restored.setLayoutManager(mLayoutManager);
    798         // use the same adapter for Rect matching
    799         restored.setAdapter(mAdapter);
    800         restored.onRestoreInstanceState(savedState);
    801         if (Looper.myLooper() == Looper.getMainLooper()) {
    802             mLayoutManager.expectLayouts(1);
    803             setRecyclerView(restored);
    804         } else {
    805             mLayoutManager.expectLayouts(1);
    806             setRecyclerView(restored);
    807             mLayoutManager.waitForLayout(2);
    808         }
    809     }
    810 
    811     public void savedStateTest(Config config, boolean waitForLayout,
    812             PostLayoutRunnable postLayoutOperations)
    813             throws Throwable {
    814         if (DEBUG) {
    815             Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config "
    816                     + config + " post layout action " + postLayoutOperations.describe());
    817         }
    818         setupByConfig(config);
    819         waitFirstLayout();
    820         if (waitForLayout) {
    821             postLayoutOperations.run();
    822         }
    823         final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt();
    824         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
    825         Parcelable savedState = mRecyclerView.onSaveInstanceState();
    826         // we append a suffix to the parcelable to test out of bounds
    827         String parcelSuffix = UUID.randomUUID().toString();
    828         Parcel parcel = Parcel.obtain();
    829         savedState.writeToParcel(parcel, 0);
    830         parcel.writeString(parcelSuffix);
    831         removeRecyclerView();
    832         // reset for reading
    833         parcel.setDataPosition(0);
    834         // re-create
    835         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
    836         removeRecyclerView();
    837 
    838         RecyclerView restored = new RecyclerView(getActivity());
    839         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
    840         mLayoutManager.setGapStrategy(config.mGapStrategy);
    841         restored.setLayoutManager(mLayoutManager);
    842         // use the same adapter for Rect matching
    843         restored.setAdapter(mAdapter);
    844         restored.onRestoreInstanceState(savedState);
    845         assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
    846                 parcel.readString());
    847         mLayoutManager.expectLayouts(1);
    848         setRecyclerView(restored);
    849         mLayoutManager.waitForLayout(2);
    850         assertEquals(config + " on saved state, reverse layout should be preserved",
    851                 config.mReverseLayout, mLayoutManager.getReverseLayout());
    852         assertEquals(config + " on saved state, orientation should be preserved",
    853                 config.mOrientation, mLayoutManager.getOrientation());
    854         assertEquals(config + " on saved state, span count should be preserved",
    855                 config.mSpanCount, mLayoutManager.getSpanCount());
    856         assertEquals(config + " on saved state, gap strategy should be preserved",
    857                 config.mGapStrategy, mLayoutManager.getGapStrategy());
    858         assertEquals(config + " on saved state, first completely visible child position should"
    859                 + " be preserved", firstCompletelyVisiblePosition,
    860                 mLayoutManager.findFirstVisibleItemPositionInt());
    861         if (waitForLayout) {
    862             assertRectSetsEqual(config + "\npost layout op:" + postLayoutOperations.describe()
    863                             + ": on restore, previous view positions should be preserved",
    864                     before, mLayoutManager.collectChildCoordinates()
    865             );
    866         }
    867         // TODO add tests for changing values after restore before layout
    868     }
    869 
    870     public void testScrollToPositionWithOffset() throws Throwable {
    871         for (Config config : mBaseVariations) {
    872             scrollToPositionWithOffsetTest(config);
    873             removeRecyclerView();
    874         }
    875     }
    876 
    877     public void scrollToPositionWithOffsetTest(Config config) throws Throwable {
    878         setupByConfig(config);
    879         waitFirstLayout();
    880         OrientationHelper orientationHelper = OrientationHelper
    881                 .createOrientationHelper(mLayoutManager, config.mOrientation);
    882         Rect layoutBounds = getDecoratedRecyclerViewBounds();
    883         // try scrolling towards head, should not affect anything
    884         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
    885         scrollToPositionWithOffset(0, 20);
    886         assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
    887                 before, mLayoutManager.collectChildCoordinates());
    888         // try offsetting some visible children
    889         int testCount = 10;
    890         while (testCount-- > 0) {
    891             // get middle child
    892             final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
    893             final int position = mRecyclerView.getChildPosition(child);
    894             final int startOffset = config.mReverseLayout ?
    895                     orientationHelper.getEndAfterPadding() - orientationHelper
    896                             .getDecoratedEnd(child)
    897                     : orientationHelper.getDecoratedStart(child) - orientationHelper
    898                             .getStartAfterPadding();
    899             final int scrollOffset = startOffset / 2;
    900             mLayoutManager.expectLayouts(1);
    901             scrollToPositionWithOffset(position, scrollOffset);
    902             mLayoutManager.waitForLayout(2);
    903             final int finalOffset = config.mReverseLayout ?
    904                     orientationHelper.getEndAfterPadding() - orientationHelper
    905                             .getDecoratedEnd(child)
    906                     : orientationHelper.getDecoratedStart(child) - orientationHelper
    907                             .getStartAfterPadding();
    908             assertEquals(config + " scroll with offset on a visible child should work fine",
    909                     scrollOffset, finalOffset);
    910         }
    911 
    912         // try scrolling to invisible children
    913         testCount = 10;
    914         // we test above and below, one by one
    915         int offsetMultiplier = -1;
    916         while (testCount-- > 0) {
    917             final TargetTuple target = findInvisibleTarget(config);
    918             mLayoutManager.expectLayouts(1);
    919             final int offset = offsetMultiplier
    920                     * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
    921             scrollToPositionWithOffset(target.mPosition, offset);
    922             mLayoutManager.waitForLayout(2);
    923             final View child = mLayoutManager.findViewByPosition(target.mPosition);
    924             assertNotNull(config + " scrolling to a mPosition with offset " + offset
    925                     + " should layout it", child);
    926             final Rect bounds = mLayoutManager.getViewBounds(child);
    927             if (DEBUG) {
    928                 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
    929                         + layoutBounds + " with offset " + offset);
    930             }
    931 
    932             if (config.mReverseLayout) {
    933                 assertEquals(config + " when scrolling with offset to an invisible in reverse "
    934                                 + "layout, its end should align with recycler view's end - offset",
    935                         orientationHelper.getEndAfterPadding() - offset,
    936                         orientationHelper.getDecoratedEnd(child)
    937                 );
    938             } else {
    939                 assertEquals(config + " when scrolling with offset to an invisible child in normal"
    940                                 + " layout its start should align with recycler view's start + "
    941                                 + "offset",
    942                         orientationHelper.getStartAfterPadding() + offset,
    943                         orientationHelper.getDecoratedStart(child)
    944                 );
    945             }
    946             offsetMultiplier *= -1;
    947         }
    948     }
    949 
    950     public void testScrollToPosition() throws Throwable {
    951         for (Config config : mBaseVariations) {
    952             scrollToPositionTest(config);
    953             removeRecyclerView();
    954         }
    955     }
    956 
    957     private TargetTuple findInvisibleTarget(Config config) {
    958         int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
    959         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
    960             View child = mLayoutManager.getChildAt(i);
    961             int position = mRecyclerView.getChildPosition(child);
    962             if (position < minPosition) {
    963                 minPosition = position;
    964             }
    965             if (position > maxPosition) {
    966                 maxPosition = position;
    967             }
    968         }
    969         final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
    970         final int headTarget = minPosition / 2;
    971         final int target;
    972         // where will the child come from ?
    973         final int itemLayoutDirection;
    974         if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
    975             target = tailTarget;
    976             itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
    977         } else {
    978             target = headTarget;
    979             itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
    980         }
    981         if (DEBUG) {
    982             Log.d(TAG,
    983                     config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
    984         }
    985         return new TargetTuple(target, itemLayoutDirection);
    986     }
    987 
    988     public void scrollToPositionTest(Config config) throws Throwable {
    989         setupByConfig(config);
    990         waitFirstLayout();
    991         OrientationHelper orientationHelper = OrientationHelper
    992                 .createOrientationHelper(mLayoutManager, config.mOrientation);
    993         Rect layoutBounds = getDecoratedRecyclerViewBounds();
    994         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
    995             View view = mLayoutManager.getChildAt(i);
    996             Rect bounds = mLayoutManager.getViewBounds(view);
    997             if (layoutBounds.contains(bounds)) {
    998                 Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
    999                 final int position = mRecyclerView.getChildPosition(view);
   1000                 LayoutParams layoutParams
   1001                         = (LayoutParams) (view.getLayoutParams());
   1002                 TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
   1003                 assertEquals("recycler view mPosition should match adapter mPosition", position,
   1004                         vh.mBindedItem.mAdapterIndex);
   1005                 if (DEBUG) {
   1006                     Log.d(TAG, "testing scroll to visible mPosition at " + position
   1007                             + " " + bounds + " inside " + layoutBounds);
   1008                 }
   1009                 mLayoutManager.expectLayouts(1);
   1010                 scrollToPosition(position);
   1011                 mLayoutManager.waitForLayout(2);
   1012                 if (DEBUG) {
   1013                     view = mLayoutManager.findViewByPosition(position);
   1014                     Rect newBounds = mLayoutManager.getViewBounds(view);
   1015                     Log.d(TAG, "after scrolling to visible mPosition " +
   1016                             bounds + " equals " + newBounds);
   1017                 }
   1018 
   1019                 assertRectSetsEqual(
   1020                         config + "scroll to mPosition on fully visible child should be no-op",
   1021                         initialBounds, mLayoutManager.collectChildCoordinates());
   1022             } else {
   1023                 final int position = mRecyclerView.getChildPosition(view);
   1024                 if (DEBUG) {
   1025                     Log.d(TAG,
   1026                             "child(" + position + ") not fully visible " + bounds + " not inside "
   1027                                     + layoutBounds
   1028                                     + mRecyclerView.getChildPosition(view)
   1029                     );
   1030                 }
   1031                 mLayoutManager.expectLayouts(1);
   1032                 runTestOnUiThread(new Runnable() {
   1033                     @Override
   1034                     public void run() {
   1035                         mLayoutManager.scrollToPosition(position);
   1036                     }
   1037                 });
   1038                 mLayoutManager.waitForLayout(2);
   1039                 view = mLayoutManager.findViewByPosition(position);
   1040                 bounds = mLayoutManager.getViewBounds(view);
   1041                 if (DEBUG) {
   1042                     Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
   1043                             + layoutBounds);
   1044                 }
   1045                 assertTrue(config
   1046                                 + " after scrolling to a partially visible child, it should become fully "
   1047                                 + " visible. " + bounds + " not inside " + layoutBounds,
   1048                         layoutBounds.contains(bounds)
   1049                 );
   1050                 assertTrue(config + " when scrolling to a partially visible item, one of its edges "
   1051                         + "should be on the boundaries", orientationHelper.getStartAfterPadding() ==
   1052                         orientationHelper.getDecoratedStart(view)
   1053                         || orientationHelper.getEndAfterPadding() ==
   1054                         orientationHelper.getDecoratedEnd(view));
   1055             }
   1056         }
   1057 
   1058         // try scrolling to invisible children
   1059         int testCount = 10;
   1060         while (testCount-- > 0) {
   1061             final TargetTuple target = findInvisibleTarget(config);
   1062             mLayoutManager.expectLayouts(1);
   1063             scrollToPosition(target.mPosition);
   1064             mLayoutManager.waitForLayout(2);
   1065             final View child = mLayoutManager.findViewByPosition(target.mPosition);
   1066             assertNotNull(config + " scrolling to a mPosition should lay it out", child);
   1067             final Rect bounds = mLayoutManager.getViewBounds(child);
   1068             if (DEBUG) {
   1069                 Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
   1070                         + layoutBounds);
   1071             }
   1072             assertTrue(config + " scrolling to a mPosition should make it fully visible",
   1073                     layoutBounds.contains(bounds));
   1074             if (target.mLayoutDirection == LAYOUT_START) {
   1075                 assertEquals(
   1076                         config + " when scrolling to an invisible child above, its start should"
   1077                                 + " align with recycler view's start",
   1078                         orientationHelper.getStartAfterPadding(),
   1079                         orientationHelper.getDecoratedStart(child)
   1080                 );
   1081             } else {
   1082                 assertEquals(config + " when scrolling to an invisible child below, its end "
   1083                                 + "should align with recycler view's end",
   1084                         orientationHelper.getEndAfterPadding(),
   1085                         orientationHelper.getDecoratedEnd(child)
   1086                 );
   1087             }
   1088         }
   1089     }
   1090 
   1091     private void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
   1092         runTestOnUiThread(new Runnable() {
   1093             @Override
   1094             public void run() {
   1095                 mLayoutManager.scrollToPositionWithOffset(position, offset);
   1096             }
   1097         });
   1098     }
   1099 
   1100     public void testLayoutOrder() throws Throwable {
   1101         for (Config config : mBaseVariations) {
   1102             layoutOrderTest(config);
   1103             removeRecyclerView();
   1104         }
   1105     }
   1106 
   1107     public void layoutOrderTest(Config config) throws Throwable {
   1108         setupByConfig(config);
   1109         assertViewPositions(config);
   1110     }
   1111 
   1112     void assertViewPositions(Config config) {
   1113         ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
   1114         OrientationHelper orientationHelper = OrientationHelper
   1115                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1116         for (ArrayList<View> span : viewsBySpan) {
   1117             // validate all children's order. first child should have min start mPosition
   1118             final int count = span.size();
   1119             for (int i = 0, j = 1; j < count; i++, j++) {
   1120                 View prev = span.get(i);
   1121                 View next = span.get(j);
   1122                 assertTrue(config + " prev item should be above next item",
   1123                         orientationHelper.getDecoratedEnd(prev) <= orientationHelper
   1124                                 .getDecoratedStart(next)
   1125                 );
   1126 
   1127             }
   1128         }
   1129     }
   1130 
   1131     public void testScrollBy() throws Throwable {
   1132         for (Config config : mBaseVariations) {
   1133             scrollByTest(config);
   1134             removeRecyclerView();
   1135         }
   1136     }
   1137 
   1138     void waitFirstLayout() throws Throwable {
   1139         mLayoutManager.expectLayouts(1);
   1140         setRecyclerView(mRecyclerView);
   1141         mLayoutManager.waitForLayout(2);
   1142         getInstrumentation().waitForIdleSync();
   1143     }
   1144 
   1145     public void scrollByTest(Config config) throws Throwable {
   1146         setupByConfig(config);
   1147         waitFirstLayout();
   1148         // try invalid scroll. should not happen
   1149         final View first = mLayoutManager.getChildAt(0);
   1150         OrientationHelper primaryOrientation = OrientationHelper
   1151                 .createOrientationHelper(mLayoutManager, config.mOrientation);
   1152         int scrollDist;
   1153         if (config.mReverseLayout) {
   1154             scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
   1155         } else {
   1156             scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
   1157         }
   1158         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1159         scrollBy(scrollDist);
   1160         Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
   1161         assertRectSetsEqual(
   1162                 config + " if there are no more items, scroll should not happen (dt:" + scrollDist
   1163                         + ")",
   1164                 before, after
   1165         );
   1166 
   1167         scrollDist = -scrollDist * 3;
   1168         before = mLayoutManager.collectChildCoordinates();
   1169         scrollBy(scrollDist);
   1170         after = mLayoutManager.collectChildCoordinates();
   1171         int layoutStart = primaryOrientation.getStartAfterPadding();
   1172         int layoutEnd = primaryOrientation.getEndAfterPadding();
   1173         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1174             Rect afterRect = after.get(entry.getKey());
   1175             // offset rect
   1176             if (config.mOrientation == VERTICAL) {
   1177                 entry.getValue().offset(0, -scrollDist);
   1178             } else {
   1179                 entry.getValue().offset(-scrollDist, 0);
   1180             }
   1181             if (afterRect == null || afterRect.isEmpty()) {
   1182                 // assert item is out of bounds
   1183                 int start, end;
   1184                 if (config.mOrientation == VERTICAL) {
   1185                     start = entry.getValue().top;
   1186                     end = entry.getValue().bottom;
   1187                 } else {
   1188                     start = entry.getValue().left;
   1189                     end = entry.getValue().right;
   1190                 }
   1191                 assertTrue(
   1192                         config + " if item is missing after relayout, it should be out of bounds."
   1193                                 + "item start: " + start + ", end:" + end + " layout start:"
   1194                                 + layoutStart +
   1195                                 ", layout end:" + layoutEnd,
   1196                         start <= layoutStart && end <= layoutEnd ||
   1197                                 start >= layoutEnd && end >= layoutEnd
   1198                 );
   1199             } else {
   1200                 assertEquals(config + " Item should be laid out at the scroll offset coordinates",
   1201                         entry.getValue(),
   1202                         afterRect);
   1203             }
   1204         }
   1205         assertViewPositions(config);
   1206     }
   1207 
   1208     public void testAccessibilityPositions() throws Throwable {
   1209         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
   1210         waitFirstLayout();
   1211         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
   1212                 .getCompatAccessibilityDelegate();
   1213         final AccessibilityEvent event = AccessibilityEvent.obtain();
   1214         runTestOnUiThread(new Runnable() {
   1215             @Override
   1216             public void run() {
   1217                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
   1218             }
   1219         });
   1220         final AccessibilityRecordCompat record = AccessibilityEventCompat
   1221                 .asRecord(event);
   1222         final int start = mRecyclerView
   1223                 .getChildPosition(mLayoutManager.findFirstVisibleItemClosestToStart(false));
   1224         final int end = mRecyclerView
   1225                 .getChildPosition(mLayoutManager.findFirstVisibleItemClosestToEnd(false));
   1226         assertEquals("first item position should match",
   1227                 Math.min(start, end), record.getFromIndex());
   1228         assertEquals("last item position should match",
   1229                 Math.max(start, end), record.getToIndex());
   1230 
   1231     }
   1232 
   1233     public void testConsistentRelayout() throws Throwable {
   1234         for (Config config : mBaseVariations) {
   1235             for (boolean firstChildMultiSpan : new boolean[]{false, true}) {
   1236                 consistentRelayoutTest(config, firstChildMultiSpan);
   1237             }
   1238             removeRecyclerView();
   1239         }
   1240     }
   1241 
   1242     public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
   1243             throws Throwable {
   1244         setupByConfig(config);
   1245         if (firstChildMultiSpan) {
   1246             mAdapter.mFullSpanItems.add(0);
   1247         }
   1248         waitFirstLayout();
   1249         // record all child positions
   1250         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
   1251         requestLayoutOnUIThread(mRecyclerView);
   1252         Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
   1253         assertRectSetsEqual(
   1254                 config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
   1255                 after);
   1256         // scroll some to create inconsistency
   1257         View firstChild = mLayoutManager.getChildAt(0);
   1258         final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
   1259                 .getDecoratedStart(firstChild);
   1260         int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
   1261         if (config.mReverseLayout) {
   1262             distance *= -1;
   1263         }
   1264         scrollBy(distance);
   1265         waitForMainThread(2);
   1266         assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
   1267                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
   1268         before = mLayoutManager.collectChildCoordinates();
   1269         mLayoutManager.expectLayouts(1);
   1270         requestLayoutOnUIThread(mRecyclerView);
   1271         mLayoutManager.waitForLayout(2);
   1272         after = mLayoutManager.collectChildCoordinates();
   1273         assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
   1274     }
   1275 
   1276     /**
   1277      * enqueues an empty runnable to main thread so that we can be assured it did run
   1278      *
   1279      * @param count Number of times to run
   1280      */
   1281     private void waitForMainThread(int count) throws Throwable {
   1282         final AtomicInteger i = new AtomicInteger(count);
   1283         while (i.get() > 0) {
   1284             runTestOnUiThread(new Runnable() {
   1285                 @Override
   1286                 public void run() {
   1287                     i.decrementAndGet();
   1288                 }
   1289             });
   1290         }
   1291     }
   1292 
   1293     public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
   1294             Map<Item, Rect> after) {
   1295         Throwable throwable = null;
   1296         try {
   1297             assertRectSetsEqual("NOT " + message, before, after);
   1298         } catch (Throwable t) {
   1299             throwable = t;
   1300         }
   1301         assertNotNull(message + " two layout should be different", throwable);
   1302     }
   1303 
   1304     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
   1305         StringBuilder log = new StringBuilder();
   1306         if (DEBUG) {
   1307             log.append("checking rectangle equality.\n");
   1308             log.append("before:");
   1309             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1310                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
   1311                         .append(entry.getValue());
   1312             }
   1313             log.append("\nafter:");
   1314             for (Map.Entry<Item, Rect> entry : after.entrySet()) {
   1315                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
   1316                         .append(entry.getValue());
   1317             }
   1318             message += "\n\n" + log.toString();
   1319         }
   1320         assertEquals(message + ": item counts should be equal", before.size()
   1321                 , after.size());
   1322         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
   1323             Rect afterRect = after.get(entry.getKey());
   1324             assertNotNull(message + ": Same item should be visible after simple re-layout",
   1325                     afterRect);
   1326             assertEquals(message + ": Item should be laid out at the same coordinates",
   1327                     entry.getValue(),
   1328                     afterRect);
   1329         }
   1330     }
   1331 
   1332     // test layout params assignment
   1333 
   1334     static class OnLayoutListener {
   1335         void before(RecyclerView.Recycler recycler, RecyclerView.State state){}
   1336         void after(RecyclerView.Recycler recycler, RecyclerView.State state){}
   1337     }
   1338 
   1339     class WrappedLayoutManager extends StaggeredGridLayoutManager {
   1340 
   1341         CountDownLatch layoutLatch;
   1342         OnLayoutListener mOnLayoutListener;
   1343         // gradle does not yet let us customize manifest for tests which is necessary to test RTL.
   1344         // until bug is fixed, we'll fake it.
   1345         // public issue id: 57819
   1346         Boolean mFakeRTL;
   1347 
   1348         @Override
   1349         boolean isLayoutRTL() {
   1350             return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
   1351         }
   1352 
   1353         public void expectLayouts(int count) {
   1354             layoutLatch = new CountDownLatch(count);
   1355         }
   1356 
   1357         public void waitForLayout(long timeout) throws InterruptedException {
   1358             waitForLayout(timeout * (DEBUG ? 1000 : 1), TimeUnit.SECONDS);
   1359         }
   1360 
   1361         public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
   1362             layoutLatch.await(timeout, timeUnit);
   1363             assertEquals("all expected layouts should be executed at the expected time",
   1364                     0, layoutLatch.getCount());
   1365         }
   1366 
   1367         public void assertNoLayout(String msg, long timeout) throws Throwable {
   1368             layoutLatch.await(timeout, TimeUnit.SECONDS);
   1369             assertFalse(msg, layoutLatch.getCount() == 0);
   1370         }
   1371 
   1372         @Override
   1373         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
   1374             try {
   1375                 if (mOnLayoutListener != null) {
   1376                     mOnLayoutListener.before(recycler, state);
   1377                 }
   1378                 super.onLayoutChildren(recycler, state);
   1379                 if (mOnLayoutListener != null) {
   1380                     mOnLayoutListener.after(recycler, state);
   1381                 }
   1382             } catch (Throwable t) {
   1383                 postExceptionToInstrumentation(t);
   1384             }
   1385             layoutLatch.countDown();
   1386         }
   1387 
   1388         public WrappedLayoutManager(int spanCount, int orientation) {
   1389             super(spanCount, orientation);
   1390         }
   1391 
   1392         ArrayList<ArrayList<View>> collectChildrenBySpan() {
   1393             ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
   1394             for (int i = 0; i < getSpanCount(); i++) {
   1395                 viewsBySpan.add(new ArrayList<View>());
   1396             }
   1397             for (int i = 0; i < getChildCount(); i++) {
   1398                 View view = getChildAt(i);
   1399                 LayoutParams lp
   1400                         = (LayoutParams) view
   1401                         .getLayoutParams();
   1402                 viewsBySpan.get(lp.mSpan.mIndex).add(view);
   1403             }
   1404             return viewsBySpan;
   1405         }
   1406 
   1407         Rect getViewBounds(View view) {
   1408             if (getOrientation() == HORIZONTAL) {
   1409                 return new Rect(
   1410                         mPrimaryOrientation.getDecoratedStart(view),
   1411                         mSecondaryOrientation.getDecoratedStart(view),
   1412                         mPrimaryOrientation.getDecoratedEnd(view),
   1413                         mSecondaryOrientation.getDecoratedEnd(view));
   1414             } else {
   1415                 return new Rect(
   1416                         mSecondaryOrientation.getDecoratedStart(view),
   1417                         mPrimaryOrientation.getDecoratedStart(view),
   1418                         mSecondaryOrientation.getDecoratedEnd(view),
   1419                         mPrimaryOrientation.getDecoratedEnd(view));
   1420             }
   1421         }
   1422 
   1423         public String getBoundsLog() {
   1424             StringBuilder sb = new StringBuilder();
   1425             sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding())
   1426                     .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding());
   1427             sb.append("\nchildren bounds\n");
   1428             final int childCount = getChildCount();
   1429             for (int i = 0; i < childCount; i++) {
   1430                 View child = getChildAt(i);
   1431                 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
   1432                         .append("[").append("start:").append(
   1433                         mPrimaryOrientation.getDecoratedStart(child)).append(", end:")
   1434                         .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n");
   1435             }
   1436             return sb.toString();
   1437         }
   1438 
   1439         public VisibleChildren traverseAndFindVisibleChildren() {
   1440             int childCount = getChildCount();
   1441             final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount());
   1442             final int start = mPrimaryOrientation.getStartAfterPadding();
   1443             final int end = mPrimaryOrientation.getEndAfterPadding();
   1444             for (int i = 0; i < childCount; i++) {
   1445                 View child = getChildAt(i);
   1446                 final int childStart = mPrimaryOrientation.getDecoratedStart(child);
   1447                 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
   1448                 final boolean fullyVisible = childStart >= start && childEnd <= end;
   1449                 final boolean hidden = childEnd <= start || childStart >= end;
   1450                 if (hidden) {
   1451                     continue;
   1452                 }
   1453                 final int position = getPosition(child);
   1454                 final int span = getLp(child).getSpanIndex();
   1455                 if (fullyVisible) {
   1456                     if (position < visibleChildren.firstFullyVisiblePositions[span] ||
   1457                             visibleChildren.firstFullyVisiblePositions[span]
   1458                                     == RecyclerView.NO_POSITION) {
   1459                         visibleChildren.firstFullyVisiblePositions[span] = position;
   1460                     }
   1461 
   1462                     if (position > visibleChildren.lastFullyVisiblePositions[span]) {
   1463                         visibleChildren.lastFullyVisiblePositions[span] = position;
   1464                     }
   1465                 }
   1466 
   1467                 if (position < visibleChildren.firstVisiblePositions[span] ||
   1468                         visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) {
   1469                     visibleChildren.firstVisiblePositions[span] = position;
   1470                 }
   1471 
   1472                 if (position > visibleChildren.lastVisiblePositions[span]) {
   1473                     visibleChildren.lastVisiblePositions[span] = position;
   1474                 }
   1475 
   1476             }
   1477             return visibleChildren;
   1478         }
   1479 
   1480         Map<Item, Rect> collectChildCoordinates() throws Throwable {
   1481             final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
   1482             runTestOnUiThread(new Runnable() {
   1483                 @Override
   1484                 public void run() {
   1485                     final int childCount = getChildCount();
   1486                     for (int i = 0; i < childCount; i++) {
   1487                         View child = getChildAt(i);
   1488                         // do it if and only if child is visible
   1489                         if (child.getRight() < 0 || child.getBottom() < 0 ||
   1490                                 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
   1491                             // invisible children may be drawn in cases like scrolling so we should
   1492                             // ignore them
   1493                             continue;
   1494                         }
   1495                         LayoutParams lp = (LayoutParams) child
   1496                                 .getLayoutParams();
   1497                         TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
   1498                         items.put(vh.mBindedItem, getViewBounds(child));
   1499                     }
   1500                 }
   1501             });
   1502             return items;
   1503         }
   1504 
   1505 
   1506         public void setFakeRtl(Boolean fakeRtl) {
   1507             mFakeRTL = fakeRtl;
   1508             try {
   1509                 requestLayoutOnUIThread(mRecyclerView);
   1510             } catch (Throwable throwable) {
   1511                 postExceptionToInstrumentation(throwable);
   1512             }
   1513         }
   1514     }
   1515 
   1516     static class VisibleChildren {
   1517 
   1518         int[] firstVisiblePositions;
   1519 
   1520         int[] firstFullyVisiblePositions;
   1521 
   1522         int[] lastVisiblePositions;
   1523 
   1524         int[] lastFullyVisiblePositions;
   1525 
   1526         VisibleChildren(int spanCount) {
   1527             firstFullyVisiblePositions = new int[spanCount];
   1528             firstVisiblePositions = new int[spanCount];
   1529             lastVisiblePositions = new int[spanCount];
   1530             lastFullyVisiblePositions = new int[spanCount];
   1531             for (int i = 0; i < spanCount; i++) {
   1532                 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
   1533                 firstVisiblePositions[i] = RecyclerView.NO_POSITION;
   1534                 lastVisiblePositions[i] = RecyclerView.NO_POSITION;
   1535                 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
   1536             }
   1537         }
   1538 
   1539         @Override
   1540         public boolean equals(Object o) {
   1541             if (this == o) {
   1542                 return true;
   1543             }
   1544             if (o == null || getClass() != o.getClass()) {
   1545                 return false;
   1546             }
   1547 
   1548             VisibleChildren that = (VisibleChildren) o;
   1549 
   1550             if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) {
   1551                 return false;
   1552             }
   1553             if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) {
   1554                 return false;
   1555             }
   1556             if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) {
   1557                 return false;
   1558             }
   1559             if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) {
   1560                 return false;
   1561             }
   1562 
   1563             return true;
   1564         }
   1565 
   1566         @Override
   1567         public int hashCode() {
   1568             int result = firstVisiblePositions != null ? Arrays.hashCode(firstVisiblePositions) : 0;
   1569             result = 31 * result + (firstFullyVisiblePositions != null ? Arrays
   1570                     .hashCode(firstFullyVisiblePositions) : 0);
   1571             result = 31 * result + (lastVisiblePositions != null ? Arrays
   1572                     .hashCode(lastVisiblePositions)
   1573                     : 0);
   1574             result = 31 * result + (lastFullyVisiblePositions != null ? Arrays
   1575                     .hashCode(lastFullyVisiblePositions) : 0);
   1576             return result;
   1577         }
   1578 
   1579         @Override
   1580         public String toString() {
   1581             return "VisibleChildren{" +
   1582                     "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) +
   1583                     ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) +
   1584                     ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) +
   1585                     ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) +
   1586                     '}';
   1587         }
   1588     }
   1589 
   1590     class GridTestAdapter extends TestAdapter {
   1591 
   1592         int mOrientation;
   1593 
   1594         // original ids of items that should be full span
   1595         HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
   1596 
   1597         private boolean mViewsHaveEqualSize = false; // size in the scrollable direction
   1598 
   1599         private OnBindHandler mOnBindHandler;
   1600 
   1601         GridTestAdapter(int count, int orientation) {
   1602             super(count);
   1603             mOrientation = orientation;
   1604         }
   1605 
   1606         @Override
   1607         public void offsetOriginalIndices(int start, int offset) {
   1608             if (mFullSpanItems.size() > 0) {
   1609                 HashSet<Integer> old = mFullSpanItems;
   1610                 mFullSpanItems = new HashSet<Integer>();
   1611                 for (Integer i : old) {
   1612                     if (i < start) {
   1613                         mFullSpanItems.add(i);
   1614                     } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
   1615                         mFullSpanItems.add(i + offset);
   1616                     } else if (DEBUG) {
   1617                         Log.d(TAG, "removed full span item " + i);
   1618                     }
   1619                 }
   1620             }
   1621             super.offsetOriginalIndices(start, offset);
   1622         }
   1623 
   1624         @Override
   1625         public void onBindViewHolder(TestViewHolder holder,
   1626                 int position) {
   1627             super.onBindViewHolder(holder, position);
   1628             Item item = mItems.get(position);
   1629             final int minSize = mViewsHaveEqualSize ? 200 : 200 + 20 * (position % 10);
   1630             if (mOrientation == OrientationHelper.HORIZONTAL) {
   1631                 holder.itemView.setMinimumWidth(minSize);
   1632             } else {
   1633                 holder.itemView.setMinimumHeight(minSize);
   1634             }
   1635             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
   1636                     .getLayoutParams();
   1637             if (lp instanceof LayoutParams) {
   1638                 ((LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
   1639             } else {
   1640                 LayoutParams slp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   1641                         ViewGroup.LayoutParams.WRAP_CONTENT);
   1642                 holder.itemView.setLayoutParams(slp);
   1643                 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
   1644                 lp = slp;
   1645             }
   1646             lp.topMargin = 3;
   1647             lp.leftMargin = 5;
   1648             lp.rightMargin = 7;
   1649             lp.bottomMargin = 9;
   1650 
   1651             if (mOnBindHandler != null) {
   1652                 mOnBindHandler.onBoundItem(holder, position);
   1653             }
   1654         }
   1655     }
   1656 
   1657     static interface OnBindHandler {
   1658         void onBoundItem(TestViewHolder vh, int postion);
   1659     }
   1660 
   1661     static class Config implements Cloneable {
   1662 
   1663         private static final int DEFAULT_ITEM_COUNT = 300;
   1664 
   1665         int mOrientation = OrientationHelper.VERTICAL;
   1666 
   1667         boolean mReverseLayout = false;
   1668 
   1669         int mSpanCount = 3;
   1670 
   1671         int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
   1672 
   1673         int mItemCount = DEFAULT_ITEM_COUNT;
   1674 
   1675         Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
   1676             mOrientation = orientation;
   1677             mReverseLayout = reverseLayout;
   1678             mSpanCount = spanCount;
   1679             mGapStrategy = gapStrategy;
   1680         }
   1681 
   1682         public Config() {
   1683 
   1684         }
   1685 
   1686         Config orientation(int orientation) {
   1687             mOrientation = orientation;
   1688             return this;
   1689         }
   1690 
   1691         Config reverseLayout(boolean reverseLayout) {
   1692             mReverseLayout = reverseLayout;
   1693             return this;
   1694         }
   1695 
   1696         Config spanCount(int spanCount) {
   1697             mSpanCount = spanCount;
   1698             return this;
   1699         }
   1700 
   1701         Config gapStrategy(int gapStrategy) {
   1702             mGapStrategy = gapStrategy;
   1703             return this;
   1704         }
   1705 
   1706         public Config itemCount(int itemCount) {
   1707             mItemCount = itemCount;
   1708             return this;
   1709         }
   1710 
   1711         @Override
   1712         public String toString() {
   1713             return "[CONFIG:" +
   1714                     " span:" + mSpanCount + "," +
   1715                     " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
   1716                     " reverse:" + (mReverseLayout ? "T" : "F") +
   1717                     " itemCount:" + mItemCount +
   1718                     " gap strategy: " + gapStrategyName(mGapStrategy);
   1719         }
   1720 
   1721         private static String gapStrategyName(int gapStrategy) {
   1722             switch (gapStrategy) {
   1723                 case GAP_HANDLING_NONE:
   1724                     return "none";
   1725                 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
   1726                     return "move spans";
   1727             }
   1728             return "gap strategy: unknown";
   1729         }
   1730 
   1731         @Override
   1732         public Object clone() throws CloneNotSupportedException {
   1733             return super.clone();
   1734         }
   1735     }
   1736 
   1737     private interface PostLayoutRunnable {
   1738 
   1739         void run() throws Throwable;
   1740 
   1741         String describe();
   1742     }
   1743 
   1744 }
   1745