BaseLinearLayoutManagerTest.java revision 4143554adb9b31b700b6876a251a64419e6111e2
1/*
2 * Copyright (C) 2015 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 */
16package android.support.v7.widget;
17
18import android.content.Context;
19import android.graphics.Rect;
20import android.util.Log;
21import android.view.View;
22import android.view.ViewGroup;
23
24import java.lang.reflect.Field;
25import java.util.ArrayList;
26import java.util.LinkedHashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.concurrent.CountDownLatch;
30import java.util.concurrent.TimeUnit;
31
32import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
33import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
34import static android.view.ViewGroup.LayoutParams.FILL_PARENT;
35import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
36import static org.junit.Assert.*;
37
38public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
39
40    protected static final boolean DEBUG = false;
41    protected static final String TAG = "LinearLayoutManagerTest";
42
43    protected static List<Config> createBaseVariations() {
44        List<Config> variations = new ArrayList<>();
45        for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
46            for (boolean reverseLayout : new boolean[]{false, true}) {
47                for (boolean stackFromBottom : new boolean[]{false, true}) {
48                    for (boolean wrap : new boolean[]{false, true}) {
49                        variations.add(
50                                new Config(orientation, reverseLayout, stackFromBottom).wrap(wrap));
51                    }
52
53                }
54            }
55        }
56        return variations;
57    }
58
59    WrappedLinearLayoutManager mLayoutManager;
60    TestAdapter mTestAdapter;
61
62    protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
63            Object... variations)
64            throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
65        List<Config> newConfigs = new ArrayList<Config>();
66        Field field = Config.class.getDeclaredField(fieldName);
67        for (Config config : base) {
68            for (Object variation : variations) {
69                Config newConfig = (Config) config.clone();
70                field.set(newConfig, variation);
71                newConfigs.add(newConfig);
72            }
73        }
74        return newConfigs;
75    }
76
77    void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
78        mRecyclerView = inflateWrappedRV();
79
80        mRecyclerView.setHasFixedSize(true);
81        mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount)
82                : config.mTestAdapter;
83        mRecyclerView.setAdapter(mTestAdapter);
84        mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
85                config.mReverseLayout);
86        mLayoutManager.setStackFromEnd(config.mStackFromEnd);
87        mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
88        mRecyclerView.setLayoutManager(mLayoutManager);
89        if (config.mWrap) {
90            mRecyclerView.setLayoutParams(
91                    new ViewGroup.LayoutParams(
92                            config.mOrientation == HORIZONTAL ? WRAP_CONTENT : FILL_PARENT,
93                            config.mOrientation == VERTICAL ? WRAP_CONTENT : FILL_PARENT
94                    )
95            );
96        }
97        if (waitForFirstLayout) {
98            waitForFirstLayout();
99        }
100    }
101
102    public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
103            throws Throwable {
104        setupByConfig(new Config(VERTICAL, false, false), true);
105
106        mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
107            @Override
108            void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
109                if (state.isPreLayout()) {
110                    assertEquals("pending scroll position should still be pending",
111                            scrollPosition, mLayoutManager.mPendingScrollPosition);
112                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
113                        assertEquals("pending scroll position offset should still be pending",
114                                scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
115                    }
116                } else {
117                    RecyclerView.ViewHolder vh =
118                            mRecyclerView.findViewHolderForLayoutPosition(scrollPosition);
119                    assertNotNull("scroll to position should work", vh);
120                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
121                        assertEquals("scroll offset should be applied properly",
122                                mLayoutManager.getPaddingTop() + scrollOffset +
123                                        ((RecyclerView.LayoutParams) vh.itemView
124                                                .getLayoutParams()).topMargin,
125                                mLayoutManager.getDecoratedTop(vh.itemView));
126                    }
127                }
128            }
129        };
130        mLayoutManager.expectLayouts(2);
131        runTestOnUiThread(new Runnable() {
132            @Override
133            public void run() {
134                try {
135                    mTestAdapter.addAndNotify(0, 1);
136                    if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
137                        mLayoutManager.scrollToPosition(scrollPosition);
138                    } else {
139                        mLayoutManager.scrollToPositionWithOffset(scrollPosition,
140                                scrollOffset);
141                    }
142
143                } catch (Throwable throwable) {
144                    throwable.printStackTrace();
145                }
146
147            }
148        });
149        mLayoutManager.waitForLayout(2);
150        checkForMainThreadException();
151    }
152
153    protected void waitForFirstLayout() throws Throwable {
154        mLayoutManager.expectLayouts(1);
155        setRecyclerView(mRecyclerView);
156        mLayoutManager.waitForLayout(2);
157    }
158
159    void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
160        runTestOnUiThread(new Runnable() {
161            @Override
162            public void run() {
163                mLayoutManager.scrollToPositionWithOffset(position, offset);
164            }
165        });
166    }
167
168    public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
169            Map<Item, Rect> after, boolean strictItemEquality) {
170        Throwable throwable = null;
171        try {
172            assertRectSetsEqual("NOT " + message, before, after, strictItemEquality);
173        } catch (Throwable t) {
174            throwable = t;
175        }
176        assertNotNull(message + "\ntwo layout should be different", throwable);
177    }
178
179    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
180        assertRectSetsEqual(message, before, after, true);
181    }
182
183    public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after,
184            boolean strictItemEquality) {
185        StringBuilder sb = new StringBuilder();
186        sb.append("checking rectangle equality.\n");
187        sb.append("before:\n");
188        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
189            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
190        }
191        sb.append("after:\n");
192        for (Map.Entry<Item, Rect> entry : after.entrySet()) {
193            sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
194        }
195        message = message + "\n" + sb.toString();
196        assertEquals(message + ":\nitem counts should be equal", before.size()
197                , after.size());
198        for (Map.Entry<Item, Rect> entry : before.entrySet()) {
199            final Item beforeItem = entry.getKey();
200            Rect afterRect = null;
201            if (strictItemEquality) {
202                afterRect = after.get(beforeItem);
203                assertNotNull(message + ":\nSame item should be visible after simple re-layout",
204                        afterRect);
205            } else {
206                for (Map.Entry<Item, Rect> afterEntry : after.entrySet()) {
207                    final Item afterItem = afterEntry.getKey();
208                    if (afterItem.mAdapterIndex == beforeItem.mAdapterIndex) {
209                        afterRect = afterEntry.getValue();
210                        break;
211                    }
212                }
213                assertNotNull(message + ":\nItem with same adapter index should be visible " +
214                                "after simple re-layout",
215                        afterRect);
216            }
217            assertEquals(message + ":\nItem should be laid out at the same coordinates",
218                    entry.getValue(), afterRect);
219        }
220    }
221
222    static class VisibleChildren {
223
224        int firstVisiblePosition = RecyclerView.NO_POSITION;
225
226        int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
227
228        int lastVisiblePosition = RecyclerView.NO_POSITION;
229
230        int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
231
232        @Override
233        public String toString() {
234            return "VisibleChildren{" +
235                    "firstVisiblePosition=" + firstVisiblePosition +
236                    ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
237                    ", lastVisiblePosition=" + lastVisiblePosition +
238                    ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
239                    '}';
240        }
241    }
242
243    static class OnLayoutListener {
244
245        void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
246        }
247
248        void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
249        }
250    }
251
252    static class Config implements Cloneable {
253
254        static final int DEFAULT_ITEM_COUNT = 100;
255
256        boolean mStackFromEnd;
257
258        int mOrientation = VERTICAL;
259
260        boolean mReverseLayout = false;
261
262        boolean mRecycleChildrenOnDetach = false;
263
264        int mItemCount = DEFAULT_ITEM_COUNT;
265
266        boolean mWrap = false;
267
268        TestAdapter mTestAdapter;
269
270        Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
271            mOrientation = orientation;
272            mReverseLayout = reverseLayout;
273            mStackFromEnd = stackFromEnd;
274        }
275
276        public Config() {
277
278        }
279
280        Config adapter(TestAdapter adapter) {
281            mTestAdapter = adapter;
282            return this;
283        }
284
285        Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
286            mRecycleChildrenOnDetach = recycleChildrenOnDetach;
287            return this;
288        }
289
290        Config orientation(int orientation) {
291            mOrientation = orientation;
292            return this;
293        }
294
295        Config stackFromBottom(boolean stackFromBottom) {
296            mStackFromEnd = stackFromBottom;
297            return this;
298        }
299
300        Config reverseLayout(boolean reverseLayout) {
301            mReverseLayout = reverseLayout;
302            return this;
303        }
304
305        public Config itemCount(int itemCount) {
306            mItemCount = itemCount;
307            return this;
308        }
309
310        // required by convention
311        @Override
312        public Object clone() throws CloneNotSupportedException {
313            return super.clone();
314        }
315
316        @Override
317        public String toString() {
318            return "Config{" +
319                    "mStackFromEnd=" + mStackFromEnd +
320                    ", mOrientation=" + mOrientation +
321                    ", mReverseLayout=" + mReverseLayout +
322                    ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
323                    ", mItemCount=" + mItemCount +
324                    ", wrap=" + mWrap +
325                    '}';
326        }
327
328        public Config wrap(boolean wrap) {
329            mWrap = wrap;
330            return this;
331        }
332    }
333
334    class WrappedLinearLayoutManager extends LinearLayoutManager {
335
336        CountDownLatch layoutLatch;
337
338        OrientationHelper mSecondaryOrientation;
339
340        OnLayoutListener mOnLayoutListener;
341
342        public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
343            super(context, orientation, reverseLayout);
344        }
345
346        public void expectLayouts(int count) {
347            layoutLatch = new CountDownLatch(count);
348        }
349
350        public void waitForLayout(long timeout) throws InterruptedException {
351            waitForLayout(timeout, TimeUnit.SECONDS);
352        }
353
354        @Override
355        public void setOrientation(int orientation) {
356            super.setOrientation(orientation);
357            mSecondaryOrientation = null;
358        }
359
360        @Override
361        public void removeAndRecycleView(View child, RecyclerView.Recycler recycler) {
362            if (DEBUG) {
363                Log.d(TAG, "recycling view " + mRecyclerView.getChildViewHolder(child));
364            }
365            super.removeAndRecycleView(child, recycler);
366        }
367
368        @Override
369        public void removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler) {
370            if (DEBUG) {
371                Log.d(TAG,
372                        "recycling view at" + mRecyclerView.getChildViewHolder(getChildAt(index)));
373            }
374            super.removeAndRecycleViewAt(index, recycler);
375        }
376
377        @Override
378        void ensureLayoutState() {
379            super.ensureLayoutState();
380            if (mSecondaryOrientation == null) {
381                mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
382                        1 - getOrientation());
383            }
384        }
385
386        private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
387            layoutLatch.await(timeout * (DEBUG ? 100 : 1), timeUnit);
388            assertEquals("all expected layouts should be executed at the expected time",
389                    0, layoutLatch.getCount());
390            getInstrumentation().waitForIdleSync();
391        }
392
393        @Override
394        LayoutState createLayoutState() {
395            return new LayoutState() {
396                @Override
397                View next(RecyclerView.Recycler recycler) {
398                    final boolean hadMore = hasMore(mRecyclerView.mState);
399                    final int position = mCurrentPosition;
400                    View next = super.next(recycler);
401                    assertEquals("if has more, should return a view", hadMore, next != null);
402                    assertEquals("position of the returned view must match current position",
403                            position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
404                    return next;
405                }
406            };
407        }
408
409        public String getBoundsLog() {
410            StringBuilder sb = new StringBuilder();
411            sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
412                    .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
413            sb.append("\nchildren bounds\n");
414            final int childCount = getChildCount();
415            for (int i = 0; i < childCount; i++) {
416                View child = getChildAt(i);
417                sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
418                        .append("[").append("start:").append(
419                        mOrientationHelper.getDecoratedStart(child)).append(", end:")
420                        .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
421            }
422            return sb.toString();
423        }
424
425        public void waitForAnimationsToEnd(int timeoutInSeconds) throws InterruptedException {
426            RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator();
427            if (itemAnimator == null) {
428                return;
429            }
430            final CountDownLatch latch = new CountDownLatch(1);
431            final boolean running = itemAnimator.isRunning(
432                    new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
433                        @Override
434                        public void onAnimationsFinished() {
435                            latch.countDown();
436                        }
437                    }
438            );
439            if (running) {
440                latch.await(timeoutInSeconds, TimeUnit.SECONDS);
441            }
442        }
443
444        public VisibleChildren traverseAndFindVisibleChildren() {
445            int childCount = getChildCount();
446            final VisibleChildren visibleChildren = new VisibleChildren();
447            final int start = mOrientationHelper.getStartAfterPadding();
448            final int end = mOrientationHelper.getEndAfterPadding();
449            for (int i = 0; i < childCount; i++) {
450                View child = getChildAt(i);
451                final int childStart = mOrientationHelper.getDecoratedStart(child);
452                final int childEnd = mOrientationHelper.getDecoratedEnd(child);
453                final boolean fullyVisible = childStart >= start && childEnd <= end;
454                final boolean hidden = childEnd <= start || childStart >= end;
455                if (hidden) {
456                    continue;
457                }
458                final int position = getPosition(child);
459                if (fullyVisible) {
460                    if (position < visibleChildren.firstFullyVisiblePosition ||
461                            visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
462                        visibleChildren.firstFullyVisiblePosition = position;
463                    }
464
465                    if (position > visibleChildren.lastFullyVisiblePosition) {
466                        visibleChildren.lastFullyVisiblePosition = position;
467                    }
468                }
469
470                if (position < visibleChildren.firstVisiblePosition ||
471                        visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
472                    visibleChildren.firstVisiblePosition = position;
473                }
474
475                if (position > visibleChildren.lastVisiblePosition) {
476                    visibleChildren.lastVisiblePosition = position;
477                }
478
479            }
480            return visibleChildren;
481        }
482
483        Rect getViewBounds(View view) {
484            if (getOrientation() == HORIZONTAL) {
485                return new Rect(
486                        mOrientationHelper.getDecoratedStart(view),
487                        mSecondaryOrientation.getDecoratedStart(view),
488                        mOrientationHelper.getDecoratedEnd(view),
489                        mSecondaryOrientation.getDecoratedEnd(view));
490            } else {
491                return new Rect(
492                        mSecondaryOrientation.getDecoratedStart(view),
493                        mOrientationHelper.getDecoratedStart(view),
494                        mSecondaryOrientation.getDecoratedEnd(view),
495                        mOrientationHelper.getDecoratedEnd(view));
496            }
497
498        }
499
500        Map<Item, Rect> collectChildCoordinates() throws Throwable {
501            final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
502            runTestOnUiThread(new Runnable() {
503                @Override
504                public void run() {
505                    final int childCount = getChildCount();
506                    Rect layoutBounds = new Rect(0, 0,
507                            mLayoutManager.getWidth(), mLayoutManager.getHeight());
508                    for (int i = 0; i < childCount; i++) {
509                        View child = getChildAt(i);
510                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
511                                .getLayoutParams();
512                        TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
513                        Rect childBounds = getViewBounds(child);
514                        if (new Rect(childBounds).intersect(layoutBounds)) {
515                            items.put(vh.mBoundItem, childBounds);
516                        }
517                    }
518                }
519            });
520            return items;
521        }
522
523        @Override
524        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
525            try {
526                if (mOnLayoutListener != null) {
527                    mOnLayoutListener.before(recycler, state);
528                }
529                super.onLayoutChildren(recycler, state);
530                if (mOnLayoutListener != null) {
531                    mOnLayoutListener.after(recycler, state);
532                }
533            } catch (Throwable t) {
534                postExceptionToInstrumentation(t);
535            }
536            layoutLatch.countDown();
537        }
538
539
540    }
541}
542