LinearLayoutManagerTest.java revision 3d8453880afb3e32c4c59c52b8b580f91d78b29f
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                                -10);  // Some tests break if this value is below the item height.
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        boolean[] loadDataAfterRestoreOptions = new boolean[]{true, false};
981        List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300);
982        variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true);
983        for (Config config : variations) {
984            for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) {
985                for (boolean waitForLayout : waitForLayoutOptions) {
986                    for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) {
987                        for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) {
988                            savedStateTest((Config) config.clone(), waitForLayout,
989                                    loadDataAfterRestore, postLayoutRunnable, postRestoreRunnable);
990                            removeRecyclerView();
991                        }
992                    }
993
994                }
995            }
996        }
997    }
998
999    public void savedStateTest(Config config, boolean waitForLayout, boolean loadDataAfterRestore,
1000            PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)
1001            throws Throwable {
1002        if (DEBUG) {
1003            Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " +
1004                    config + " post layout action " + postLayoutOperation.describe() +
1005                    "post restore action " + postRestoreOperation.describe());
1006        }
1007        setupByConfig(config, false);
1008        if (waitForLayout) {
1009            waitForFirstLayout();
1010            postLayoutOperation.run();
1011        }
1012        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
1013        Parcelable savedState = mRecyclerView.onSaveInstanceState();
1014        // we append a suffix to the parcelable to test out of bounds
1015        String parcelSuffix = UUID.randomUUID().toString();
1016        Parcel parcel = Parcel.obtain();
1017        savedState.writeToParcel(parcel, 0);
1018        parcel.writeString(parcelSuffix);
1019        removeRecyclerView();
1020        // reset for reading
1021        parcel.setDataPosition(0);
1022        // re-create
1023        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
1024        removeRecyclerView();
1025
1026        final int itemCount = mTestAdapter.getItemCount();
1027        if (loadDataAfterRestore) {
1028            mTestAdapter.deleteAndNotify(0, itemCount);
1029        }
1030
1031        RecyclerView restored = new RecyclerView(getActivity());
1032        // this config should be no op.
1033        mLayoutManager = new WrappedLinearLayoutManager(getActivity(),
1034                config.mOrientation, config.mReverseLayout);
1035        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
1036        restored.setLayoutManager(mLayoutManager);
1037        // use the same adapter for Rect matching
1038        restored.setAdapter(mTestAdapter);
1039        restored.onRestoreInstanceState(savedState);
1040
1041        if (loadDataAfterRestore) {
1042            mTestAdapter.addAndNotify(itemCount);
1043        }
1044
1045        postRestoreOperation.onAfterRestore(config);
1046        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
1047                parcel.readString());
1048        mLayoutManager.expectLayouts(1);
1049        setRecyclerView(restored);
1050        mLayoutManager.waitForLayout(2);
1051        // calculate prefix here instead of above to include post restore changes
1052        final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() +
1053                "\npostRestore:" + postRestoreOperation.describe() + "\n";
1054        assertEquals(logPrefix + " on saved state, reverse layout should be preserved",
1055                config.mReverseLayout, mLayoutManager.getReverseLayout());
1056        assertEquals(logPrefix + " on saved state, orientation should be preserved",
1057                config.mOrientation, mLayoutManager.getOrientation());
1058        assertEquals(logPrefix + " on saved state, stack from end should be preserved",
1059                config.mStackFromEnd, mLayoutManager.getStackFromEnd());
1060        if (waitForLayout) {
1061            final boolean strictItemEquality = !loadDataAfterRestore;
1062            if (postRestoreOperation.shouldLayoutMatch(config)) {
1063                assertRectSetsEqual(
1064                        logPrefix + ": on restore, previous view positions should be preserved",
1065                        before, mLayoutManager.collectChildCoordinates(), strictItemEquality);
1066            } else {
1067                assertRectSetsNotEqual(
1068                        logPrefix
1069                                + ": on restore with changes, previous view positions should NOT "
1070                                + "be preserved",
1071                        before, mLayoutManager.collectChildCoordinates(), strictItemEquality);
1072            }
1073            postRestoreOperation.onAfterReLayout(config);
1074        }
1075    }
1076
1077    public void testScrollAndClear() throws Throwable {
1078        setupByConfig(new Config(), true);
1079
1080        assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
1081
1082        mLayoutManager.expectLayouts(1);
1083        runTestOnUiThread(new Runnable() {
1084            @Override
1085            public void run() {
1086                mLayoutManager.scrollToPositionWithOffset(1, 0);
1087                mTestAdapter.clearOnUIThread();
1088            }
1089        });
1090        mLayoutManager.waitForLayout(2);
1091
1092        assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
1093    }
1094
1095
1096    void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
1097        runTestOnUiThread(new Runnable() {
1098            @Override
1099            public void run() {
1100                mLayoutManager.scrollToPositionWithOffset(position, offset);
1101            }
1102        });
1103    }
1104
1105    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
1106            Map<Item, Rect> after, boolean strictItemEquality) {
1107        Throwable throwable = null;
1108        try {
1109            assertRectSetsEqual("NOT " + message, before, after, strictItemEquality);
1110        } catch (Throwable t) {
1111            throwable = t;
1112        }
1113        assertNotNull(message + "\ntwo layout should be different", throwable);
1114    }
1115
1116    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
1117        assertRectSetsEqual(message, before, after, true);
1118    }
1119
1120    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
1121            boolean strictItemEquality) {
1122        StringBuilder sb = new StringBuilder();
1123        sb.append("checking rectangle equality.\n");
1124        sb.append("before:\n");
1125        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
1126            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
1127        }
1128        sb.append("after:\n");
1129        for (Map.Entry<Item, Rect> entry : after.entrySet()) {
1130            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
1131        }
1132        message = message + "\n" + sb.toString();
1133        assertEquals(message + ":\nitem counts should be equal", before.size()
1134                , after.size());
1135        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
1136            final Item beforeItem = entry.getKey();
1137            Rect afterRect = null;
1138            if (strictItemEquality) {
1139                afterRect = after.get(beforeItem);
1140                assertNotNull(message + ":\nSame item should be visible after simple re-layout",
1141                        afterRect);
1142            } else {
1143                for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) {
1144                    final Item afterItem = afterEntry.getKey();
1145                    if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
1146                        afterRect = afterEntry.getValue();
1147                        break;
1148                    }
1149                }
1150                assertNotNull(message + ":\nItem with same adapter index should be visible " +
1151                                "after simple re-layout",
1152                        afterRect);
1153            }
1154            assertEquals(message + ":\nItem should be laid out at the same coordinates",
1155                    entry.getValue(), afterRect);
1156        }
1157    }
1158
1159    public void testAccessibilityPositions() throws Throwable {
1160        setupByConfig(new Config(VERTICAL, false, false), true);
1161        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
1162                .getCompatAccessibilityDelegate();
1163        final AccessibilityEvent event = AccessibilityEvent.obtain();
1164        runTestOnUiThread(new Runnable() {
1165            @Override
1166            public void run() {
1167                delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
1168            }
1169        });
1170        final AccessibilityRecordCompat record = AccessibilityEventCompat
1171                .asRecord(event);
1172        assertEquals("result should have first position",
1173                record.getFromIndex(),
1174                mLayoutManager.findFirstVisibleItemPosition());
1175        assertEquals("result should have last position",
1176                record.getToIndex(),
1177                mLayoutManager.findLastVisibleItemPosition());
1178    }
1179
1180    public void testPrepareForDrop() throws Throwable {
1181        SelectTargetChildren[] selectors = new SelectTargetChildren[] {
1182                new SelectTargetChildren() {
1183                    @Override
1184                    public int[] selectTargetChildren(int childCount) {
1185                        return new int[]{1, 0};
1186                    }
1187                },
1188                new SelectTargetChildren() {
1189                    @Override
1190                    public int[] selectTargetChildren(int childCount) {
1191                        return new int[]{0, 1};
1192                    }
1193                },
1194                new SelectTargetChildren() {
1195                    @Override
1196                    public int[] selectTargetChildren(int childCount) {
1197                        return new int[]{childCount - 1, childCount - 2};
1198                    }
1199                },
1200                new SelectTargetChildren() {
1201                    @Override
1202                    public int[] selectTargetChildren(int childCount) {
1203                        return new int[]{childCount - 2, childCount - 1};
1204                    }
1205                },
1206                new SelectTargetChildren() {
1207                    @Override
1208                    public int[] selectTargetChildren(int childCount) {
1209                        return new int[]{childCount / 2, childCount / 2 + 1};
1210                    }
1211                },
1212                new SelectTargetChildren() {
1213                    @Override
1214                    public int[] selectTargetChildren(int childCount) {
1215                        return new int[]{childCount / 2 + 1, childCount / 2};
1216                    }
1217                }
1218        };
1219        for (SelectTargetChildren selector : selectors) {
1220            for (Config config : mBaseVariations) {
1221                prepareForDropTest(config, selector);
1222                removeRecyclerView();
1223            }
1224        }
1225    }
1226
1227    public void prepareForDropTest(final Config config, SelectTargetChildren selectTargetChildren)
1228            throws Throwable {
1229        config.mTestAdapter = new TestAdapter(100) {
1230            @Override
1231            public void onBindViewHolder(TestViewHolder holder,
1232                    int position) {
1233                super.onBindViewHolder(holder, position);
1234                if (config.mOrientation == HORIZONTAL) {
1235                    final int base = mRecyclerView.getWidth() / 5;
1236                    final int itemRand = holder.mBoundItem.mText.hashCode() % base;
1237                    holder.itemView.setMinimumWidth(base + itemRand);
1238                } else {
1239                    final int base = mRecyclerView.getHeight() / 5;
1240                    final int itemRand = holder.mBoundItem.mText.hashCode() % base;
1241                    holder.itemView.setMinimumHeight(base + itemRand);
1242                }
1243            }
1244        };
1245        setupByConfig(config, true);
1246        mLayoutManager.expectLayouts(1);
1247        scrollToPosition(mTestAdapter.getItemCount() / 2);
1248        mLayoutManager.waitForLayout(1);
1249        int[] positions = selectTargetChildren.selectTargetChildren(mRecyclerView.getChildCount());
1250        final View fromChild = mLayoutManager.getChildAt(positions[0]);
1251        final int fromPos = mLayoutManager.getPosition(fromChild);
1252        final View onChild = mLayoutManager.getChildAt(positions[1]);
1253        final int toPos = mLayoutManager.getPosition(onChild);
1254        final OrientationHelper helper = mLayoutManager.mOrientationHelper;
1255        final int dragCoordinate;
1256        final boolean towardsHead = toPos < fromPos;
1257        final int referenceLine;
1258        if (config.mReverseLayout == towardsHead) {
1259            referenceLine = helper.getDecoratedEnd(onChild);
1260            dragCoordinate = referenceLine + 3 -
1261                    helper.getDecoratedMeasurement(fromChild);
1262        } else {
1263            referenceLine = helper.getDecoratedStart(onChild);
1264            dragCoordinate = referenceLine - 3;
1265        }
1266        mLayoutManager.expectLayouts(2);
1267
1268        final int x,y;
1269        if (config.mOrientation == HORIZONTAL) {
1270            x = dragCoordinate;
1271            y = fromChild.getTop();
1272        } else {
1273            y = dragCoordinate;
1274            x = fromChild.getLeft();
1275        }
1276        runTestOnUiThread(new Runnable() {
1277            @Override
1278            public void run() {
1279                mTestAdapter.moveInUIThread(fromPos, toPos);
1280                mTestAdapter.notifyItemMoved(fromPos, toPos);
1281                mLayoutManager.prepareForDrop(fromChild, onChild, x, y);
1282            }
1283        });
1284        mLayoutManager.waitForLayout(2);
1285
1286        assertSame(fromChild, mRecyclerView.findViewHolderForAdapterPosition(toPos).itemView);
1287        // make sure it has the position we wanted
1288        if (config.mReverseLayout == towardsHead) {
1289            assertEquals(referenceLine, helper.getDecoratedEnd(fromChild));
1290        } else {
1291            assertEquals(referenceLine, helper.getDecoratedStart(fromChild));
1292        }
1293    }
1294
1295    static class VisibleChildren {
1296
1297        int firstVisiblePosition = RecyclerView.NO_POSITION;
1298
1299        int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
1300
1301        int lastVisiblePosition = RecyclerView.NO_POSITION;
1302
1303        int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
1304
1305        @Override
1306        public String toString() {
1307            return "VisibleChildren{" +
1308                    "firstVisiblePosition=" + firstVisiblePosition +
1309                    ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
1310                    ", lastVisiblePosition=" + lastVisiblePosition +
1311                    ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
1312                    '}';
1313        }
1314    }
1315
1316    abstract private class PostLayoutRunnable {
1317
1318        abstract void run() throws Throwable;
1319
1320        abstract String describe();
1321    }
1322
1323    abstract private class PostRestoreRunnable {
1324
1325        void onAfterRestore(Config config) throws Throwable {
1326        }
1327
1328        abstract String describe();
1329
1330        boolean shouldLayoutMatch(Config config) {
1331            return true;
1332        }
1333
1334        void onAfterReLayout(Config config) {
1335
1336        };
1337    }
1338
1339    class WrappedLinearLayoutManager extends LinearLayoutManager {
1340
1341        CountDownLatch layoutLatch;
1342
1343        OrientationHelper mSecondaryOrientation;
1344
1345        OnLayoutListener mOnLayoutListener;
1346
1347        public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
1348            super(context, orientation, reverseLayout);
1349        }
1350
1351        public void expectLayouts(int count) {
1352            layoutLatch = new CountDownLatch(count);
1353        }
1354
1355        public void waitForLayout(long timeout) throws InterruptedException {
1356            waitForLayout(timeout, TimeUnit.SECONDS);
1357        }
1358
1359        @Override
1360        public void setOrientation(int orientation) {
1361            super.setOrientation(orientation);
1362            mSecondaryOrientation = null;
1363        }
1364
1365        @Override
1366        public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) {
1367            if (DEBUG) {
1368                Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child));
1369            }
1370            super.removeAndRecycleView(child, recycler);
1371        }
1372
1373        @Override
1374        public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) {
1375            if (DEBUG) {
1376                Log.d(TAG, "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index)));
1377            }
1378            super.removeAndRecycleViewAt(index, recycler);
1379        }
1380
1381        @Override
1382        void ensureLayoutState() {
1383            super.ensureLayoutState();
1384            if (mSecondaryOrientation == null) {
1385                mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
1386                        1 - getOrientation());
1387            }
1388        }
1389
1390        private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
1391            layoutLatch.await(timeout * (DEBUG ? 100 : 1), timeUnit);
1392            assertEquals("all expected layouts should be executed at the expected time",
1393                    0, layoutLatch.getCount());
1394            getInstrumentation().waitForIdleSync();
1395        }
1396
1397        @Override
1398        LayoutState createLayoutState() {
1399            return new LayoutState() {
1400                @Override
1401                View next(RecyclerView.Recycler recycler) {
1402                    final boolean hadMore = hasMore(mRecyclerView.mState);
1403                    final int position = mCurrentPosition;
1404                    View next = super.next(recycler);
1405                    assertEquals("if has more, should return a view", hadMore, next != null);
1406                    assertEquals("position of the returned view must match current position",
1407                            position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
1408                    return next;
1409                }
1410            };
1411        }
1412
1413        public String getBoundsLog() {
1414            StringBuilder sb = new StringBuilder();
1415            sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
1416                    .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
1417            sb.append("\nchildren bounds\n");
1418            final int childCount = getChildCount();
1419            for (int i = 0; i < childCount; i++) {
1420                View child = getChildAt(i);
1421                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
1422                        .append("[").append("start:").append(
1423                        mOrientationHelper.getDecoratedStart(child)).append(", end:")
1424                        .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
1425            }
1426            return sb.toString();
1427        }
1428
1429        public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException {
1430            RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
1431            if (itemAnimator == null) {
1432                return;
1433            }
1434            final CountDownLatch latch = new CountDownLatch(1);
1435            final boolean running = itemAnimator.isRunning(
1436                    new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
1437                        @Override
1438                        public void onAnimationsFinished() {
1439                            latch.countDown();
1440                        }
1441                    }
1442            );
1443            if (running) {
1444                latch.await(timeoutInSeconds, TimeUnit.SECONDS);
1445            }
1446        }
1447
1448        public VisibleChildren traverseAndFindVisibleChildren() {
1449            int childCount = getChildCount();
1450            final VisibleChildren visibleChildren = new VisibleChildren();
1451            final int start = mOrientationHelper.getStartAfterPadding();
1452            final int end = mOrientationHelper.getEndAfterPadding();
1453            for (int i = 0; i < childCount; i++) {
1454                View child = getChildAt(i);
1455                final int childStart = mOrientationHelper.getDecoratedStart(child);
1456                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
1457                final boolean fullyVisible = childStart >= start && childEnd <= end;
1458                final boolean hidden = childEnd <= start || childStart >= end;
1459                if (hidden) {
1460                    continue;
1461                }
1462                final int position = getPosition(child);
1463                if (fullyVisible) {
1464                    if (position < visibleChildren.firstFullyVisiblePosition ||
1465                            visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
1466                        visibleChildren.firstFullyVisiblePosition = position;
1467                    }
1468
1469                    if (position > visibleChildren.lastFullyVisiblePosition) {
1470                        visibleChildren.lastFullyVisiblePosition = position;
1471                    }
1472                }
1473
1474                if (position < visibleChildren.firstVisiblePosition ||
1475                        visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
1476                    visibleChildren.firstVisiblePosition = position;
1477                }
1478
1479                if (position > visibleChildren.lastVisiblePosition) {
1480                    visibleChildren.lastVisiblePosition = position;
1481                }
1482
1483            }
1484            return visibleChildren;
1485        }
1486
1487        Rect getViewBounds(View view) {
1488            if (getOrientation() == HORIZONTAL) {
1489                return new Rect(
1490                        mOrientationHelper.getDecoratedStart(view),
1491                        mSecondaryOrientation.getDecoratedStart(view),
1492                        mOrientationHelper.getDecoratedEnd(view),
1493                        mSecondaryOrientation.getDecoratedEnd(view));
1494            } else {
1495                return new Rect(
1496                        mSecondaryOrientation.getDecoratedStart(view),
1497                        mOrientationHelper.getDecoratedStart(view),
1498                        mSecondaryOrientation.getDecoratedEnd(view),
1499                        mOrientationHelper.getDecoratedEnd(view));
1500            }
1501
1502        }
1503
1504        Map<Item, Rect> collectChildCoordinates() throws Throwable {
1505            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
1506            runTestOnUiThread(new Runnable() {
1507                @Override
1508                public void run() {
1509                    final int childCount = getChildCount();
1510                    for (int i = 0; i < childCount; i++) {
1511                        View child = getChildAt(i);
1512                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
1513                                .getLayoutParams();
1514                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
1515                        items.put(vh.mBoundItem, getViewBounds(child));
1516                    }
1517                }
1518            });
1519            return items;
1520        }
1521
1522        @Override
1523        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1524            try {
1525                if (mOnLayoutListener != null) {
1526                    mOnLayoutListener.before(recycler, state);
1527                }
1528                super.onLayoutChildren(recycler, state);
1529                if (mOnLayoutListener != null) {
1530                    mOnLayoutListener.after(recycler, state);
1531                }
1532            } catch (Throwable t) {
1533                postExceptionToInstrumentation(t);
1534            }
1535            layoutLatch.countDown();
1536        }
1537
1538
1539    }
1540
1541    static class OnLayoutListener {
1542        void before(RecyclerView.Recycler recycler, RecyclerView.State state){}
1543        void after(RecyclerView.Recycler recycler, RecyclerView.State state){}
1544    }
1545
1546    static class Config implements Cloneable {
1547
1548        private static final int DEFAULT_ITEM_COUNT = 100;
1549
1550        private boolean mStackFromEnd;
1551
1552        int mOrientation = VERTICAL;
1553
1554        boolean mReverseLayout = false;
1555
1556        boolean mRecycleChildrenOnDetach = false;
1557
1558        int mItemCount = DEFAULT_ITEM_COUNT;
1559
1560        TestAdapter mTestAdapter;
1561
1562        Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
1563            mOrientation = orientation;
1564            mReverseLayout = reverseLayout;
1565            mStackFromEnd = stackFromEnd;
1566        }
1567
1568        public Config() {
1569
1570        }
1571
1572        Config adapter(TestAdapter adapter) {
1573            mTestAdapter = adapter;
1574            return this;
1575        }
1576
1577        Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
1578            mRecycleChildrenOnDetach = recycleChildrenOnDetach;
1579            return this;
1580        }
1581
1582        Config orientation(int orientation) {
1583            mOrientation = orientation;
1584            return this;
1585        }
1586
1587        Config stackFromBottom(boolean stackFromBottom) {
1588            mStackFromEnd = stackFromBottom;
1589            return this;
1590        }
1591
1592        Config reverseLayout(boolean reverseLayout) {
1593            mReverseLayout = reverseLayout;
1594            return this;
1595        }
1596
1597        public Config itemCount(int itemCount) {
1598            mItemCount = itemCount;
1599            return this;
1600        }
1601
1602        // required by convention
1603        @Override
1604        public Object clone() throws CloneNotSupportedException {
1605            return super.clone();
1606        }
1607
1608        @Override
1609        public String toString() {
1610            return "Config{" +
1611                    "mStackFromEnd=" + mStackFromEnd +
1612                    ", mOrientation=" + mOrientation +
1613                    ", mReverseLayout=" + mReverseLayout +
1614                    ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
1615                    ", mItemCount=" + mItemCount +
1616                    '}';
1617        }
1618    }
1619
1620    private interface SelectTargetChildren {
1621        int[] selectTargetChildren(int childCount);
1622    }
1623}
1624