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