• 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 
18 package android.support.v7.widget;
19 
20 
21 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
22 import static android.support.v7.widget.StaggeredGridLayoutManager
23         .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
24 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
25 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
26 import static android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
27 
28 import static org.junit.Assert.assertEquals;
29 import static org.junit.Assert.assertFalse;
30 import static org.junit.Assert.assertNotNull;
31 import static org.junit.Assert.assertNull;
32 import static org.junit.Assert.assertSame;
33 import static org.junit.Assert.assertThat;
34 import static org.junit.Assert.assertTrue;
35 
36 import android.graphics.Color;
37 import android.graphics.Rect;
38 import android.graphics.drawable.ColorDrawable;
39 import android.graphics.drawable.StateListDrawable;
40 import android.os.Parcel;
41 import android.os.Parcelable;
42 import android.support.v4.view.AccessibilityDelegateCompat;
43 import android.support.v4.view.accessibility.AccessibilityEventCompat;
44 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
45 import android.test.suitebuilder.annotation.MediumTest;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.util.StateSet;
49 import android.view.View;
50 import android.view.ViewGroup;
51 import android.view.accessibility.AccessibilityEvent;
52 import android.widget.EditText;
53 import android.widget.FrameLayout;
54 
55 import org.hamcrest.CoreMatchers;
56 import org.hamcrest.MatcherAssert;
57 import org.junit.Test;
58 
59 import java.util.HashMap;
60 import java.util.Map;
61 import java.util.UUID;
62 
63 
64 @MediumTest
65 public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
66     @Test
forceLayoutOnDetach()67     public void forceLayoutOnDetach() throws Throwable {
68         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
69         waitFirstLayout();
70         assertFalse("test sanity", mRecyclerView.isLayoutRequested());
71         runTestOnUiThread(new Runnable() {
72             @Override
73             public void run() {
74                 mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler);
75             }
76         });
77         assertTrue(mRecyclerView.isLayoutRequested());
78     }
79     @Test
areAllStartsTheSame()80     public void areAllStartsTheSame() throws Throwable {
81         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
82         waitFirstLayout();
83         smoothScrollToPosition(100);
84         mLayoutManager.expectLayouts(1);
85         mAdapter.deleteAndNotify(0, 2);
86         mLayoutManager.waitForLayout(2);
87         smoothScrollToPosition(0);
88         assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
89     }
90 
91     @Test
areAllEndsTheSame()92     public void areAllEndsTheSame() throws Throwable {
93         setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
94         waitFirstLayout();
95         smoothScrollToPosition(100);
96         mLayoutManager.expectLayouts(1);
97         mAdapter.deleteAndNotify(0, 2);
98         mLayoutManager.waitForLayout(2);
99         smoothScrollToPosition(0);
100         assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
101     }
102 
103     @Test
getPositionsBeforeInitialization()104     public void getPositionsBeforeInitialization() throws Throwable {
105         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
106         int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null);
107         MatcherAssert.assertThat(positions,
108                 CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION,
109                         RecyclerView.NO_POSITION}));
110     }
111 
112     @Test
findLastInUnevenDistribution()113     public void findLastInUnevenDistribution() throws Throwable {
114         setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
115                 .itemCount(5));
116         mAdapter.mOnBindCallback = new OnBindCallback() {
117             @Override
118             void onBoundItem(TestViewHolder vh, int position) {
119                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
120                 if (position == 1) {
121                     lp.height = mRecyclerView.getHeight() - 10;
122                 } else {
123                     lp.height = 5;
124                 }
125                 vh.itemView.setMinimumHeight(0);
126             }
127         };
128         waitFirstLayout();
129         int[] into = new int[2];
130         mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
131         assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
132         assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
133         mLayoutManager.findLastCompletelyVisibleItemPositions(into);
134         assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
135         assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
136         assertEquals("first fully visible child should be at position",
137                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
138                         findFirstVisibleItemClosestToStart(true, true)).getPosition());
139         assertEquals("last fully visible child should be at position",
140                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
141                         findFirstVisibleItemClosestToEnd(true, true)).getPosition());
142 
143         assertEquals("first visible child should be at position",
144                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
145                         findFirstVisibleItemClosestToStart(false, true)).getPosition());
146         assertEquals("last visible child should be at position",
147                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
148                         findFirstVisibleItemClosestToEnd(false, true)).getPosition());
149 
150     }
151 
152     @Test
customWidthInHorizontal()153     public void customWidthInHorizontal() throws Throwable {
154         customSizeInScrollDirectionTest(
155                 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
156     }
157 
158     @Test
customHeightInVertical()159     public void customHeightInVertical() throws Throwable {
160         customSizeInScrollDirectionTest(
161                 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
162     }
163 
customSizeInScrollDirectionTest(final Config config)164     public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
165         setupByConfig(config);
166         final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
167         mAdapter.mOnBindCallback = new OnBindCallback() {
168             @Override
169             void onBoundItem(TestViewHolder vh, int position) {
170                 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
171                 final int size = 1 + position * 5;
172                 if (config.mOrientation == HORIZONTAL) {
173                     layoutParams.width = size;
174                 } else {
175                     layoutParams.height = size;
176                 }
177                 sizeMap.put(vh.itemView, size);
178                 if (position == 3) {
179                     getLp(vh.itemView).setFullSpan(true);
180                 }
181             }
182 
183             @Override
184             boolean assignRandomSize() {
185                 return false;
186             }
187         };
188         waitFirstLayout();
189         assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
190         for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
191             View child = mRecyclerView.getChildAt(i);
192             final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
193                     : child.getHeight();
194             assertEquals("child " + i + " should have the size specified in its layout params",
195                     sizeMap.get(child).intValue(), size);
196         }
197         checkForMainThreadException();
198     }
199 
200     @Test
gapHandlingWhenItemMovesToTop()201     public void gapHandlingWhenItemMovesToTop() throws Throwable {
202         gapHandlingWhenItemMovesToTopTest();
203     }
204 
205     @Test
gapHandlingWhenItemMovesToTopWithFullSpan()206     public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
207         gapHandlingWhenItemMovesToTopTest(0);
208     }
209 
210     @Test
gapHandlingWhenItemMovesToTopWithFullSpan2()211     public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
212         gapHandlingWhenItemMovesToTopTest(1);
213     }
214 
gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices)215     public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
216         Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
217         config.itemCount(3);
218         setupByConfig(config);
219         mAdapter.mOnBindCallback = new OnBindCallback() {
220             @Override
221             void onBoundItem(TestViewHolder vh, int position) {
222             }
223 
224             @Override
225             boolean assignRandomSize() {
226                 return false;
227             }
228         };
229         for (int i : fullSpanIndices) {
230             mAdapter.mFullSpanItems.add(i);
231         }
232         waitFirstLayout();
233         mLayoutManager.expectLayouts(1);
234         mAdapter.moveItem(1, 0, true);
235         mLayoutManager.waitForLayout(2);
236         final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
237         // move back.
238         mLayoutManager.expectLayouts(1);
239         mAdapter.moveItem(0, 1, true);
240         mLayoutManager.waitForLayout(2);
241         mLayoutManager.expectLayouts(2);
242         mAdapter.moveAndNotify(1, 0);
243         mLayoutManager.waitForLayout(2);
244         Thread.sleep(1000);
245         getInstrumentation().waitForIdleSync();
246         checkForMainThreadException();
247         // item should be positioned properly
248         assertRectSetsEqual("final position after a move", desiredPositions,
249                 mLayoutManager.collectChildCoordinates());
250 
251     }
252 
253     @Test
focusSearchFailureUp()254     public void focusSearchFailureUp() throws Throwable {
255         focusSearchFailure(false);
256     }
257 
258     @Test
focusSearchFailureDown()259     public void focusSearchFailureDown() throws Throwable {
260         focusSearchFailure(true);
261     }
262 
263     @Test
focusSearchFailureFromSubChild()264     public void focusSearchFailureFromSubChild() throws Throwable {
265         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
266                 new GridTestAdapter(1000, VERTICAL) {
267 
268                     @Override
269                     public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
270                         FrameLayout fl = new FrameLayout(parent.getContext());
271                         EditText editText = new EditText(parent.getContext());
272                         fl.addView(editText);
273                         editText.setEllipsize(TextUtils.TruncateAt.END);
274                         return new TestViewHolder(fl);
275                     }
276 
277                     @Override
278                     public void onBindViewHolder(TestViewHolder holder, int position) {
279                         Item item = mItems.get(position);
280                         holder.mBoundItem = item;
281                         ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
282                                 item.mText + " (" + item.mId + ")");
283                     }
284                 });
285         waitFirstLayout();
286         ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
287                 mRecyclerView.getChildCount() - 1);
288         RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
289         View subChildToFocus = lastChild.getChildAt(0);
290         requestFocus(subChildToFocus, true);
291         assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true));
292         focusSearch(subChildToFocus, View.FOCUS_FORWARD);
293         waitForIdleScroll(mRecyclerView);
294         checkForMainThreadException();
295         View focusedChild = mRecyclerView.getFocusedChild();
296         if (focusedChild == subChildToFocus.getParent()) {
297             focusSearch(focusedChild, View.FOCUS_FORWARD);
298             waitForIdleScroll(mRecyclerView);
299             focusedChild = mRecyclerView.getFocusedChild();
300         }
301         RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder(
302                 focusedChild);
303         assertTrue("new focused view should have a larger position "
304                         + lastViewHolder.getAdapterPosition() + " vs "
305                         + containingViewHolder.getAdapterPosition(),
306                 lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition());
307     }
308 
focusSearchFailure(boolean scrollDown)309     public void focusSearchFailure(boolean scrollDown) throws Throwable {
310         int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP;
311         setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
312                 , new GridTestAdapter(31, 1) {
313                     RecyclerView mAttachedRv;
314 
315                     @Override
316                     public TestViewHolder onCreateViewHolder(ViewGroup parent,
317                             int viewType) {
318                         TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
319                         testViewHolder.itemView.setFocusable(true);
320                         testViewHolder.itemView.setFocusableInTouchMode(true);
321                         // Good to have colors for debugging
322                         StateListDrawable stl = new StateListDrawable();
323                         stl.addState(new int[]{android.R.attr.state_focused},
324                                 new ColorDrawable(Color.RED));
325                         stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
326                         testViewHolder.itemView.setBackground(stl);
327                         return testViewHolder;
328                     }
329 
330                     @Override
331                     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
332                         mAttachedRv = recyclerView;
333                     }
334 
335                     @Override
336                     public void onBindViewHolder(TestViewHolder holder,
337                             int position) {
338                         super.onBindViewHolder(holder, position);
339                         holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
340                     }
341                 });
342         /**
343          * 0  1  2
344          * 3  4  5
345          * 6  7  8
346          * 9  10 11
347          * 12 13 14
348          * 15 16 17
349          * 18 18 18
350          * 19
351          * 20 20 20
352          * 21 22
353          * 23 23 23
354          * 24 25 26
355          * 27 28 29
356          * 30
357          */
358         mAdapter.mFullSpanItems.add(18);
359         mAdapter.mFullSpanItems.add(20);
360         mAdapter.mFullSpanItems.add(23);
361         waitFirstLayout();
362         View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView;
363         assertTrue(requestFocus(viewToFocus, true));
364         assertSame(viewToFocus, mRecyclerView.getFocusedChild());
365         int pos = 1;
366         View focusedView = viewToFocus;
367         while (pos < 16) {
368             focusSearchAndWaitForScroll(focusedView, focusDir);
369             focusedView = mRecyclerView.getFocusedChild();
370             assertEquals(pos + 3,
371                     mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
372             pos += 3;
373         }
374         for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
375             focusSearchAndWaitForScroll(focusedView, focusDir);
376             focusedView = mRecyclerView.getFocusedChild();
377             assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
378         }
379         // now move right
380         focusSearch(focusedView, View.FOCUS_RIGHT);
381         waitForIdleScroll(mRecyclerView);
382         focusedView = mRecyclerView.getFocusedChild();
383         assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
384         for (int i : new int[]{28, 30}) {
385             focusSearchAndWaitForScroll(focusedView, focusDir);
386             focusedView = mRecyclerView.getFocusedChild();
387             assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
388         }
389     }
390 
focusSearchAndWaitForScroll(View focused, int dir)391     private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
392         focusSearch(focused, dir);
393         waitForIdleScroll(mRecyclerView);
394     }
395 
396 
397     @Test
scrollToPositionWithPredictive()398     public void scrollToPositionWithPredictive() throws Throwable {
399         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
400         removeRecyclerView();
401         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
402                 LinearLayoutManager.INVALID_OFFSET);
403         removeRecyclerView();
404         scrollToPositionWithPredictive(9, 20);
405         removeRecyclerView();
406         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
407 
408     }
409 
scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)410     public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
411             throws Throwable {
412         setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
413                 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
414         waitFirstLayout();
415         mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
416             @Override
417             void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
418                 RecyclerView rv = mLayoutManager.mRecyclerView;
419                 if (state.isPreLayout()) {
420                     assertEquals("pending scroll position should still be pending",
421                             scrollPosition, mLayoutManager.mPendingScrollPosition);
422                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
423                         assertEquals("pending scroll position offset should still be pending",
424                                 scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
425                     }
426                 } else {
427                     RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
428                     assertNotNull("scroll to position should work", vh);
429                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
430                         assertEquals("scroll offset should be applied properly",
431                                 mLayoutManager.getPaddingTop() + scrollOffset
432                                         + ((RecyclerView.LayoutParams) vh.itemView
433                                         .getLayoutParams()).topMargin,
434                                 mLayoutManager.getDecoratedTop(vh.itemView));
435                     }
436                 }
437             }
438         };
439         mLayoutManager.expectLayouts(2);
440         runTestOnUiThread(new Runnable() {
441             @Override
442             public void run() {
443                 try {
444                     mAdapter.addAndNotify(0, 1);
445                     if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
446                         mLayoutManager.scrollToPosition(scrollPosition);
447                     } else {
448                         mLayoutManager.scrollToPositionWithOffset(scrollPosition,
449                                 scrollOffset);
450                     }
451 
452                 } catch (Throwable throwable) {
453                     throwable.printStackTrace();
454                 }
455 
456             }
457         });
458         mLayoutManager.waitForLayout(2);
459         checkForMainThreadException();
460     }
461 
462     @Test
moveGapHandling()463     public void moveGapHandling() throws Throwable {
464         Config config = new Config().spanCount(2).itemCount(40);
465         setupByConfig(config);
466         waitFirstLayout();
467         mLayoutManager.expectLayouts(2);
468         mAdapter.moveAndNotify(4, 1);
469         mLayoutManager.waitForLayout(2);
470         assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
471     }
472 
473     @Test
updateAfterFullSpan()474     public void updateAfterFullSpan() throws Throwable {
475         updateAfterFullSpanGapHandlingTest(0);
476     }
477 
478     @Test
updateAfterFullSpan2()479     public void updateAfterFullSpan2() throws Throwable {
480         updateAfterFullSpanGapHandlingTest(20);
481     }
482 
483     @Test
temporaryGapHandling()484     public void temporaryGapHandling() throws Throwable {
485         int fullSpanIndex = 200;
486         setupByConfig(new Config().spanCount(2).itemCount(500));
487         mAdapter.mFullSpanItems.add(fullSpanIndex);
488         waitFirstLayout();
489         smoothScrollToPosition(fullSpanIndex + 200);// go far away
490         assertNull("test sanity. full span item should not be visible",
491                 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
492         mLayoutManager.expectLayouts(1);
493         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
494         mLayoutManager.waitForLayout(1);
495         smoothScrollToPosition(0);
496         mLayoutManager.expectLayouts(1);
497         smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
498         String log = mLayoutManager.layoutToString("post gap");
499         mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
500                 + "relayout " + log, 2);
501         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
502         assertNotNull("full span item should be there:\n" + log, fullSpan);
503         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
504         assertNotNull("next view should be there\n" + log, view1);
505         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
506         assertNotNull("+2 view should be there\n" + log, view2);
507 
508         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
509         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
510         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
511         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
512         assertEquals("no gap between span and view 1",
513                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
514                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
515         assertEquals("no gap between span and view 2",
516                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
517                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
518     }
519 
updateAfterFullSpanGapHandlingTest(int fullSpanIndex)520     public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
521         setupByConfig(new Config().spanCount(2).itemCount(100));
522         mAdapter.mFullSpanItems.add(fullSpanIndex);
523         waitFirstLayout();
524         smoothScrollToPosition(fullSpanIndex + 30);
525         mLayoutManager.expectLayouts(1);
526         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
527         mLayoutManager.waitForLayout(1);
528         smoothScrollToPosition(fullSpanIndex);
529         // give it some time to fix the gap
530         Thread.sleep(500);
531         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
532 
533         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
534         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
535 
536         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
537         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
538         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
539         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
540         assertEquals("no gap between span and view 1",
541                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
542                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
543         assertEquals("no gap between span and view 2",
544                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
545                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
546     }
547 
548     @Test
innerGapHandling()549     public void innerGapHandling() throws Throwable {
550         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
551         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
552     }
553 
innerGapHandlingTest(int strategy)554     public void innerGapHandlingTest(int strategy) throws Throwable {
555         Config config = new Config().spanCount(3).itemCount(500);
556         setupByConfig(config);
557         mLayoutManager.setGapStrategy(strategy);
558         mAdapter.mFullSpanItems.add(100);
559         mAdapter.mFullSpanItems.add(104);
560         mAdapter.mViewsHaveEqualSize = true;
561         mAdapter.mOnBindCallback = new OnBindCallback() {
562             @Override
563             void onBoundItem(TestViewHolder vh, int position) {
564 
565             }
566 
567             @Override
568             void onCreatedViewHolder(TestViewHolder vh) {
569                 super.onCreatedViewHolder(vh);
570                 //make sure we have enough views
571                 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
572             }
573         };
574         waitFirstLayout();
575         mLayoutManager.expectLayouts(1);
576         scrollToPosition(400);
577         mLayoutManager.waitForLayout(2);
578         View view400 = mLayoutManager.findViewByPosition(400);
579         assertNotNull("test sanity, scrollToPos should succeed", view400);
580         assertTrue("test sanity, view should be visible top",
581                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
582                         mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
583         assertTrue("test sanity, view should be visible bottom",
584                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
585                         mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
586         mLayoutManager.expectLayouts(2);
587         mAdapter.addAndNotify(101, 1);
588         mLayoutManager.waitForLayout(2);
589         checkForMainThreadException();
590         if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
591             mLayoutManager.expectLayouts(1);
592         }
593         // state
594         // now smooth scroll to 99 to trigger a layout around 100
595         mLayoutManager.validateChildren();
596         smoothScrollToPosition(99);
597         switch (strategy) {
598             case GAP_HANDLING_NONE:
599                 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
600                         new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
601                         new int[]{105, 0});
602                 break;
603             case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
604                 mLayoutManager.waitForLayout(2);
605                 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
606                         new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
607                 break;
608         }
609 
610     }
611 
612     @Test
fullSizeSpans()613     public void fullSizeSpans() throws Throwable {
614         Config config = new Config().spanCount(5).itemCount(30);
615         setupByConfig(config);
616         mAdapter.mFullSpanItems.add(3);
617         waitFirstLayout();
618         assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
619                 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
620                 new int[]{7, 3}, new int[]{8, 4});
621     }
622 
assertSpans(String msg, int[]... childSpanTuples)623     void assertSpans(String msg, int[]... childSpanTuples) {
624         msg = msg + mLayoutManager.layoutToString("\n\n");
625         for (int i = 0; i < childSpanTuples.length; i++) {
626             assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
627         }
628     }
629 
assertSpan(String msg, int childPosition, int expectedSpan)630     void assertSpan(String msg, int childPosition, int expectedSpan) {
631         View view = mLayoutManager.findViewByPosition(childPosition);
632         assertNotNull(msg + " view at position " + childPosition + " should exists", view);
633         assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
634                 getLp(view).mSpan.mIndex);
635     }
636 
637     @Test
partialSpanInvalidation()638     public void partialSpanInvalidation() throws Throwable {
639         Config config = new Config().spanCount(5).itemCount(100);
640         setupByConfig(config);
641         for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
642             mAdapter.mFullSpanItems.add(i);
643         }
644         waitFirstLayout();
645         smoothScrollToPosition(50);
646         int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
647         mAdapter.changeAndNotify(15, 2);
648         Thread.sleep(200);
649         assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
650                 mLayoutManager.mLazySpanLookup.mData[30]);
651         assertEquals("item in invalidated range should have clear span id",
652                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
653         smoothScrollToPosition(85);
654         int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
655         mAdapter.deleteAndNotify(55, 2);
656         Thread.sleep(200);
657         assertEquals("item in invalidated range should have clear span id",
658                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
659         int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
660         assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
661                 newSpans, 0, 0, newSpans.length);
662     }
663 
664     // Same as Arrays.copyOfRange but for API 7
copyOfRange(int[] original, int from, int to)665     private int[] copyOfRange(int[] original, int from, int to) {
666         int newLength = to - from;
667         if (newLength < 0) {
668             throw new IllegalArgumentException(from + " > " + to);
669         }
670         int[] copy = new int[newLength];
671         System.arraycopy(original, from, copy, 0,
672                 Math.min(original.length - from, newLength));
673         return copy;
674     }
675 
676     @Test
spanReassignmentsOnItemChange()677     public void spanReassignmentsOnItemChange() throws Throwable {
678         Config config = new Config().spanCount(5);
679         setupByConfig(config);
680         waitFirstLayout();
681         smoothScrollToPosition(mAdapter.getItemCount() / 2);
682         final int changePosition = mAdapter.getItemCount() / 4;
683         mLayoutManager.expectLayouts(1);
684         mAdapter.changeAndNotify(changePosition, 1);
685         mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
686                 1);
687         // delete an item before visible area
688         int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
689         assertTrue("test sanity", deletedPosition >= 0);
690         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
691         if (DEBUG) {
692             Log.d(TAG, "before:");
693             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
694                 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
695             }
696         }
697         mLayoutManager.expectLayouts(1);
698         mAdapter.deleteAndNotify(deletedPosition, 1);
699         mLayoutManager.waitForLayout(2);
700         assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
701                         + "should not affect the layout if it is not visible", before,
702                 mLayoutManager.collectChildCoordinates()
703         );
704         deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
705         mLayoutManager.expectLayouts(1);
706         mAdapter.deleteAndNotify(deletedPosition, 1);
707         mLayoutManager.waitForLayout(2);
708         assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
709                 + "layout", before, mLayoutManager.collectChildCoordinates());
710     }
711 
assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2, int length)712     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
713             int length) {
714         for (int i = 0; i < length; i++) {
715             assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
716                     set2[start2 + i]);
717         }
718     }
719 
720     @Test
spanCountChangeOnRestoreSavedState()721     public void spanCountChangeOnRestoreSavedState() throws Throwable {
722         Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE).itemCount(50);
723         setupByConfig(config);
724         waitFirstLayout();
725 
726         int beforeChildCount = mLayoutManager.getChildCount();
727         Parcelable savedState = mRecyclerView.onSaveInstanceState();
728         // we append a suffix to the parcelable to test out of bounds
729         String parcelSuffix = UUID.randomUUID().toString();
730         Parcel parcel = Parcel.obtain();
731         savedState.writeToParcel(parcel, 0);
732         parcel.writeString(parcelSuffix);
733         removeRecyclerView();
734         // reset for reading
735         parcel.setDataPosition(0);
736         // re-create
737         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
738         removeRecyclerView();
739 
740         RecyclerView restored = new RecyclerView(getActivity());
741         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
742         mLayoutManager.setReverseLayout(config.mReverseLayout);
743         mLayoutManager.setGapStrategy(config.mGapStrategy);
744         restored.setLayoutManager(mLayoutManager);
745         // use the same adapter for Rect matching
746         restored.setAdapter(mAdapter);
747         restored.onRestoreInstanceState(savedState);
748         mLayoutManager.setSpanCount(1);
749         mLayoutManager.expectLayouts(1);
750         setRecyclerView(restored);
751         mLayoutManager.waitForLayout(2);
752         assertEquals("on saved state, reverse layout should be preserved",
753                 config.mReverseLayout, mLayoutManager.getReverseLayout());
754         assertEquals("on saved state, orientation should be preserved",
755                 config.mOrientation, mLayoutManager.getOrientation());
756         assertEquals("after setting new span count, layout manager should keep new value",
757                 1, mLayoutManager.getSpanCount());
758         assertEquals("on saved state, gap strategy should be preserved",
759                 config.mGapStrategy, mLayoutManager.getGapStrategy());
760         assertTrue("when span count is dramatically changed after restore, # of child views "
761                 + "should change", beforeChildCount > mLayoutManager.getChildCount());
762         // make sure SGLM can layout all children. is some span info is leaked, this would crash
763         smoothScrollToPosition(mAdapter.getItemCount() - 1);
764     }
765 
766     @Test
scrollAndClear()767     public void scrollAndClear() throws Throwable {
768         setupByConfig(new Config());
769         waitFirstLayout();
770 
771         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
772 
773         mLayoutManager.expectLayouts(1);
774         runTestOnUiThread(new Runnable() {
775             @Override
776             public void run() {
777                 mLayoutManager.scrollToPositionWithOffset(1, 0);
778                 mAdapter.clearOnUIThread();
779             }
780         });
781         mLayoutManager.waitForLayout(2);
782 
783         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
784     }
785 
786     @Test
accessibilityPositions()787     public void accessibilityPositions() throws Throwable {
788         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
789         waitFirstLayout();
790         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
791                 .getCompatAccessibilityDelegate();
792         final AccessibilityEvent event = AccessibilityEvent.obtain();
793         runTestOnUiThread(new Runnable() {
794             @Override
795             public void run() {
796                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
797             }
798         });
799         final AccessibilityRecordCompat record = AccessibilityEventCompat
800                 .asRecord(event);
801         final int start = mRecyclerView
802                 .getChildLayoutPosition(
803                         mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
804         final int end = mRecyclerView
805                 .getChildLayoutPosition(
806                         mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
807         assertEquals("first item position should match",
808                 Math.min(start, end), record.getFromIndex());
809         assertEquals("last item position should match",
810                 Math.max(start, end), record.getToIndex());
811 
812     }
813 }
814