• 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 android.content.Context;
19 import android.graphics.Rect;
20 import android.util.Log;
21 import android.view.View;
22 import android.view.ViewGroup;
23 
24 import java.lang.reflect.Field;
25 import java.util.ArrayList;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.concurrent.CountDownLatch;
30 import java.util.concurrent.TimeUnit;
31 
32 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
33 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
34 import static android.view.ViewGroup.LayoutParams.FILL_PARENT;
35 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
36 import static org.junit.Assert.*;
37 
38 import static java.util.concurrent.TimeUnit.SECONDS;
39 
40 import org.hamcrest.CoreMatchers;
41 import org.hamcrest.MatcherAssert;
42 
43 public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
44 
45     protected static final boolean DEBUG = false;
46     protected static final String TAG = "LinearLayoutManagerTest";
47 
createBaseVariations()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 
addConfigVariation(List<Config> base, String fieldName, Object... variations)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 
setupByConfig(Config config, boolean waitForFirstLayout)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 
scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)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 
waitForFirstLayout()158     protected void waitForFirstLayout() throws Throwable {
159         mLayoutManager.expectLayouts(1);
160         setRecyclerView(mRecyclerView);
161         mLayoutManager.waitForLayout(2);
162     }
163 
scrollToPositionWithOffset(final int position, final int offset)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 
assertRectSetsNotEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)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 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after)184     public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
185         assertRectSetsEqual(message, before, after, true);
186     }
187 
assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after, boolean strictItemEquality)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
toString()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 
before(RecyclerView.Recycler recycler, RecyclerView.State state)250         void before(RecyclerView.Recycler recycler, RecyclerView.State state) {
251         }
252 
after(RecyclerView.Recycler recycler, RecyclerView.State state)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 
Config(int orientation, boolean reverseLayout, boolean stackFromEnd)275         Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
276             mOrientation = orientation;
277             mReverseLayout = reverseLayout;
278             mStackFromEnd = stackFromEnd;
279         }
280 
Config()281         public Config() {
282 
283         }
284 
adapter(TestAdapter adapter)285         Config adapter(TestAdapter adapter) {
286             mTestAdapter = adapter;
287             return this;
288         }
289 
recycleChildrenOnDetach(boolean recycleChildrenOnDetach)290         Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
291             mRecycleChildrenOnDetach = recycleChildrenOnDetach;
292             return this;
293         }
294 
orientation(int orientation)295         Config orientation(int orientation) {
296             mOrientation = orientation;
297             return this;
298         }
299 
stackFromBottom(boolean stackFromBottom)300         Config stackFromBottom(boolean stackFromBottom) {
301             mStackFromEnd = stackFromBottom;
302             return this;
303         }
304 
reverseLayout(boolean reverseLayout)305         Config reverseLayout(boolean reverseLayout) {
306             mReverseLayout = reverseLayout;
307             return this;
308         }
309 
itemCount(int itemCount)310         public Config itemCount(int itemCount) {
311             mItemCount = itemCount;
312             return this;
313         }
314 
315         // required by convention
316         @Override
clone()317         public Object clone() throws CloneNotSupportedException {
318             return super.clone();
319         }
320 
321         @Override
toString()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 
wrap(boolean wrap)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 
WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout)347         public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
348             super(context, orientation, reverseLayout);
349         }
350 
expectLayouts(int count)351         public void expectLayouts(int count) {
352             layoutLatch = new CountDownLatch(count);
353         }
354 
waitForLayout(int seconds)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
setOrientation(int orientation)369         public void setOrientation(int orientation) {
370             super.setOrientation(orientation);
371             mSecondaryOrientation = null;
372         }
373 
374         @Override
removeAndRecycleView(View child, RecyclerView.Recycler recycler)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
removeAndRecycleViewAt(int index, RecyclerView.Recycler recycler)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
ensureLayoutState()392         void ensureLayoutState() {
393             super.ensureLayoutState();
394             if (mSecondaryOrientation == null) {
395                 mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
396                         1 - getOrientation());
397             }
398         }
399 
400         @Override
createLayoutState()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 
getBoundsLog()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 
waitForAnimationsToEnd(int timeoutInSeconds)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 
traverseAndFindVisibleChildren()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 
getViewBounds(View view)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 
collectChildCoordinates()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
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)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