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