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