StaggeredGridLayoutManagerTest.java revision c95a6f1f125ad3a7e1f9f79bccf4b2603bc40eba
1/*
2 * Copyright 2018 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 androidx.recyclerview.widget;
18
19import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
20import static androidx.recyclerview.widget.StaggeredGridLayoutManager
21        .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
22import static androidx.recyclerview.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
23import static androidx.recyclerview.widget.StaggeredGridLayoutManager.HORIZONTAL;
24import static androidx.recyclerview.widget.StaggeredGridLayoutManager.LayoutParams;
25
26import static org.hamcrest.CoreMatchers.equalTo;
27import static org.junit.Assert.assertEquals;
28import static org.junit.Assert.assertFalse;
29import static org.junit.Assert.assertNotNull;
30import static org.junit.Assert.assertNull;
31import static org.junit.Assert.assertSame;
32import static org.junit.Assert.assertThat;
33import static org.junit.Assert.assertTrue;
34
35import android.graphics.Color;
36import android.graphics.Rect;
37import android.graphics.drawable.ColorDrawable;
38import android.graphics.drawable.StateListDrawable;
39import android.os.Parcel;
40import android.os.Parcelable;
41import android.support.test.filters.LargeTest;
42import android.text.TextUtils;
43import android.util.Log;
44import android.util.StateSet;
45import android.view.View;
46import android.view.ViewGroup;
47import android.view.accessibility.AccessibilityEvent;
48import android.widget.EditText;
49import android.widget.FrameLayout;
50
51import androidx.annotation.NonNull;
52import androidx.core.view.AccessibilityDelegateCompat;
53
54import org.hamcrest.CoreMatchers;
55import org.hamcrest.MatcherAssert;
56import org.junit.Test;
57
58import java.util.HashMap;
59import java.util.Map;
60import java.util.UUID;
61
62@LargeTest
63public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
64
65    @Test
66    public void layout_rvHasPaddingChildIsMatchParentVertical_childrenAreInsideParent()
67            throws Throwable {
68        layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, false);
69    }
70
71    @Test
72    public void layout_rvHasPaddingChildIsMatchParentHorizontal_childrenAreInsideParent()
73            throws Throwable {
74        layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, false);
75    }
76
77    @Test
78    public void layout_rvHasPaddingChildIsMatchParentVerticalFullSpan_childrenAreInsideParent()
79            throws Throwable {
80        layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(VERTICAL, true);
81    }
82
83    @Test
84    public void layout_rvHasPaddingChildIsMatchParentHorizontalFullSpan_childrenAreInsideParent()
85            throws Throwable {
86        layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(HORIZONTAL, true);
87    }
88
89    private void layout_rvHasPaddingChildIsMatchParent_childrenAreInsideParent(
90            final int orientation, final boolean fullSpan)
91            throws Throwable {
92
93        setupByConfig(new Config(orientation, false, 1, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
94                new GridTestAdapter(10, orientation) {
95
96                    @NonNull
97                    @Override
98                    public TestViewHolder onCreateViewHolder(
99                            @NonNull ViewGroup parent, int viewType) {
100                        View view = new View(parent.getContext());
101                        StaggeredGridLayoutManager.LayoutParams layoutParams =
102                                new StaggeredGridLayoutManager.LayoutParams(
103                                        ViewGroup.LayoutParams.MATCH_PARENT,
104                                        ViewGroup.LayoutParams.MATCH_PARENT);
105                        layoutParams.setFullSpan(fullSpan);
106                        view.setLayoutParams(layoutParams);
107                        return new TestViewHolder(view);
108                    }
109
110                    @Override
111                    public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
112                        // No actual binding needed, but we need to override this to prevent default
113                        // behavior of GridTestAdapter.
114                    }
115                });
116        mRecyclerView.setPadding(1, 2, 3, 4);
117
118        waitFirstLayout();
119
120        mActivityRule.runOnUiThread(new Runnable() {
121            @Override
122            public void run() {
123                int childDimension;
124                int recyclerViewDimensionMinusPadding;
125                if (orientation == VERTICAL) {
126                    childDimension = mRecyclerView.getChildAt(0).getHeight();
127                    recyclerViewDimensionMinusPadding = mRecyclerView.getHeight()
128                            - mRecyclerView.getPaddingTop()
129                            - mRecyclerView.getPaddingBottom();
130                } else {
131                    childDimension = mRecyclerView.getChildAt(0).getWidth();
132                    recyclerViewDimensionMinusPadding = mRecyclerView.getWidth()
133                            - mRecyclerView.getPaddingLeft()
134                            - mRecyclerView.getPaddingRight();
135                }
136                assertThat(childDimension, equalTo(recyclerViewDimensionMinusPadding));
137            }
138        });
139    }
140
141    @Test
142    public void forceLayoutOnDetach() throws Throwable {
143        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
144        waitFirstLayout();
145        assertFalse("test sanity", mRecyclerView.isLayoutRequested());
146        mActivityRule.runOnUiThread(new Runnable() {
147            @Override
148            public void run() {
149                mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler);
150            }
151        });
152        assertTrue(mRecyclerView.isLayoutRequested());
153    }
154
155    @Test
156    public void areAllStartsTheSame() throws Throwable {
157        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
158        waitFirstLayout();
159        smoothScrollToPosition(100);
160        mLayoutManager.expectLayouts(1);
161        mAdapter.deleteAndNotify(0, 2);
162        mLayoutManager.waitForLayout(2000);
163        smoothScrollToPosition(0);
164        assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
165    }
166
167    @Test
168    public void areAllEndsTheSame() throws Throwable {
169        setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
170        waitFirstLayout();
171        smoothScrollToPosition(100);
172        mLayoutManager.expectLayouts(1);
173        mAdapter.deleteAndNotify(0, 2);
174        mLayoutManager.waitForLayout(2);
175        smoothScrollToPosition(0);
176        assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
177    }
178
179    @Test
180    public void getPositionsBeforeInitialization() throws Throwable {
181        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
182        int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null);
183        MatcherAssert.assertThat(positions,
184                CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION,
185                        RecyclerView.NO_POSITION}));
186    }
187
188    @Test
189    public void findLastInUnevenDistribution() throws Throwable {
190        setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
191                .itemCount(5));
192        mAdapter.mOnBindCallback = new OnBindCallback() {
193            @Override
194            void onBoundItem(TestViewHolder vh, int position) {
195                LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
196                if (position == 1) {
197                    lp.height = mRecyclerView.getHeight() - 10;
198                } else {
199                    lp.height = 5;
200                }
201                vh.itemView.setMinimumHeight(0);
202            }
203        };
204        waitFirstLayout();
205        int[] into = new int[2];
206        mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
207        assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
208        assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
209        mLayoutManager.findLastCompletelyVisibleItemPositions(into);
210        assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
211        assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
212        assertEquals("first fully visible child should be at position",
213                0, mRecyclerView.getChildViewHolder(mLayoutManager.
214                        findFirstVisibleItemClosestToStart(true)).getPosition());
215        assertEquals("last fully visible child should be at position",
216                4, mRecyclerView.getChildViewHolder(mLayoutManager.
217                        findFirstVisibleItemClosestToEnd(true)).getPosition());
218
219        assertEquals("first visible child should be at position",
220                0, mRecyclerView.getChildViewHolder(mLayoutManager.
221                        findFirstVisibleItemClosestToStart(false)).getPosition());
222        assertEquals("last visible child should be at position",
223                4, mRecyclerView.getChildViewHolder(mLayoutManager.
224                        findFirstVisibleItemClosestToEnd(false)).getPosition());
225
226    }
227
228    @Test
229    public void customWidthInHorizontal() throws Throwable {
230        customSizeInScrollDirectionTest(
231                new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
232    }
233
234    @Test
235    public void customHeightInVertical() throws Throwable {
236        customSizeInScrollDirectionTest(
237                new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
238    }
239
240    public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
241        setupByConfig(config);
242        final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
243        mAdapter.mOnBindCallback = new OnBindCallback() {
244            @Override
245            void onBoundItem(TestViewHolder vh, int position) {
246                final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
247                final int size = 1 + position * 5;
248                if (config.mOrientation == HORIZONTAL) {
249                    layoutParams.width = size;
250                } else {
251                    layoutParams.height = size;
252                }
253                sizeMap.put(vh.itemView, size);
254                if (position == 3) {
255                    getLp(vh.itemView).setFullSpan(true);
256                }
257            }
258
259            @Override
260            boolean assignRandomSize() {
261                return false;
262            }
263        };
264        waitFirstLayout();
265        assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
266        for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
267            View child = mRecyclerView.getChildAt(i);
268            final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
269                    : child.getHeight();
270            assertEquals("child " + i + " should have the size specified in its layout params",
271                    sizeMap.get(child).intValue(), size);
272        }
273        checkForMainThreadException();
274    }
275
276    @Test
277    public void gapHandlingWhenItemMovesToTop() throws Throwable {
278        gapHandlingWhenItemMovesToTopTest();
279    }
280
281    @Test
282    public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
283        gapHandlingWhenItemMovesToTopTest(0);
284    }
285
286    @Test
287    public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
288        gapHandlingWhenItemMovesToTopTest(1);
289    }
290
291    public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
292        Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
293        config.itemCount(3);
294        setupByConfig(config);
295        mAdapter.mOnBindCallback = new OnBindCallback() {
296            @Override
297            void onBoundItem(TestViewHolder vh, int position) {
298            }
299
300            @Override
301            boolean assignRandomSize() {
302                return false;
303            }
304        };
305        for (int i : fullSpanIndices) {
306            mAdapter.mFullSpanItems.add(i);
307        }
308        waitFirstLayout();
309        mLayoutManager.expectLayouts(1);
310        mAdapter.moveItem(1, 0, true);
311        mLayoutManager.waitForLayout(2);
312        final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
313        // move back.
314        mLayoutManager.expectLayouts(1);
315        mAdapter.moveItem(0, 1, true);
316        mLayoutManager.waitForLayout(2);
317        mLayoutManager.expectLayouts(2);
318        mAdapter.moveAndNotify(1, 0);
319        mLayoutManager.waitForLayout(2);
320        Thread.sleep(1000);
321        getInstrumentation().waitForIdleSync();
322        checkForMainThreadException();
323        // item should be positioned properly
324        assertRectSetsEqual("final position after a move", desiredPositions,
325                mLayoutManager.collectChildCoordinates());
326
327    }
328
329    @Test
330    public void focusSearchFailureUp() throws Throwable {
331        focusSearchFailure(false);
332    }
333
334    @Test
335    public void focusSearchFailureDown() throws Throwable {
336        focusSearchFailure(true);
337    }
338
339    @Test
340    public void focusSearchFailureFromSubChild() throws Throwable {
341        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
342                new GridTestAdapter(1000, VERTICAL) {
343
344                    @NonNull
345                    @Override
346                    public TestViewHolder onCreateViewHolder(
347                            @NonNull ViewGroup parent, int viewType) {
348                        FrameLayout fl = new FrameLayout(parent.getContext());
349                        EditText editText = new EditText(parent.getContext());
350                        fl.addView(editText);
351                        editText.setEllipsize(TextUtils.TruncateAt.END);
352                        return new TestViewHolder(fl);
353                    }
354
355                    @Override
356                    public void onBindViewHolder(@NonNull TestViewHolder holder, int position) {
357                        Item item = mItems.get(position);
358                        holder.mBoundItem = item;
359                        ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
360                                item.mText + " (" + item.mId + ")");
361                        // Good to have colors for debugging
362                        StateListDrawable stl = new StateListDrawable();
363                        stl.addState(new int[]{android.R.attr.state_focused},
364                                new ColorDrawable(Color.RED));
365                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
366                        //noinspection deprecation using this for kitkat tests
367                        holder.itemView.setBackgroundDrawable(stl);
368                        if (mOnBindCallback != null) {
369                            mOnBindCallback.onBoundItem(holder, position);
370                        }
371                    }
372                });
373        mLayoutManager.expectLayouts(1);
374        setRecyclerView(mRecyclerView);
375        mLayoutManager.waitForLayout(10);
376        getInstrumentation().waitForIdleSync();
377        ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
378                mRecyclerView.getChildCount() - 1);
379        RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
380        View subChildToFocus = lastChild.getChildAt(0);
381        requestFocus(subChildToFocus, true);
382        assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true));
383        focusSearch(subChildToFocus, View.FOCUS_FORWARD);
384        waitForIdleScroll(mRecyclerView);
385        checkForMainThreadException();
386        View focusedChild = mRecyclerView.getFocusedChild();
387        if (focusedChild == subChildToFocus.getParent()) {
388            focusSearch(focusedChild, View.FOCUS_FORWARD);
389            waitForIdleScroll(mRecyclerView);
390            focusedChild = mRecyclerView.getFocusedChild();
391        }
392        RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder(
393                focusedChild);
394        assertTrue("new focused view should have a larger position "
395                        + lastViewHolder.getAdapterPosition() + " vs "
396                        + containingViewHolder.getAdapterPosition(),
397                lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition());
398    }
399
400    public void focusSearchFailure(boolean scrollDown) throws Throwable {
401        int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP;
402        setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
403                , new GridTestAdapter(31, 1) {
404                    RecyclerView mAttachedRv;
405
406                    @Override
407                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
408                            int viewType) {
409                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
410                        testViewHolder.itemView.setFocusable(true);
411                        testViewHolder.itemView.setFocusableInTouchMode(true);
412                        // Good to have colors for debugging
413                        StateListDrawable stl = new StateListDrawable();
414                        stl.addState(new int[]{android.R.attr.state_focused},
415                                new ColorDrawable(Color.RED));
416                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
417                        //noinspection deprecation used to support kitkat tests
418                        testViewHolder.itemView.setBackgroundDrawable(stl);
419                        return testViewHolder;
420                    }
421
422                    @Override
423                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
424                        mAttachedRv = recyclerView;
425                    }
426
427                    @Override
428                    public void onBindViewHolder(@NonNull TestViewHolder holder,
429                            int position) {
430                        super.onBindViewHolder(holder, position);
431                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
432                    }
433                });
434        /**
435         * 0  1  2
436         * 3  4  5
437         * 6  7  8
438         * 9  10 11
439         * 12 13 14
440         * 15 16 17
441         * 18 18 18
442         * 19
443         * 20 20 20
444         * 21 22
445         * 23 23 23
446         * 24 25 26
447         * 27 28 29
448         * 30
449         */
450        mAdapter.mFullSpanItems.add(18);
451        mAdapter.mFullSpanItems.add(20);
452        mAdapter.mFullSpanItems.add(23);
453        waitFirstLayout();
454        View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView;
455        assertTrue(requestFocus(viewToFocus, true));
456        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
457        int pos = 1;
458        View focusedView = viewToFocus;
459        while (pos < 16) {
460            focusSearchAndWaitForScroll(focusedView, focusDir);
461            focusedView = mRecyclerView.getFocusedChild();
462            assertEquals(pos + 3,
463                    mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
464            pos += 3;
465        }
466        for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
467            focusSearchAndWaitForScroll(focusedView, focusDir);
468            focusedView = mRecyclerView.getFocusedChild();
469            assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
470        }
471        // now move right
472        focusSearch(focusedView, View.FOCUS_RIGHT);
473        waitForIdleScroll(mRecyclerView);
474        focusedView = mRecyclerView.getFocusedChild();
475        assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
476        for (int i : new int[]{28, 30}) {
477            focusSearchAndWaitForScroll(focusedView, focusDir);
478            focusedView = mRecyclerView.getFocusedChild();
479            assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
480        }
481    }
482
483    private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
484        focusSearch(focused, dir);
485        waitForIdleScroll(mRecyclerView);
486    }
487
488    @Test
489    public void topUnfocusableViewsVisibility() throws Throwable {
490        // The maximum number of rows that can be fully in-bounds of RV.
491        final int visibleRowCount = 5;
492        final int spanCount = 3;
493        final int lastFocusableIndex = 6;
494
495        setupByConfig(new Config(VERTICAL, true, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
496                new GridTestAdapter(18, 1) {
497                    RecyclerView mAttachedRv;
498
499                    @Override
500                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
501                            int viewType) {
502                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
503                        testViewHolder.itemView.setFocusable(true);
504                        testViewHolder.itemView.setFocusableInTouchMode(true);
505                        // Good to have colors for debugging
506                        StateListDrawable stl = new StateListDrawable();
507                        stl.addState(new int[]{android.R.attr.state_focused},
508                                new ColorDrawable(Color.RED));
509                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
510                        //noinspection deprecation used to support kitkat tests
511                        testViewHolder.itemView.setBackgroundDrawable(stl);
512                        return testViewHolder;
513                    }
514
515                    @Override
516                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
517                        mAttachedRv = recyclerView;
518                    }
519
520                    @Override
521                    public void onBindViewHolder(@NonNull TestViewHolder holder,
522                            int position) {
523                        super.onBindViewHolder(holder, position);
524                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
525                                .getLayoutParams();
526                        if (position <= lastFocusableIndex) {
527                            holder.itemView.setFocusable(true);
528                            holder.itemView.setFocusableInTouchMode(true);
529                        } else {
530                            holder.itemView.setFocusable(false);
531                            holder.itemView.setFocusableInTouchMode(false);
532                        }
533                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
534                        lp.topMargin = 0;
535                        lp.leftMargin = 0;
536                        lp.rightMargin = 0;
537                        lp.bottomMargin = 0;
538                        if (position == 11) {
539                            lp.bottomMargin = 9;
540                        }
541                    }
542                });
543
544        /**
545         *
546         * 15 16 17
547         * 12 13 14
548         * 11 11 11
549         * 9 10
550         * 8 8 8
551         * 7
552         * 6 6 6
553         * 3 4 5
554         * 0 1 2
555         */
556        mAdapter.mFullSpanItems.add(6);
557        mAdapter.mFullSpanItems.add(8);
558        mAdapter.mFullSpanItems.add(11);
559        waitFirstLayout();
560
561
562        // adapter position of the currently focused item.
563        int focusIndex = 1;
564        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
565                focusIndex);
566        View viewToFocus = toFocus.itemView;
567        assertTrue(requestFocus(viewToFocus, true));
568        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
569
570        // The VH of the unfocusable item that just became fully visible after focusSearch.
571        RecyclerView.ViewHolder toVisible = null;
572
573        View focusedView = viewToFocus;
574        int actualFocusIndex = -1;
575        // First, scroll until the last focusable row.
576        for (int i : new int[]{4, 6}) {
577            focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
578            focusedView = mRecyclerView.getFocusedChild();
579            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
580            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
581                    + actualFocusIndex, i, actualFocusIndex);
582        }
583
584        // Further scroll up in order to make the unfocusable rows visible. This process should
585        // continue until the currently focused item is still visible. The focused item should not
586        // change in this loop.
587        for (int i : new int[]{9, 11, 11, 11}) {
588            focusSearchAndWaitForScroll(focusedView, View.FOCUS_UP);
589            focusedView = mRecyclerView.getFocusedChild();
590            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
591            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
592
593            assertEquals("Focused view should not be changed, whereas it's now at "
594                    + actualFocusIndex, 6, actualFocusIndex);
595            assertTrue("Focused child should be at least partially visible.",
596                    isViewPartiallyInBound(mRecyclerView, focusedView));
597            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
598                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
599        }
600    }
601
602    @Test
603    public void bottomUnfocusableViewsVisibility() throws Throwable {
604        // The maximum number of rows that can be fully in-bounds of RV.
605        final int visibleRowCount = 5;
606        final int spanCount = 3;
607        final int lastFocusableIndex = 6;
608
609        setupByConfig(new Config(VERTICAL, false, spanCount, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
610                new GridTestAdapter(18, 1) {
611                    RecyclerView mAttachedRv;
612
613                    @Override
614                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
615                            int viewType) {
616                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
617                        testViewHolder.itemView.setFocusable(true);
618                        testViewHolder.itemView.setFocusableInTouchMode(true);
619                        // Good to have colors for debugging
620                        StateListDrawable stl = new StateListDrawable();
621                        stl.addState(new int[]{android.R.attr.state_focused},
622                                new ColorDrawable(Color.RED));
623                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
624                        //noinspection deprecation used to support kitkat tests
625                        testViewHolder.itemView.setBackgroundDrawable(stl);
626                        return testViewHolder;
627                    }
628
629                    @Override
630                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
631                        mAttachedRv = recyclerView;
632                    }
633
634                    @Override
635                    public void onBindViewHolder(@NonNull TestViewHolder holder,
636                            int position) {
637                        super.onBindViewHolder(holder, position);
638                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
639                                .getLayoutParams();
640                        if (position <= lastFocusableIndex) {
641                            holder.itemView.setFocusable(true);
642                            holder.itemView.setFocusableInTouchMode(true);
643                        } else {
644                            holder.itemView.setFocusable(false);
645                            holder.itemView.setFocusableInTouchMode(false);
646                        }
647                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
648                        lp.topMargin = 0;
649                        lp.leftMargin = 0;
650                        lp.rightMargin = 0;
651                        lp.bottomMargin = 0;
652                        if (position == 11) {
653                            lp.topMargin = 9;
654                        }
655                    }
656                });
657
658        /**
659         * 0 1 2
660         * 3 4 5
661         * 6 6 6
662         * 7
663         * 8 8 8
664         * 9 10
665         * 11 11 11
666         * 12 13 14
667         * 15 16 17
668         */
669        mAdapter.mFullSpanItems.add(6);
670        mAdapter.mFullSpanItems.add(8);
671        mAdapter.mFullSpanItems.add(11);
672        waitFirstLayout();
673
674
675        // adapter position of the currently focused item.
676        int focusIndex = 1;
677        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
678                focusIndex);
679        View viewToFocus = toFocus.itemView;
680        assertTrue(requestFocus(viewToFocus, true));
681        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
682
683        // The VH of the unfocusable item that just became fully visible after focusSearch.
684        RecyclerView.ViewHolder toVisible = null;
685
686        View focusedView = viewToFocus;
687        int actualFocusIndex = -1;
688        // First, scroll until the last focusable row.
689        for (int i : new int[]{4, 6}) {
690            focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
691            focusedView = mRecyclerView.getFocusedChild();
692            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
693            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
694                    + actualFocusIndex, i, actualFocusIndex);
695        }
696
697        // Further scroll down in order to make the unfocusable rows visible. This process should
698        // continue until the currently focused item is still visible. The focused item should not
699        // change in this loop.
700        for (int i : new int[]{9, 11, 11, 11}) {
701            focusSearchAndWaitForScroll(focusedView, View.FOCUS_DOWN);
702            focusedView = mRecyclerView.getFocusedChild();
703            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
704            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
705
706            assertEquals("Focused view should not be changed, whereas it's now at "
707                    + actualFocusIndex, 6, actualFocusIndex);
708            assertTrue("Focused child should be at least partially visible.",
709                    isViewPartiallyInBound(mRecyclerView, focusedView));
710            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
711                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
712        }
713    }
714
715    @Test
716    public void leftUnfocusableViewsVisibility() throws Throwable {
717        // The maximum number of columns that can be fully in-bounds of RV.
718        final int visibleColCount = 5;
719        final int spanCount = 3;
720        final int lastFocusableIndex = 6;
721
722        // Reverse layout so that views are placed from right to left.
723        setupByConfig(new Config(HORIZONTAL, true, spanCount,
724                        GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
725                new GridTestAdapter(18, 1) {
726                    RecyclerView mAttachedRv;
727
728                    @Override
729                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
730                            int viewType) {
731                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
732                        testViewHolder.itemView.setFocusable(true);
733                        testViewHolder.itemView.setFocusableInTouchMode(true);
734                        // Good to have colors for debugging
735                        StateListDrawable stl = new StateListDrawable();
736                        stl.addState(new int[]{android.R.attr.state_focused},
737                                new ColorDrawable(Color.RED));
738                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
739                        //noinspection deprecation used to support kitkat tests
740                        testViewHolder.itemView.setBackgroundDrawable(stl);
741                        return testViewHolder;
742                    }
743
744                    @Override
745                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
746                        mAttachedRv = recyclerView;
747                    }
748
749                    @Override
750                    public void onBindViewHolder(@NonNull TestViewHolder holder,
751                            int position) {
752                        super.onBindViewHolder(holder, position);
753                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
754                                .getLayoutParams();
755                        if (position <= lastFocusableIndex) {
756                            holder.itemView.setFocusable(true);
757                            holder.itemView.setFocusableInTouchMode(true);
758                        } else {
759                            holder.itemView.setFocusable(false);
760                            holder.itemView.setFocusableInTouchMode(false);
761                        }
762                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
763                        lp.topMargin = 0;
764                        lp.leftMargin = 0;
765                        lp.rightMargin = 0;
766                        lp.bottomMargin = 0;
767                        if (position == 11) {
768                            lp.rightMargin = 9;
769                        }
770                    }
771                });
772
773        /**
774         * 15 12 11 9  8 7 6 3 0
775         * 16 13 11 10 8   6 4 1
776         * 17 14 11    8   6 5 2
777         */
778        mAdapter.mFullSpanItems.add(6);
779        mAdapter.mFullSpanItems.add(8);
780        mAdapter.mFullSpanItems.add(11);
781        waitFirstLayout();
782
783
784        // adapter position of the currently focused item.
785        int focusIndex = 1;
786        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
787                focusIndex);
788        View viewToFocus = toFocus.itemView;
789        assertTrue(requestFocus(viewToFocus, true));
790        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
791
792        // The VH of the unfocusable item that just became fully visible after focusSearch.
793        RecyclerView.ViewHolder toVisible = null;
794
795        View focusedView = viewToFocus;
796        int actualFocusIndex = -1;
797        // First, scroll until the last focusable column.
798        for (int i : new int[]{4, 6}) {
799            focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
800            focusedView = mRecyclerView.getFocusedChild();
801            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
802            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
803                    + actualFocusIndex, i, actualFocusIndex);
804        }
805
806        // Further scroll left in order to make the unfocusable columns visible. This process should
807        // continue until the currently focused item is still visible. The focused item should not
808        // change in this loop.
809        for (int i : new int[]{9, 11, 11, 11}) {
810            focusSearchAndWaitForScroll(focusedView, View.FOCUS_LEFT);
811            focusedView = mRecyclerView.getFocusedChild();
812            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
813            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
814
815            assertEquals("Focused view should not be changed, whereas it's now at "
816                    + actualFocusIndex, 6, actualFocusIndex);
817            assertTrue("Focused child should be at least partially visible.",
818                    isViewPartiallyInBound(mRecyclerView, focusedView));
819            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
820                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
821        }
822    }
823
824    @Test
825    public void rightUnfocusableViewsVisibility() throws Throwable {
826        // The maximum number of columns that can be fully in-bounds of RV.
827        final int visibleColCount = 5;
828        final int spanCount = 3;
829        final int lastFocusableIndex = 6;
830
831        setupByConfig(new Config(HORIZONTAL, false, spanCount,
832                        GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
833                new GridTestAdapter(18, 1) {
834                    RecyclerView mAttachedRv;
835
836                    @Override
837                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
838                            int viewType) {
839                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
840                        testViewHolder.itemView.setFocusable(true);
841                        testViewHolder.itemView.setFocusableInTouchMode(true);
842                        // Good to have colors for debugging
843                        StateListDrawable stl = new StateListDrawable();
844                        stl.addState(new int[]{android.R.attr.state_focused},
845                                new ColorDrawable(Color.RED));
846                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
847                        //noinspection deprecation used to support kitkat tests
848                        testViewHolder.itemView.setBackgroundDrawable(stl);
849                        return testViewHolder;
850                    }
851
852                    @Override
853                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
854                        mAttachedRv = recyclerView;
855                    }
856
857                    @Override
858                    public void onBindViewHolder(@NonNull TestViewHolder holder,
859                            int position) {
860                        super.onBindViewHolder(holder, position);
861                        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
862                                .getLayoutParams();
863                        if (position <= lastFocusableIndex) {
864                            holder.itemView.setFocusable(true);
865                            holder.itemView.setFocusableInTouchMode(true);
866                        } else {
867                            holder.itemView.setFocusable(false);
868                            holder.itemView.setFocusableInTouchMode(false);
869                        }
870                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
871                        lp.topMargin = 0;
872                        lp.leftMargin = 0;
873                        lp.rightMargin = 0;
874                        lp.bottomMargin = 0;
875                        if (position == 11) {
876                            lp.leftMargin = 9;
877                        }
878                    }
879                });
880
881        /**
882         * 0 3 6 7 8 9  11 12 15
883         * 1 4 6   8 10 11 13 16
884         * 2 5 6   8    11 14 17
885         */
886        mAdapter.mFullSpanItems.add(6);
887        mAdapter.mFullSpanItems.add(8);
888        mAdapter.mFullSpanItems.add(11);
889        waitFirstLayout();
890
891
892        // adapter position of the currently focused item.
893        int focusIndex = 1;
894        RecyclerView.ViewHolder toFocus = mRecyclerView.findViewHolderForAdapterPosition(
895                focusIndex);
896        View viewToFocus = toFocus.itemView;
897        assertTrue(requestFocus(viewToFocus, true));
898        assertSame(viewToFocus, mRecyclerView.getFocusedChild());
899
900        // The VH of the unfocusable item that just became fully visible after focusSearch.
901        RecyclerView.ViewHolder toVisible = null;
902
903        View focusedView = viewToFocus;
904        int actualFocusIndex = -1;
905        // First, scroll until the last focusable column.
906        for (int i : new int[]{4, 6}) {
907            focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
908            focusedView = mRecyclerView.getFocusedChild();
909            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
910            assertEquals("Focused view should be at adapter position " + i + " whereas it's at "
911                    + actualFocusIndex, i, actualFocusIndex);
912        }
913
914        // Further scroll right in order to make the unfocusable rows visible. This process should
915        // continue until the currently focused item is still visible. The focused item should not
916        // change in this loop.
917        for (int i : new int[]{9, 11, 11, 11}) {
918            focusSearchAndWaitForScroll(focusedView, View.FOCUS_RIGHT);
919            focusedView = mRecyclerView.getFocusedChild();
920            actualFocusIndex = mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition();
921            toVisible = mRecyclerView.findViewHolderForAdapterPosition(i);
922
923            assertEquals("Focused view should not be changed, whereas it's now at "
924                    + actualFocusIndex, 6, actualFocusIndex);
925            assertTrue("Focused child should be at least partially visible.",
926                    isViewPartiallyInBound(mRecyclerView, focusedView));
927            assertTrue("Child view at adapter pos " + i + " should be fully visible.",
928                    isViewFullyInBound(mRecyclerView, toVisible.itemView));
929        }
930    }
931
932    @Test
933    public void scrollToPositionWithPredictive() throws Throwable {
934        scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
935        removeRecyclerView();
936        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
937                LinearLayoutManager.INVALID_OFFSET);
938        removeRecyclerView();
939        scrollToPositionWithPredictive(9, 20);
940        removeRecyclerView();
941        scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
942
943    }
944
945    public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
946            throws Throwable {
947        setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
948                false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
949        waitFirstLayout();
950        mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
951            @Override
952            void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
953                RecyclerView rv = mLayoutManager.mRecyclerView;
954                if (state.isPreLayout()) {
955                    assertEquals("pending scroll position should still be pending",
956                            scrollPosition, mLayoutManager.mPendingScrollPosition);
957                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
958                        assertEquals("pending scroll position offset should still be pending",
959                                scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
960                    }
961                } else {
962                    RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
963                    assertNotNull("scroll to position should work", vh);
964                    if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
965                        assertEquals("scroll offset should be applied properly",
966                                mLayoutManager.getPaddingTop() + scrollOffset
967                                        + ((RecyclerView.LayoutParams) vh.itemView
968                                        .getLayoutParams()).topMargin,
969                                mLayoutManager.getDecoratedTop(vh.itemView));
970                    }
971                }
972            }
973        };
974        mLayoutManager.expectLayouts(2);
975        mActivityRule.runOnUiThread(new Runnable() {
976            @Override
977            public void run() {
978                try {
979                    mAdapter.addAndNotify(0, 1);
980                    if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
981                        mLayoutManager.scrollToPosition(scrollPosition);
982                    } else {
983                        mLayoutManager.scrollToPositionWithOffset(scrollPosition,
984                                scrollOffset);
985                    }
986
987                } catch (Throwable throwable) {
988                    throwable.printStackTrace();
989                }
990
991            }
992        });
993        mLayoutManager.waitForLayout(2);
994        checkForMainThreadException();
995    }
996
997    @Test
998    public void moveGapHandling() throws Throwable {
999        Config config = new Config().spanCount(2).itemCount(40);
1000        setupByConfig(config);
1001        waitFirstLayout();
1002        mLayoutManager.expectLayouts(2);
1003        mAdapter.moveAndNotify(4, 1);
1004        mLayoutManager.waitForLayout(2);
1005        assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
1006    }
1007
1008    @Test
1009    public void updateAfterFullSpan() throws Throwable {
1010        updateAfterFullSpanGapHandlingTest(0);
1011    }
1012
1013    @Test
1014    public void updateAfterFullSpan2() throws Throwable {
1015        updateAfterFullSpanGapHandlingTest(20);
1016    }
1017
1018    @Test
1019    public void temporaryGapHandling() throws Throwable {
1020        int fullSpanIndex = 200;
1021        setupByConfig(new Config().spanCount(2).itemCount(500));
1022        mAdapter.mFullSpanItems.add(fullSpanIndex);
1023        waitFirstLayout();
1024        smoothScrollToPosition(fullSpanIndex + 200);// go far away
1025        assertNull("test sanity. full span item should not be visible",
1026                mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
1027        mLayoutManager.expectLayouts(1);
1028        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
1029        mLayoutManager.waitForLayout(1);
1030        smoothScrollToPosition(0);
1031        mLayoutManager.expectLayouts(1);
1032        smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
1033        String log = mLayoutManager.layoutToString("post gap");
1034        mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
1035                + "relayout " + log, 2);
1036        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
1037        assertNotNull("full span item should be there:\n" + log, fullSpan);
1038        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
1039        assertNotNull("next view should be there\n" + log, view1);
1040        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
1041        assertNotNull("+2 view should be there\n" + log, view2);
1042
1043        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
1044        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
1045        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
1046        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
1047        assertEquals("no gap between span and view 1",
1048                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
1049                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
1050        assertEquals("no gap between span and view 2",
1051                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
1052                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
1053    }
1054
1055    public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
1056        setupByConfig(new Config().spanCount(2).itemCount(100));
1057        mAdapter.mFullSpanItems.add(fullSpanIndex);
1058        waitFirstLayout();
1059        smoothScrollToPosition(fullSpanIndex + 30);
1060        mLayoutManager.expectLayouts(1);
1061        mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
1062        mLayoutManager.waitForLayout(1);
1063        smoothScrollToPosition(fullSpanIndex);
1064        // give it some time to fix the gap
1065        Thread.sleep(500);
1066        View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
1067
1068        View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
1069        View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
1070
1071        LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
1072        LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
1073        assertEquals("view 1 span index", 0, lp1.getSpanIndex());
1074        assertEquals("view 2 span index", 1, lp2.getSpanIndex());
1075        assertEquals("no gap between span and view 1",
1076                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
1077                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
1078        assertEquals("no gap between span and view 2",
1079                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
1080                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
1081    }
1082
1083    @Test
1084    public void innerGapHandling() throws Throwable {
1085        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
1086        innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
1087    }
1088
1089    public void innerGapHandlingTest(int strategy) throws Throwable {
1090        Config config = new Config().spanCount(3).itemCount(500);
1091        setupByConfig(config);
1092        mLayoutManager.setGapStrategy(strategy);
1093        mAdapter.mFullSpanItems.add(100);
1094        mAdapter.mFullSpanItems.add(104);
1095        mAdapter.mViewsHaveEqualSize = true;
1096        mAdapter.mOnBindCallback = new OnBindCallback() {
1097            @Override
1098            void onBoundItem(TestViewHolder vh, int position) {
1099
1100            }
1101
1102            @Override
1103            void onCreatedViewHolder(TestViewHolder vh) {
1104                super.onCreatedViewHolder(vh);
1105                //make sure we have enough views
1106                mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
1107            }
1108        };
1109        waitFirstLayout();
1110        mLayoutManager.expectLayouts(1);
1111        scrollToPosition(400);
1112        mLayoutManager.waitForLayout(2);
1113        View view400 = mLayoutManager.findViewByPosition(400);
1114        assertNotNull("test sanity, scrollToPos should succeed", view400);
1115        assertTrue("test sanity, view should be visible top",
1116                mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
1117                        mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
1118        assertTrue("test sanity, view should be visible bottom",
1119                mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
1120                        mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
1121        mLayoutManager.expectLayouts(2);
1122        mAdapter.addAndNotify(101, 1);
1123        mLayoutManager.waitForLayout(2);
1124        checkForMainThreadException();
1125        if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
1126            mLayoutManager.expectLayouts(1);
1127        }
1128        // state
1129        // now smooth scroll to 99 to trigger a layout around 100
1130        mLayoutManager.validateChildren();
1131        smoothScrollToPosition(99);
1132        switch (strategy) {
1133            case GAP_HANDLING_NONE:
1134                assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
1135                        new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
1136                        new int[]{105, 0});
1137                break;
1138            case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
1139                mLayoutManager.waitForLayout(2);
1140                assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
1141                        new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
1142                break;
1143        }
1144
1145    }
1146
1147    @Test
1148    public void fullSizeSpans() throws Throwable {
1149        Config config = new Config().spanCount(5).itemCount(30);
1150        setupByConfig(config);
1151        mAdapter.mFullSpanItems.add(3);
1152        waitFirstLayout();
1153        assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
1154                new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
1155                new int[]{7, 3}, new int[]{8, 4});
1156    }
1157
1158    void assertSpans(String msg, int[]... childSpanTuples) {
1159        msg = msg + mLayoutManager.layoutToString("\n\n");
1160        for (int i = 0; i < childSpanTuples.length; i++) {
1161            assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
1162        }
1163    }
1164
1165    void assertSpan(String msg, int childPosition, int expectedSpan) {
1166        View view = mLayoutManager.findViewByPosition(childPosition);
1167        assertNotNull(msg + " view at position " + childPosition + " should exists", view);
1168        assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
1169                getLp(view).mSpan.mIndex);
1170    }
1171
1172    @Test
1173    public void partialSpanInvalidation() throws Throwable {
1174        Config config = new Config().spanCount(5).itemCount(100);
1175        setupByConfig(config);
1176        for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
1177            mAdapter.mFullSpanItems.add(i);
1178        }
1179        waitFirstLayout();
1180        smoothScrollToPosition(50);
1181        int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
1182        mAdapter.changeAndNotify(15, 2);
1183        Thread.sleep(200);
1184        assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
1185                mLayoutManager.mLazySpanLookup.mData[30]);
1186        assertEquals("item in invalidated range should have clear span id",
1187                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
1188        smoothScrollToPosition(85);
1189        int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
1190        mAdapter.deleteAndNotify(55, 2);
1191        Thread.sleep(200);
1192        assertEquals("item in invalidated range should have clear span id",
1193                LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
1194        int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
1195        assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
1196                newSpans, 0, 0, newSpans.length);
1197    }
1198
1199    // Same as Arrays.copyOfRange but for API 7
1200    private int[] copyOfRange(int[] original, int from, int to) {
1201        int newLength = to - from;
1202        if (newLength < 0) {
1203            throw new IllegalArgumentException(from + " > " + to);
1204        }
1205        int[] copy = new int[newLength];
1206        System.arraycopy(original, from, copy, 0,
1207                Math.min(original.length - from, newLength));
1208        return copy;
1209    }
1210
1211    @Test
1212    public void spanReassignmentsOnItemChange() throws Throwable {
1213        Config config = new Config().spanCount(5);
1214        setupByConfig(config);
1215        waitFirstLayout();
1216        smoothScrollToPosition(mAdapter.getItemCount() / 2);
1217        final int changePosition = mAdapter.getItemCount() / 4;
1218        mLayoutManager.expectLayouts(1);
1219        if (RecyclerView.POST_UPDATES_ON_ANIMATION) {
1220            mAdapter.changeAndNotify(changePosition, 1);
1221            mLayoutManager.assertNoLayout("no layout should happen when an invisible child is "
1222                    + "updated", 1);
1223        } else {
1224            mAdapter.changeAndNotify(changePosition, 1);
1225            mLayoutManager.waitForLayout(1);
1226        }
1227
1228        // delete an item before visible area
1229        int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
1230        assertTrue("test sanity", deletedPosition >= 0);
1231        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
1232        if (DEBUG) {
1233            Log.d(TAG, "before:");
1234            for (Map.Entry<Item, Rect> entry : before.entrySet()) {
1235                Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
1236            }
1237        }
1238        mLayoutManager.expectLayouts(1);
1239        mAdapter.deleteAndNotify(deletedPosition, 1);
1240        mLayoutManager.waitForLayout(2);
1241        assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
1242                        + "should not affect the layout if it is not visible", before,
1243                mLayoutManager.collectChildCoordinates()
1244        );
1245        deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
1246        mLayoutManager.expectLayouts(1);
1247        mAdapter.deleteAndNotify(deletedPosition, 1);
1248        mLayoutManager.waitForLayout(2);
1249        assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
1250                + "layout", before, mLayoutManager.collectChildCoordinates());
1251    }
1252
1253    void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
1254            int length) {
1255        for (int i = 0; i < length; i++) {
1256            assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
1257                    set2[start2 + i]);
1258        }
1259    }
1260
1261    @Test
1262    public void spanCountChangeOnRestoreSavedState() throws Throwable {
1263        Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE).itemCount(50);
1264        setupByConfig(config);
1265        waitFirstLayout();
1266
1267        int beforeChildCount = mLayoutManager.getChildCount();
1268        Parcelable savedState = mRecyclerView.onSaveInstanceState();
1269        // we append a suffix to the parcelable to test out of bounds
1270        String parcelSuffix = UUID.randomUUID().toString();
1271        Parcel parcel = Parcel.obtain();
1272        savedState.writeToParcel(parcel, 0);
1273        parcel.writeString(parcelSuffix);
1274        removeRecyclerView();
1275        // reset for reading
1276        parcel.setDataPosition(0);
1277        // re-create
1278        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
1279        removeRecyclerView();
1280
1281        RecyclerView restored = new RecyclerView(getActivity());
1282        mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
1283        mLayoutManager.setReverseLayout(config.mReverseLayout);
1284        mLayoutManager.setGapStrategy(config.mGapStrategy);
1285        restored.setLayoutManager(mLayoutManager);
1286        // use the same adapter for Rect matching
1287        restored.setAdapter(mAdapter);
1288        restored.onRestoreInstanceState(savedState);
1289        mLayoutManager.setSpanCount(1);
1290        mLayoutManager.expectLayouts(1);
1291        setRecyclerView(restored);
1292        mLayoutManager.waitForLayout(2);
1293        assertEquals("on saved state, reverse layout should be preserved",
1294                config.mReverseLayout, mLayoutManager.getReverseLayout());
1295        assertEquals("on saved state, orientation should be preserved",
1296                config.mOrientation, mLayoutManager.getOrientation());
1297        assertEquals("after setting new span count, layout manager should keep new value",
1298                1, mLayoutManager.getSpanCount());
1299        assertEquals("on saved state, gap strategy should be preserved",
1300                config.mGapStrategy, mLayoutManager.getGapStrategy());
1301        assertTrue("when span count is dramatically changed after restore, # of child views "
1302                + "should change", beforeChildCount > mLayoutManager.getChildCount());
1303        // make sure SGLM can layout all children. is some span info is leaked, this would crash
1304        smoothScrollToPosition(mAdapter.getItemCount() - 1);
1305    }
1306
1307    @Test
1308    public void scrollAndClear() throws Throwable {
1309        setupByConfig(new Config());
1310        waitFirstLayout();
1311
1312        assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
1313
1314        mLayoutManager.expectLayouts(1);
1315        mActivityRule.runOnUiThread(new Runnable() {
1316            @Override
1317            public void run() {
1318                mLayoutManager.scrollToPositionWithOffset(1, 0);
1319                mAdapter.clearOnUIThread();
1320            }
1321        });
1322        mLayoutManager.waitForLayout(2);
1323
1324        assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
1325    }
1326
1327    @Test
1328    public void accessibilityPositions() throws Throwable {
1329        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
1330        waitFirstLayout();
1331        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
1332                .getCompatAccessibilityDelegate();
1333        final AccessibilityEvent event = AccessibilityEvent.obtain();
1334        mActivityRule.runOnUiThread(new Runnable() {
1335            @Override
1336            public void run() {
1337                delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
1338            }
1339        });
1340        final int start = mRecyclerView
1341                .getChildLayoutPosition(
1342                        mLayoutManager.findFirstVisibleItemClosestToStart(false));
1343        final int end = mRecyclerView
1344                .getChildLayoutPosition(
1345                        mLayoutManager.findFirstVisibleItemClosestToEnd(false));
1346        assertEquals("first item position should match",
1347                Math.min(start, end), event.getFromIndex());
1348        assertEquals("last item position should match",
1349                Math.max(start, end), event.getToIndex());
1350
1351    }
1352}
1353