LinearLayoutManagerTest.java revision e71a1df9b3c0e1bd3c21a1b3dd20a41790d4a950
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.graphics.Rect;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.support.v4.view.AccessibilityDelegateCompat;
24import android.support.v4.view.accessibility.AccessibilityEventCompat;
25import android.support.v4.view.accessibility.AccessibilityRecordCompat;
26import android.util.Log;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.accessibility.AccessibilityEvent;
30import android.widget.FrameLayout;
31
32import static android.support.v7.widget.LayoutState.LAYOUT_END;
33import static android.support.v7.widget.LayoutState.LAYOUT_START;
34import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
35import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
36import java.lang.reflect.Field;
37import java.util.ArrayList;
38import java.util.LinkedHashMap;
39import java.util.List;
40import java.util.Map;
41import java.util.UUID;
42import java.util.concurrent.CountDownLatch;
43import java.util.concurrent.TimeUnit;
44import java.util.concurrent.atomic.AtomicInteger;
45
46/**
47 * Includes tests for {@link LinearLayoutManager}.
48 * <p>
49 * Since most UI tests are not practical, these tests are focused on internal data representation
50 * and stability of LinearLayoutManager in response to different events (state change, scrolling
51 * etc) where it is very hard to do manual testing.
52 */
53public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
54
55    private static final boolean DEBUG = false;
56
57    private static final String TAG = "LinearLayoutManagerTest";
58
59    WrappedLinearLayoutManager mLayoutManager;
60
61    TestAdapter mTestAdapter;
62
63    final List<Config> mBaseVariations = new ArrayList<Config>();
64
65    @Override
66    protected void setUp() throws Exception {
67        super.setUp();
68        for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
69            for (boolean reverseLayout : new boolean[]{false, true}) {
70                for (boolean stackFromBottom : new boolean[]{false, true}) {
71                    mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom));
72                }
73            }
74        }
75    }
76
77    protected List<Config> addConfigVariation(List<Config> base, String fieldName,
78            Object... variations)
79            throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
80        List<Config> newConfigs = new ArrayList<Config>();
81        Field field = Config.class.getDeclaredField(fieldName);
82        for (Config config : base) {
83            for (Object variation : variations) {
84                Config newConfig = (Config) config.clone();
85                field.set(newConfig, variation);
86                newConfigs.add(newConfig);
87            }
88        }
89        return newConfigs;
90    }
91
92    void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
93        mRecyclerView = inflateWrappedRV();
94
95        mRecyclerView.setHasFixedSize(true);
96        mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount)
97                : config.mTestAdapter;
98        mRecyclerView.setAdapter(mTestAdapter);
99        mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
100                config.mReverseLayout);
101        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
102        mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
103        mRecyclerView.setLayoutManager(mLayoutManager);
104        if (waitForFirstLayout) {
105            waitForFirstLayout();
106        }
107    }
108
109    public void testRemoveAnchorItem() throws Throwable {
110        removeAnchorItemTest(
111                new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(
112                        false), 100, 0);
113    }
114
115    public void testRemoveAnchorItemReverse() throws Throwable {
116        removeAnchorItemTest(
117                new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100,
118                0);
119    }
120
121    public void testRemoveAnchorItemStackFromEnd() throws Throwable {
122        removeAnchorItemTest(
123                new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100,
124                99);
125    }
126
127    public void testRemoveAnchorItemStackFromEndAndReverse() throws Throwable {
128        removeAnchorItemTest(
129                new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100,
130                99);
131    }
132
133    public void testRemoveAnchorItemHorizontal() throws Throwable {
134        removeAnchorItemTest(
135                new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(
136                        false), 100, 0);
137    }
138
139    public void testRemoveAnchorItemReverseHorizontal() throws Throwable {
140        removeAnchorItemTest(
141                new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true),
142                100, 0);
143    }
144
145    public void testRemoveAnchorItemStackFromEndHorizontal() throws Throwable {
146        removeAnchorItemTest(
147                new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false),
148                100, 99);
149    }
150
151    public void testRemoveAnchorItemStackFromEndAndReverseHorizontal() throws Throwable {
152        removeAnchorItemTest(
153                new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100,
154                99);
155    }
156
157    /**
158     * This tests a regression where predictive animations were not working as expected when the
159     * first item is removed and there aren't any more items to add from that direction.
160     * First item refers to the default anchor item.
161     */
162    public void removeAnchorItemTest(final Config config, int adapterSize,
163            final int removePos) throws Throwable {
164        config.adapter(new TestAdapter(adapterSize) {
165            @Override
166            public void onBindViewHolder(TestViewHolder holder,
167                    int position) {
168                super.onBindViewHolder(holder, position);
169                ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
170                if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
171                    lp = new ViewGroup.MarginLayoutParams(0, 0);
172                    holder.itemView.setLayoutParams(lp);
173                }
174                ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
175                final int maxSize;
176                if (config.mOrientation == HORIZONTAL) {
177                    maxSize = mRecyclerView.getWidth();
178                    mlp.height = ViewGroup.MarginLayoutParams.FILL_PARENT;
179                } else {
180                    maxSize = mRecyclerView.getHeight();
181                    mlp.width = ViewGroup.MarginLayoutParams.FILL_PARENT;
182                }
183
184                final int desiredSize;
185                if (position == removePos) {
186                    // make it large
187                    desiredSize = maxSize / 4;
188                } else {
189                    // make it small
190                    desiredSize = maxSize / 8;
191                }
192                if (config.mOrientation == HORIZONTAL) {
193                    mlp.width = desiredSize;
194                } else {
195                    mlp.height = desiredSize;
196                }
197            }
198        });
199        setupByConfig(config, true);
200        final int childCount = mLayoutManager.getChildCount();
201        RecyclerView.ViewHolder toBeRemoved = null;
202        List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
203        for (int i = 0; i < childCount; i++) {
204            View child = mLayoutManager.getChildAt(i);
205            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
206            if (holder.getAdapterPosition() == removePos) {
207                toBeRemoved = holder;
208            } else {
209                toBeMoved.add(holder);
210            }
211        }
212        assertNotNull("test sanity", toBeRemoved);
213        assertEquals("test sanity", childCount - 1, toBeMoved.size());
214        LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
215        mRecyclerView.setItemAnimator(loggingItemAnimator);
216        loggingItemAnimator.reset();
217        loggingItemAnimator.expectRunPendingAnimationsCall(1);
218        mLayoutManager.expectLayouts(2);
219        mTestAdapter.deleteAndNotify(removePos, 1);
220        mLayoutManager.waitForLayout(1);
221        loggingItemAnimator.waitForPendingAnimationsCall(2);
222        assertTrue("removed child should receive remove animation",
223                loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
224        for (RecyclerView.ViewHolder vh : toBeMoved) {
225            assertTrue("view holder should be in moved list",
226                    loggingItemAnimator.mMoveVHs.contains(vh));
227        }
228        List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
229        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
230            View child = mLayoutManager.getChildAt(i);
231            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
232            if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
233                newHolders.add(holder);
234            }
235        }
236        assertTrue("some new children should show up for the new space", newHolders.size() > 0);
237        assertEquals("no items should receive animate add since they are not new", 0,
238                loggingItemAnimator.mAddVHs.size());
239        for (RecyclerView.ViewHolder holder : newHolders) {
240            assertTrue("new holder should receive a move animation",
241                    loggingItemAnimator.mMoveVHs.contains(holder));
242        }
243        assertTrue("control against adding too many children due to bad layout state preparation."
244                        + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
245                mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/);
246    }
247
248    public void testKeepFocusOnRelayout() throws Throwable {
249        setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true);
250        int center = (mLayoutManager.findLastVisibleItemPosition()
251                - mLayoutManager.findFirstVisibleItemPosition()) / 2;
252        final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center);
253        final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView);
254        runTestOnUiThread(new Runnable() {
255            @Override
256            public void run() {
257                vh.itemView.requestFocus();
258            }
259        });
260        assertTrue("view should have the focus", vh.itemView.hasFocus());
261        // add a bunch of items right before that view, make sure it keeps its position
262        mLayoutManager.expectLayouts(2);
263        final int childCountToAdd = mRecyclerView.getChildCount() * 2;
264        mTestAdapter.addAndNotify(center, childCountToAdd);
265        center += childCountToAdd; // offset item
266        mLayoutManager.waitForLayout(2);
267        mLayoutManager.waitForAnimationsToEnd(20);
268        final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center);
269        assertNotNull("focused child should stay in layout", postVH);
270        assertSame("same view holder should be kept for unchanged child", vh, postVH);
271        assertEquals("focused child's screen position should stay unchanged", top,
272                mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView));
273    }
274
275    public void testKeepFullFocusOnResize() throws Throwable {
276        keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true);
277    }
278
279    public void testKeepPartialFocusOnResize() throws Throwable {
280        keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false);
281    }
282
283    public void testKeepReverseFullFocusOnResize() throws Throwable {
284        keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true);
285    }
286
287    public void testKeepReversePartialFocusOnResize() throws Throwable {
288        keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false);
289    }
290
291    public void testKeepStackFromEndFullFocusOnResize() throws Throwable {
292        keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true);
293    }
294
295    public void testKeepStackFromEndPartialFocusOnResize() throws Throwable {
296        keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false);
297    }
298
299    public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable {
300        setupByConfig(config, true);
301        final int targetPosition;
302        if (config.mStackFromEnd) {
303            targetPosition = mLayoutManager.findFirstVisibleItemPosition();
304        } else {
305            targetPosition = mLayoutManager.findLastVisibleItemPosition();
306        }
307        final OrientationHelper helper = mLayoutManager.mOrientationHelper;
308        final RecyclerView.ViewHolder vh = mRecyclerView
309                .findViewHolderForLayoutPosition(targetPosition);
310
311        // scroll enough to offset the child
312        int startMargin = helper.getDecoratedStart(vh.itemView) -
313                helper.getStartAfterPadding();
314        int endMargin = helper.getEndAfterPadding() -
315                helper.getDecoratedEnd(vh.itemView);
316        Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin);
317        requestFocus(vh.itemView);
318        runTestOnUiThread(new Runnable() {
319            @Override
320            public void run() {
321                assertTrue("view should gain the focus", vh.itemView.hasFocus());
322            }
323        });
324        do {
325            Thread.sleep(100);
326        } while (mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE);
327        // scroll enough to offset the child
328        startMargin = helper.getDecoratedStart(vh.itemView) -
329                helper.getStartAfterPadding();
330        endMargin = helper.getEndAfterPadding() -
331                helper.getDecoratedEnd(vh.itemView);
332
333        Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin);
334        assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0);
335
336        int expectedOffset = 0;
337        boolean offsetAtStart = false;
338        if (!fullyVisible) {
339            // move it a bit such that it is no more fully visible
340            final int childSize = helper
341                    .getDecoratedMeasurement(vh.itemView);
342            expectedOffset = childSize / 3;
343            if (startMargin < endMargin) {
344                scrollBy(expectedOffset);
345                offsetAtStart = true;
346            } else {
347                scrollBy(-expectedOffset);
348                offsetAtStart = false;
349            }
350            startMargin = helper.getDecoratedStart(vh.itemView) -
351                    helper.getStartAfterPadding();
352            endMargin = helper.getEndAfterPadding() -
353                    helper.getDecoratedEnd(vh.itemView);
354            assertTrue("test sanity, view should not be fully visible", startMargin < 0
355                    || endMargin < 0);
356        }
357
358        mLayoutManager.expectLayouts(1);
359        runTestOnUiThread(new Runnable() {
360            @Override
361            public void run() {
362                final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams();
363                if (config.mOrientation == HORIZONTAL) {
364                    layoutParams.width = mRecyclerView.getWidth() / 2;
365                } else {
366                    layoutParams.height = mRecyclerView.getHeight() / 2;
367                }
368                mRecyclerView.setLayoutParams(layoutParams);
369            }
370        });
371        Thread.sleep(100);
372        // add a bunch of items right before that view, make sure it keeps its position
373        mLayoutManager.waitForLayout(2);
374        mLayoutManager.waitForAnimationsToEnd(20);
375        assertTrue("view should preserve the focus", vh.itemView.hasFocus());
376        final RecyclerView.ViewHolder postVH = mRecyclerView
377                .findViewHolderForLayoutPosition(targetPosition);
378        assertNotNull("focused child should stay in layout", postVH);
379        assertSame("same view holder should be kept for unchanged child", vh, postVH);
380        View focused = postVH.itemView;
381
382        startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding();
383        endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused);
384
385        assertTrue("focused child should be somewhat visible",
386                helper.getDecoratedStart(focused) < helper.getEndAfterPadding()
387                        && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding());
388        if (fullyVisible) {
389            assertTrue("focused child end should stay fully visible",
390                    endMargin >= 0);
391            assertTrue("focused child start should stay fully visible",
392                    startMargin >= 0);
393        } else {
394            if (offsetAtStart) {
395                assertTrue("start should preserve its offset", startMargin < 0);
396                assertTrue("end should be visible", endMargin >= 0);
397            } else {
398                assertTrue("end should preserve its offset", endMargin < 0);
399                assertTrue("start should be visible", startMargin >= 0);
400            }
401        }
402    }
403
404    public void testResize() throws Throwable {
405        for(Config config : addConfigVariation(mBaseVariations, "mItemCount", 5
406                , Config.DEFAULT_ITEM_COUNT)) {
407            stackFromEndTest(config);
408            removeRecyclerView();
409        }
410    }
411
412    public void testScrollToPositionWithOffset() throws Throwable {
413        for (Config config : mBaseVariations) {
414            scrollToPositionWithOffsetTest(config.itemCount(300));
415            removeRecyclerView();
416        }
417    }
418
419    public void scrollToPositionWithOffsetTest(Config config) throws Throwable {
420        setupByConfig(config, true);
421        OrientationHelper orientationHelper = OrientationHelper
422                .createOrientationHelper(mLayoutManager, config.mOrientation);
423        Rect layoutBounds = getDecoratedRecyclerViewBounds();
424        // try scrolling towards head, should not affect anything
425        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
426        if (config.mStackFromEnd) {
427            scrollToPositionWithOffset(mTestAdapter.getItemCount() - 1,
428                    mLayoutManager.mOrientationHelper.getEnd() - 500);
429        } else {
430            scrollToPositionWithOffset(0, 20);
431        }
432        assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
433                before, mLayoutManager.collectChildCoordinates());
434        // try offsetting some visible children
435        int testCount = 10;
436        while (testCount-- > 0) {
437            // get middle child
438            final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
439            final int position = mRecyclerView.getChildLayoutPosition(child);
440            final int startOffset = config.mReverseLayout ?
441                    orientationHelper.getEndAfterPadding() - orientationHelper
442                            .getDecoratedEnd(child)
443                    : orientationHelper.getDecoratedStart(child) - orientationHelper
444                            .getStartAfterPadding();
445            final int scrollOffset = config.mStackFromEnd ? startOffset + startOffset / 2
446                    : startOffset / 2;
447            mLayoutManager.expectLayouts(1);
448            scrollToPositionWithOffset(position, scrollOffset);
449            mLayoutManager.waitForLayout(2);
450            final int finalOffset = config.mReverseLayout ?
451                    orientationHelper.getEndAfterPadding() - orientationHelper
452                            .getDecoratedEnd(child)
453                    : orientationHelper.getDecoratedStart(child) - orientationHelper
454                            .getStartAfterPadding();
455            assertEquals(config + " scroll with offset on a visible child should work fine " +
456                    " offset:" + finalOffset + " , existing offset:" + startOffset + ", "
457                            + "child " + position,
458                    scrollOffset, finalOffset);
459        }
460
461        // try scrolling to invisible children
462        testCount = 10;
463        // we test above and below, one by one
464        int offsetMultiplier = -1;
465        while (testCount-- > 0) {
466            final TargetTuple target = findInvisibleTarget(config);
467            final String logPrefix = config + " " + target;
468            mLayoutManager.expectLayouts(1);
469            final int offset = offsetMultiplier
470                    * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
471            scrollToPositionWithOffset(target.mPosition, offset);
472            mLayoutManager.waitForLayout(2);
473            final View child = mLayoutManager.findViewByPosition(target.mPosition);
474            assertNotNull(logPrefix + " scrolling to a mPosition with offset " + offset
475                    + " should layout it", child);
476            final Rect bounds = mLayoutManager.getViewBounds(child);
477            if (DEBUG) {
478                Log.d(TAG, logPrefix + " post scroll to invisible mPosition " + bounds + " in "
479                        + layoutBounds + " with offset " + offset);
480            }
481
482            if (config.mReverseLayout) {
483                assertEquals(logPrefix + " when scrolling with offset to an invisible in reverse "
484                                + "layout, its end should align with recycler view's end - offset",
485                        orientationHelper.getEndAfterPadding() - offset,
486                        orientationHelper.getDecoratedEnd(child)
487                );
488            } else {
489                assertEquals(logPrefix + " when scrolling with offset to an invisible child in normal"
490                                + " layout its start should align with recycler view's start + "
491                                + "offset",
492                        orientationHelper.getStartAfterPadding() + offset,
493                        orientationHelper.getDecoratedStart(child)
494                );
495            }
496            offsetMultiplier *= -1;
497        }
498    }
499
500    private TargetTuple findInvisibleTarget(Config config) {
501        int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
502        for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
503            View child = mLayoutManager.getChildAt(i);
504            int position = mRecyclerView.getChildLayoutPosition(child);
505            if (position < minPosition) {
506                minPosition = position;
507            }
508            if (position > maxPosition) {
509                maxPosition = position;
510            }
511        }
512        final int tailTarget = maxPosition +
513                (mRecyclerView.getAdapter().getItemCount() - maxPosition) / 2;
514        final int headTarget = minPosition / 2;
515        final int target;
516        // where will the child come from ?
517        final int itemLayoutDirection;
518        if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
519            target = tailTarget;
520            itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
521        } else {
522            target = headTarget;
523            itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
524        }
525        if (DEBUG) {
526            Log.d(TAG,
527                    config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
528        }
529        return new TargetTuple(target, itemLayoutDirection);
530    }
531
532    public void stackFromEndTest(final Config config) throws Throwable {
533        final FrameLayout container = getRecyclerViewContainer();
534        runTestOnUiThread(new Runnable() {
535            @Override
536            public void run() {
537                container.setPadding(0, 0, 0, 0);
538            }
539        });
540
541        setupByConfig(config, true);
542        int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
543        int firstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
544        int lastCompletelyVisibleItemPosition = mLayoutManager.findLastCompletelyVisibleItemPosition();
545        int firstCompletelyVisibleItemPosition = mLayoutManager.findFirstCompletelyVisibleItemPosition();
546        mLayoutManager.expectLayouts(1);
547        // resize the recycler view to half
548        runTestOnUiThread(new Runnable() {
549            @Override
550            public void run() {
551                if (config.mOrientation == HORIZONTAL) {
552                    container.setPadding(0, 0, container.getWidth() / 2, 0);
553                } else {
554                    container.setPadding(0, 0, 0, container.getWidth() / 2);
555                }
556            }
557        });
558        mLayoutManager.waitForLayout(1);
559        if (config.mStackFromEnd) {
560            assertEquals("[" + config + "]: last visible position should not change.",
561                    lastVisibleItemPosition, mLayoutManager.findLastVisibleItemPosition());
562            assertEquals("[" + config + "]: last completely visible position should not change",
563                    lastCompletelyVisibleItemPosition,
564                    mLayoutManager.findLastCompletelyVisibleItemPosition());
565        } else {
566            assertEquals("[" + config + "]: first visible position should not change.",
567                    firstVisibleItemPosition, mLayoutManager.findFirstVisibleItemPosition());
568            assertEquals("[" + config + "]: last completely visible position should not change",
569                    firstCompletelyVisibleItemPosition,
570                    mLayoutManager.findFirstCompletelyVisibleItemPosition());
571        }
572    }
573
574    public void testScrollToPositionWithPredictive() throws Throwable {
575        scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
576        removeRecyclerView();
577        scrollToPositionWithPredictive(3, 20);
578        removeRecyclerView();
579        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
580                LinearLayoutManager.INVALID_OFFSET);
581        removeRecyclerView();
582        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
583    }
584
585    public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
586            throws Throwable {
587        setupByConfig(new Config(VERTICAL, false, false), true);
588
589        mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
590            @Override
591            void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
592                if (state.isPreLayout()) {
593                    assertEquals("pending scroll position should still be pending",
594                            scrollPosition, mLayoutManager.mPendingScrollPosition);
595                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
596                        assertEquals("pending scroll position offset should still be pending",
597                                scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
598                    }
599                } else {
600                    RecyclerView.ViewHolder vh =
601                            mRecyclerView.findViewHolderForLayoutPosition(scrollPosition);
602                    assertNotNull("scroll to position should work", vh);
603                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
604                        assertEquals("scroll offset should be applied properly",
605                                mLayoutManager.getPaddingTop() + scrollOffset +
606                                        ((RecyclerView.LayoutParams) vh.itemView
607                                                .getLayoutParams()).topMargin,
608                                mLayoutManager.getDecoratedTop(vh.itemView));
609                    }
610                }
611            }
612        };
613        mLayoutManager.expectLayouts(2);
614        runTestOnUiThread(new Runnable() {
615            @Override
616            public void run() {
617                try {
618                    mTestAdapter.addAndNotify(0, 1);
619                    if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
620                        mLayoutManager.scrollToPosition(scrollPosition);
621                    } else {
622                        mLayoutManager.scrollToPositionWithOffset(scrollPosition,
623                                scrollOffset);
624                    }
625
626                } catch (Throwable throwable) {
627                    throwable.printStackTrace();
628                }
629
630            }
631        });
632        mLayoutManager.waitForLayout(2);
633        checkForMainThreadException();
634    }
635
636    private void waitForFirstLayout() throws Throwable {
637        mLayoutManager.expectLayouts(1);
638        setRecyclerView(mRecyclerView);
639        mLayoutManager.waitForLayout(2);
640    }
641
642    public void testRecycleDuringAnimations() throws Throwable {
643        final AtomicInteger childCount = new AtomicInteger(0);
644        final TestAdapter adapter = new TestAdapter(300) {
645            @Override
646            public TestViewHolder onCreateViewHolder(ViewGroup parent,
647                    int viewType) {
648                final int cnt = childCount.incrementAndGet();
649                final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
650                if (DEBUG) {
651                    Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder);
652                }
653                return testViewHolder;
654            }
655        };
656        setupByConfig(new Config(VERTICAL, false, false).itemCount(300)
657                .adapter(adapter), true);
658
659        final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
660            @Override
661            public void putRecycledView(RecyclerView.ViewHolder scrap) {
662                super.putRecycledView(scrap);
663                int cnt = childCount.decrementAndGet();
664                if (DEBUG) {
665                    Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap);
666                }
667            }
668
669            @Override
670            public RecyclerView.ViewHolder getRecycledView(int viewType) {
671                final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
672                if (recycledView != null) {
673                    final int cnt = childCount.incrementAndGet();
674                    if (DEBUG) {
675                        Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView);
676                    }
677                }
678                return recycledView;
679            }
680        };
681        pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500);
682        mRecyclerView.setRecycledViewPool(pool);
683
684
685        // now keep adding children to trigger more children being created etc.
686        for (int i = 0; i < 100; i ++) {
687            adapter.addAndNotify(15, 1);
688            Thread.sleep(15);
689        }
690        getInstrumentation().waitForIdleSync();
691        waitForAnimations(2);
692        assertEquals("Children count should add up", childCount.get(),
693                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
694
695        // now trigger lots of add again, followed by a scroll to position
696        for (int i = 0; i < 100; i ++) {
697            adapter.addAndNotify(5 + (i % 3) * 3, 1);
698            Thread.sleep(25);
699        }
700        smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
701        waitForAnimations(2);
702        getInstrumentation().waitForIdleSync();
703        assertEquals("Children count should add up", childCount.get(),
704                mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
705    }
706
707
708    public void testGetFirstLastChildrenTest() throws Throwable {
709        for (Config config : mBaseVariations) {
710            getFirstLastChildrenTest(config);
711        }
712    }
713
714    public void testDontRecycleChildrenOnDetach() throws Throwable {
715        setupByConfig(new Config().recycleChildrenOnDetach(false), true);
716        runTestOnUiThread(new Runnable() {
717            @Override
718            public void run() {
719                int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
720                mRecyclerView.setLayoutManager(new TestLayoutManager());
721                assertEquals("No views are recycled", recyclerSize,
722                        mRecyclerView.mRecycler.getRecycledViewPool().size());
723            }
724        });
725    }
726
727    public void testRecycleChildrenOnDetach() throws Throwable {
728        setupByConfig(new Config().recycleChildrenOnDetach(true), true);
729        final int childCount = mLayoutManager.getChildCount();
730        runTestOnUiThread(new Runnable() {
731            @Override
732            public void run() {
733                int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
734                mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
735                        mTestAdapter.getItemViewType(0), recyclerSize + childCount);
736                mRecyclerView.setLayoutManager(new TestLayoutManager());
737                assertEquals("All children should be recycled", childCount + recyclerSize,
738                        mRecyclerView.mRecycler.getRecycledViewPool().size());
739            }
740        });
741    }
742
743    public void getFirstLastChildrenTest(final Config config) throws Throwable {
744        setupByConfig(config, true);
745        Runnable viewInBoundsTest = new Runnable() {
746            @Override
747            public void run() {
748                VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
749                final String boundsLog = mLayoutManager.getBoundsLog();
750                assertEquals(config + ":\nfirst visible child should match traversal result\n"
751                                + boundsLog, visibleChildren.firstVisiblePosition,
752                        mLayoutManager.findFirstVisibleItemPosition()
753                );
754                assertEquals(
755                        config + ":\nfirst fully visible child should match traversal result\n"
756                                + boundsLog, visibleChildren.firstFullyVisiblePosition,
757                        mLayoutManager.findFirstCompletelyVisibleItemPosition()
758                );
759
760                assertEquals(config + ":\nlast visible child should match traversal result\n"
761                                + boundsLog, visibleChildren.lastVisiblePosition,
762                        mLayoutManager.findLastVisibleItemPosition()
763                );
764                assertEquals(
765                        config + ":\nlast fully visible child should match traversal result\n"
766                                + boundsLog, visibleChildren.lastFullyVisiblePosition,
767                        mLayoutManager.findLastCompletelyVisibleItemPosition()
768                );
769            }
770        };
771        runTestOnUiThread(viewInBoundsTest);
772        // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
773        // case
774        final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount();
775        runTestOnUiThread(new Runnable() {
776            @Override
777            public void run() {
778                mRecyclerView.smoothScrollToPosition(scrollPosition);
779            }
780        });
781        while (mLayoutManager.isSmoothScrolling() ||
782                mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
783            runTestOnUiThread(viewInBoundsTest);
784            Thread.sleep(400);
785        }
786        // delete all items
787        mLayoutManager.expectLayouts(2);
788        mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount());
789        mLayoutManager.waitForLayout(2);
790        // test empty case
791        runTestOnUiThread(viewInBoundsTest);
792        // set a new adapter with huge items to test full bounds check
793        mLayoutManager.expectLayouts(1);
794        final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace();
795        final TestAdapter newAdapter = new TestAdapter(100) {
796            @Override
797            public void onBindViewHolder(TestViewHolder holder,
798                    int position) {
799                super.onBindViewHolder(holder, position);
800                if (config.mOrientation == HORIZONTAL) {
801                    holder.itemView.setMinimumWidth(totalSpace + 5);
802                } else {
803                    holder.itemView.setMinimumHeight(totalSpace + 5);
804                }
805            }
806        };
807        runTestOnUiThread(new Runnable() {
808            @Override
809            public void run() {
810                mRecyclerView.setAdapter(newAdapter);
811            }
812        });
813        mLayoutManager.waitForLayout(2);
814        runTestOnUiThread(viewInBoundsTest);
815    }
816
817    public void testSavedState() throws Throwable {
818        PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
819                new PostLayoutRunnable() {
820                    @Override
821                    public void run() throws Throwable {
822                        // do nothing
823                    }
824
825                    @Override
826                    public String describe() {
827                        return "doing nothing";
828                    }
829                },
830                new PostLayoutRunnable() {
831                    @Override
832                    public void run() throws Throwable {
833                        mLayoutManager.expectLayouts(1);
834                        scrollToPosition(mTestAdapter.getItemCount() * 3 / 4);
835                        mLayoutManager.waitForLayout(2);
836                    }
837
838                    @Override
839                    public String describe() {
840                        return "scroll to position";
841                    }
842                },
843                new PostLayoutRunnable() {
844                    @Override
845                    public void run() throws Throwable {
846                        mLayoutManager.expectLayouts(1);
847                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3,
848                                50);
849                        mLayoutManager.waitForLayout(2);
850                    }
851
852                    @Override
853                    public String describe() {
854                        return "scroll to position with positive offset";
855                    }
856                },
857                new PostLayoutRunnable() {
858                    @Override
859                    public void run() throws Throwable {
860                        mLayoutManager.expectLayouts(1);
861                        scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3,
862                                -50);
863                        mLayoutManager.waitForLayout(2);
864                    }
865
866                    @Override
867                    public String describe() {
868                        return "scroll to position with negative offset";
869                    }
870                }
871        };
872
873        PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{
874                new PostRestoreRunnable() {
875                    @Override
876                    public String describe() {
877                        return "Doing nothing";
878                    }
879                },
880                new PostRestoreRunnable() {
881                    @Override
882                    void onAfterRestore(Config config) throws Throwable {
883                        // update config as well so that restore assertions will work
884                        config.mOrientation = 1 - config.mOrientation;
885                        mLayoutManager.setOrientation(config.mOrientation);
886                    }
887
888                    @Override
889                    boolean shouldLayoutMatch(Config config) {
890                        return config.mItemCount == 0;
891                    }
892
893                    @Override
894                    public String describe() {
895                        return "Changing orientation";
896                    }
897                },
898                new PostRestoreRunnable() {
899                    @Override
900                    void onAfterRestore(Config config) throws Throwable {
901                        config.mStackFromEnd = !config.mStackFromEnd;
902                        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
903                    }
904
905                    @Override
906                    boolean shouldLayoutMatch(Config config) {
907                        return true; //stack from end should not move items on change
908                    }
909
910                    @Override
911                    public String describe() {
912                        return "Changing stack from end";
913                    }
914                },
915                new PostRestoreRunnable() {
916                    @Override
917                    void onAfterRestore(Config config) throws Throwable {
918                        config.mReverseLayout = !config.mReverseLayout;
919                        mLayoutManager.setReverseLayout(config.mReverseLayout);
920                    }
921
922                    @Override
923                    boolean shouldLayoutMatch(Config config) {
924                        return config.mItemCount == 0;
925                    }
926
927                    @Override
928                    public String describe() {
929                        return "Changing reverse layout";
930                    }
931                },
932                new PostRestoreRunnable() {
933                    @Override
934                    void onAfterRestore(Config config) throws Throwable {
935                        config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach;
936                        mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
937                    }
938
939                    @Override
940                    boolean shouldLayoutMatch(Config config) {
941                        return true;
942                    }
943
944                    @Override
945                    String describe() {
946                        return "Change should recycle children";
947                    }
948                },
949                new PostRestoreRunnable() {
950                    int position;
951                    @Override
952                    void onAfterRestore(Config config) throws Throwable {
953                        position = mTestAdapter.getItemCount() / 2;
954                        mLayoutManager.scrollToPosition(position);
955                    }
956
957                    @Override
958                    boolean shouldLayoutMatch(Config config) {
959                        return mTestAdapter.getItemCount() == 0;
960                    }
961
962                    @Override
963                    String describe() {
964                        return "Scroll to position " + position ;
965                    }
966
967                    @Override
968                    void onAfterReLayout(Config config) {
969                        if (mTestAdapter.getItemCount() > 0) {
970                            assertEquals(config + ":scrolled view should be last completely visible",
971                                    position,
972                                    config.mStackFromEnd ?
973                                            mLayoutManager.findLastCompletelyVisibleItemPosition()
974                                        : mLayoutManager.findFirstCompletelyVisibleItemPosition());
975                        }
976                    }
977                }
978        };
979        boolean[] waitForLayoutOptions = new boolean[]{true, false};
980        List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300);
981        variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true);
982        for (Config config : variations) {
983            for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) {
984                for (boolean waitForLayout : waitForLayoutOptions) {
985                    for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) {
986                        savedStateTest((Config) config.clone(), waitForLayout, postLayoutRunnable,
987                                postRestoreRunnable);
988                        removeRecyclerView();
989                    }
990
991                }
992            }
993        }
994    }
995
996    public void savedStateTest(Config config, boolean waitForLayout,
997            PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)
998            throws Throwable {
999        if (DEBUG) {
1000            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " +
1001                    config + " post layout action " + postLayoutOperation.describe() +
1002                    "post restore action " + postRestoreOperation.describe());
1003        }
1004        setupByConfig(config, false);
1005        if (waitForLayout) {
1006            waitForFirstLayout();
1007            postLayoutOperation.run();
1008        }
1009        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
1010        Parcelable savedState = mRecyclerView.onSaveInstanceState();
1011        // we append a suffix to the parcelable to test out of bounds
1012        String parcelSuffix = UUID.randomUUID().toString();
1013        Parcel parcel = Parcel.obtain();
1014        savedState.writeToParcel(parcel, 0);
1015        parcel.writeString(parcelSuffix);
1016        removeRecyclerView();
1017        // reset for reading
1018        parcel.setDataPosition(0);
1019        // re-create
1020        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
1021        removeRecyclerView();
1022
1023        RecyclerView restored = new RecyclerView(getActivity());
1024        // this config should be no op.
1025        mLayoutManager = new WrappedLinearLayoutManager(getActivity(),
1026                config.mOrientation, config.mReverseLayout);
1027        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
1028        restored.setLayoutManager(mLayoutManager);
1029        // use the same adapter for Rect matching
1030        restored.setAdapter(mTestAdapter);
1031        restored.onRestoreInstanceState(savedState);
1032        postRestoreOperation.onAfterRestore(config);
1033        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
1034                parcel.readString());
1035        mLayoutManager.expectLayouts(1);
1036        setRecyclerView(restored);
1037        mLayoutManager.waitForLayout(2);
1038        // calculate prefix here instead of above to include post restore changes
1039        final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() +
1040                "\npostRestore:" + postRestoreOperation.describe() + "\n";
1041        assertEquals(logPrefix + " on saved state, reverse layout should be preserved",
1042                config.mReverseLayout, mLayoutManager.getReverseLayout());
1043        assertEquals(logPrefix + " on saved state, orientation should be preserved",
1044                config.mOrientation, mLayoutManager.getOrientation());
1045        assertEquals(logPrefix + " on saved state, stack from end should be preserved",
1046                config.mStackFromEnd, mLayoutManager.getStackFromEnd());
1047        if (waitForLayout) {
1048            if (postRestoreOperation.shouldLayoutMatch(config)) {
1049                assertRectSetsEqual(
1050                        logPrefix + ": on restore, previous view positions should be preserved",
1051                        before, mLayoutManager.collectChildCoordinates());
1052            } else {
1053                assertRectSetsNotEqual(
1054                        logPrefix
1055                                + ": on restore with changes, previous view positions should NOT "
1056                                + "be preserved",
1057                        before, mLayoutManager.collectChildCoordinates());
1058            }
1059            postRestoreOperation.onAfterReLayout(config);
1060        }
1061    }
1062
1063    void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
1064        runTestOnUiThread(new Runnable() {
1065            @Override
1066            public void run() {
1067                mLayoutManager.scrollToPositionWithOffset(position, offset);
1068            }
1069        });
1070    }
1071
1072    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
1073            Map<Item, Rect> after) {
1074        Throwable throwable = null;
1075        try {
1076            assertRectSetsEqual("NOT " + message, before, after);
1077        } catch (Throwable t) {
1078            throwable = t;
1079        }
1080        assertNotNull(message + "\ntwo layout should be different", throwable);
1081    }
1082
1083    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
1084        StringBuilder sb = new StringBuilder();
1085        sb.append("checking rectangle equality.");
1086         sb.append("before:\n");
1087        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
1088            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
1089        }
1090        sb.append("after:\n");
1091        for (Map.Entry<Item, Rect> entry : after.entrySet()) {
1092            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
1093        }
1094        message = message + "\n" + sb.toString();
1095        assertEquals(message + ":\nitem counts should be equal", before.size()
1096                , after.size());
1097        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
1098            Rect afterRect = after.get(entry.getKey());
1099            assertNotNull(message + ":\nSame item should be visible after simple re-layout",
1100                    afterRect);
1101            assertEquals(message + ":\nItem should be laid out at the same coordinates",
1102                    entry.getValue(), afterRect);
1103        }
1104    }
1105
1106    public void testAccessibilityPositions() throws Throwable {
1107        setupByConfig(new Config(VERTICAL, false, false), true);
1108        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
1109                .getCompatAccessibilityDelegate();
1110        final AccessibilityEvent event = AccessibilityEvent.obtain();
1111        runTestOnUiThread(new Runnable() {
1112            @Override
1113            public void run() {
1114                delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
1115            }
1116        });
1117        final AccessibilityRecordCompat record = AccessibilityEventCompat
1118                .asRecord(event);
1119        assertEquals("result should have first position",
1120                record.getFromIndex(),
1121                mLayoutManager.findFirstVisibleItemPosition());
1122        assertEquals("result should have last position",
1123                record.getToIndex(),
1124                mLayoutManager.findLastVisibleItemPosition());
1125    }
1126
1127    public void testPrepareForDrop() throws Throwable {
1128        SelectTargetChildren[] selectors = new SelectTargetChildren[] {
1129                new SelectTargetChildren() {
1130                    @Override
1131                    public int[] selectTargetChildren(int childCount) {
1132                        return new int[]{1, 0};
1133                    }
1134                },
1135                new SelectTargetChildren() {
1136                    @Override
1137                    public int[] selectTargetChildren(int childCount) {
1138                        return new int[]{0, 1};
1139                    }
1140                },
1141                new SelectTargetChildren() {
1142                    @Override
1143                    public int[] selectTargetChildren(int childCount) {
1144                        return new int[]{childCount - 1, childCount - 2};
1145                    }
1146                },
1147                new SelectTargetChildren() {
1148                    @Override
1149                    public int[] selectTargetChildren(int childCount) {
1150                        return new int[]{childCount - 2, childCount - 1};
1151                    }
1152                },
1153                new SelectTargetChildren() {
1154                    @Override
1155                    public int[] selectTargetChildren(int childCount) {
1156                        return new int[]{childCount / 2, childCount / 2 + 1};
1157                    }
1158                },
1159                new SelectTargetChildren() {
1160                    @Override
1161                    public int[] selectTargetChildren(int childCount) {
1162                        return new int[]{childCount / 2 + 1, childCount / 2};
1163                    }
1164                }
1165        };
1166        for (SelectTargetChildren selector : selectors) {
1167            for (Config config : mBaseVariations) {
1168                prepareForDropTest(config, selector);
1169                removeRecyclerView();
1170            }
1171        }
1172    }
1173
1174    public void prepareForDropTest(final Config config, SelectTargetChildren selectTargetChildren)
1175            throws Throwable {
1176        config.mTestAdapter = new TestAdapter(100) {
1177            @Override
1178            public void onBindViewHolder(TestViewHolder holder,
1179                    int position) {
1180                super.onBindViewHolder(holder, position);
1181                if (config.mOrientation == HORIZONTAL) {
1182                    final int base = mRecyclerView.getWidth() / 5;
1183                    final int itemRand = holder.mBoundItem.mText.hashCode() % base;
1184                    holder.itemView.setMinimumWidth(base + itemRand);
1185                } else {
1186                    final int base = mRecyclerView.getHeight() / 5;
1187                    final int itemRand = holder.mBoundItem.mText.hashCode() % base;
1188                    holder.itemView.setMinimumHeight(base + itemRand);
1189                }
1190            }
1191        };
1192        setupByConfig(config, true);
1193        mLayoutManager.expectLayouts(1);
1194        scrollToPosition(mTestAdapter.getItemCount() / 2);
1195        mLayoutManager.waitForLayout(1);
1196        int[] positions = selectTargetChildren.selectTargetChildren(mRecyclerView.getChildCount());
1197        final View fromChild = mLayoutManager.getChildAt(positions[0]);
1198        final int fromPos = mLayoutManager.getPosition(fromChild);
1199        final View onChild = mLayoutManager.getChildAt(positions[1]);
1200        final int toPos = mLayoutManager.getPosition(onChild);
1201        final OrientationHelper helper = mLayoutManager.mOrientationHelper;
1202        final int dragCoordinate;
1203        final boolean towardsHead = toPos < fromPos;
1204        final int referenceLine;
1205        if (config.mReverseLayout == towardsHead) {
1206            referenceLine = helper.getDecoratedEnd(onChild);
1207            dragCoordinate = referenceLine + 3 -
1208                    helper.getDecoratedMeasurement(fromChild);
1209        } else {
1210            referenceLine = helper.getDecoratedStart(onChild);
1211            dragCoordinate = referenceLine - 3;
1212        }
1213        mLayoutManager.expectLayouts(2);
1214
1215        final int x,y;
1216        if (config.mOrientation == HORIZONTAL) {
1217            x = dragCoordinate;
1218            y = fromChild.getTop();
1219        } else {
1220            y = dragCoordinate;
1221            x = fromChild.getLeft();
1222        }
1223        runTestOnUiThread(new Runnable() {
1224            @Override
1225            public void run() {
1226                mTestAdapter.moveInUIThread(fromPos, toPos);
1227                mTestAdapter.notifyItemMoved(fromPos, toPos);
1228                mLayoutManager.prepareForDrop(fromChild, onChild, x, y);
1229            }
1230        });
1231        mLayoutManager.waitForLayout(2);
1232
1233        assertSame(fromChild, mRecyclerView.findViewHolderForAdapterPosition(toPos).itemView);
1234        // make sure it has the position we wanted
1235        if (config.mReverseLayout == towardsHead) {
1236            assertEquals(referenceLine, helper.getDecoratedEnd(fromChild));
1237        } else {
1238            assertEquals(referenceLine, helper.getDecoratedStart(fromChild));
1239        }
1240    }
1241
1242    static class VisibleChildren {
1243
1244        int firstVisiblePosition = RecyclerView.NO_POSITION;
1245
1246        int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
1247
1248        int lastVisiblePosition = RecyclerView.NO_POSITION;
1249
1250        int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
1251
1252        @Override
1253        public String toString() {
1254            return "VisibleChildren{" +
1255                    "firstVisiblePosition=" + firstVisiblePosition +
1256                    ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
1257                    ", lastVisiblePosition=" + lastVisiblePosition +
1258                    ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
1259                    '}';
1260        }
1261    }
1262
1263    abstract private class PostLayoutRunnable {
1264
1265        abstract void run() throws Throwable;
1266
1267        abstract String describe();
1268    }
1269
1270    abstract private class PostRestoreRunnable {
1271
1272        void onAfterRestore(Config config) throws Throwable {
1273        }
1274
1275        abstract String describe();
1276
1277        boolean shouldLayoutMatch(Config config) {
1278            return true;
1279        }
1280
1281        void onAfterReLayout(Config config) {
1282
1283        };
1284    }
1285
1286    class WrappedLinearLayoutManager extends LinearLayoutManager {
1287
1288        CountDownLatch layoutLatch;
1289
1290        OrientationHelper mSecondaryOrientation;
1291
1292        OnLayoutListener mOnLayoutListener;
1293
1294        public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
1295            super(context, orientation, reverseLayout);
1296        }
1297
1298        public void expectLayouts(int count) {
1299            layoutLatch = new CountDownLatch(count);
1300        }
1301
1302        public void waitForLayout(long timeout) throws InterruptedException {
1303            waitForLayout(timeout, TimeUnit.SECONDS);
1304        }
1305
1306        @Override
1307        public void setOrientation(int orientation) {
1308            super.setOrientation(orientation);
1309            mSecondaryOrientation = null;
1310        }
1311
1312        @Override
1313        public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) {
1314            if (DEBUG) {
1315                Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child));
1316            }
1317            super.removeAndRecycleView(child, recycler);
1318        }
1319
1320        @Override
1321        public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) {
1322            if (DEBUG) {
1323                Log.d(TAG, "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index)));
1324            }
1325            super.removeAndRecycleViewAt(index, recycler);
1326        }
1327
1328        @Override
1329        void ensureLayoutState() {
1330            super.ensureLayoutState();
1331            if (mSecondaryOrientation == null) {
1332                mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
1333                        1 - getOrientation());
1334            }
1335        }
1336
1337        private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
1338            layoutLatch.await(timeout * (DEBUG ? 100 : 1), timeUnit);
1339            assertEquals("all expected layouts should be executed at the expected time",
1340                    0, layoutLatch.getCount());
1341            getInstrumentation().waitForIdleSync();
1342        }
1343
1344        @Override
1345        LayoutState createLayoutState() {
1346            return new LayoutState() {
1347                @Override
1348                View next(RecyclerView.Recycler recycler) {
1349                    final boolean hadMore = hasMore(mRecyclerView.mState);
1350                    final int position = mCurrentPosition;
1351                    View next = super.next(recycler);
1352                    assertEquals("if has more, should return a view", hadMore, next != null);
1353                    assertEquals("position of the returned view must match current position",
1354                            position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
1355                    return next;
1356                }
1357            };
1358        }
1359
1360        public String getBoundsLog() {
1361            StringBuilder sb = new StringBuilder();
1362            sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
1363                    .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
1364            sb.append("\nchildren bounds\n");
1365            final int childCount = getChildCount();
1366            for (int i = 0; i < childCount; i++) {
1367                View child = getChildAt(i);
1368                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
1369                        .append("[").append("start:").append(
1370                        mOrientationHelper.getDecoratedStart(child)).append(", end:")
1371                        .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
1372            }
1373            return sb.toString();
1374        }
1375
1376        public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException {
1377            RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
1378            if (itemAnimator == null) {
1379                return;
1380            }
1381            final CountDownLatch latch = new CountDownLatch(1);
1382            final boolean running = itemAnimator.isRunning(
1383                    new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
1384                        @Override
1385                        public void onAnimationsFinished() {
1386                            latch.countDown();
1387                        }
1388                    }
1389            );
1390            if (running) {
1391                latch.await(timeoutInSeconds, TimeUnit.SECONDS);
1392            }
1393        }
1394
1395        public VisibleChildren traverseAndFindVisibleChildren() {
1396            int childCount = getChildCount();
1397            final VisibleChildren visibleChildren = new VisibleChildren();
1398            final int start = mOrientationHelper.getStartAfterPadding();
1399            final int end = mOrientationHelper.getEndAfterPadding();
1400            for (int i = 0; i < childCount; i++) {
1401                View child = getChildAt(i);
1402                final int childStart = mOrientationHelper.getDecoratedStart(child);
1403                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
1404                final boolean fullyVisible = childStart >= start && childEnd <= end;
1405                final boolean hidden = childEnd <= start || childStart >= end;
1406                if (hidden) {
1407                    continue;
1408                }
1409                final int position = getPosition(child);
1410                if (fullyVisible) {
1411                    if (position < visibleChildren.firstFullyVisiblePosition ||
1412                            visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
1413                        visibleChildren.firstFullyVisiblePosition = position;
1414                    }
1415
1416                    if (position > visibleChildren.lastFullyVisiblePosition) {
1417                        visibleChildren.lastFullyVisiblePosition = position;
1418                    }
1419                }
1420
1421                if (position < visibleChildren.firstVisiblePosition ||
1422                        visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
1423                    visibleChildren.firstVisiblePosition = position;
1424                }
1425
1426                if (position > visibleChildren.lastVisiblePosition) {
1427                    visibleChildren.lastVisiblePosition = position;
1428                }
1429
1430            }
1431            return visibleChildren;
1432        }
1433
1434        Rect getViewBounds(View view) {
1435            if (getOrientation() == HORIZONTAL) {
1436                return new Rect(
1437                        mOrientationHelper.getDecoratedStart(view),
1438                        mSecondaryOrientation.getDecoratedStart(view),
1439                        mOrientationHelper.getDecoratedEnd(view),
1440                        mSecondaryOrientation.getDecoratedEnd(view));
1441            } else {
1442                return new Rect(
1443                        mSecondaryOrientation.getDecoratedStart(view),
1444                        mOrientationHelper.getDecoratedStart(view),
1445                        mSecondaryOrientation.getDecoratedEnd(view),
1446                        mOrientationHelper.getDecoratedEnd(view));
1447            }
1448
1449        }
1450
1451        Map<Item, Rect> collectChildCoordinates() throws Throwable {
1452            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
1453            runTestOnUiThread(new Runnable() {
1454                @Override
1455                public void run() {
1456                    final int childCount = getChildCount();
1457                    for (int i = 0; i < childCount; i++) {
1458                        View child = getChildAt(i);
1459                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
1460                                .getLayoutParams();
1461                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
1462                        items.put(vh.mBoundItem, getViewBounds(child));
1463                    }
1464                }
1465            });
1466            return items;
1467        }
1468
1469        @Override
1470        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1471            try {
1472                if (mOnLayoutListener != null) {
1473                    mOnLayoutListener.before(recycler, state);
1474                }
1475                super.onLayoutChildren(recycler, state);
1476                if (mOnLayoutListener != null) {
1477                    mOnLayoutListener.after(recycler, state);
1478                }
1479            } catch (Throwable t) {
1480                postExceptionToInstrumentation(t);
1481            }
1482            layoutLatch.countDown();
1483        }
1484
1485
1486    }
1487
1488    static class OnLayoutListener {
1489        void before(RecyclerView.Recycler recycler, RecyclerView.State state){}
1490        void after(RecyclerView.Recycler recycler, RecyclerView.State state){}
1491    }
1492
1493    static class Config implements Cloneable {
1494
1495        private static final int DEFAULT_ITEM_COUNT = 100;
1496
1497        private boolean mStackFromEnd;
1498
1499        int mOrientation = VERTICAL;
1500
1501        boolean mReverseLayout = false;
1502
1503        boolean mRecycleChildrenOnDetach = false;
1504
1505        int mItemCount = DEFAULT_ITEM_COUNT;
1506
1507        TestAdapter mTestAdapter;
1508
1509        Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
1510            mOrientation = orientation;
1511            mReverseLayout = reverseLayout;
1512            mStackFromEnd = stackFromEnd;
1513        }
1514
1515        public Config() {
1516
1517        }
1518
1519        Config adapter(TestAdapter adapter) {
1520            mTestAdapter = adapter;
1521            return this;
1522        }
1523
1524        Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
1525            mRecycleChildrenOnDetach = recycleChildrenOnDetach;
1526            return this;
1527        }
1528
1529        Config orientation(int orientation) {
1530            mOrientation = orientation;
1531            return this;
1532        }
1533
1534        Config stackFromBottom(boolean stackFromBottom) {
1535            mStackFromEnd = stackFromBottom;
1536            return this;
1537        }
1538
1539        Config reverseLayout(boolean reverseLayout) {
1540            mReverseLayout = reverseLayout;
1541            return this;
1542        }
1543
1544        public Config itemCount(int itemCount) {
1545            mItemCount = itemCount;
1546            return this;
1547        }
1548
1549        // required by convention
1550        @Override
1551        public Object clone() throws CloneNotSupportedException {
1552            return super.clone();
1553        }
1554
1555        @Override
1556        public String toString() {
1557            return "Config{" +
1558                    "mStackFromEnd=" + mStackFromEnd +
1559                    ", mOrientation=" + mOrientation +
1560                    ", mReverseLayout=" + mReverseLayout +
1561                    ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
1562                    ", mItemCount=" + mItemCount +
1563                    '}';
1564        }
1565    }
1566
1567    private interface SelectTargetChildren {
1568        int[] selectTargetChildren(int childCount);
1569    }
1570}
1571