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