Home | History | Annotate | Download | only in widget
      1 package android.support.v7.widget;
      2 
      3 import android.graphics.Rect;
      4 import android.support.annotation.Nullable;
      5 import android.util.Log;
      6 import android.view.View;
      7 import android.view.ViewGroup;
      8 
      9 import java.lang.reflect.Field;
     10 import java.util.ArrayList;
     11 import java.util.Arrays;
     12 import java.util.HashSet;
     13 import java.util.LinkedHashMap;
     14 import java.util.List;
     15 import java.util.Map;
     16 import java.util.concurrent.CountDownLatch;
     17 import java.util.concurrent.TimeUnit;
     18 import java.util.concurrent.atomic.AtomicInteger;
     19 
     20 import static android.support.v7.widget.LayoutState.LAYOUT_END;
     21 import static android.support.v7.widget.LayoutState.LAYOUT_START;
     22 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     23 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
     24 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
     25 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
     26 import static org.junit.Assert.assertEquals;
     27 import static org.junit.Assert.assertFalse;
     28 import static org.junit.Assert.assertNotNull;
     29 import static org.junit.Assert.assertTrue;
     30 
     31 import static java.util.concurrent.TimeUnit.SECONDS;
     32 
     33 import org.hamcrest.CoreMatchers;
     34 import org.hamcrest.MatcherAssert;
     35 
     36 public class BaseStaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
     37 
     38     protected static final boolean DEBUG = false;
     39     protected static final int AVG_ITEM_PER_VIEW = 3;
     40     protected static final String TAG = "StaggeredGridLayoutManagerTest";
     41     volatile WrappedLayoutManager mLayoutManager;
     42     GridTestAdapter mAdapter;
     43 
     44     protected static List<Config> createBaseVariations() {
     45         List<Config> variations = new ArrayList<>();
     46         for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
     47             for (boolean reverseLayout : new boolean[]{false, true}) {
     48                 for (int spanCount : new int[]{1, 3}) {
     49                     for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
     50                             GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
     51                         for (boolean wrap : new boolean[]{true, false}) {
     52                             variations.add(new Config(orientation, reverseLayout, spanCount,
     53                                     gapStrategy).wrap(wrap));
     54                         }
     55 
     56                     }
     57                 }
     58             }
     59         }
     60         return variations;
     61     }
     62 
     63     protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
     64             Object... variations)
     65             throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
     66         List<Config> newConfigs = new ArrayList<Config>();
     67         Field field = Config.class.getDeclaredField(fieldName);
     68         for (Config config : base) {
     69             for (Object variation : variations) {
     70                 Config newConfig = (Config) config.clone();
     71                 field.set(newConfig, variation);
     72                 newConfigs.add(newConfig);
     73             }
     74         }
     75         return newConfigs;
     76     }
     77 
     78     void setupByConfig(Config config) throws Throwable {
     79         setupByConfig(config, new GridTestAdapter(config.mItemCount, config.mOrientation));
     80     }
     81 
     82     void setupByConfig(Config config, GridTestAdapter adapter) throws Throwable {
     83         mAdapter = adapter;
     84         mRecyclerView = new RecyclerView(getActivity());
     85         mRecyclerView.setAdapter(mAdapter);
     86         mRecyclerView.setHasFixedSize(true);
     87         mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
     88                 config.mOrientation);
     89         mLayoutManager.setGapStrategy(config.mGapStrategy);
     90         mLayoutManager.setReverseLayout(config.mReverseLayout);
     91         mRecyclerView.setLayoutManager(mLayoutManager);
     92         mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
     93             @Override
     94             public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
     95                     RecyclerView.State state) {
     96                 try {
     97                     StaggeredGridLayoutManager.LayoutParams
     98                             lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
     99                     assertNotNull("view should have layout params assigned", lp);
    100                     assertNotNull("when item offsets are requested, view should have a valid span",
    101                             lp.mSpan);
    102                 } catch (Throwable t) {
    103                     postExceptionToInstrumentation(t);
    104                 }
    105             }
    106         });
    107     }
    108 
    109     StaggeredGridLayoutManager.LayoutParams getLp(View view) {
    110         return (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
    111     }
    112 
    113     void waitFirstLayout() throws Throwable {
    114         mLayoutManager.expectLayouts(1);
    115         setRecyclerView(mRecyclerView);
    116         mLayoutManager.waitForLayout(3);
    117         getInstrumentation().waitForIdleSync();
    118     }
    119 
    120     /**
    121      * enqueues an empty runnable to main thread so that we can be assured it did run
    122      *
    123      * @param count Number of times to run
    124      */
    125     protected void waitForMainThread(int count) throws Throwable {
    126         final AtomicInteger i = new AtomicInteger(count);
    127         while (i.get() > 0) {
    128             runTestOnUiThread(new Runnable() {
    129                 @Override
    130                 public void run() {
    131                     i.decrementAndGet();
    132                 }
    133             });
    134         }
    135     }
    136 
    137     public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
    138             Map<Item, Rect> after) {
    139         Throwable throwable = null;
    140         try {
    141             assertRectSetsEqual("NOT " + message, before, after);
    142         } catch (Throwable t) {
    143             throwable = t;
    144         }
    145         assertNotNull(message + " two layout should be different", throwable);
    146     }
    147 
    148     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
    149         assertRectSetsEqual(message, before, after, true);
    150     }
    151 
    152     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
    153             boolean strictItemEquality) {
    154         StringBuilder log = new StringBuilder();
    155         if (DEBUG) {
    156             log.append("checking rectangle equality.\n");
    157             log.append("total space:" + mLayoutManager.mPrimaryOrientation.getTotalSpace());
    158             log.append("before:");
    159             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
    160                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
    161                         .append(entry.getValue());
    162             }
    163             log.append("\nafter:");
    164             for (Map.Entry<Item, Rect> entry : after.entrySet()) {
    165                 log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
    166                         .append(entry.getValue());
    167             }
    168             message += "\n\n" + log.toString();
    169         }
    170         assertEquals(message + ": item counts should be equal", before.size()
    171                 , after.size());
    172         for (Map.Entry<Item, Rect> entry : before.entrySet()) {
    173             final Item beforeItem = entry.getKey();
    174             Rect afterRect = null;
    175             if (strictItemEquality) {
    176                 afterRect = after.get(beforeItem);
    177                 assertNotNull(message + ": Same item should be visible after simple re-layout",
    178                         afterRect);
    179             } else {
    180                 for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) {
    181                     final Item afterItem = afterEntry.getKey();
    182                     if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
    183                         afterRect = afterEntry.getValue();
    184                         break;
    185                     }
    186                 }
    187                 assertNotNull(message + ": Item with same adapter index should be visible " +
    188                                 "after simple re-layout",
    189                         afterRect);
    190             }
    191             assertEquals(message + ": Item should be laid out at the same coordinates",
    192                     entry.getValue(),
    193                     afterRect);
    194         }
    195     }
    196 
    197     protected void assertViewPositions(Config config) {
    198         ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
    199         OrientationHelper orientationHelper = OrientationHelper
    200                 .createOrientationHelper(mLayoutManager, config.mOrientation);
    201         for (ArrayList<View> span : viewsBySpan) {
    202             // validate all children's order. first child should have min start mPosition
    203             final int count = span.size();
    204             for (int i = 0, j = 1; j < count; i++, j++) {
    205                 View prev = span.get(i);
    206                 View next = span.get(j);
    207                 assertTrue(config + " prev item should be above next item",
    208                         orientationHelper.getDecoratedEnd(prev) <= orientationHelper
    209                                 .getDecoratedStart(next)
    210                 );
    211 
    212             }
    213         }
    214     }
    215 
    216     protected TargetTuple findInvisibleTarget(Config config) {
    217         int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
    218         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
    219             View child = mLayoutManager.getChildAt(i);
    220             int position = mRecyclerView.getChildLayoutPosition(child);
    221             if (position < minPosition) {
    222                 minPosition = position;
    223             }
    224             if (position > maxPosition) {
    225                 maxPosition = position;
    226             }
    227         }
    228         final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
    229         final int headTarget = minPosition / 2;
    230         final int target;
    231         // where will the child come from ?
    232         final int itemLayoutDirection;
    233         if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
    234             target = tailTarget;
    235             itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
    236         } else {
    237             target = headTarget;
    238             itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
    239         }
    240         if (DEBUG) {
    241             Log.d(TAG,
    242                     config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
    243         }
    244         return new TargetTuple(target, itemLayoutDirection);
    245     }
    246 
    247     protected void scrollToPositionWithOffset(final int position, final int offset)
    248             throws Throwable {
    249         runTestOnUiThread(new Runnable() {
    250             @Override
    251             public void run() {
    252                 mLayoutManager.scrollToPositionWithOffset(position, offset);
    253             }
    254         });
    255     }
    256 
    257     static class OnLayoutListener {
    258 
    259         void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
    260         }
    261 
    262         void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
    263         }
    264     }
    265 
    266     static class VisibleChildren {
    267 
    268         int[] firstVisiblePositions;
    269 
    270         int[] firstFullyVisiblePositions;
    271 
    272         int[] lastVisiblePositions;
    273 
    274         int[] lastFullyVisiblePositions;
    275 
    276         View findFirstPartialVisibleClosestToStart;
    277         View findFirstPartialVisibleClosestToEnd;
    278 
    279         VisibleChildren(int spanCount) {
    280             firstFullyVisiblePositions = new int[spanCount];
    281             firstVisiblePositions = new int[spanCount];
    282             lastVisiblePositions = new int[spanCount];
    283             lastFullyVisiblePositions = new int[spanCount];
    284             for (int i = 0; i < spanCount; i++) {
    285                 firstFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
    286                 firstVisiblePositions[i] = RecyclerView.NO_POSITION;
    287                 lastVisiblePositions[i] = RecyclerView.NO_POSITION;
    288                 lastFullyVisiblePositions[i] = RecyclerView.NO_POSITION;
    289             }
    290         }
    291 
    292         @Override
    293         public boolean equals(Object o) {
    294             if (this == o) {
    295                 return true;
    296             }
    297             if (o == null || getClass() != o.getClass()) {
    298                 return false;
    299             }
    300 
    301             VisibleChildren that = (VisibleChildren) o;
    302 
    303             if (!Arrays.equals(firstFullyVisiblePositions, that.firstFullyVisiblePositions)) {
    304                 return false;
    305             }
    306             if (findFirstPartialVisibleClosestToStart
    307                     != null ? !findFirstPartialVisibleClosestToStart
    308                     .equals(that.findFirstPartialVisibleClosestToStart)
    309                     : that.findFirstPartialVisibleClosestToStart != null) {
    310                 return false;
    311             }
    312             if (!Arrays.equals(firstVisiblePositions, that.firstVisiblePositions)) {
    313                 return false;
    314             }
    315             if (!Arrays.equals(lastFullyVisiblePositions, that.lastFullyVisiblePositions)) {
    316                 return false;
    317             }
    318             if (findFirstPartialVisibleClosestToEnd != null ? !findFirstPartialVisibleClosestToEnd
    319                     .equals(that.findFirstPartialVisibleClosestToEnd)
    320                     : that.findFirstPartialVisibleClosestToEnd
    321                             != null) {
    322                 return false;
    323             }
    324             if (!Arrays.equals(lastVisiblePositions, that.lastVisiblePositions)) {
    325                 return false;
    326             }
    327 
    328             return true;
    329         }
    330 
    331         @Override
    332         public int hashCode() {
    333             int result = Arrays.hashCode(firstVisiblePositions);
    334             result = 31 * result + Arrays.hashCode(firstFullyVisiblePositions);
    335             result = 31 * result + Arrays.hashCode(lastVisiblePositions);
    336             result = 31 * result + Arrays.hashCode(lastFullyVisiblePositions);
    337             result = 31 * result + (findFirstPartialVisibleClosestToStart != null
    338                     ? findFirstPartialVisibleClosestToStart
    339                     .hashCode() : 0);
    340             result = 31 * result + (findFirstPartialVisibleClosestToEnd != null
    341                     ? findFirstPartialVisibleClosestToEnd
    342                     .hashCode()
    343                     : 0);
    344             return result;
    345         }
    346 
    347         @Override
    348         public String toString() {
    349             return "VisibleChildren{" +
    350                     "firstVisiblePositions=" + Arrays.toString(firstVisiblePositions) +
    351                     ", firstFullyVisiblePositions=" + Arrays.toString(firstFullyVisiblePositions) +
    352                     ", lastVisiblePositions=" + Arrays.toString(lastVisiblePositions) +
    353                     ", lastFullyVisiblePositions=" + Arrays.toString(lastFullyVisiblePositions) +
    354                     ", findFirstPartialVisibleClosestToStart=" +
    355                     viewToString(findFirstPartialVisibleClosestToStart) +
    356                     ", findFirstPartialVisibleClosestToEnd=" +
    357                     viewToString(findFirstPartialVisibleClosestToEnd) +
    358                     '}';
    359         }
    360 
    361         private String viewToString(View view) {
    362             if (view == null) {
    363                 return null;
    364             }
    365             ViewGroup.LayoutParams lp = view.getLayoutParams();
    366             if (lp instanceof RecyclerView.LayoutParams == false) {
    367                 return System.identityHashCode(view) + "(?)";
    368             }
    369             RecyclerView.LayoutParams rvlp = (RecyclerView.LayoutParams) lp;
    370             return System.identityHashCode(view) + "(" + rvlp.getViewAdapterPosition() + ")";
    371         }
    372     }
    373 
    374     abstract static class OnBindCallback {
    375 
    376         abstract void onBoundItem(TestViewHolder vh, int position);
    377 
    378         boolean assignRandomSize() {
    379             return true;
    380         }
    381 
    382         void onCreatedViewHolder(TestViewHolder vh) {
    383         }
    384     }
    385 
    386     static class Config implements Cloneable {
    387 
    388         static final int DEFAULT_ITEM_COUNT = 300;
    389 
    390         int mOrientation = OrientationHelper.VERTICAL;
    391 
    392         boolean mReverseLayout = false;
    393 
    394         int mSpanCount = 3;
    395 
    396         int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
    397 
    398         int mItemCount = DEFAULT_ITEM_COUNT;
    399 
    400         boolean mWrap = false;
    401 
    402         Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
    403             mOrientation = orientation;
    404             mReverseLayout = reverseLayout;
    405             mSpanCount = spanCount;
    406             mGapStrategy = gapStrategy;
    407         }
    408 
    409         public Config() {
    410 
    411         }
    412 
    413         Config orientation(int orientation) {
    414             mOrientation = orientation;
    415             return this;
    416         }
    417 
    418         Config reverseLayout(boolean reverseLayout) {
    419             mReverseLayout = reverseLayout;
    420             return this;
    421         }
    422 
    423         Config spanCount(int spanCount) {
    424             mSpanCount = spanCount;
    425             return this;
    426         }
    427 
    428         Config gapStrategy(int gapStrategy) {
    429             mGapStrategy = gapStrategy;
    430             return this;
    431         }
    432 
    433         public Config itemCount(int itemCount) {
    434             mItemCount = itemCount;
    435             return this;
    436         }
    437 
    438         public Config wrap(boolean wrap) {
    439             mWrap = wrap;
    440             return this;
    441         }
    442 
    443         @Override
    444         public String toString() {
    445             return "[CONFIG:" +
    446                     " span:" + mSpanCount + "," +
    447                     " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
    448                     " reverse:" + (mReverseLayout ? "T" : "F") +
    449                     " itemCount:" + mItemCount +
    450                     " wrapContent:" + mWrap +
    451                     " gap strategy: " + gapStrategyName(mGapStrategy);
    452         }
    453 
    454         protected static String gapStrategyName(int gapStrategy) {
    455             switch (gapStrategy) {
    456                 case GAP_HANDLING_NONE:
    457                     return "none";
    458                 case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
    459                     return "move spans";
    460             }
    461             return "gap strategy: unknown";
    462         }
    463 
    464         @Override
    465         public Object clone() throws CloneNotSupportedException {
    466             return super.clone();
    467         }
    468     }
    469 
    470     class WrappedLayoutManager extends StaggeredGridLayoutManager {
    471 
    472         CountDownLatch layoutLatch;
    473         OnLayoutListener mOnLayoutListener;
    474         // gradle does not yet let us customize manifest for tests which is necessary to test RTL.
    475         // until bug is fixed, we'll fake it.
    476         // public issue id: 57819
    477         Boolean mFakeRTL;
    478 
    479         @Override
    480         boolean isLayoutRTL() {
    481             return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
    482         }
    483 
    484         public void expectLayouts(int count) {
    485             layoutLatch = new CountDownLatch(count);
    486         }
    487 
    488         public void waitForLayout(int seconds) throws Throwable {
    489             layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
    490             checkForMainThreadException();
    491             MatcherAssert.assertThat("all layouts should complete on time",
    492                     layoutLatch.getCount(), CoreMatchers.is(0L));
    493             // use a runnable to ensure RV layout is finished
    494             getInstrumentation().runOnMainSync(new Runnable() {
    495                 @Override
    496                 public void run() {
    497                 }
    498             });
    499         }
    500 
    501         public void assertNoLayout(String msg, long timeout) throws Throwable {
    502             layoutLatch.await(timeout, TimeUnit.SECONDS);
    503             assertFalse(msg, layoutLatch.getCount() == 0);
    504         }
    505 
    506         @Override
    507         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    508             String before;
    509             if (DEBUG) {
    510                 before = layoutToString("before");
    511             } else {
    512                 before = "enable DEBUG";
    513             }
    514             try {
    515                 if (mOnLayoutListener != null) {
    516                     mOnLayoutListener.before(recycler, state);
    517                 }
    518                 super.onLayoutChildren(recycler, state);
    519                 if (mOnLayoutListener != null) {
    520                     mOnLayoutListener.after(recycler, state);
    521                 }
    522                 validateChildren(before);
    523             } catch (Throwable t) {
    524                 postExceptionToInstrumentation(t);
    525             }
    526 
    527             layoutLatch.countDown();
    528         }
    529 
    530         @Override
    531         int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
    532             try {
    533                 int result = super.scrollBy(dt, recycler, state);
    534                 validateChildren();
    535                 return result;
    536             } catch (Throwable t) {
    537                 postExceptionToInstrumentation(t);
    538             }
    539 
    540             return 0;
    541         }
    542 
    543         public WrappedLayoutManager(int spanCount, int orientation) {
    544             super(spanCount, orientation);
    545         }
    546 
    547         ArrayList<ArrayList<View>> collectChildrenBySpan() {
    548             ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
    549             for (int i = 0; i < getSpanCount(); i++) {
    550                 viewsBySpan.add(new ArrayList<View>());
    551             }
    552             for (int i = 0; i < getChildCount(); i++) {
    553                 View view = getChildAt(i);
    554                 LayoutParams lp
    555                         = (LayoutParams) view
    556                         .getLayoutParams();
    557                 viewsBySpan.get(lp.mSpan.mIndex).add(view);
    558             }
    559             return viewsBySpan;
    560         }
    561 
    562         @Nullable
    563         @Override
    564         public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
    565                 RecyclerView.State state) {
    566             View result = null;
    567             try {
    568                 result = super.onFocusSearchFailed(focused, direction, recycler, state);
    569                 validateChildren();
    570             } catch (Throwable t) {
    571                 postExceptionToInstrumentation(t);
    572             }
    573             return result;
    574         }
    575 
    576         Rect getViewBounds(View view) {
    577             if (getOrientation() == HORIZONTAL) {
    578                 return new Rect(
    579                         mPrimaryOrientation.getDecoratedStart(view),
    580                         mSecondaryOrientation.getDecoratedStart(view),
    581                         mPrimaryOrientation.getDecoratedEnd(view),
    582                         mSecondaryOrientation.getDecoratedEnd(view));
    583             } else {
    584                 return new Rect(
    585                         mSecondaryOrientation.getDecoratedStart(view),
    586                         mPrimaryOrientation.getDecoratedStart(view),
    587                         mSecondaryOrientation.getDecoratedEnd(view),
    588                         mPrimaryOrientation.getDecoratedEnd(view));
    589             }
    590         }
    591 
    592         public String getBoundsLog() {
    593             StringBuilder sb = new StringBuilder();
    594             sb.append("view bounds:[start:").append(mPrimaryOrientation.getStartAfterPadding())
    595                     .append(",").append(" end").append(mPrimaryOrientation.getEndAfterPadding());
    596             sb.append("\nchildren bounds\n");
    597             final int childCount = getChildCount();
    598             for (int i = 0; i < childCount; i++) {
    599                 View child = getChildAt(i);
    600                 sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
    601                         .append("[").append("start:").append(
    602                         mPrimaryOrientation.getDecoratedStart(child)).append(", end:")
    603                         .append(mPrimaryOrientation.getDecoratedEnd(child)).append("]\n");
    604             }
    605             return sb.toString();
    606         }
    607 
    608         public VisibleChildren traverseAndFindVisibleChildren() {
    609             int childCount = getChildCount();
    610             final VisibleChildren visibleChildren = new VisibleChildren(getSpanCount());
    611             final int start = mPrimaryOrientation.getStartAfterPadding();
    612             final int end = mPrimaryOrientation.getEndAfterPadding();
    613             for (int i = 0; i < childCount; i++) {
    614                 View child = getChildAt(i);
    615                 final int childStart = mPrimaryOrientation.getDecoratedStart(child);
    616                 final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
    617                 final boolean fullyVisible = childStart >= start && childEnd <= end;
    618                 final boolean hidden = childEnd <= start || childStart >= end;
    619                 if (hidden) {
    620                     continue;
    621                 }
    622                 final int position = getPosition(child);
    623                 final int span = getLp(child).getSpanIndex();
    624                 if (fullyVisible) {
    625                     if (position < visibleChildren.firstFullyVisiblePositions[span] ||
    626                             visibleChildren.firstFullyVisiblePositions[span]
    627                                     == RecyclerView.NO_POSITION) {
    628                         visibleChildren.firstFullyVisiblePositions[span] = position;
    629                     }
    630 
    631                     if (position > visibleChildren.lastFullyVisiblePositions[span]) {
    632                         visibleChildren.lastFullyVisiblePositions[span] = position;
    633                     }
    634                 }
    635 
    636                 if (position < visibleChildren.firstVisiblePositions[span] ||
    637                         visibleChildren.firstVisiblePositions[span] == RecyclerView.NO_POSITION) {
    638                     visibleChildren.firstVisiblePositions[span] = position;
    639                 }
    640 
    641                 if (position > visibleChildren.lastVisiblePositions[span]) {
    642                     visibleChildren.lastVisiblePositions[span] = position;
    643                 }
    644                 if (visibleChildren.findFirstPartialVisibleClosestToStart == null) {
    645                     visibleChildren.findFirstPartialVisibleClosestToStart = child;
    646                 }
    647                 visibleChildren.findFirstPartialVisibleClosestToEnd = child;
    648             }
    649             return visibleChildren;
    650         }
    651 
    652         Map<Item, Rect> collectChildCoordinates() throws Throwable {
    653             final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
    654             runTestOnUiThread(new Runnable() {
    655                 @Override
    656                 public void run() {
    657                     final int childCount = getChildCount();
    658                     for (int i = 0; i < childCount; i++) {
    659                         View child = getChildAt(i);
    660                         // do it if and only if child is visible
    661                         if (child.getRight() < 0 || child.getBottom() < 0 ||
    662                                 child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
    663                             // invisible children may be drawn in cases like scrolling so we should
    664                             // ignore them
    665                             continue;
    666                         }
    667                         LayoutParams lp = (LayoutParams) child
    668                                 .getLayoutParams();
    669                         TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
    670                         items.put(vh.mBoundItem, getViewBounds(child));
    671                     }
    672                 }
    673             });
    674             return items;
    675         }
    676 
    677 
    678         public void setFakeRtl(Boolean fakeRtl) {
    679             mFakeRTL = fakeRtl;
    680             try {
    681                 requestLayoutOnUIThread(mRecyclerView);
    682             } catch (Throwable throwable) {
    683                 postExceptionToInstrumentation(throwable);
    684             }
    685         }
    686 
    687         String layoutToString(String hint) {
    688             StringBuilder sb = new StringBuilder();
    689             sb.append("LAYOUT POSITIONS AND INDICES ").append(hint).append("\n");
    690             for (int i = 0; i < getChildCount(); i++) {
    691                 final View view = getChildAt(i);
    692                 final LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
    693                 sb.append(String.format("index: %d pos: %d top: %d bottom: %d span: %d isFull:%s",
    694                         i, getPosition(view),
    695                         mPrimaryOrientation.getDecoratedStart(view),
    696                         mPrimaryOrientation.getDecoratedEnd(view),
    697                         layoutParams.getSpanIndex(), layoutParams.isFullSpan())).append("\n");
    698             }
    699             return sb.toString();
    700         }
    701 
    702         protected void validateChildren() {
    703             validateChildren(null);
    704         }
    705 
    706         private void validateChildren(String msg) {
    707             if (getChildCount() == 0 || mRecyclerView.mState.isPreLayout()) {
    708                 return;
    709             }
    710             final int dir = mShouldReverseLayout ? -1 : 1;
    711             int i = 0;
    712             int pos = -1;
    713             while (i < getChildCount()) {
    714                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
    715                 if (lp.isItemRemoved()) {
    716                     i++;
    717                     continue;
    718                 }
    719                 pos = getPosition(getChildAt(i));
    720                 break;
    721             }
    722             if (pos == -1) {
    723                 return;
    724             }
    725             while (++i < getChildCount()) {
    726                 LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
    727                 if (lp.isItemRemoved()) {
    728                     continue;
    729                 }
    730                 pos += dir;
    731                 if (getPosition(getChildAt(i)) != pos) {
    732                     throw new RuntimeException("INVALID POSITION FOR CHILD " + i + "\n" +
    733                             layoutToString("ERROR") + "\n msg:" + msg);
    734                 }
    735             }
    736         }
    737     }
    738 
    739     class GridTestAdapter extends TestAdapter {
    740 
    741         int mOrientation;
    742         int mRecyclerViewWidth;
    743         int mRecyclerViewHeight;
    744         Integer mSizeReference = null;
    745 
    746         // original ids of items that should be full span
    747         HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
    748 
    749         protected boolean mViewsHaveEqualSize = false; // size in the scrollable direction
    750 
    751         protected OnBindCallback mOnBindCallback;
    752 
    753         GridTestAdapter(int count, int orientation) {
    754             super(count);
    755             mOrientation = orientation;
    756         }
    757 
    758         @Override
    759         public TestViewHolder onCreateViewHolder(ViewGroup parent,
    760                 int viewType) {
    761             mRecyclerViewWidth = parent.getWidth();
    762             mRecyclerViewHeight = parent.getHeight();
    763             TestViewHolder vh = super.onCreateViewHolder(parent, viewType);
    764             if (mOnBindCallback != null) {
    765                 mOnBindCallback.onCreatedViewHolder(vh);
    766             }
    767             return vh;
    768         }
    769 
    770         @Override
    771         public void offsetOriginalIndices(int start, int offset) {
    772             if (mFullSpanItems.size() > 0) {
    773                 HashSet<Integer> old = mFullSpanItems;
    774                 mFullSpanItems = new HashSet<Integer>();
    775                 for (Integer i : old) {
    776                     if (i < start) {
    777                         mFullSpanItems.add(i);
    778                     } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
    779                         mFullSpanItems.add(i + offset);
    780                     } else if (DEBUG) {
    781                         Log.d(TAG, "removed full span item " + i);
    782                     }
    783                 }
    784             }
    785             super.offsetOriginalIndices(start, offset);
    786         }
    787 
    788         @Override
    789         protected void moveInUIThread(int from, int to) {
    790             boolean setAsFullSpanAgain = mFullSpanItems.contains(from);
    791             super.moveInUIThread(from, to);
    792             if (setAsFullSpanAgain) {
    793                 mFullSpanItems.add(to);
    794             }
    795         }
    796 
    797         @Override
    798         public void onBindViewHolder(TestViewHolder holder,
    799                 int position) {
    800             if (mSizeReference == null) {
    801                 mSizeReference = mOrientation == OrientationHelper.HORIZONTAL ? mRecyclerViewWidth
    802                         / AVG_ITEM_PER_VIEW : mRecyclerViewHeight / AVG_ITEM_PER_VIEW;
    803             }
    804             super.onBindViewHolder(holder, position);
    805             Item item = mItems.get(position);
    806             RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
    807                     .getLayoutParams();
    808             if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
    809                 ((StaggeredGridLayoutManager.LayoutParams) lp)
    810                         .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
    811             } else {
    812                 StaggeredGridLayoutManager.LayoutParams slp
    813                         = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager
    814                         .generateDefaultLayoutParams();
    815                 holder.itemView.setLayoutParams(slp);
    816                 slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
    817                 lp = slp;
    818             }
    819 
    820             if (mOnBindCallback == null || mOnBindCallback.assignRandomSize()) {
    821                 final int minSize = mViewsHaveEqualSize ? mSizeReference :
    822                         mSizeReference + 20 * (item.mId % 10);
    823                 if (mOrientation == OrientationHelper.HORIZONTAL) {
    824                     holder.itemView.setMinimumWidth(minSize);
    825                 } else {
    826                     holder.itemView.setMinimumHeight(minSize);
    827                 }
    828                 lp.topMargin = 3;
    829                 lp.leftMargin = 5;
    830                 lp.rightMargin = 7;
    831                 lp.bottomMargin = 9;
    832             }
    833 
    834             if (mOnBindCallback != null) {
    835                 mOnBindCallback.onBoundItem(holder, position);
    836             }
    837         }
    838     }
    839 }
    840