• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v7.widget;
18 
19 import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
20 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
21 
22 import static org.junit.Assert.assertEquals;
23 import static org.junit.Assert.assertNotNull;
24 import static org.junit.Assert.assertSame;
25 import static org.junit.Assert.assertTrue;
26 
27 import android.support.v4.view.AccessibilityDelegateCompat;
28 import android.support.v4.view.accessibility.AccessibilityEventCompat;
29 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
30 import android.test.suitebuilder.annotation.MediumTest;
31 import android.util.Log;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.accessibility.AccessibilityEvent;
35 
36 import org.junit.Test;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 import java.util.concurrent.atomic.AtomicInteger;
41 
42 /**
43  * Includes tests for {@link LinearLayoutManager}.
44  * <p>
45  * Since most UI tests are not practical, these tests are focused on internal data representation
46  * and stability of LinearLayoutManager in response to different events (state change, scrolling
47  * etc) where it is very hard to do manual testing.
48  */
49 @MediumTest
50 public class LinearLayoutManagerTest extends BaseLinearLayoutManagerTest {
51 
52     @Test
removeAnchorItem()53     public void removeAnchorItem() throws Throwable {
54         removeAnchorItemTest(
55                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(
56                         false), 100, 0);
57     }
58 
59     @Test
removeAnchorItemReverse()60     public void removeAnchorItemReverse() throws Throwable {
61         removeAnchorItemTest(
62                 new Config().orientation(VERTICAL).stackFromBottom(false).reverseLayout(true), 100,
63                 0);
64     }
65 
66     @Test
removeAnchorItemStackFromEnd()67     public void removeAnchorItemStackFromEnd() throws Throwable {
68         removeAnchorItemTest(
69                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(false), 100,
70                 99);
71     }
72 
73     @Test
removeAnchorItemStackFromEndAndReverse()74     public void removeAnchorItemStackFromEndAndReverse() throws Throwable {
75         removeAnchorItemTest(
76                 new Config().orientation(VERTICAL).stackFromBottom(true).reverseLayout(true), 100,
77                 99);
78     }
79 
80     @Test
removeAnchorItemHorizontal()81     public void removeAnchorItemHorizontal() throws Throwable {
82         removeAnchorItemTest(
83                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(
84                         false), 100, 0);
85     }
86 
87     @Test
removeAnchorItemReverseHorizontal()88     public void removeAnchorItemReverseHorizontal() throws Throwable {
89         removeAnchorItemTest(
90                 new Config().orientation(HORIZONTAL).stackFromBottom(false).reverseLayout(true),
91                 100, 0);
92     }
93 
94     @Test
removeAnchorItemStackFromEndHorizontal()95     public void removeAnchorItemStackFromEndHorizontal() throws Throwable {
96         removeAnchorItemTest(
97                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(false),
98                 100, 99);
99     }
100 
101     @Test
removeAnchorItemStackFromEndAndReverseHorizontal()102     public void removeAnchorItemStackFromEndAndReverseHorizontal() throws Throwable {
103         removeAnchorItemTest(
104                 new Config().orientation(HORIZONTAL).stackFromBottom(true).reverseLayout(true), 100,
105                 99);
106     }
107 
108     /**
109      * This tests a regression where predictive animations were not working as expected when the
110      * first item is removed and there aren't any more items to add from that direction.
111      * First item refers to the default anchor item.
112      */
removeAnchorItemTest(final Config config, int adapterSize, final int removePos)113     public void removeAnchorItemTest(final Config config, int adapterSize,
114             final int removePos) throws Throwable {
115         config.adapter(new TestAdapter(adapterSize) {
116             @Override
117             public void onBindViewHolder(TestViewHolder holder,
118                     int position) {
119                 super.onBindViewHolder(holder, position);
120                 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
121                 if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
122                     lp = new ViewGroup.MarginLayoutParams(0, 0);
123                     holder.itemView.setLayoutParams(lp);
124                 }
125                 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
126                 final int maxSize;
127                 if (config.mOrientation == HORIZONTAL) {
128                     maxSize = mRecyclerView.getWidth();
129                     mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
130                 } else {
131                     maxSize = mRecyclerView.getHeight();
132                     mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
133                 }
134 
135                 final int desiredSize;
136                 if (position == removePos) {
137                     // make it large
138                     desiredSize = maxSize / 4;
139                 } else {
140                     // make it small
141                     desiredSize = maxSize / 8;
142                 }
143                 if (config.mOrientation == HORIZONTAL) {
144                     mlp.width = desiredSize;
145                 } else {
146                     mlp.height = desiredSize;
147                 }
148             }
149         });
150         setupByConfig(config, true);
151         final int childCount = mLayoutManager.getChildCount();
152         RecyclerView.ViewHolder toBeRemoved = null;
153         List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
154         for (int i = 0; i < childCount; i++) {
155             View child = mLayoutManager.getChildAt(i);
156             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
157             if (holder.getAdapterPosition() == removePos) {
158                 toBeRemoved = holder;
159             } else {
160                 toBeMoved.add(holder);
161             }
162         }
163         assertNotNull("test sanity", toBeRemoved);
164         assertEquals("test sanity", childCount - 1, toBeMoved.size());
165         LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
166         mRecyclerView.setItemAnimator(loggingItemAnimator);
167         loggingItemAnimator.reset();
168         loggingItemAnimator.expectRunPendingAnimationsCall(1);
169         mLayoutManager.expectLayouts(2);
170         mTestAdapter.deleteAndNotify(removePos, 1);
171         mLayoutManager.waitForLayout(1);
172         loggingItemAnimator.waitForPendingAnimationsCall(2);
173         assertTrue("removed child should receive remove animation",
174                 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
175         for (RecyclerView.ViewHolder vh : toBeMoved) {
176             assertTrue("view holder should be in moved list",
177                     loggingItemAnimator.mMoveVHs.contains(vh));
178         }
179         List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
180         for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
181             View child = mLayoutManager.getChildAt(i);
182             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
183             if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
184                 newHolders.add(holder);
185             }
186         }
187         assertTrue("some new children should show up for the new space", newHolders.size() > 0);
188         assertEquals("no items should receive animate add since they are not new", 0,
189                 loggingItemAnimator.mAddVHs.size());
190         for (RecyclerView.ViewHolder holder : newHolders) {
191             assertTrue("new holder should receive a move animation",
192                     loggingItemAnimator.mMoveVHs.contains(holder));
193         }
194         assertTrue("control against adding too many children due to bad layout state preparation."
195                         + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
196                 mRecyclerView.getChildCount() <= childCount + 3 /*1 for removed view, 2 for its size*/);
197     }
198 
199     @Test
keepFocusOnRelayout()200     public void keepFocusOnRelayout() throws Throwable {
201         setupByConfig(new Config(VERTICAL, false, false).itemCount(500), true);
202         int center = (mLayoutManager.findLastVisibleItemPosition()
203                 - mLayoutManager.findFirstVisibleItemPosition()) / 2;
204         final RecyclerView.ViewHolder vh = mRecyclerView.findViewHolderForLayoutPosition(center);
205         final int top = mLayoutManager.mOrientationHelper.getDecoratedStart(vh.itemView);
206         requestFocus(vh.itemView, true);
207         assertTrue("view should have the focus", vh.itemView.hasFocus());
208         // add a bunch of items right before that view, make sure it keeps its position
209         mLayoutManager.expectLayouts(2);
210         final int childCountToAdd = mRecyclerView.getChildCount() * 2;
211         mTestAdapter.addAndNotify(center, childCountToAdd);
212         center += childCountToAdd; // offset item
213         mLayoutManager.waitForLayout(2);
214         mLayoutManager.waitForAnimationsToEnd(20);
215         final RecyclerView.ViewHolder postVH = mRecyclerView.findViewHolderForLayoutPosition(center);
216         assertNotNull("focused child should stay in layout", postVH);
217         assertSame("same view holder should be kept for unchanged child", vh, postVH);
218         assertEquals("focused child's screen position should stay unchanged", top,
219                 mLayoutManager.mOrientationHelper.getDecoratedStart(postVH.itemView));
220     }
221 
222     @Test
keepFullFocusOnResize()223     public void keepFullFocusOnResize() throws Throwable {
224         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), true);
225     }
226 
227     @Test
keepPartialFocusOnResize()228     public void keepPartialFocusOnResize() throws Throwable {
229         keepFocusOnResizeTest(new Config(VERTICAL, false, false).itemCount(500), false);
230     }
231 
232     @Test
keepReverseFullFocusOnResize()233     public void keepReverseFullFocusOnResize() throws Throwable {
234         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), true);
235     }
236 
237     @Test
keepReversePartialFocusOnResize()238     public void keepReversePartialFocusOnResize() throws Throwable {
239         keepFocusOnResizeTest(new Config(VERTICAL, true, false).itemCount(500), false);
240     }
241 
242     @Test
keepStackFromEndFullFocusOnResize()243     public void keepStackFromEndFullFocusOnResize() throws Throwable {
244         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), true);
245     }
246 
247     @Test
keepStackFromEndPartialFocusOnResize()248     public void keepStackFromEndPartialFocusOnResize() throws Throwable {
249         keepFocusOnResizeTest(new Config(VERTICAL, false, true).itemCount(500), false);
250     }
251 
keepFocusOnResizeTest(final Config config, boolean fullyVisible)252     public void keepFocusOnResizeTest(final Config config, boolean fullyVisible) throws Throwable {
253         setupByConfig(config, true);
254         final int targetPosition;
255         if (config.mStackFromEnd) {
256             targetPosition = mLayoutManager.findFirstVisibleItemPosition();
257         } else {
258             targetPosition = mLayoutManager.findLastVisibleItemPosition();
259         }
260         final OrientationHelper helper = mLayoutManager.mOrientationHelper;
261         final RecyclerView.ViewHolder vh = mRecyclerView
262                 .findViewHolderForLayoutPosition(targetPosition);
263 
264         // scroll enough to offset the child
265         int startMargin = helper.getDecoratedStart(vh.itemView) -
266                 helper.getStartAfterPadding();
267         int endMargin = helper.getEndAfterPadding() -
268                 helper.getDecoratedEnd(vh.itemView);
269         Log.d(TAG, "initial start margin " + startMargin + " , end margin:" + endMargin);
270         requestFocus(vh.itemView, true);
271         assertTrue("view should gain the focus", vh.itemView.hasFocus());
272         // scroll enough to offset the child
273         startMargin = helper.getDecoratedStart(vh.itemView) -
274                 helper.getStartAfterPadding();
275         endMargin = helper.getEndAfterPadding() -
276                 helper.getDecoratedEnd(vh.itemView);
277 
278         Log.d(TAG, "start margin " + startMargin + " , end margin:" + endMargin);
279         assertTrue("View should become fully visible", startMargin >= 0 && endMargin >= 0);
280 
281         int expectedOffset = 0;
282         boolean offsetAtStart = false;
283         if (!fullyVisible) {
284             // move it a bit such that it is no more fully visible
285             final int childSize = helper
286                     .getDecoratedMeasurement(vh.itemView);
287             expectedOffset = childSize / 3;
288             if (startMargin < endMargin) {
289                 scrollBy(expectedOffset);
290                 offsetAtStart = true;
291             } else {
292                 scrollBy(-expectedOffset);
293                 offsetAtStart = false;
294             }
295             startMargin = helper.getDecoratedStart(vh.itemView) -
296                     helper.getStartAfterPadding();
297             endMargin = helper.getEndAfterPadding() -
298                     helper.getDecoratedEnd(vh.itemView);
299             assertTrue("test sanity, view should not be fully visible", startMargin < 0
300                     || endMargin < 0);
301         }
302 
303         mLayoutManager.expectLayouts(1);
304         runTestOnUiThread(new Runnable() {
305             @Override
306             public void run() {
307                 final ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams();
308                 if (config.mOrientation == HORIZONTAL) {
309                     layoutParams.width = mRecyclerView.getWidth() / 2;
310                 } else {
311                     layoutParams.height = mRecyclerView.getHeight() / 2;
312                 }
313                 mRecyclerView.setLayoutParams(layoutParams);
314             }
315         });
316         Thread.sleep(100);
317         // add a bunch of items right before that view, make sure it keeps its position
318         mLayoutManager.waitForLayout(2);
319         mLayoutManager.waitForAnimationsToEnd(20);
320         assertTrue("view should preserve the focus", vh.itemView.hasFocus());
321         final RecyclerView.ViewHolder postVH = mRecyclerView
322                 .findViewHolderForLayoutPosition(targetPosition);
323         assertNotNull("focused child should stay in layout", postVH);
324         assertSame("same view holder should be kept for unchanged child", vh, postVH);
325         View focused = postVH.itemView;
326 
327         startMargin = helper.getDecoratedStart(focused) - helper.getStartAfterPadding();
328         endMargin = helper.getEndAfterPadding() - helper.getDecoratedEnd(focused);
329 
330         assertTrue("focused child should be somewhat visible",
331                 helper.getDecoratedStart(focused) < helper.getEndAfterPadding()
332                         && helper.getDecoratedEnd(focused) > helper.getStartAfterPadding());
333         if (fullyVisible) {
334             assertTrue("focused child end should stay fully visible",
335                     endMargin >= 0);
336             assertTrue("focused child start should stay fully visible",
337                     startMargin >= 0);
338         } else {
339             if (offsetAtStart) {
340                 assertTrue("start should preserve its offset", startMargin < 0);
341                 assertTrue("end should be visible", endMargin >= 0);
342             } else {
343                 assertTrue("end should preserve its offset", endMargin < 0);
344                 assertTrue("start should be visible", startMargin >= 0);
345             }
346         }
347     }
348 
349     @Test
scrollToPositionWithPredictive()350     public void scrollToPositionWithPredictive() throws Throwable {
351         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
352         removeRecyclerView();
353         scrollToPositionWithPredictive(3, 20);
354         removeRecyclerView();
355         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
356                 LinearLayoutManager.INVALID_OFFSET);
357         removeRecyclerView();
358         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
359     }
360 
361     @Test
recycleDuringAnimations()362     public void recycleDuringAnimations() throws Throwable {
363         final AtomicInteger childCount = new AtomicInteger(0);
364         final TestAdapter adapter = new TestAdapter(300) {
365             @Override
366             public TestViewHolder onCreateViewHolder(ViewGroup parent,
367                     int viewType) {
368                 final int cnt = childCount.incrementAndGet();
369                 final TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
370                 if (DEBUG) {
371                     Log.d(TAG, "CHILD_CNT(create):" + cnt + ", " + testViewHolder);
372                 }
373                 return testViewHolder;
374             }
375         };
376         setupByConfig(new Config(VERTICAL, false, false).itemCount(300)
377                 .adapter(adapter), true);
378 
379         final RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
380             @Override
381             public void putRecycledView(RecyclerView.ViewHolder scrap) {
382                 super.putRecycledView(scrap);
383                 int cnt = childCount.decrementAndGet();
384                 if (DEBUG) {
385                     Log.d(TAG, "CHILD_CNT(put):" + cnt + ", " + scrap);
386                 }
387             }
388 
389             @Override
390             public RecyclerView.ViewHolder getRecycledView(int viewType) {
391                 final RecyclerView.ViewHolder recycledView = super.getRecycledView(viewType);
392                 if (recycledView != null) {
393                     final int cnt = childCount.incrementAndGet();
394                     if (DEBUG) {
395                         Log.d(TAG, "CHILD_CNT(get):" + cnt + ", " + recycledView);
396                     }
397                 }
398                 return recycledView;
399             }
400         };
401         pool.setMaxRecycledViews(mTestAdapter.getItemViewType(0), 500);
402         mRecyclerView.setRecycledViewPool(pool);
403 
404 
405         // now keep adding children to trigger more children being created etc.
406         for (int i = 0; i < 100; i ++) {
407             adapter.addAndNotify(15, 1);
408             Thread.sleep(15);
409         }
410         getInstrumentation().waitForIdleSync();
411         waitForAnimations(2);
412         assertEquals("Children count should add up", childCount.get(),
413                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
414 
415         // now trigger lots of add again, followed by a scroll to position
416         for (int i = 0; i < 100; i ++) {
417             adapter.addAndNotify(5 + (i % 3) * 3, 1);
418             Thread.sleep(25);
419         }
420         smoothScrollToPosition(mLayoutManager.findLastVisibleItemPosition() + 20);
421         waitForAnimations(2);
422         getInstrumentation().waitForIdleSync();
423         assertEquals("Children count should add up", childCount.get(),
424                 mRecyclerView.getChildCount() + mRecyclerView.mRecycler.mCachedViews.size());
425     }
426 
427 
428     @Test
dontRecycleChildrenOnDetach()429     public void dontRecycleChildrenOnDetach() throws Throwable {
430         setupByConfig(new Config().recycleChildrenOnDetach(false), true);
431         runTestOnUiThread(new Runnable() {
432             @Override
433             public void run() {
434                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
435                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
436                 assertEquals("No views are recycled", recyclerSize,
437                         mRecyclerView.mRecycler.getRecycledViewPool().size());
438             }
439         });
440     }
441 
442     @Test
recycleChildrenOnDetach()443     public void recycleChildrenOnDetach() throws Throwable {
444         setupByConfig(new Config().recycleChildrenOnDetach(true), true);
445         final int childCount = mLayoutManager.getChildCount();
446         runTestOnUiThread(new Runnable() {
447             @Override
448             public void run() {
449                 int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
450                 mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
451                         mTestAdapter.getItemViewType(0), recyclerSize + childCount);
452                 ((ViewGroup)mRecyclerView.getParent()).removeView(mRecyclerView);
453                 assertEquals("All children should be recycled", childCount + recyclerSize,
454                         mRecyclerView.mRecycler.getRecycledViewPool().size());
455             }
456         });
457     }
458 
459     @Test
scrollAndClear()460     public void scrollAndClear() throws Throwable {
461         setupByConfig(new Config(), true);
462 
463         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
464 
465         mLayoutManager.expectLayouts(1);
466         runTestOnUiThread(new Runnable() {
467             @Override
468             public void run() {
469                 mLayoutManager.scrollToPositionWithOffset(1, 0);
470                 mTestAdapter.clearOnUIThread();
471             }
472         });
473         mLayoutManager.waitForLayout(2);
474 
475         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
476     }
477 
478 
479     @Test
accessibilityPositions()480     public void accessibilityPositions() throws Throwable {
481         setupByConfig(new Config(VERTICAL, false, false), true);
482         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
483                 .getCompatAccessibilityDelegate();
484         final AccessibilityEvent event = AccessibilityEvent.obtain();
485         runTestOnUiThread(new Runnable() {
486             @Override
487             public void run() {
488                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
489             }
490         });
491         final AccessibilityRecordCompat record = AccessibilityEventCompat
492                 .asRecord(event);
493         assertEquals("result should have first position",
494                 record.getFromIndex(),
495                 mLayoutManager.findFirstVisibleItemPosition());
496         assertEquals("result should have last position",
497                 record.getToIndex(),
498                 mLayoutManager.findLastVisibleItemPosition());
499     }
500 }
501