• 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.assertFalse;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertSame;
26 import static org.junit.Assert.assertThat;
27 import static org.junit.Assert.assertTrue;
28 
29 import android.graphics.Color;
30 import android.graphics.drawable.ColorDrawable;
31 import android.graphics.drawable.StateListDrawable;
32 import android.support.test.runner.AndroidJUnit4;
33 import android.support.v4.view.AccessibilityDelegateCompat;
34 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
35 import android.test.UiThreadTest;
36 import android.test.suitebuilder.annotation.MediumTest;
37 import android.util.SparseIntArray;
38 import android.util.StateSet;
39 import android.view.View;
40 import android.view.ViewGroup;
41 
42 import org.hamcrest.CoreMatchers;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.concurrent.atomic.AtomicBoolean;
51 
52 @MediumTest
53 @RunWith(AndroidJUnit4.class)
54 public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
55 
56     @Test
focusSearchFailureUp()57     public void focusSearchFailureUp() throws Throwable {
58         focusSearchFailure(false);
59     }
60 
61     @Test
focusSearchFailureDown()62     public void focusSearchFailureDown() throws Throwable {
63         focusSearchFailure(true);
64     }
65 
66     @Test
scrollToBadOffset()67     public void scrollToBadOffset() throws Throwable {
68         scrollToBadOffset(false);
69     }
70 
71     @Test
scrollToBadOffsetReverse()72     public void scrollToBadOffsetReverse() throws Throwable {
73         scrollToBadOffset(true);
74     }
75 
scrollToBadOffset(boolean reverseLayout)76     private void scrollToBadOffset(boolean reverseLayout) throws Throwable {
77         final int w = 500;
78         final int h = 1000;
79         RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout),
80                 new GridTestAdapter(100) {
81                     @Override
82                     public void onBindViewHolder(TestViewHolder holder,
83                             int position) {
84                         super.onBindViewHolder(holder, position);
85                         ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
86                         if (lp == null) {
87                             lp = new ViewGroup.LayoutParams(w / 2, h / 2);
88                             holder.itemView.setLayoutParams(lp);
89                         } else {
90                             lp.width = w / 2;
91                             lp.height = h / 2;
92                             holder.itemView.setLayoutParams(lp);
93                         }
94                     }
95                 });
96         TestedFrameLayout.FullControlLayoutParams lp
97                 = new TestedFrameLayout.FullControlLayoutParams(w, h);
98         recyclerView.setLayoutParams(lp);
99         waitForFirstLayout(recyclerView);
100         mGlm.expectLayout(1);
101         scrollToPosition(11);
102         mGlm.waitForLayout(2);
103         // assert spans and position etc
104         for (int i = 0; i < mGlm.getChildCount(); i++) {
105             View child = mGlm.getChildAt(i);
106             GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child
107                     .getLayoutParams();
108             assertThat("span index for child at " + i + " with position " + params
109                             .getViewAdapterPosition(),
110                     params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2));
111         }
112         // assert spans and positions etc.
113         int lastVisible = mGlm.findLastVisibleItemPosition();
114         // this should be the scrolled child
115         assertThat(lastVisible, CoreMatchers.is(11));
116     }
117 
focusSearchFailure(boolean scrollDown)118     private void focusSearchFailure(boolean scrollDown) throws Throwable {
119         final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown)
120                 , new GridTestAdapter(31, 1) {
121                     RecyclerView mAttachedRv;
122 
123                     @Override
124                     public TestViewHolder onCreateViewHolder(ViewGroup parent,
125                             int viewType) {
126                         TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
127                         testViewHolder.itemView.setFocusable(true);
128                         testViewHolder.itemView.setFocusableInTouchMode(true);
129                         // Good to have colors for debugging
130                         StateListDrawable stl = new StateListDrawable();
131                         stl.addState(new int[]{android.R.attr.state_focused},
132                                 new ColorDrawable(Color.RED));
133                         stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
134                         testViewHolder.itemView.setBackground(stl);
135                         return testViewHolder;
136                     }
137 
138                     @Override
139                     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
140                         mAttachedRv = recyclerView;
141                     }
142 
143                     @Override
144                     public void onBindViewHolder(TestViewHolder holder,
145                             int position) {
146                         super.onBindViewHolder(holder, position);
147                         holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
148                     }
149                 });
150         waitForFirstLayout(recyclerView);
151 
152         View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView;
153         assertTrue(requestFocus(viewToFocus, true));
154         assertSame(viewToFocus, recyclerView.getFocusedChild());
155         int pos = 1;
156         View focusedView = viewToFocus;
157         while (pos < 31) {
158             focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP);
159             waitForIdleScroll(recyclerView);
160             focusedView = recyclerView.getFocusedChild();
161             assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1),
162                     recyclerView.getChildViewHolder(focusedView).getAdapterPosition());
163             pos += 3;
164         }
165     }
166 
167     @UiThreadTest
168     @Test
scrollWithoutLayout()169     public void scrollWithoutLayout() throws Throwable {
170         final RecyclerView recyclerView = setupBasic(new Config(3, 100));
171         mGlm.expectLayout(1);
172         setRecyclerView(recyclerView);
173         mGlm.setSpanCount(5);
174         recyclerView.scrollBy(0, 10);
175     }
176 
177     @Test
scrollWithoutLayoutAfterInvalidate()178     public void scrollWithoutLayoutAfterInvalidate() throws Throwable {
179         final RecyclerView recyclerView = setupBasic(new Config(3, 100));
180         waitForFirstLayout(recyclerView);
181         runTestOnUiThread(new Runnable() {
182             @Override
183             public void run() {
184                 mGlm.setSpanCount(5);
185                 recyclerView.scrollBy(0, 10);
186             }
187         });
188     }
189 
190     @Test
predictiveSpanLookup1()191     public void predictiveSpanLookup1() throws Throwable {
192         predictiveSpanLookupTest(0, false);
193     }
194 
195     @Test
predictiveSpanLookup2()196     public void predictiveSpanLookup2() throws Throwable {
197         predictiveSpanLookupTest(0, true);
198     }
199 
200     @Test
predictiveSpanLookup3()201     public void predictiveSpanLookup3() throws Throwable {
202         predictiveSpanLookupTest(1, false);
203     }
204 
205     @Test
predictiveSpanLookup4()206     public void predictiveSpanLookup4() throws Throwable {
207         predictiveSpanLookupTest(1, true);
208     }
209 
predictiveSpanLookupTest(int remaining, boolean removeFromStart)210     public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable {
211         RecyclerView recyclerView = setupBasic(new Config(3, 10));
212         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
213             @Override
214             public int getSpanSize(int position) {
215                 if (position < 0 || position >= mAdapter.getItemCount()) {
216                     postExceptionToInstrumentation(new AssertionError("position is not within " +
217                             "adapter range. pos:" + position + ", adapter size:" +
218                             mAdapter.getItemCount()));
219                 }
220                 return 1;
221             }
222 
223             @Override
224             public int getSpanIndex(int position, int spanCount) {
225                 if (position < 0 || position >= mAdapter.getItemCount()) {
226                     postExceptionToInstrumentation(new AssertionError("position is not within " +
227                             "adapter range. pos:" + position + ", adapter size:" +
228                             mAdapter.getItemCount()));
229                 }
230                 return super.getSpanIndex(position, spanCount);
231             }
232         });
233         waitForFirstLayout(recyclerView);
234         checkForMainThreadException();
235         assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations());
236         mGlm.expectLayout(2);
237         int deleteCnt = 10 - remaining;
238         int deleteStart = removeFromStart ? 0 : remaining;
239         mAdapter.deleteAndNotify(deleteStart, deleteCnt);
240         mGlm.waitForLayout(2);
241         checkForMainThreadException();
242     }
243 
244     @Test
movingAGroupOffScreenForAddedItems()245     public void movingAGroupOffScreenForAddedItems() throws Throwable {
246         final RecyclerView rv = setupBasic(new Config(3, 100));
247         final int[] maxId = new int[1];
248         maxId[0] = -1;
249         final SparseIntArray spanLookups = new SparseIntArray();
250         final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false);
251         mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
252             @Override
253             public int getSpanSize(int position) {
254                 if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) {
255                     return 1;
256                 } else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) {
257                     spanLookups.put(position, spanLookups.get(position, 0) + 1);
258                 }
259                 return 3;
260             }
261         });
262         ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true);
263         waitForFirstLayout(rv);
264         View lastView = rv.getChildAt(rv.getChildCount() - 1);
265         final int lastPos = rv.getChildAdapterPosition(lastView);
266         maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId;
267         // now add a lot of items below this and those new views should have span size 3
268         enableSpanLookupLogging.set(true);
269         mGlm.expectLayout(2);
270         mAdapter.addAndNotify(lastPos - 2, 30);
271         mGlm.waitForLayout(2);
272         checkForMainThreadException();
273 
274         assertEquals("last items span count should be queried twice", 2,
275                 spanLookups.get(lastPos + 30));
276 
277     }
278 
279     @Test
layoutParams()280     public void layoutParams() throws Throwable {
281         layoutParamsTest(GridLayoutManager.HORIZONTAL);
282         removeRecyclerView();
283         layoutParamsTest(GridLayoutManager.VERTICAL);
284     }
285 
286     @Test
horizontalAccessibilitySpanIndices()287     public void horizontalAccessibilitySpanIndices() throws Throwable {
288         accessibilitySpanIndicesTest(HORIZONTAL);
289     }
290 
291     @Test
verticalAccessibilitySpanIndices()292     public void verticalAccessibilitySpanIndices() throws Throwable {
293         accessibilitySpanIndicesTest(VERTICAL);
294     }
295 
accessibilitySpanIndicesTest(int orientation)296     public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
297         final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
298         waitForFirstLayout(recyclerView);
299         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
300                 .getCompatAccessibilityDelegate().getItemDelegate();
301         final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
302         final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
303         final int position = recyclerView.getChildLayoutPosition(chosen);
304         runTestOnUiThread(new Runnable() {
305             @Override
306             public void run() {
307                 delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
308             }
309         });
310         GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
311         AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
312                 .getCollectionItemInfo();
313         assertNotNull(itemInfo);
314         assertEquals("result should have span group position",
315                 ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
316                 orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
317         assertEquals("result should have span index",
318                 ssl.getSpanIndex(position, mGlm.getSpanCount()),
319                 orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex());
320         assertEquals("result should have span size",
321                 ssl.getSpanSize(position),
322                 orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan());
323     }
324 
ensureGridLp(View view)325     public GridLayoutManager.LayoutParams ensureGridLp(View view) {
326         ViewGroup.LayoutParams lp = view.getLayoutParams();
327         GridLayoutManager.LayoutParams glp;
328         if (lp instanceof GridLayoutManager.LayoutParams) {
329             glp = (GridLayoutManager.LayoutParams) lp;
330         } else if (lp == null) {
331             glp = (GridLayoutManager.LayoutParams) mGlm
332                     .generateDefaultLayoutParams();
333             view.setLayoutParams(glp);
334         } else {
335             glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
336             view.setLayoutParams(glp);
337         }
338         return glp;
339     }
340 
layoutParamsTest(final int orientation)341     public void layoutParamsTest(final int orientation) throws Throwable {
342         final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
343                 new GridTestAdapter(100) {
344                     @Override
345                     public void onBindViewHolder(TestViewHolder holder,
346                             int position) {
347                         super.onBindViewHolder(holder, position);
348                         GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
349                         int val = 0;
350                         switch (position % 5) {
351                             case 0:
352                                 val = 10;
353                                 break;
354                             case 1:
355                                 val = 30;
356                                 break;
357                             case 2:
358                                 val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
359                                 break;
360                             case 3:
361                                 val = GridLayoutManager.LayoutParams.MATCH_PARENT;
362                                 break;
363                             case 4:
364                                 val = 200;
365                                 break;
366                         }
367                         if (orientation == GridLayoutManager.VERTICAL) {
368                             glp.height = val;
369                         } else {
370                             glp.width = val;
371                         }
372                         holder.itemView.setLayoutParams(glp);
373                     }
374                 });
375         waitForFirstLayout(rv);
376         final OrientationHelper helper = mGlm.mOrientationHelper;
377         final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
378         assertEquals(firstRowSize,
379                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
380         assertEquals(firstRowSize,
381                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
382         assertEquals(firstRowSize,
383                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
384         assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
385         assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
386         assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
387 
388         final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
389         assertEquals(secondRowSize,
390                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
391         assertEquals(secondRowSize,
392                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
393         assertEquals(secondRowSize,
394                 helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
395         assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
396         assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
397         assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
398     }
399 
400     @Test
anchorUpdate()401     public void anchorUpdate() throws InterruptedException {
402         GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
403         final GridLayoutManager.SpanSizeLookup spanSizeLookup
404                 = new GridLayoutManager.SpanSizeLookup() {
405             @Override
406             public int getSpanSize(int position) {
407                 if (position > 200) {
408                     return 100;
409                 }
410                 if (position > 20) {
411                     return 2;
412                 }
413                 return 1;
414             }
415         };
416         glm.setSpanSizeLookup(spanSizeLookup);
417         glm.mAnchorInfo.mPosition = 11;
418         RecyclerView.State state = new RecyclerView.State();
419         mRecyclerView = new RecyclerView(getActivity());
420         state.mItemCount = 1000;
421         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
422                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
423         assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
424 
425         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
426                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
427         assertEquals("gm should keep anchor in last span in the row", 20,
428                 glm.mAnchorInfo.mPosition);
429 
430         glm.mAnchorInfo.mPosition = 5;
431         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
432                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
433         assertEquals("gm should keep anchor in last span in the row", 10,
434                 glm.mAnchorInfo.mPosition);
435 
436         glm.mAnchorInfo.mPosition = 13;
437         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
438                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
439         assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
440 
441         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
442                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
443         assertEquals("gm should keep anchor in last span in the row", 20,
444                 glm.mAnchorInfo.mPosition);
445 
446         glm.mAnchorInfo.mPosition = 23;
447         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
448                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
449         assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
450 
451         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
452                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
453         assertEquals("gm should keep anchor in last span in the row", 25,
454                 glm.mAnchorInfo.mPosition);
455 
456         glm.mAnchorInfo.mPosition = 35;
457         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
458                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
459         assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
460         glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
461                 LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
462         assertEquals("gm should keep anchor in last span in the row", 35,
463                 glm.mAnchorInfo.mPosition);
464     }
465 
466     @Test
spanLookup()467     public void spanLookup() {
468         spanLookupTest(false);
469     }
470 
471     @Test
spanLookupWithCache()472     public void spanLookupWithCache() {
473         spanLookupTest(true);
474     }
475 
476     @Test
spanLookupCache()477     public void spanLookupCache() {
478         final GridLayoutManager.SpanSizeLookup ssl
479                 = new GridLayoutManager.SpanSizeLookup() {
480             @Override
481             public int getSpanSize(int position) {
482                 if (position > 6) {
483                     return 2;
484                 }
485                 return 1;
486             }
487         };
488         ssl.setSpanIndexCacheEnabled(true);
489         assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
490         ssl.getCachedSpanIndex(4, 5);
491         assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
492         // this should not happen and if happens, it is better to return -1
493         assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
494         assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
495         assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
496         ssl.getCachedSpanIndex(6, 5);
497         assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
498         assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
499         assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
500         ssl.getCachedSpanIndex(12, 5);
501         assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
502         assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
503         assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
504         for (int i = 0; i < 6; i++) {
505             ssl.getCachedSpanIndex(i, 5);
506         }
507 
508         for (int i = 1; i < 7; i++) {
509             assertEquals("reference child right before " + i, i - 1,
510                     ssl.findReferenceIndexFromCache(i));
511         }
512         assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
513     }
514 
spanLookupTest(boolean enableCache)515     public void spanLookupTest(boolean enableCache) {
516         final GridLayoutManager.SpanSizeLookup ssl
517                 = new GridLayoutManager.SpanSizeLookup() {
518             @Override
519             public int getSpanSize(int position) {
520                 if (position > 200) {
521                     return 100;
522                 }
523                 if (position > 6) {
524                     return 2;
525                 }
526                 return 1;
527             }
528         };
529         ssl.setSpanIndexCacheEnabled(enableCache);
530         assertEquals(0, ssl.getCachedSpanIndex(0, 5));
531         assertEquals(4, ssl.getCachedSpanIndex(4, 5));
532         assertEquals(0, ssl.getCachedSpanIndex(5, 5));
533         assertEquals(1, ssl.getCachedSpanIndex(6, 5));
534         assertEquals(2, ssl.getCachedSpanIndex(7, 5));
535         assertEquals(2, ssl.getCachedSpanIndex(9, 5));
536         assertEquals(0, ssl.getCachedSpanIndex(8, 5));
537     }
538 
539     @Test
removeAnchorItem()540     public void removeAnchorItem() throws Throwable {
541         removeAnchorItemTest(
542                 new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
543     }
544 
545     @Test
removeAnchorItemReverse()546     public void removeAnchorItemReverse() throws Throwable {
547         removeAnchorItemTest(
548                 new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
549                 0);
550     }
551 
552     @Test
removeAnchorItemHorizontal()553     public void removeAnchorItemHorizontal() throws Throwable {
554         removeAnchorItemTest(
555                 new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
556                         false), 100, 0);
557     }
558 
559     @Test
removeAnchorItemReverseHorizontal()560     public void removeAnchorItemReverseHorizontal() throws Throwable {
561         removeAnchorItemTest(
562                 new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true),
563                 100, 0);
564     }
565 
566     /**
567      * This tests a regression where predictive animations were not working as expected when the
568      * first item is removed and there aren't any more items to add from that direction.
569      * First item refers to the default anchor item.
570      */
removeAnchorItemTest(final Config config, int adapterSize, final int removePos)571     public void removeAnchorItemTest(final Config config, int adapterSize,
572             final int removePos) throws Throwable {
573         GridTestAdapter adapter = new GridTestAdapter(adapterSize) {
574             @Override
575             public void onBindViewHolder(TestViewHolder holder,
576                     int position) {
577                 super.onBindViewHolder(holder, position);
578                 ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
579                 if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
580                     lp = new ViewGroup.MarginLayoutParams(0, 0);
581                     holder.itemView.setLayoutParams(lp);
582                 }
583                 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
584                 final int maxSize;
585                 if (config.mOrientation == HORIZONTAL) {
586                     maxSize = mRecyclerView.getWidth();
587                     mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
588                 } else {
589                     maxSize = mRecyclerView.getHeight();
590                     mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
591                 }
592 
593                 final int desiredSize;
594                 if (position == removePos) {
595                     // make it large
596                     desiredSize = maxSize / 4;
597                 } else {
598                     // make it small
599                     desiredSize = maxSize / 8;
600                 }
601                 if (config.mOrientation == HORIZONTAL) {
602                     mlp.width = desiredSize;
603                 } else {
604                     mlp.height = desiredSize;
605                 }
606             }
607         };
608         RecyclerView recyclerView = setupBasic(config, adapter);
609         waitForFirstLayout(recyclerView);
610         final int childCount = mGlm.getChildCount();
611         RecyclerView.ViewHolder toBeRemoved = null;
612         List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
613         for (int i = 0; i < childCount; i++) {
614             View child = mGlm.getChildAt(i);
615             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
616             if (holder.getAdapterPosition() == removePos) {
617                 toBeRemoved = holder;
618             } else {
619                 toBeMoved.add(holder);
620             }
621         }
622         assertNotNull("test sanity", toBeRemoved);
623         assertEquals("test sanity", childCount - 1, toBeMoved.size());
624         LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
625         mRecyclerView.setItemAnimator(loggingItemAnimator);
626         loggingItemAnimator.reset();
627         loggingItemAnimator.expectRunPendingAnimationsCall(1);
628         mGlm.expectLayout(2);
629         adapter.deleteAndNotify(removePos, 1);
630         mGlm.waitForLayout(1);
631         loggingItemAnimator.waitForPendingAnimationsCall(2);
632         assertTrue("removed child should receive remove animation",
633                 loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
634         for (RecyclerView.ViewHolder vh : toBeMoved) {
635             assertTrue("view holder should be in moved list",
636                     loggingItemAnimator.mMoveVHs.contains(vh));
637         }
638         List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
639         for (int i = 0; i < mGlm.getChildCount(); i++) {
640             View child = mGlm.getChildAt(i);
641             RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
642             if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
643                 newHolders.add(holder);
644             }
645         }
646         assertTrue("some new children should show up for the new space", newHolders.size() > 0);
647         assertEquals("no items should receive animate add since they are not new", 0,
648                 loggingItemAnimator.mAddVHs.size());
649         for (RecyclerView.ViewHolder holder : newHolders) {
650             assertTrue("new holder should receive a move animation",
651                     loggingItemAnimator.mMoveVHs.contains(holder));
652         }
653         // for removed view, 3 for new row
654         assertTrue("control against adding too many children due to bad layout state preparation."
655                         + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
656                 mRecyclerView.getChildCount() <= childCount + 1 + 3);
657     }
658 
659     @Test
spanGroupIndex()660     public void spanGroupIndex() {
661         final GridLayoutManager.SpanSizeLookup ssl
662                 = new GridLayoutManager.SpanSizeLookup() {
663             @Override
664             public int getSpanSize(int position) {
665                 if (position > 200) {
666                     return 100;
667                 }
668                 if (position > 6) {
669                     return 2;
670                 }
671                 return 1;
672             }
673         };
674         assertEquals(0, ssl.getSpanGroupIndex(0, 5));
675         assertEquals(0, ssl.getSpanGroupIndex(4, 5));
676         assertEquals(1, ssl.getSpanGroupIndex(5, 5));
677         assertEquals(1, ssl.getSpanGroupIndex(6, 5));
678         assertEquals(1, ssl.getSpanGroupIndex(7, 5));
679         assertEquals(2, ssl.getSpanGroupIndex(9, 5));
680         assertEquals(2, ssl.getSpanGroupIndex(8, 5));
681     }
682 
683     @Test
notifyDataSetChange()684     public void notifyDataSetChange() throws Throwable {
685         final RecyclerView recyclerView = setupBasic(new Config(3, 100));
686         final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
687         ssl.setSpanIndexCacheEnabled(true);
688         waitForFirstLayout(recyclerView);
689         assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
690         final Callback callback = new Callback() {
691             @Override
692             public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
693                 if (!state.isPreLayout()) {
694                     assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
695                 }
696             }
697 
698             @Override
699             public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
700                 if (!state.isPreLayout()) {
701                     assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
702                 }
703             }
704         };
705         mGlm.mCallbacks.add(callback);
706         mGlm.expectLayout(2);
707         mAdapter.deleteAndNotify(2, 3);
708         mGlm.waitForLayout(2);
709         checkForMainThreadException();
710     }
711 
712     @Test
unevenHeights()713     public void unevenHeights() throws Throwable {
714         final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
715                 new HashMap<Integer, RecyclerView.ViewHolder>();
716         RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
717             @Override
718             public void onBindViewHolder(TestViewHolder holder,
719                     int position) {
720                 super.onBindViewHolder(holder, position);
721                 final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
722                 glp.height = 50 + position * 50;
723                 viewHolderMap.put(position, holder);
724             }
725         });
726         waitForFirstLayout(recyclerView);
727         for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
728             assertEquals("all items should get max height", 150,
729                     vh.itemView.getHeight());
730         }
731 
732         for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
733             assertEquals("all items should have measured the max height", 150,
734                     vh.itemView.getMeasuredHeight());
735         }
736     }
737 
738     @Test
unevenWidths()739     public void unevenWidths() throws Throwable {
740         final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
741                 new HashMap<Integer, RecyclerView.ViewHolder>();
742         RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
743                 new GridTestAdapter(3) {
744                     @Override
745                     public void onBindViewHolder(TestViewHolder holder,
746                             int position) {
747                         super.onBindViewHolder(holder, position);
748                         final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
749                         glp.width = 50 + position * 50;
750                         viewHolderMap.put(position, holder);
751                     }
752                 });
753         waitForFirstLayout(recyclerView);
754         for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
755             assertEquals("all items should get max width", 150,
756                     vh.itemView.getWidth());
757         }
758 
759         for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
760             assertEquals("all items should have measured the max width", 150,
761                     vh.itemView.getMeasuredWidth());
762         }
763     }
764 
765     @Test
spanSizeChange()766     public void spanSizeChange() throws Throwable {
767         final RecyclerView rv = setupBasic(new Config(3, 100));
768         waitForFirstLayout(rv);
769         assertTrue(mGlm.supportsPredictiveItemAnimations());
770         mGlm.expectLayout(1);
771         runTestOnUiThread(new Runnable() {
772             @Override
773             public void run() {
774                 mGlm.setSpanCount(5);
775                 assertFalse(mGlm.supportsPredictiveItemAnimations());
776             }
777         });
778         mGlm.waitForLayout(2);
779         mGlm.expectLayout(2);
780         mAdapter.deleteAndNotify(3, 2);
781         mGlm.waitForLayout(2);
782         assertTrue(mGlm.supportsPredictiveItemAnimations());
783     }
784 
785     @Test
cacheSpanIndices()786     public void cacheSpanIndices() throws Throwable {
787         final RecyclerView rv = setupBasic(new Config(3, 100));
788         mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
789         waitForFirstLayout(rv);
790         GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
791         assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
792         assertEquals("item index 5 should be in span 2", 2,
793                 getLp(mGlm.findViewByPosition(5)).getSpanIndex());
794         mGlm.expectLayout(2);
795         mAdapter.mFullSpanItems.add(4);
796         mAdapter.changeAndNotify(4, 1);
797         mGlm.waitForLayout(2);
798         assertEquals("item index 5 should be in span 2", 0,
799                 getLp(mGlm.findViewByPosition(5)).getSpanIndex());
800     }
801 }
802