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 
     18 package android.support.v7.widget;
     19 
     20 
     21 import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
     22 import static android.support.v7.widget.StaggeredGridLayoutManager
     23         .GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
     24 import static android.support.v7.widget.StaggeredGridLayoutManager.GAP_HANDLING_NONE;
     25 import static android.support.v7.widget.StaggeredGridLayoutManager.HORIZONTAL;
     26 import static android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
     27 
     28 import static org.junit.Assert.assertEquals;
     29 import static org.junit.Assert.assertFalse;
     30 import static org.junit.Assert.assertNotNull;
     31 import static org.junit.Assert.assertNull;
     32 import static org.junit.Assert.assertSame;
     33 import static org.junit.Assert.assertThat;
     34 import static org.junit.Assert.assertTrue;
     35 
     36 import android.graphics.Color;
     37 import android.graphics.Rect;
     38 import android.graphics.drawable.ColorDrawable;
     39 import android.graphics.drawable.StateListDrawable;
     40 import android.os.Parcel;
     41 import android.os.Parcelable;
     42 import android.support.v4.view.AccessibilityDelegateCompat;
     43 import android.support.v4.view.accessibility.AccessibilityEventCompat;
     44 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
     45 import android.test.suitebuilder.annotation.MediumTest;
     46 import android.text.TextUtils;
     47 import android.util.Log;
     48 import android.util.StateSet;
     49 import android.view.View;
     50 import android.view.ViewGroup;
     51 import android.view.accessibility.AccessibilityEvent;
     52 import android.widget.EditText;
     53 import android.widget.FrameLayout;
     54 
     55 import org.hamcrest.CoreMatchers;
     56 import org.hamcrest.MatcherAssert;
     57 import org.junit.Test;
     58 
     59 import java.util.HashMap;
     60 import java.util.Map;
     61 import java.util.UUID;
     62 
     63 
     64 @MediumTest
     65 public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
     66     @Test
     67     public void forceLayoutOnDetach() throws Throwable {
     68         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
     69         waitFirstLayout();
     70         assertFalse("test sanity", mRecyclerView.isLayoutRequested());
     71         runTestOnUiThread(new Runnable() {
     72             @Override
     73             public void run() {
     74                 mLayoutManager.onDetachedFromWindow(mRecyclerView, mRecyclerView.mRecycler);
     75             }
     76         });
     77         assertTrue(mRecyclerView.isLayoutRequested());
     78     }
     79     @Test
     80     public void areAllStartsTheSame() throws Throwable {
     81         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(300));
     82         waitFirstLayout();
     83         smoothScrollToPosition(100);
     84         mLayoutManager.expectLayouts(1);
     85         mAdapter.deleteAndNotify(0, 2);
     86         mLayoutManager.waitForLayout(2);
     87         smoothScrollToPosition(0);
     88         assertFalse("all starts should not be the same", mLayoutManager.areAllStartsEqual());
     89     }
     90 
     91     @Test
     92     public void areAllEndsTheSame() throws Throwable {
     93         setupByConfig(new Config(VERTICAL, true, 3, GAP_HANDLING_NONE).itemCount(300));
     94         waitFirstLayout();
     95         smoothScrollToPosition(100);
     96         mLayoutManager.expectLayouts(1);
     97         mAdapter.deleteAndNotify(0, 2);
     98         mLayoutManager.waitForLayout(2);
     99         smoothScrollToPosition(0);
    100         assertFalse("all ends should not be the same", mLayoutManager.areAllEndsEqual());
    101     }
    102 
    103     @Test
    104     public void getPositionsBeforeInitialization() throws Throwable {
    105         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    106         int[] positions = mLayoutManager.findFirstCompletelyVisibleItemPositions(null);
    107         MatcherAssert.assertThat(positions,
    108                 CoreMatchers.is(new int[]{RecyclerView.NO_POSITION, RecyclerView.NO_POSITION,
    109                         RecyclerView.NO_POSITION}));
    110     }
    111 
    112     @Test
    113     public void findLastInUnevenDistribution() throws Throwable {
    114         setupByConfig(new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
    115                 .itemCount(5));
    116         mAdapter.mOnBindCallback = new OnBindCallback() {
    117             @Override
    118             void onBoundItem(TestViewHolder vh, int position) {
    119                 LayoutParams lp = (LayoutParams) vh.itemView.getLayoutParams();
    120                 if (position == 1) {
    121                     lp.height = mRecyclerView.getHeight() - 10;
    122                 } else {
    123                     lp.height = 5;
    124                 }
    125                 vh.itemView.setMinimumHeight(0);
    126             }
    127         };
    128         waitFirstLayout();
    129         int[] into = new int[2];
    130         mLayoutManager.findFirstCompletelyVisibleItemPositions(into);
    131         assertEquals("first completely visible item from span 0 should be 0", 0, into[0]);
    132         assertEquals("first completely visible item from span 1 should be 1", 1, into[1]);
    133         mLayoutManager.findLastCompletelyVisibleItemPositions(into);
    134         assertEquals("last completely visible item from span 0 should be 4", 4, into[0]);
    135         assertEquals("last completely visible item from span 1 should be 1", 1, into[1]);
    136         assertEquals("first fully visible child should be at position",
    137                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
    138                         findFirstVisibleItemClosestToStart(true, true)).getPosition());
    139         assertEquals("last fully visible child should be at position",
    140                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
    141                         findFirstVisibleItemClosestToEnd(true, true)).getPosition());
    142 
    143         assertEquals("first visible child should be at position",
    144                 0, mRecyclerView.getChildViewHolder(mLayoutManager.
    145                         findFirstVisibleItemClosestToStart(false, true)).getPosition());
    146         assertEquals("last visible child should be at position",
    147                 4, mRecyclerView.getChildViewHolder(mLayoutManager.
    148                         findFirstVisibleItemClosestToEnd(false, true)).getPosition());
    149 
    150     }
    151 
    152     @Test
    153     public void customWidthInHorizontal() throws Throwable {
    154         customSizeInScrollDirectionTest(
    155                 new Config(HORIZONTAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    156     }
    157 
    158     @Test
    159     public void customHeightInVertical() throws Throwable {
    160         customSizeInScrollDirectionTest(
    161                 new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS));
    162     }
    163 
    164     public void customSizeInScrollDirectionTest(final Config config) throws Throwable {
    165         setupByConfig(config);
    166         final Map<View, Integer> sizeMap = new HashMap<View, Integer>();
    167         mAdapter.mOnBindCallback = new OnBindCallback() {
    168             @Override
    169             void onBoundItem(TestViewHolder vh, int position) {
    170                 final ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
    171                 final int size = 1 + position * 5;
    172                 if (config.mOrientation == HORIZONTAL) {
    173                     layoutParams.width = size;
    174                 } else {
    175                     layoutParams.height = size;
    176                 }
    177                 sizeMap.put(vh.itemView, size);
    178                 if (position == 3) {
    179                     getLp(vh.itemView).setFullSpan(true);
    180                 }
    181             }
    182 
    183             @Override
    184             boolean assignRandomSize() {
    185                 return false;
    186             }
    187         };
    188         waitFirstLayout();
    189         assertTrue("[test sanity] some views should be laid out", sizeMap.size() > 0);
    190         for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
    191             View child = mRecyclerView.getChildAt(i);
    192             final int size = config.mOrientation == HORIZONTAL ? child.getWidth()
    193                     : child.getHeight();
    194             assertEquals("child " + i + " should have the size specified in its layout params",
    195                     sizeMap.get(child).intValue(), size);
    196         }
    197         checkForMainThreadException();
    198     }
    199 
    200     @Test
    201     public void gapHandlingWhenItemMovesToTop() throws Throwable {
    202         gapHandlingWhenItemMovesToTopTest();
    203     }
    204 
    205     @Test
    206     public void gapHandlingWhenItemMovesToTopWithFullSpan() throws Throwable {
    207         gapHandlingWhenItemMovesToTopTest(0);
    208     }
    209 
    210     @Test
    211     public void gapHandlingWhenItemMovesToTopWithFullSpan2() throws Throwable {
    212         gapHandlingWhenItemMovesToTopTest(1);
    213     }
    214 
    215     public void gapHandlingWhenItemMovesToTopTest(int... fullSpanIndices) throws Throwable {
    216         Config config = new Config(VERTICAL, false, 2, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    217         config.itemCount(3);
    218         setupByConfig(config);
    219         mAdapter.mOnBindCallback = new OnBindCallback() {
    220             @Override
    221             void onBoundItem(TestViewHolder vh, int position) {
    222             }
    223 
    224             @Override
    225             boolean assignRandomSize() {
    226                 return false;
    227             }
    228         };
    229         for (int i : fullSpanIndices) {
    230             mAdapter.mFullSpanItems.add(i);
    231         }
    232         waitFirstLayout();
    233         mLayoutManager.expectLayouts(1);
    234         mAdapter.moveItem(1, 0, true);
    235         mLayoutManager.waitForLayout(2);
    236         final Map<Item, Rect> desiredPositions = mLayoutManager.collectChildCoordinates();
    237         // move back.
    238         mLayoutManager.expectLayouts(1);
    239         mAdapter.moveItem(0, 1, true);
    240         mLayoutManager.waitForLayout(2);
    241         mLayoutManager.expectLayouts(2);
    242         mAdapter.moveAndNotify(1, 0);
    243         mLayoutManager.waitForLayout(2);
    244         Thread.sleep(1000);
    245         getInstrumentation().waitForIdleSync();
    246         checkForMainThreadException();
    247         // item should be positioned properly
    248         assertRectSetsEqual("final position after a move", desiredPositions,
    249                 mLayoutManager.collectChildCoordinates());
    250 
    251     }
    252 
    253     @Test
    254     public void focusSearchFailureUp() throws Throwable {
    255         focusSearchFailure(false);
    256     }
    257 
    258     @Test
    259     public void focusSearchFailureDown() throws Throwable {
    260         focusSearchFailure(true);
    261     }
    262 
    263     @Test
    264     public void focusSearchFailureFromSubChild() throws Throwable {
    265         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS),
    266                 new GridTestAdapter(1000, VERTICAL) {
    267 
    268                     @Override
    269                     public TestViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    270                         FrameLayout fl = new FrameLayout(parent.getContext());
    271                         EditText editText = new EditText(parent.getContext());
    272                         fl.addView(editText);
    273                         editText.setEllipsize(TextUtils.TruncateAt.END);
    274                         return new TestViewHolder(fl);
    275                     }
    276 
    277                     @Override
    278                     public void onBindViewHolder(TestViewHolder holder, int position) {
    279                         Item item = mItems.get(position);
    280                         holder.mBoundItem = item;
    281                         ((EditText) ((FrameLayout) holder.itemView).getChildAt(0)).setText(
    282                                 item.mText + " (" + item.mId + ")");
    283                     }
    284                 });
    285         waitFirstLayout();
    286         ViewGroup lastChild = (ViewGroup) mRecyclerView.getChildAt(
    287                 mRecyclerView.getChildCount() - 1);
    288         RecyclerView.ViewHolder lastViewHolder = mRecyclerView.getChildViewHolder(lastChild);
    289         View subChildToFocus = lastChild.getChildAt(0);
    290         requestFocus(subChildToFocus, true);
    291         assertThat("test sanity", subChildToFocus.isFocused(), CoreMatchers.is(true));
    292         focusSearch(subChildToFocus, View.FOCUS_FORWARD);
    293         waitForIdleScroll(mRecyclerView);
    294         checkForMainThreadException();
    295         View focusedChild = mRecyclerView.getFocusedChild();
    296         if (focusedChild == subChildToFocus.getParent()) {
    297             focusSearch(focusedChild, View.FOCUS_FORWARD);
    298             waitForIdleScroll(mRecyclerView);
    299             focusedChild = mRecyclerView.getFocusedChild();
    300         }
    301         RecyclerView.ViewHolder containingViewHolder = mRecyclerView.findContainingViewHolder(
    302                 focusedChild);
    303         assertTrue("new focused view should have a larger position "
    304                         + lastViewHolder.getAdapterPosition() + " vs "
    305                         + containingViewHolder.getAdapterPosition(),
    306                 lastViewHolder.getAdapterPosition() < containingViewHolder.getAdapterPosition());
    307     }
    308 
    309     public void focusSearchFailure(boolean scrollDown) throws Throwable {
    310         int focusDir = scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP;
    311         setupByConfig(new Config(VERTICAL, !scrollDown, 3, GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS)
    312                 , new GridTestAdapter(31, 1) {
    313                     RecyclerView mAttachedRv;
    314 
    315                     @Override
    316                     public TestViewHolder onCreateViewHolder(ViewGroup parent,
    317                             int viewType) {
    318                         TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
    319                         testViewHolder.itemView.setFocusable(true);
    320                         testViewHolder.itemView.setFocusableInTouchMode(true);
    321                         // Good to have colors for debugging
    322                         StateListDrawable stl = new StateListDrawable();
    323                         stl.addState(new int[]{android.R.attr.state_focused},
    324                                 new ColorDrawable(Color.RED));
    325                         stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
    326                         testViewHolder.itemView.setBackground(stl);
    327                         return testViewHolder;
    328                     }
    329 
    330                     @Override
    331                     public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    332                         mAttachedRv = recyclerView;
    333                     }
    334 
    335                     @Override
    336                     public void onBindViewHolder(TestViewHolder holder,
    337                             int position) {
    338                         super.onBindViewHolder(holder, position);
    339                         holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
    340                     }
    341                 });
    342         /**
    343          * 0  1  2
    344          * 3  4  5
    345          * 6  7  8
    346          * 9  10 11
    347          * 12 13 14
    348          * 15 16 17
    349          * 18 18 18
    350          * 19
    351          * 20 20 20
    352          * 21 22
    353          * 23 23 23
    354          * 24 25 26
    355          * 27 28 29
    356          * 30
    357          */
    358         mAdapter.mFullSpanItems.add(18);
    359         mAdapter.mFullSpanItems.add(20);
    360         mAdapter.mFullSpanItems.add(23);
    361         waitFirstLayout();
    362         View viewToFocus = mRecyclerView.findViewHolderForAdapterPosition(1).itemView;
    363         assertTrue(requestFocus(viewToFocus, true));
    364         assertSame(viewToFocus, mRecyclerView.getFocusedChild());
    365         int pos = 1;
    366         View focusedView = viewToFocus;
    367         while (pos < 16) {
    368             focusSearchAndWaitForScroll(focusedView, focusDir);
    369             focusedView = mRecyclerView.getFocusedChild();
    370             assertEquals(pos + 3,
    371                     mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
    372             pos += 3;
    373         }
    374         for (int i : new int[]{18, 19, 20, 21, 23, 24}) {
    375             focusSearchAndWaitForScroll(focusedView, focusDir);
    376             focusedView = mRecyclerView.getFocusedChild();
    377             assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
    378         }
    379         // now move right
    380         focusSearch(focusedView, View.FOCUS_RIGHT);
    381         waitForIdleScroll(mRecyclerView);
    382         focusedView = mRecyclerView.getFocusedChild();
    383         assertEquals(25, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
    384         for (int i : new int[]{28, 30}) {
    385             focusSearchAndWaitForScroll(focusedView, focusDir);
    386             focusedView = mRecyclerView.getFocusedChild();
    387             assertEquals(i, mRecyclerView.getChildViewHolder(focusedView).getAdapterPosition());
    388         }
    389     }
    390 
    391     private void focusSearchAndWaitForScroll(View focused, int dir) throws Throwable {
    392         focusSearch(focused, dir);
    393         waitForIdleScroll(mRecyclerView);
    394     }
    395 
    396 
    397     @Test
    398     public void scrollToPositionWithPredictive() throws Throwable {
    399         scrollToPositionWithPredictive(0, LinearLayoutManager.INVALID_OFFSET);
    400         removeRecyclerView();
    401         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2,
    402                 LinearLayoutManager.INVALID_OFFSET);
    403         removeRecyclerView();
    404         scrollToPositionWithPredictive(9, 20);
    405         removeRecyclerView();
    406         scrollToPositionWithPredictive(Config.DEFAULT_ITEM_COUNT / 2, 10);
    407 
    408     }
    409 
    410     public void scrollToPositionWithPredictive(final int scrollPosition, final int scrollOffset)
    411             throws Throwable {
    412         setupByConfig(new Config(StaggeredGridLayoutManager.VERTICAL,
    413                 false, 3, StaggeredGridLayoutManager.GAP_HANDLING_NONE));
    414         waitFirstLayout();
    415         mLayoutManager.mOnLayoutListener = new OnLayoutListener() {
    416             @Override
    417             void after(RecyclerView.Recycler recycler, RecyclerView.State state) {
    418                 RecyclerView rv = mLayoutManager.mRecyclerView;
    419                 if (state.isPreLayout()) {
    420                     assertEquals("pending scroll position should still be pending",
    421                             scrollPosition, mLayoutManager.mPendingScrollPosition);
    422                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    423                         assertEquals("pending scroll position offset should still be pending",
    424                                 scrollOffset, mLayoutManager.mPendingScrollPositionOffset);
    425                     }
    426                 } else {
    427                     RecyclerView.ViewHolder vh = rv.findViewHolderForLayoutPosition(scrollPosition);
    428                     assertNotNull("scroll to position should work", vh);
    429                     if (scrollOffset != LinearLayoutManager.INVALID_OFFSET) {
    430                         assertEquals("scroll offset should be applied properly",
    431                                 mLayoutManager.getPaddingTop() + scrollOffset
    432                                         + ((RecyclerView.LayoutParams) vh.itemView
    433                                         .getLayoutParams()).topMargin,
    434                                 mLayoutManager.getDecoratedTop(vh.itemView));
    435                     }
    436                 }
    437             }
    438         };
    439         mLayoutManager.expectLayouts(2);
    440         runTestOnUiThread(new Runnable() {
    441             @Override
    442             public void run() {
    443                 try {
    444                     mAdapter.addAndNotify(0, 1);
    445                     if (scrollOffset == LinearLayoutManager.INVALID_OFFSET) {
    446                         mLayoutManager.scrollToPosition(scrollPosition);
    447                     } else {
    448                         mLayoutManager.scrollToPositionWithOffset(scrollPosition,
    449                                 scrollOffset);
    450                     }
    451 
    452                 } catch (Throwable throwable) {
    453                     throwable.printStackTrace();
    454                 }
    455 
    456             }
    457         });
    458         mLayoutManager.waitForLayout(2);
    459         checkForMainThreadException();
    460     }
    461 
    462     @Test
    463     public void moveGapHandling() throws Throwable {
    464         Config config = new Config().spanCount(2).itemCount(40);
    465         setupByConfig(config);
    466         waitFirstLayout();
    467         mLayoutManager.expectLayouts(2);
    468         mAdapter.moveAndNotify(4, 1);
    469         mLayoutManager.waitForLayout(2);
    470         assertNull("moving item to upper should not cause gaps", mLayoutManager.hasGapsToFix());
    471     }
    472 
    473     @Test
    474     public void updateAfterFullSpan() throws Throwable {
    475         updateAfterFullSpanGapHandlingTest(0);
    476     }
    477 
    478     @Test
    479     public void updateAfterFullSpan2() throws Throwable {
    480         updateAfterFullSpanGapHandlingTest(20);
    481     }
    482 
    483     @Test
    484     public void temporaryGapHandling() throws Throwable {
    485         int fullSpanIndex = 200;
    486         setupByConfig(new Config().spanCount(2).itemCount(500));
    487         mAdapter.mFullSpanItems.add(fullSpanIndex);
    488         waitFirstLayout();
    489         smoothScrollToPosition(fullSpanIndex + 200);// go far away
    490         assertNull("test sanity. full span item should not be visible",
    491                 mRecyclerView.findViewHolderForAdapterPosition(fullSpanIndex));
    492         mLayoutManager.expectLayouts(1);
    493         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
    494         mLayoutManager.waitForLayout(1);
    495         smoothScrollToPosition(0);
    496         mLayoutManager.expectLayouts(1);
    497         smoothScrollToPosition(fullSpanIndex + 2 * (AVG_ITEM_PER_VIEW - 1));
    498         String log = mLayoutManager.layoutToString("post gap");
    499         mLayoutManager.assertNoLayout("if an interim gap is fixed, it should not cause a "
    500                 + "relayout " + log, 2);
    501         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
    502         assertNotNull("full span item should be there:\n" + log, fullSpan);
    503         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
    504         assertNotNull("next view should be there\n" + log, view1);
    505         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
    506         assertNotNull("+2 view should be there\n" + log, view2);
    507 
    508         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
    509         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
    510         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
    511         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
    512         assertEquals("no gap between span and view 1",
    513                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    514                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
    515         assertEquals("no gap between span and view 2",
    516                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    517                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    518     }
    519 
    520     public void updateAfterFullSpanGapHandlingTest(int fullSpanIndex) throws Throwable {
    521         setupByConfig(new Config().spanCount(2).itemCount(100));
    522         mAdapter.mFullSpanItems.add(fullSpanIndex);
    523         waitFirstLayout();
    524         smoothScrollToPosition(fullSpanIndex + 30);
    525         mLayoutManager.expectLayouts(1);
    526         mAdapter.deleteAndNotify(fullSpanIndex + 1, 3);
    527         mLayoutManager.waitForLayout(1);
    528         smoothScrollToPosition(fullSpanIndex);
    529         // give it some time to fix the gap
    530         Thread.sleep(500);
    531         View fullSpan = mLayoutManager.findViewByPosition(fullSpanIndex);
    532 
    533         View view1 = mLayoutManager.findViewByPosition(fullSpanIndex + 1);
    534         View view2 = mLayoutManager.findViewByPosition(fullSpanIndex + 2);
    535 
    536         LayoutParams lp1 = (LayoutParams) view1.getLayoutParams();
    537         LayoutParams lp2 = (LayoutParams) view2.getLayoutParams();
    538         assertEquals("view 1 span index", 0, lp1.getSpanIndex());
    539         assertEquals("view 2 span index", 1, lp2.getSpanIndex());
    540         assertEquals("no gap between span and view 1",
    541                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    542                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view1));
    543         assertEquals("no gap between span and view 2",
    544                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(fullSpan),
    545                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view2));
    546     }
    547 
    548     @Test
    549     public void innerGapHandling() throws Throwable {
    550         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
    551         innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
    552     }
    553 
    554     public void innerGapHandlingTest(int strategy) throws Throwable {
    555         Config config = new Config().spanCount(3).itemCount(500);
    556         setupByConfig(config);
    557         mLayoutManager.setGapStrategy(strategy);
    558         mAdapter.mFullSpanItems.add(100);
    559         mAdapter.mFullSpanItems.add(104);
    560         mAdapter.mViewsHaveEqualSize = true;
    561         mAdapter.mOnBindCallback = new OnBindCallback() {
    562             @Override
    563             void onBoundItem(TestViewHolder vh, int position) {
    564 
    565             }
    566 
    567             @Override
    568             void onCreatedViewHolder(TestViewHolder vh) {
    569                 super.onCreatedViewHolder(vh);
    570                 //make sure we have enough views
    571                 mAdapter.mSizeReference = mRecyclerView.getHeight() / 5;
    572             }
    573         };
    574         waitFirstLayout();
    575         mLayoutManager.expectLayouts(1);
    576         scrollToPosition(400);
    577         mLayoutManager.waitForLayout(2);
    578         View view400 = mLayoutManager.findViewByPosition(400);
    579         assertNotNull("test sanity, scrollToPos should succeed", view400);
    580         assertTrue("test sanity, view should be visible top",
    581                 mLayoutManager.mPrimaryOrientation.getDecoratedStart(view400) >=
    582                         mLayoutManager.mPrimaryOrientation.getStartAfterPadding());
    583         assertTrue("test sanity, view should be visible bottom",
    584                 mLayoutManager.mPrimaryOrientation.getDecoratedEnd(view400) <=
    585                         mLayoutManager.mPrimaryOrientation.getEndAfterPadding());
    586         mLayoutManager.expectLayouts(2);
    587         mAdapter.addAndNotify(101, 1);
    588         mLayoutManager.waitForLayout(2);
    589         checkForMainThreadException();
    590         if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
    591             mLayoutManager.expectLayouts(1);
    592         }
    593         // state
    594         // now smooth scroll to 99 to trigger a layout around 100
    595         mLayoutManager.validateChildren();
    596         smoothScrollToPosition(99);
    597         switch (strategy) {
    598             case GAP_HANDLING_NONE:
    599                 assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
    600                         new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
    601                         new int[]{105, 0});
    602                 break;
    603             case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
    604                 mLayoutManager.waitForLayout(2);
    605                 assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
    606                         new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
    607                 break;
    608         }
    609 
    610     }
    611 
    612     @Test
    613     public void fullSizeSpans() throws Throwable {
    614         Config config = new Config().spanCount(5).itemCount(30);
    615         setupByConfig(config);
    616         mAdapter.mFullSpanItems.add(3);
    617         waitFirstLayout();
    618         assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
    619                 new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
    620                 new int[]{7, 3}, new int[]{8, 4});
    621     }
    622 
    623     void assertSpans(String msg, int[]... childSpanTuples) {
    624         msg = msg + mLayoutManager.layoutToString("\n\n");
    625         for (int i = 0; i < childSpanTuples.length; i++) {
    626             assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
    627         }
    628     }
    629 
    630     void assertSpan(String msg, int childPosition, int expectedSpan) {
    631         View view = mLayoutManager.findViewByPosition(childPosition);
    632         assertNotNull(msg + " view at position " + childPosition + " should exists", view);
    633         assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
    634                 getLp(view).mSpan.mIndex);
    635     }
    636 
    637     @Test
    638     public void partialSpanInvalidation() throws Throwable {
    639         Config config = new Config().spanCount(5).itemCount(100);
    640         setupByConfig(config);
    641         for (int i = 20; i < mAdapter.getItemCount(); i += 20) {
    642             mAdapter.mFullSpanItems.add(i);
    643         }
    644         waitFirstLayout();
    645         smoothScrollToPosition(50);
    646         int prevSpanId = mLayoutManager.mLazySpanLookup.mData[30];
    647         mAdapter.changeAndNotify(15, 2);
    648         Thread.sleep(200);
    649         assertEquals("Invalidation should happen within full span item boundaries", prevSpanId,
    650                 mLayoutManager.mLazySpanLookup.mData[30]);
    651         assertEquals("item in invalidated range should have clear span id",
    652                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    653         smoothScrollToPosition(85);
    654         int[] prevSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 62, 85);
    655         mAdapter.deleteAndNotify(55, 2);
    656         Thread.sleep(200);
    657         assertEquals("item in invalidated range should have clear span id",
    658                 LayoutParams.INVALID_SPAN_ID, mLayoutManager.mLazySpanLookup.mData[16]);
    659         int[] newSpans = copyOfRange(mLayoutManager.mLazySpanLookup.mData, 60, 83);
    660         assertSpanAssignmentEquality("valid spans should be shifted for deleted item", prevSpans,
    661                 newSpans, 0, 0, newSpans.length);
    662     }
    663 
    664     // Same as Arrays.copyOfRange but for API 7
    665     private int[] copyOfRange(int[] original, int from, int to) {
    666         int newLength = to - from;
    667         if (newLength < 0) {
    668             throw new IllegalArgumentException(from + " > " + to);
    669         }
    670         int[] copy = new int[newLength];
    671         System.arraycopy(original, from, copy, 0,
    672                 Math.min(original.length - from, newLength));
    673         return copy;
    674     }
    675 
    676     @Test
    677     public void spanReassignmentsOnItemChange() throws Throwable {
    678         Config config = new Config().spanCount(5);
    679         setupByConfig(config);
    680         waitFirstLayout();
    681         smoothScrollToPosition(mAdapter.getItemCount() / 2);
    682         final int changePosition = mAdapter.getItemCount() / 4;
    683         mLayoutManager.expectLayouts(1);
    684         mAdapter.changeAndNotify(changePosition, 1);
    685         mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
    686                 1);
    687         // delete an item before visible area
    688         int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
    689         assertTrue("test sanity", deletedPosition >= 0);
    690         Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
    691         if (DEBUG) {
    692             Log.d(TAG, "before:");
    693             for (Map.Entry<Item, Rect> entry : before.entrySet()) {
    694                 Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
    695             }
    696         }
    697         mLayoutManager.expectLayouts(1);
    698         mAdapter.deleteAndNotify(deletedPosition, 1);
    699         mLayoutManager.waitForLayout(2);
    700         assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
    701                         + "should not affect the layout if it is not visible", before,
    702                 mLayoutManager.collectChildCoordinates()
    703         );
    704         deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
    705         mLayoutManager.expectLayouts(1);
    706         mAdapter.deleteAndNotify(deletedPosition, 1);
    707         mLayoutManager.waitForLayout(2);
    708         assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
    709                 + "layout", before, mLayoutManager.collectChildCoordinates());
    710     }
    711 
    712     void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
    713             int length) {
    714         for (int i = 0; i < length; i++) {
    715             assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
    716                     set2[start2 + i]);
    717         }
    718     }
    719 
    720     @Test
    721     public void spanCountChangeOnRestoreSavedState() throws Throwable {
    722         Config config = new Config(HORIZONTAL, true, 5, GAP_HANDLING_NONE);
    723         setupByConfig(config);
    724         waitFirstLayout();
    725 
    726         int beforeChildCount = mLayoutManager.getChildCount();
    727         Parcelable savedState = mRecyclerView.onSaveInstanceState();
    728         // we append a suffix to the parcelable to test out of bounds
    729         String parcelSuffix = UUID.randomUUID().toString();
    730         Parcel parcel = Parcel.obtain();
    731         savedState.writeToParcel(parcel, 0);
    732         parcel.writeString(parcelSuffix);
    733         removeRecyclerView();
    734         // reset for reading
    735         parcel.setDataPosition(0);
    736         // re-create
    737         savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
    738         removeRecyclerView();
    739 
    740         RecyclerView restored = new RecyclerView(getActivity());
    741         mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
    742         mLayoutManager.setReverseLayout(config.mReverseLayout);
    743         mLayoutManager.setGapStrategy(config.mGapStrategy);
    744         restored.setLayoutManager(mLayoutManager);
    745         // use the same adapter for Rect matching
    746         restored.setAdapter(mAdapter);
    747         restored.onRestoreInstanceState(savedState);
    748         mLayoutManager.setSpanCount(1);
    749         mLayoutManager.expectLayouts(1);
    750         setRecyclerView(restored);
    751         mLayoutManager.waitForLayout(2);
    752         assertEquals("on saved state, reverse layout should be preserved",
    753                 config.mReverseLayout, mLayoutManager.getReverseLayout());
    754         assertEquals("on saved state, orientation should be preserved",
    755                 config.mOrientation, mLayoutManager.getOrientation());
    756         assertEquals("after setting new span count, layout manager should keep new value",
    757                 1, mLayoutManager.getSpanCount());
    758         assertEquals("on saved state, gap strategy should be preserved",
    759                 config.mGapStrategy, mLayoutManager.getGapStrategy());
    760         assertTrue("when span count is dramatically changed after restore, # of child views "
    761                 + "should change", beforeChildCount > mLayoutManager.getChildCount());
    762         // make sure LLM can layout all children. is some span info is leaked, this would crash
    763         smoothScrollToPosition(mAdapter.getItemCount() - 1);
    764     }
    765 
    766     @Test
    767     public void scrollAndClear() throws Throwable {
    768         setupByConfig(new Config());
    769         waitFirstLayout();
    770 
    771         assertTrue("Children not laid out", mLayoutManager.collectChildCoordinates().size() > 0);
    772 
    773         mLayoutManager.expectLayouts(1);
    774         runTestOnUiThread(new Runnable() {
    775             @Override
    776             public void run() {
    777                 mLayoutManager.scrollToPositionWithOffset(1, 0);
    778                 mAdapter.clearOnUIThread();
    779             }
    780         });
    781         mLayoutManager.waitForLayout(2);
    782 
    783         assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
    784     }
    785 
    786     @Test
    787     public void accessibilityPositions() throws Throwable {
    788         setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
    789         waitFirstLayout();
    790         final AccessibilityDelegateCompat delegateCompat = mRecyclerView
    791                 .getCompatAccessibilityDelegate();
    792         final AccessibilityEvent event = AccessibilityEvent.obtain();
    793         runTestOnUiThread(new Runnable() {
    794             @Override
    795             public void run() {
    796                 delegateCompat.onInitializeAccessibilityEvent(mRecyclerView, event);
    797             }
    798         });
    799         final AccessibilityRecordCompat record = AccessibilityEventCompat
    800                 .asRecord(event);
    801         final int start = mRecyclerView
    802                 .getChildLayoutPosition(
    803                         mLayoutManager.findFirstVisibleItemClosestToStart(false, true));
    804         final int end = mRecyclerView
    805                 .getChildLayoutPosition(
    806                         mLayoutManager.findFirstVisibleItemClosestToEnd(false, true));
    807         assertEquals("first item position should match",
    808                 Math.min(start, end), record.getFromIndex());
    809         assertEquals("last item position should match",
    810                 Math.max(start, end), record.getToIndex());
    811 
    812     }
    813 }
    814