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 android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21
22import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
23import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
24
25import static org.hamcrest.CoreMatchers.is;
26import static org.junit.Assert.assertEquals;
27import static org.junit.Assert.assertFalse;
28import static org.junit.Assert.assertNotNull;
29import static org.junit.Assert.assertSame;
30import static org.junit.Assert.assertThat;
31import static org.junit.Assert.assertTrue;
32
33import android.graphics.Color;
34import android.graphics.drawable.ColorDrawable;
35import android.graphics.drawable.StateListDrawable;
36import android.os.Build;
37import android.support.test.annotation.UiThreadTest;
38import android.support.test.filters.LargeTest;
39import android.support.test.filters.SdkSuppress;
40import android.support.test.runner.AndroidJUnit4;
41import android.util.SparseIntArray;
42import android.util.StateSet;
43import android.view.View;
44import android.view.ViewGroup;
45
46import androidx.annotation.NonNull;
47import androidx.core.view.AccessibilityDelegateCompat;
48import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
49
50import org.hamcrest.CoreMatchers;
51import org.junit.Test;
52import org.junit.runner.RunWith;
53
54import java.util.ArrayList;
55import java.util.HashMap;
56import java.util.List;
57import java.util.Map;
58import java.util.concurrent.atomic.AtomicBoolean;
59
60@LargeTest
61@RunWith(AndroidJUnit4.class)
62public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
63
64    @Test
65    public void focusSearchFailureUp() throws Throwable {
66        focusSearchFailure(false);
67    }
68
69    @Test
70    public void focusSearchFailureDown() throws Throwable {
71        focusSearchFailure(true);
72    }
73
74    @Test
75    public void scrollToBadOffset() throws Throwable {
76        scrollToBadOffset(false);
77    }
78
79    @Test
80    public void scrollToBadOffsetReverse() throws Throwable {
81        scrollToBadOffset(true);
82    }
83
84    private void scrollToBadOffset(boolean reverseLayout) throws Throwable {
85        final int w = 500;
86        final int h = 1000;
87        RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout),
88                new GridTestAdapter(100) {
89                    @Override
90                    public void onBindViewHolder(@NonNull TestViewHolder holder,
91                            int position) {
92                        super.onBindViewHolder(holder, position);
93                        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
94                        if (lp == null) {
95                            lp = new ViewGroup.LayoutParams(w / 2, h / 2);
96                            holder.itemView.setLayoutParams(lp);
97                        } else {
98                            lp.width = w / 2;
99                            lp.height = h / 2;
100                            holder.itemView.setLayoutParams(lp);
101                        }
102                    }
103                });
104        TestedFrameLayout.FullControlLayoutParams lp
105                = new TestedFrameLayout.FullControlLayoutParams(w, h);
106        recyclerView.setLayoutParams(lp);
107        waitForFirstLayout(recyclerView);
108        mGlm.expectLayout(1);
109        scrollToPosition(11);
110        mGlm.waitForLayout(2);
111        // assert spans and position etc
112        for (int i = 0; i < mGlm.getChildCount(); i++) {
113            View child = mGlm.getChildAt(i);
114            GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child
115                    .getLayoutParams();
116            assertThat("span index for child at " + i + " with position " + params
117                            .getViewAdapterPosition(),
118                    params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2));
119        }
120        // assert spans and positions etc.
121        int lastVisible = mGlm.findLastVisibleItemPosition();
122        // this should be the scrolled child
123        assertThat(lastVisible, CoreMatchers.is(11));
124    }
125
126    private void focusSearchFailure(boolean scrollDown) throws Throwable {
127        final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown)
128                , new GridTestAdapter(31, 1) {
129                    RecyclerView mAttachedRv;
130
131                    @Override
132                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
133                            int viewType) {
134                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
135                        testViewHolder.itemView.setFocusable(true);
136                        testViewHolder.itemView.setFocusableInTouchMode(true);
137                        // Good to have colors for debugging
138                        StateListDrawable stl = new StateListDrawable();
139                        stl.addState(new int[]{android.R.attr.state_focused},
140                                new ColorDrawable(Color.RED));
141                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
142                        //noinspection deprecation using this for kitkat tests
143                        testViewHolder.itemView.setBackgroundDrawable(stl);
144                        return testViewHolder;
145                    }
146
147                    @Override
148                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
149                        mAttachedRv = recyclerView;
150                    }
151
152                    @Override
153                    public void onBindViewHolder(@NonNull TestViewHolder holder,
154                            int position) {
155                        super.onBindViewHolder(holder, position);
156                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
157                    }
158                });
159        waitForFirstLayout(recyclerView);
160
161        View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView;
162        assertTrue(requestFocus(viewToFocus, true));
163        assertSame(viewToFocus, recyclerView.getFocusedChild());
164        int pos = 1;
165        View focusedView = viewToFocus;
166        while (pos < 31) {
167            focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP);
168            waitForIdleScroll(recyclerView);
169            focusedView = recyclerView.getFocusedChild();
170            assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1),
171                    recyclerView.getChildViewHolder(focusedView).getAdapterPosition());
172            pos += 3;
173        }
174    }
175
176    /**
177     * Tests that the GridLayoutManager retains the focused element after multiple measure
178     * calls to the RecyclerView.  There was a bug where the focused view was lost when the soft
179     * keyboard opened.  This test simulates the measure/layout events triggered by the opening
180     * of the soft keyboard by making two calls to measure.  A simulation was done because using
181     * the soft keyboard in the test caused many issues on API levels 15, 17 and 19.
182     */
183    @Test
184    public void focusedChildStaysInViewWhenRecyclerViewShrinks() throws Throwable {
185
186        // Arrange.
187
188        final int spanCount = 3;
189        final int itemCount = 100;
190
191        final RecyclerView recyclerView = inflateWrappedRV();
192        ViewGroup.LayoutParams lp = recyclerView.getLayoutParams();
193        lp.height = WRAP_CONTENT;
194        lp.width = MATCH_PARENT;
195
196        Config config = new Config(spanCount, itemCount);
197        mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
198                config.mReverseLayout);
199        recyclerView.setLayoutManager(mGlm);
200
201        GridFocusableAdapter gridFocusableAdapter = new GridFocusableAdapter(itemCount);
202        gridFocusableAdapter.assignSpanSizeLookup(mGlm);
203        recyclerView.setAdapter(gridFocusableAdapter);
204
205        mGlm.expectLayout(1);
206        mActivityRule.runOnUiThread(new Runnable() {
207            @Override
208            public void run() {
209                getActivity().getContainer().addView(recyclerView);
210            }
211        });
212        mGlm.waitForLayout(3);
213
214        int width = recyclerView.getWidth();
215        int height = recyclerView.getHeight();
216        final int widthMeasureSpec =
217                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
218        final int fullHeightMeasureSpec =
219                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
220        // "MinusOne" so that a measure call will appropriately trigger onMeasure after RecyclerView
221        // was previously laid out with the full height version.
222        final int fullHeightMinusOneMeasureSpec =
223                View.MeasureSpec.makeMeasureSpec(height - 1, View.MeasureSpec.AT_MOST);
224        final int halfHeightMeasureSpec =
225                View.MeasureSpec.makeMeasureSpec(height / 2, View.MeasureSpec.AT_MOST);
226
227        // Act 1.
228
229        // First focus on the last fully visible child located at span index #1.
230        View toFocus = findLastFullyVisibleChild(recyclerView);
231        int focusIndex = recyclerView.getChildAdapterPosition(toFocus);
232        focusIndex = (focusIndex / spanCount) * spanCount + 1;
233        toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
234        assertTrue(focusIndex >= 1 && focusIndex < itemCount);
235
236        requestFocus(toFocus, false);
237
238        mGlm.expectLayout(1);
239        mActivityRule.runOnUiThread(new Runnable() {
240            @Override
241            public void run() {
242                recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
243                recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
244                recyclerView.layout(
245                        0,
246                        0,
247                        recyclerView.getMeasuredWidth(),
248                        recyclerView.getMeasuredHeight());
249            }
250        });
251        mGlm.waitForLayout(3);
252
253        // Assert 1.
254
255        assertThat("Child at position " + focusIndex + " should be focused",
256                toFocus.hasFocus(), is(true));
257        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
258                isViewPartiallyInBound(recyclerView, toFocus));
259
260        // Act 2.
261
262        mGlm.expectLayout(1);
263        mActivityRule.runOnUiThread(new Runnable() {
264            @Override
265            public void run() {
266                recyclerView.measure(widthMeasureSpec, fullHeightMeasureSpec);
267                recyclerView.layout(
268                        0,
269                        0,
270                        recyclerView.getMeasuredWidth(),
271                        recyclerView.getMeasuredHeight());
272            }
273        });
274        mGlm.waitForLayout(3);
275
276        // Assert 2.
277
278        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
279                isViewPartiallyInBound(recyclerView, toFocus));
280
281        // Act 3.
282
283        // Now focus on the first fully visible EditText located at the last span index.
284        toFocus = findFirstFullyVisibleChild(recyclerView);
285        focusIndex = recyclerView.getChildAdapterPosition(toFocus);
286        focusIndex = (focusIndex / spanCount) * spanCount + (spanCount - 1);
287        toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
288
289        requestFocus(toFocus, false);
290
291        mGlm.expectLayout(1);
292        mActivityRule.runOnUiThread(new Runnable() {
293            @Override
294            public void run() {
295                recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
296                recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
297                recyclerView.layout(
298                        0,
299                        0,
300                        recyclerView.getMeasuredWidth(),
301                        recyclerView.getMeasuredHeight());
302            }
303        });
304        mGlm.waitForLayout(3);
305
306        // Assert 3.
307
308        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
309                isViewPartiallyInBound(recyclerView, toFocus));
310    }
311
312    @Test
313    public void topUnfocusableViewsVisibility() throws Throwable {
314        // The maximum number of rows that can be fully in-bounds of RV.
315        final int visibleRowCount = 5;
316        final int spanCount = 3;
317        final int consecutiveFocusableRowsCount = 4;
318        final int consecutiveUnFocusableRowsCount = 8;
319        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
320                * spanCount;
321
322        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
323                        .reverseLayout(true),
324                new GridTestAdapter(itemCount, 1) {
325                    RecyclerView mAttachedRv;
326
327                    @Override
328                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
329                            int viewType) {
330                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
331                        // Good to have colors for debugging
332                        StateListDrawable stl = new StateListDrawable();
333                        stl.addState(new int[]{android.R.attr.state_focused},
334                                new ColorDrawable(Color.RED));
335                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
336                        //noinspection deprecation using this for kitkat tests
337                        testViewHolder.itemView.setBackgroundDrawable(stl);
338                        return testViewHolder;
339                    }
340
341                    @Override
342                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
343                        mAttachedRv = recyclerView;
344                    }
345
346                    @Override
347                    public void onBindViewHolder(@NonNull TestViewHolder holder,
348                            int position) {
349                        super.onBindViewHolder(holder, position);
350                        if (position < spanCount * consecutiveFocusableRowsCount) {
351                            holder.itemView.setFocusable(true);
352                            holder.itemView.setFocusableInTouchMode(true);
353                        } else {
354                            holder.itemView.setFocusable(false);
355                            holder.itemView.setFocusableInTouchMode(false);
356                        }
357                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
358                    }
359                });
360        waitForFirstLayout(recyclerView);
361
362        // adapter position of the currently focused item.
363        int focusIndex = 1;
364        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
365        View viewToFocus = toFocus.itemView;
366        assertTrue(requestFocus(viewToFocus, true));
367        assertSame(viewToFocus, recyclerView.getFocusedChild());
368
369        // adapter position of the item (whether focusable or not) that just becomes fully
370        // visible after focusSearch.
371        int visibleIndex = focusIndex;
372        // The VH of the above adapter position
373        RecyclerView.ViewHolder toVisible = null;
374
375        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
376        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
377                * spanCount + visibleIndex;
378
379        // Navigate up through the focusable and unfocusable rows. The focusable rows should
380        // become focused one by one until hitting the last focusable row, at which point,
381        // unfocusable rows should become visible on the screen until the currently focused row
382        // stays on the screen.
383        int pos = focusIndex + spanCount;
384        while (pos < itemCount) {
385            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true);
386            waitForIdleScroll(recyclerView);
387            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
388            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
389            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
390            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
391
392            assertThat("Child at position " + focusIndex + " should be focused",
393                    toFocus.itemView.hasFocus(), is(true));
394            assertTrue("Focused child should be at least partially visible.",
395                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
396            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
397                    isViewFullyInBound(recyclerView, toVisible.itemView));
398            pos += spanCount;
399        }
400    }
401
402    @Test
403    public void bottomUnfocusableViewsVisibility() throws Throwable {
404        // The maximum number of rows that can be fully in-bounds of RV.
405        final int visibleRowCount = 5;
406        final int spanCount = 3;
407        final int consecutiveFocusableRowsCount = 4;
408        final int consecutiveUnFocusableRowsCount = 8;
409        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
410                * spanCount;
411
412        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
413                        .reverseLayout(false),
414                new GridTestAdapter(itemCount, 1) {
415                    RecyclerView mAttachedRv;
416
417                    @Override
418                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
419                            int viewType) {
420                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
421                        // Good to have colors for debugging
422                        StateListDrawable stl = new StateListDrawable();
423                        stl.addState(new int[]{android.R.attr.state_focused},
424                                new ColorDrawable(Color.RED));
425                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
426                        //noinspection deprecation using this for kitkat tests
427                        testViewHolder.itemView.setBackgroundDrawable(stl);
428                        return testViewHolder;
429                    }
430
431                    @Override
432                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
433                        mAttachedRv = recyclerView;
434                    }
435
436                    @Override
437                    public void onBindViewHolder(@NonNull TestViewHolder holder,
438                            int position) {
439                        super.onBindViewHolder(holder, position);
440                        if (position < spanCount * consecutiveFocusableRowsCount) {
441                            holder.itemView.setFocusable(true);
442                            holder.itemView.setFocusableInTouchMode(true);
443                        } else {
444                            holder.itemView.setFocusable(false);
445                            holder.itemView.setFocusableInTouchMode(false);
446                        }
447                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
448                    }
449                });
450        waitForFirstLayout(recyclerView);
451
452        // adapter position of the currently focused item.
453        int focusIndex = 1;
454        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
455        View viewToFocus = toFocus.itemView;
456        assertTrue(requestFocus(viewToFocus, true));
457        assertSame(viewToFocus, recyclerView.getFocusedChild());
458
459        // adapter position of the item (whether focusable or not) that just becomes fully
460        // visible after focusSearch.
461        int visibleIndex = focusIndex;
462        // The VH of the above adapter position
463        RecyclerView.ViewHolder toVisible = null;
464
465        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
466        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
467                * spanCount + visibleIndex;
468
469        // Navigate down through the focusable and unfocusable rows. The focusable rows should
470        // become focused one by one until hitting the last focusable row, at which point,
471        // unfocusable rows should become visible on the screen until the currently focused row
472        // stays on the screen.
473        int pos = focusIndex + spanCount;
474        while (pos < itemCount) {
475            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
476            waitForIdleScroll(recyclerView);
477            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
478            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
479            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
480            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
481
482            assertThat("Child at position " + focusIndex + " should be focused",
483                    toFocus.itemView.hasFocus(), is(true));
484            assertTrue("Focused child should be at least partially visible.",
485                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
486            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
487                    isViewFullyInBound(recyclerView, toVisible.itemView));
488            pos += spanCount;
489        }
490    }
491
492    @Test
493    public void leftUnfocusableViewsVisibility() throws Throwable {
494        // The maximum number of columns that can be fully in-bounds of RV.
495        final int visibleColCount = 5;
496        final int spanCount = 3;
497        final int consecutiveFocusableColsCount = 4;
498        final int consecutiveUnFocusableColsCount = 8;
499        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
500                * spanCount;
501
502        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
503                        .orientation(HORIZONTAL).reverseLayout(true),
504                new GridTestAdapter(itemCount, 1) {
505                    RecyclerView mAttachedRv;
506
507                    @Override
508                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
509                            int viewType) {
510                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
511                        // Good to have colors for debugging
512                        StateListDrawable stl = new StateListDrawable();
513                        stl.addState(new int[]{android.R.attr.state_focused},
514                                new ColorDrawable(Color.RED));
515                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
516                        //noinspection deprecation using this for kitkat tests
517                        testViewHolder.itemView.setBackgroundDrawable(stl);
518                        return testViewHolder;
519                    }
520
521                    @Override
522                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
523                        mAttachedRv = recyclerView;
524                    }
525
526                    @Override
527                    public void onBindViewHolder(@NonNull TestViewHolder holder,
528                            int position) {
529                        super.onBindViewHolder(holder, position);
530                        if (position < spanCount * consecutiveFocusableColsCount) {
531                            holder.itemView.setFocusable(true);
532                            holder.itemView.setFocusableInTouchMode(true);
533                        } else {
534                            holder.itemView.setFocusable(false);
535                            holder.itemView.setFocusableInTouchMode(false);
536                        }
537                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
538                    }
539                });
540        waitForFirstLayout(recyclerView);
541
542        // adapter position of the currently focused item.
543        int focusIndex = 1;
544        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
545        View viewToFocus = toFocus.itemView;
546        assertTrue(requestFocus(viewToFocus, true));
547        assertSame(viewToFocus, recyclerView.getFocusedChild());
548
549        // adapter position of the item (whether focusable or not) that just becomes fully
550        // visible after focusSearch.
551        int visibleIndex = focusIndex;
552        // The VH of the above adapter position
553        RecyclerView.ViewHolder toVisible = null;
554
555        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
556        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
557                * spanCount + visibleIndex;
558
559        // Navigate left through the focusable and unfocusable columns. The focusable columns should
560        // become focused one by one until hitting the last focusable column, at which point,
561        // unfocusable columns should become visible on the screen until the currently focused
562        // column stays on the screen.
563        int pos = focusIndex + spanCount;
564        while (pos < itemCount) {
565            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
566            waitForIdleScroll(recyclerView);
567            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
568            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
569            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
570            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
571
572            assertThat("Child at position " + focusIndex + " should be focused",
573                    toFocus.itemView.hasFocus(), is(true));
574            assertTrue("Focused child should be at least partially visible.",
575                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
576            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
577                    isViewFullyInBound(recyclerView, toVisible.itemView));
578            pos += spanCount;
579        }
580    }
581
582    @Test
583    public void rightUnfocusableViewsVisibility() throws Throwable {
584        // The maximum number of columns that can be fully in-bounds of RV.
585        final int visibleColCount = 5;
586        final int spanCount = 3;
587        final int consecutiveFocusableColsCount = 4;
588        final int consecutiveUnFocusableColsCount = 8;
589        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
590                * spanCount;
591
592        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
593                        .orientation(HORIZONTAL).reverseLayout(false),
594                new GridTestAdapter(itemCount, 1) {
595                    RecyclerView mAttachedRv;
596
597                    @Override
598                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
599                            int viewType) {
600                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
601                        // Good to have colors for debugging
602                        StateListDrawable stl = new StateListDrawable();
603                        stl.addState(new int[]{android.R.attr.state_focused},
604                                new ColorDrawable(Color.RED));
605                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
606                        //noinspection deprecation using this for kitkat tests
607                        testViewHolder.itemView.setBackgroundDrawable(stl);
608                        return testViewHolder;
609                    }
610
611                    @Override
612                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
613                        mAttachedRv = recyclerView;
614                    }
615
616                    @Override
617                    public void onBindViewHolder(@NonNull TestViewHolder holder,
618                            int position) {
619                        super.onBindViewHolder(holder, position);
620                        if (position < spanCount * consecutiveFocusableColsCount) {
621                            holder.itemView.setFocusable(true);
622                            holder.itemView.setFocusableInTouchMode(true);
623                        } else {
624                            holder.itemView.setFocusable(false);
625                            holder.itemView.setFocusableInTouchMode(false);
626                        }
627                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
628                    }
629                });
630        waitForFirstLayout(recyclerView);
631
632        // adapter position of the currently focused item.
633        int focusIndex = 1;
634        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
635        View viewToFocus = toFocus.itemView;
636        assertTrue(requestFocus(viewToFocus, true));
637        assertSame(viewToFocus, recyclerView.getFocusedChild());
638
639        // adapter position of the item (whether focusable or not) that just becomes fully
640        // visible after focusSearch.
641        int visibleIndex = focusIndex;
642        // The VH of the above adapter position
643        RecyclerView.ViewHolder toVisible = null;
644
645        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
646        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
647                * spanCount + visibleIndex;
648
649        // Navigate right through the focusable and unfocusable columns. The focusable columns
650        // should become focused one by one until hitting the last focusable column, at which point,
651        // unfocusable columns should become visible on the screen until the currently focused
652        // column stays on the screen.
653        int pos = focusIndex + spanCount;
654        while (pos < itemCount) {
655            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
656            waitForIdleScroll(recyclerView);
657            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
658            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
659            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
660            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);
661
662            assertThat("Child at position " + focusIndex + " should be focused",
663                    toFocus.itemView.hasFocus(), is(true));
664            assertTrue("Focused child should be at least partially visible.",
665                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
666            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
667                    isViewFullyInBound(recyclerView, toVisible.itemView));
668            pos += spanCount;
669        }
670    }
671
672    @UiThreadTest
673    @Test
674    public void scrollWithoutLayout() throws Throwable {
675        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
676        mGlm.expectLayout(1);
677        setRecyclerView(recyclerView);
678        mGlm.setSpanCount(5);
679        recyclerView.scrollBy(0, 10);
680    }
681
682    @Test
683    public void scrollWithoutLayoutAfterInvalidate() throws Throwable {
684        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
685        waitForFirstLayout(recyclerView);
686        mActivityRule.runOnUiThread(new Runnable() {
687            @Override
688            public void run() {
689                mGlm.setSpanCount(5);
690                recyclerView.scrollBy(0, 10);
691            }
692        });
693    }
694
695    @Test
696    public void predictiveSpanLookup1() throws Throwable {
697        predictiveSpanLookupTest(0, false);
698    }
699
700    @Test
701    public void predictiveSpanLookup2() throws Throwable {
702        predictiveSpanLookupTest(0, true);
703    }
704
705    @Test
706    public void predictiveSpanLookup3() throws Throwable {
707        predictiveSpanLookupTest(1, false);
708    }
709
710    @Test
711    public void predictiveSpanLookup4() throws Throwable {
712        predictiveSpanLookupTest(1, true);
713    }
714
715    public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable {
716        RecyclerView recyclerView = setupBasic(new Config(3, 10));
717        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
718            @Override
719            public int getSpanSize(int position) {
720                if (position < 0 || position >= mAdapter.getItemCount()) {
721                    postExceptionToInstrumentation(new AssertionError("position is not within " +
722                            "adapter range. pos:" + position + ", adapter size:" +
723                            mAdapter.getItemCount()));
724                }
725                return 1;
726            }
727
728            @Override
729            public int getSpanIndex(int position, int spanCount) {
730                if (position < 0 || position >= mAdapter.getItemCount()) {
731                    postExceptionToInstrumentation(new AssertionError("position is not within " +
732                            "adapter range. pos:" + position + ", adapter size:" +
733                            mAdapter.getItemCount()));
734                }
735                return super.getSpanIndex(position, spanCount);
736            }
737        });
738        waitForFirstLayout(recyclerView);
739        checkForMainThreadException();
740        assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations());
741        mGlm.expectLayout(2);
742        int deleteCnt = 10 - remaining;
743        int deleteStart = removeFromStart ? 0 : remaining;
744        mAdapter.deleteAndNotify(deleteStart, deleteCnt);
745        mGlm.waitForLayout(2);
746        checkForMainThreadException();
747    }
748
749    @Test
750    public void movingAGroupOffScreenForAddedItems() throws Throwable {
751        final RecyclerView rv = setupBasic(new Config(3, 100));
752        final int[] maxId = new int[1];
753        maxId[0] = -1;
754        final SparseIntArray spanLookups = new SparseIntArray();
755        final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false);
756        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
757            @Override
758            public int getSpanSize(int position) {
759                if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) {
760                    return 1;
761                } else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) {
762                    spanLookups.put(position, spanLookups.get(position, 0) + 1);
763                }
764                return 3;
765            }
766        });
767        ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true);
768        waitForFirstLayout(rv);
769        View lastView = rv.getChildAt(rv.getChildCount() - 1);
770        final int lastPos = rv.getChildAdapterPosition(lastView);
771        maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId;
772        // now add a lot of items below this and those new views should have span size 3
773        enableSpanLookupLogging.set(true);
774        mGlm.expectLayout(2);
775        mAdapter.addAndNotify(lastPos - 2, 30);
776        mGlm.waitForLayout(2);
777        checkForMainThreadException();
778
779        assertEquals("last items span count should be queried twice", 2,
780                spanLookups.get(lastPos + 30));
781
782    }
783
784    @Test
785    public void layoutParams() throws Throwable {
786        layoutParamsTest(GridLayoutManager.HORIZONTAL);
787        removeRecyclerView();
788        layoutParamsTest(GridLayoutManager.VERTICAL);
789    }
790
791    @Test
792    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
793    public void horizontalAccessibilitySpanIndices() throws Throwable {
794        accessibilitySpanIndicesTest(HORIZONTAL);
795    }
796
797    @Test
798    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
799    public void verticalAccessibilitySpanIndices() throws Throwable {
800        accessibilitySpanIndicesTest(VERTICAL);
801    }
802
803    public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
804        final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
805        waitForFirstLayout(recyclerView);
806        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
807                .getCompatAccessibilityDelegate().getItemDelegate();
808        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
809        final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
810        final int position = recyclerView.getChildLayoutPosition(chosen);
811        mActivityRule.runOnUiThread(new Runnable() {
812            @Override
813            public void run() {
814                delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
815            }
816        });
817        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
818        AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
819                .getCollectionItemInfo();
820        assertNotNull(itemInfo);
821        assertEquals("result should have span group position",
822                ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
823                orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
824        assertEquals("result should have span index",
825                ssl.getSpanIndex(position, mGlm.getSpanCount()),
826                orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex());
827        assertEquals("result should have span size",
828                ssl.getSpanSize(position),
829                orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan());
830    }
831
832    public GridLayoutManager.LayoutParams ensureGridLp(View view) {
833        ViewGroup.LayoutParams lp = view.getLayoutParams();
834        GridLayoutManager.LayoutParams glp;
835        if (lp instanceof GridLayoutManager.LayoutParams) {
836            glp = (GridLayoutManager.LayoutParams) lp;
837        } else if (lp == null) {
838            glp = (GridLayoutManager.LayoutParams) mGlm
839                    .generateDefaultLayoutParams();
840            view.setLayoutParams(glp);
841        } else {
842            glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
843            view.setLayoutParams(glp);
844        }
845        return glp;
846    }
847
848    public void layoutParamsTest(final int orientation) throws Throwable {
849        final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
850                new GridTestAdapter(100) {
851                    @Override
852                    public void onBindViewHolder(@NonNull TestViewHolder holder,
853                            int position) {
854                        super.onBindViewHolder(holder, position);
855                        GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
856                        int val = 0;
857                        switch (position % 5) {
858                            case 0:
859                                val = 10;
860                                break;
861                            case 1:
862                                val = 30;
863                                break;
864                            case 2:
865                                val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
866                                break;
867                            case 3:
868                                val = GridLayoutManager.LayoutParams.MATCH_PARENT;
869                                break;
870                            case 4:
871                                val = 200;
872                                break;
873                        }
874                        if (orientation == GridLayoutManager.VERTICAL) {
875                            glp.height = val;
876                        } else {
877                            glp.width = val;
878                        }
879                        holder.itemView.setLayoutParams(glp);
880                    }
881                });
882        waitForFirstLayout(rv);
883        final OrientationHelper helper = mGlm.mOrientationHelper;
884        final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
885        assertEquals(firstRowSize,
886                helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
887        assertEquals(firstRowSize,
888                helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
889        assertEquals(firstRowSize,
890                helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
891        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
892        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
893        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));
894
895        final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
896        assertEquals(secondRowSize,
897                helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
898        assertEquals(secondRowSize,
899                helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
900        assertEquals(secondRowSize,
901                helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
902        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
903        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
904        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
905    }
906
907    @Test
908    public void anchorUpdate() throws InterruptedException {
909        GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
910        final GridLayoutManager.SpanSizeLookup spanSizeLookup
911                = new GridLayoutManager.SpanSizeLookup() {
912            @Override
913            public int getSpanSize(int position) {
914                if (position > 200) {
915                    return 100;
916                }
917                if (position > 20) {
918                    return 2;
919                }
920                return 1;
921            }
922        };
923        glm.setSpanSizeLookup(spanSizeLookup);
924        glm.mAnchorInfo.mPosition = 11;
925        RecyclerView.State state = new RecyclerView.State();
926        mRecyclerView = new RecyclerView(getActivity());
927        state.mItemCount = 1000;
928        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
929                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
930        assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);
931
932        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
933                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
934        assertEquals("gm should keep anchor in last span in the row", 20,
935                glm.mAnchorInfo.mPosition);
936
937        glm.mAnchorInfo.mPosition = 5;
938        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
939                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
940        assertEquals("gm should keep anchor in last span in the row", 10,
941                glm.mAnchorInfo.mPosition);
942
943        glm.mAnchorInfo.mPosition = 13;
944        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
945                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
946        assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);
947
948        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
949                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
950        assertEquals("gm should keep anchor in last span in the row", 20,
951                glm.mAnchorInfo.mPosition);
952
953        glm.mAnchorInfo.mPosition = 23;
954        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
955                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
956        assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);
957
958        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
959                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
960        assertEquals("gm should keep anchor in last span in the row", 25,
961                glm.mAnchorInfo.mPosition);
962
963        glm.mAnchorInfo.mPosition = 35;
964        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
965                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
966        assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
967        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
968                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
969        assertEquals("gm should keep anchor in last span in the row", 35,
970                glm.mAnchorInfo.mPosition);
971    }
972
973    @Test
974    public void spanLookup() {
975        spanLookupTest(false);
976    }
977
978    @Test
979    public void spanLookupWithCache() {
980        spanLookupTest(true);
981    }
982
983    @Test
984    public void spanLookupCache() {
985        final GridLayoutManager.SpanSizeLookup ssl
986                = new GridLayoutManager.SpanSizeLookup() {
987            @Override
988            public int getSpanSize(int position) {
989                if (position > 6) {
990                    return 2;
991                }
992                return 1;
993            }
994        };
995        ssl.setSpanIndexCacheEnabled(true);
996        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
997        ssl.getCachedSpanIndex(4, 5);
998        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
999        // this should not happen and if happens, it is better to return -1
1000        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
1001        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
1002        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
1003        ssl.getCachedSpanIndex(6, 5);
1004        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
1005        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
1006        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
1007        ssl.getCachedSpanIndex(12, 5);
1008        assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
1009        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
1010        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
1011        for (int i = 0; i < 6; i++) {
1012            ssl.getCachedSpanIndex(i, 5);
1013        }
1014
1015        for (int i = 1; i < 7; i++) {
1016            assertEquals("reference child right before " + i, i - 1,
1017                    ssl.findReferenceIndexFromCache(i));
1018        }
1019        assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
1020    }
1021
1022    public void spanLookupTest(boolean enableCache) {
1023        final GridLayoutManager.SpanSizeLookup ssl
1024                = new GridLayoutManager.SpanSizeLookup() {
1025            @Override
1026            public int getSpanSize(int position) {
1027                if (position > 200) {
1028                    return 100;
1029                }
1030                if (position > 6) {
1031                    return 2;
1032                }
1033                return 1;
1034            }
1035        };
1036        ssl.setSpanIndexCacheEnabled(enableCache);
1037        assertEquals(0, ssl.getCachedSpanIndex(0, 5));
1038        assertEquals(4, ssl.getCachedSpanIndex(4, 5));
1039        assertEquals(0, ssl.getCachedSpanIndex(5, 5));
1040        assertEquals(1, ssl.getCachedSpanIndex(6, 5));
1041        assertEquals(2, ssl.getCachedSpanIndex(7, 5));
1042        assertEquals(2, ssl.getCachedSpanIndex(9, 5));
1043        assertEquals(0, ssl.getCachedSpanIndex(8, 5));
1044    }
1045
1046    @Test
1047    public void removeAnchorItem() throws Throwable {
1048        removeAnchorItemTest(
1049                new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
1050    }
1051
1052    @Test
1053    public void removeAnchorItemReverse() throws Throwable {
1054        removeAnchorItemTest(
1055                new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
1056                0);
1057    }
1058
1059    @Test
1060    public void removeAnchorItemHorizontal() throws Throwable {
1061        removeAnchorItemTest(
1062                new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
1063                        false), 100, 0);
1064    }
1065
1066    @Test
1067    public void removeAnchorItemReverseHorizontal() throws Throwable {
1068        removeAnchorItemTest(
1069                new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true),
1070                100, 0);
1071    }
1072
1073    /**
1074     * This tests a regression where predictive animations were not working as expected when the
1075     * first item is removed and there aren't any more items to add from that direction.
1076     * First item refers to the default anchor item.
1077     */
1078    public void removeAnchorItemTest(final Config config, int adapterSize,
1079            final int removePos) throws Throwable {
1080        GridTestAdapter adapter = new GridTestAdapter(adapterSize) {
1081            @Override
1082            public void onBindViewHolder(@NonNull TestViewHolder holder,
1083                    int position) {
1084                super.onBindViewHolder(holder, position);
1085                ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
1086                if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
1087                    lp = new ViewGroup.MarginLayoutParams(0, 0);
1088                    holder.itemView.setLayoutParams(lp);
1089                }
1090                ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
1091                final int maxSize;
1092                if (config.mOrientation == HORIZONTAL) {
1093                    maxSize = mRecyclerView.getWidth();
1094                    mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
1095                } else {
1096                    maxSize = mRecyclerView.getHeight();
1097                    mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
1098                }
1099
1100                final int desiredSize;
1101                if (position == removePos) {
1102                    // make it large
1103                    desiredSize = maxSize / 4;
1104                } else {
1105                    // make it small
1106                    desiredSize = maxSize / 8;
1107                }
1108                if (config.mOrientation == HORIZONTAL) {
1109                    mlp.width = desiredSize;
1110                } else {
1111                    mlp.height = desiredSize;
1112                }
1113            }
1114        };
1115        RecyclerView recyclerView = setupBasic(config, adapter);
1116        waitForFirstLayout(recyclerView);
1117        final int childCount = mGlm.getChildCount();
1118        RecyclerView.ViewHolder toBeRemoved = null;
1119        List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
1120        for (int i = 0; i < childCount; i++) {
1121            View child = mGlm.getChildAt(i);
1122            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
1123            if (holder.getAdapterPosition() == removePos) {
1124                toBeRemoved = holder;
1125            } else {
1126                toBeMoved.add(holder);
1127            }
1128        }
1129        assertNotNull("test sanity", toBeRemoved);
1130        assertEquals("test sanity", childCount - 1, toBeMoved.size());
1131        LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
1132        mRecyclerView.setItemAnimator(loggingItemAnimator);
1133        loggingItemAnimator.reset();
1134        loggingItemAnimator.expectRunPendingAnimationsCall(1);
1135        mGlm.expectLayout(2);
1136        adapter.deleteAndNotify(removePos, 1);
1137        mGlm.waitForLayout(1);
1138        loggingItemAnimator.waitForPendingAnimationsCall(2);
1139        assertTrue("removed child should receive remove animation",
1140                loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
1141        for (RecyclerView.ViewHolder vh : toBeMoved) {
1142            assertTrue("view holder should be in moved list",
1143                    loggingItemAnimator.mMoveVHs.contains(vh));
1144        }
1145        List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
1146        for (int i = 0; i < mGlm.getChildCount(); i++) {
1147            View child = mGlm.getChildAt(i);
1148            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
1149            if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
1150                newHolders.add(holder);
1151            }
1152        }
1153        assertTrue("some new children should show up for the new space", newHolders.size() > 0);
1154        assertEquals("no items should receive animate add since they are not new", 0,
1155                loggingItemAnimator.mAddVHs.size());
1156        for (RecyclerView.ViewHolder holder : newHolders) {
1157            assertTrue("new holder should receive a move animation",
1158                    loggingItemAnimator.mMoveVHs.contains(holder));
1159        }
1160        // for removed view, 3 for new row
1161        assertTrue("control against adding too many children due to bad layout state preparation."
1162                        + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
1163                mRecyclerView.getChildCount() <= childCount + 1 + 3);
1164    }
1165
1166    @Test
1167    public void spanGroupIndex() {
1168        final GridLayoutManager.SpanSizeLookup ssl
1169                = new GridLayoutManager.SpanSizeLookup() {
1170            @Override
1171            public int getSpanSize(int position) {
1172                if (position > 200) {
1173                    return 100;
1174                }
1175                if (position > 6) {
1176                    return 2;
1177                }
1178                return 1;
1179            }
1180        };
1181        assertEquals(0, ssl.getSpanGroupIndex(0, 5));
1182        assertEquals(0, ssl.getSpanGroupIndex(4, 5));
1183        assertEquals(1, ssl.getSpanGroupIndex(5, 5));
1184        assertEquals(1, ssl.getSpanGroupIndex(6, 5));
1185        assertEquals(1, ssl.getSpanGroupIndex(7, 5));
1186        assertEquals(2, ssl.getSpanGroupIndex(9, 5));
1187        assertEquals(2, ssl.getSpanGroupIndex(8, 5));
1188    }
1189
1190    @Test
1191    public void notifyDataSetChange() throws Throwable {
1192        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
1193        final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
1194        ssl.setSpanIndexCacheEnabled(true);
1195        waitForFirstLayout(recyclerView);
1196        assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
1197        final Callback callback = new Callback() {
1198            @Override
1199            public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
1200                if (!state.isPreLayout()) {
1201                    assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
1202                }
1203            }
1204
1205            @Override
1206            public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
1207                if (!state.isPreLayout()) {
1208                    assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
1209                }
1210            }
1211        };
1212        mGlm.mCallbacks.add(callback);
1213        mGlm.expectLayout(2);
1214        mAdapter.deleteAndNotify(2, 3);
1215        mGlm.waitForLayout(2);
1216        checkForMainThreadException();
1217    }
1218
1219    @Test
1220    public void unevenHeights() throws Throwable {
1221        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
1222                new HashMap<Integer, RecyclerView.ViewHolder>();
1223        RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
1224            @Override
1225            public void onBindViewHolder(@NonNull TestViewHolder holder,
1226                    int position) {
1227                super.onBindViewHolder(holder, position);
1228                final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
1229                glp.height = 50 + position * 50;
1230                viewHolderMap.put(position, holder);
1231            }
1232        });
1233        waitForFirstLayout(recyclerView);
1234        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
1235            assertEquals("all items should get max height", 150,
1236                    vh.itemView.getHeight());
1237        }
1238
1239        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
1240            assertEquals("all items should have measured the max height", 150,
1241                    vh.itemView.getMeasuredHeight());
1242        }
1243    }
1244
1245    @Test
1246    public void unevenWidths() throws Throwable {
1247        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
1248                new HashMap<Integer, RecyclerView.ViewHolder>();
1249        RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
1250                new GridTestAdapter(3) {
1251                    @Override
1252                    public void onBindViewHolder(@NonNull TestViewHolder holder,
1253                            int position) {
1254                        super.onBindViewHolder(holder, position);
1255                        final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
1256                        glp.width = 50 + position * 50;
1257                        viewHolderMap.put(position, holder);
1258                    }
1259                });
1260        waitForFirstLayout(recyclerView);
1261        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
1262            assertEquals("all items should get max width", 150,
1263                    vh.itemView.getWidth());
1264        }
1265
1266        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
1267            assertEquals("all items should have measured the max width", 150,
1268                    vh.itemView.getMeasuredWidth());
1269        }
1270    }
1271
1272    @Test
1273    public void spanSizeChange() throws Throwable {
1274        final RecyclerView rv = setupBasic(new Config(3, 100));
1275        waitForFirstLayout(rv);
1276        assertTrue(mGlm.supportsPredictiveItemAnimations());
1277        mGlm.expectLayout(1);
1278        mActivityRule.runOnUiThread(new Runnable() {
1279            @Override
1280            public void run() {
1281                mGlm.setSpanCount(5);
1282                assertFalse(mGlm.supportsPredictiveItemAnimations());
1283            }
1284        });
1285        mGlm.waitForLayout(2);
1286        mGlm.expectLayout(2);
1287        mAdapter.deleteAndNotify(3, 2);
1288        mGlm.waitForLayout(2);
1289        assertTrue(mGlm.supportsPredictiveItemAnimations());
1290    }
1291
1292    @Test
1293    public void cacheSpanIndices() throws Throwable {
1294        final RecyclerView rv = setupBasic(new Config(3, 100));
1295        mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
1296        waitForFirstLayout(rv);
1297        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
1298        assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
1299        assertEquals("item index 5 should be in span 2", 2,
1300                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
1301        mGlm.expectLayout(2);
1302        mAdapter.mFullSpanItems.add(4);
1303        mAdapter.changeAndNotify(4, 1);
1304        mGlm.waitForLayout(2);
1305        assertEquals("item index 5 should be in span 2", 0,
1306                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
1307    }
1308}
1309