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