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