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