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
17package androidx.recyclerview.widget;
18
19import static androidx.recyclerview.widget.LayoutState.LAYOUT_END;
20import static androidx.recyclerview.widget.LayoutState.LAYOUT_START;
21import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
22import static androidx.recyclerview.widget.StaggeredGridLayoutManager
23        .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
24import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
25import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL;
26
27import static org.junit.Assert.assertEquals;
28import static org.junit.Assert.assertFalse;
29import static org.junit.Assert.assertNotNull;
30import static org.junit.Assert.assertTrue;
31
32import static java.util.concurrent.TimeUnit.SECONDS;
33
34import android.graphics.Color;
35import android.graphics.Rect;
36import android.graphics.drawable.ColorDrawable;
37import android.graphics.drawable.StateListDrawable;
38import android.util.Log;
39import android.util.StateSet;
40import android.view.View;
41import android.view.ViewGroup;
42
43import androidx.annotation.NonNull;
44import androidx.annotation.Nullable;
45
46import org.hamcrest.CoreMatchers;
47import org.hamcrest.MatcherAssert;
48
49import java.lang.reflect.Field;
50import java.util.ArrayList;
51import java.util.Arrays;
52import java.util.HashSet;
53import java.util.LinkedHashMap;
54import java.util.List;
55import java.util.Map;
56import java.util.concurrent.CountDownLatch;
57import java.util.concurrent.TimeUnit;
58import java.util.concurrent.atomic.AtomicInteger;
59
60abstract 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