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