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