[go: nahoru, domu]

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
17package android.support.v7.widget;
18
19import org.hamcrest.CoreMatchers;
20import org.junit.Test;
21import org.junit.runner.RunWith;
22
23import android.graphics.Color;
24import android.graphics.Rect;
25import android.graphics.drawable.ColorDrawable;
26import android.graphics.drawable.StateListDrawable;
27import android.support.test.runner.AndroidJUnit4;
28import android.support.v4.view.AccessibilityDelegateCompat;
29import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
30import android.test.UiThreadTest;
31import android.test.suitebuilder.annotation.MediumTest;
32import android.util.SparseIntArray;
33import android.util.StateSet;
34import android.view.View;
35import android.view.ViewGroup;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.List;
40import java.util.Map;
41import java.util.concurrent.atomic.AtomicBoolean;
42
43import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
44import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
45import static org.junit.Assert.assertEquals;
46import static org.junit.Assert.assertFalse;
47import static org.junit.Assert.assertNotNull;
48import static org.junit.Assert.assertSame;
49import static org.junit.Assert.assertThat;
50import static org.junit.Assert.assertTrue;
51
52@MediumTest
53@RunWith(AndroidJUnit4.class)
54public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
55
56    @Test
57    public void focusSearchFailureUp() throws Throwable {
58        focusSearchFailure(false);
59    }
60
61    @Test
62    public void focusSearchFailureDown() throws Throwable {
63        focusSearchFailure(true);
64    }
65
66    @Test
67    public void scrollToBadOffset() throws Throwable {
68        scrollToBadOffset(false);
69    }
70
71    @Test
72    public void scrollToBadOffsetReverse() throws Throwable {
73        scrollToBadOffset(true);
74    }
75
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
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
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
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
191    public void predictiveSpanLookup1() throws Throwable {
192        predictiveSpanLookupTest(0, false);
193    }
194
195    @Test
196    public void predictiveSpanLookup2() throws Throwable {
197        predictiveSpanLookupTest(0, true);
198    }
199
200    @Test
201    public void predictiveSpanLookup3() throws Throwable {
202        predictiveSpanLookupTest(1, false);
203    }
204
205    @Test
206    public void predictiveSpanLookup4() throws Throwable {
207        predictiveSpanLookupTest(1, true);
208    }
209
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
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
280    public void layoutParams() throws Throwable {
281        layoutParamsTest(GridLayoutManager.HORIZONTAL);
282        removeRecyclerView();
283        layoutParamsTest(GridLayoutManager.VERTICAL);
284    }
285
286    @Test
287    public void horizontalAccessibilitySpanIndices() throws Throwable {
288        accessibilitySpanIndicesTest(HORIZONTAL);
289    }
290
291    @Test
292    public void verticalAccessibilitySpanIndices() throws Throwable {
293        accessibilitySpanIndicesTest(VERTICAL);
294    }
295
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
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
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.FILL_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
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
467    public void spanLookup() {
468        spanLookupTest(false);
469    }
470
471    @Test
472    public void spanLookupWithCache() {
473        spanLookupTest(true);
474    }
475
476    @Test
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
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
540    public void removeAnchorItem() throws Throwable {
541        removeAnchorItemTest(
542                new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
543    }
544
545    @Test
546    public void removeAnchorItemReverse() throws Throwable {
547        removeAnchorItemTest(
548                new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
549                0);
550    }
551
552    @Test
553    public void removeAnchorItemHorizontal() throws Throwable {
554        removeAnchorItemTest(
555                new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
556                        false), 100, 0);
557    }
558
559    @Test
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     */
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.FILL_PARENT;
588                } else {
589                    maxSize = mRecyclerView.getHeight();
590                    mlp.width = ViewGroup.MarginLayoutParams.FILL_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
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
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
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
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
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
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