Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.recyclerview.widget;
     18 
     19 import static org.junit.Assert.assertEquals;
     20 import static org.junit.Assert.assertNotSame;
     21 import static org.junit.Assert.assertSame;
     22 import static org.junit.Assert.assertTrue;
     23 
     24 import android.support.test.filters.LargeTest;
     25 import android.view.View;
     26 
     27 import androidx.annotation.Nullable;
     28 
     29 import org.junit.Test;
     30 import org.junit.runner.RunWith;
     31 import org.junit.runners.Parameterized;
     32 
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.concurrent.atomic.AtomicBoolean;
     36 
     37 @LargeTest
     38 @RunWith(Parameterized.class)
     39 public class StaggeredGridLayoutManagerSnappingTest extends BaseStaggeredGridLayoutManagerTest {
     40 
     41     final Config mConfig;
     42     final boolean mReverseScroll;
     43 
     44     public StaggeredGridLayoutManagerSnappingTest(Config config, boolean reverseScroll) {
     45         mConfig = config;
     46         mReverseScroll = reverseScroll;
     47     }
     48 
     49     @Parameterized.Parameters(name = "config:{0},reverseScroll:{1}")
     50     public static List<Object[]> getParams() {
     51         List<Object[]> result = new ArrayList<>();
     52         List<Config> configs = createBaseVariations();
     53         for (Config config : configs) {
     54             for (boolean reverseScroll : new boolean[] {true, false}) {
     55                 result.add(new Object[]{config, reverseScroll});
     56             }
     57         }
     58         return result;
     59     }
     60 
     61     @Test
     62     public void snapOnScrollSameViewFixedSize() throws Throwable {
     63         // This test is a special case for fixed sized children.
     64         final Config config = ((Config) mConfig.clone()).itemCount(10);
     65         setupByConfig(config);
     66         RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(1000, 950);
     67         mRecyclerView.setLayoutParams(lp);
     68         mAdapter.mOnBindCallback = new OnBindCallback() {
     69             @Override
     70             void onBoundItem(TestViewHolder vh, int position) {
     71                 StaggeredGridLayoutManager.LayoutParams slp = getLayoutParamsForPosition(position);
     72                 vh.itemView.setLayoutParams(slp);
     73             }
     74 
     75             @Override
     76             boolean assignRandomSize() {
     77                 return false;
     78             }
     79         };
     80         waitFirstLayout();
     81         setupSnapHelper();
     82 
     83         // Record the current center view.
     84         View view = findCenterView(mLayoutManager);
     85         assertCenterAligned(view);
     86         // This number comes from the sizes of the fixed views that are created for this config/
     87         // See getLayoutParamsForPosition(int) below. Obtained manually.
     88         int scrollDistance = mLayoutManager.canScrollHorizontally() ? 52 : 52;
     89         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
     90         mLayoutManager.expectIdleState(2);
     91         smoothScrollBy(scrollDist);
     92         mLayoutManager.waitForSnap(10);
     93 
     94         // Views have not changed
     95         View viewAfterScroll = findCenterView(mLayoutManager);
     96         assertSame("The view should NOT have scrolled", view, viewAfterScroll);
     97         assertCenterAligned(viewAfterScroll);
     98     }
     99 
    100     @Test
    101     public void snapOnScrollSameView() throws Throwable {
    102         final Config config = (Config) mConfig.clone();
    103         setupByConfig(config);
    104         waitFirstLayout();
    105         setupSnapHelper();
    106 
    107         // Record the current center view.
    108         View view = findCenterView(mLayoutManager);
    109         assertCenterAligned(view);
    110         // For a staggered grid layout manager with unknown item size we need to keep the distance
    111         // small enough to ensure we do not scroll over to an offset view in a different span.
    112         int scrollDistance = findMinSafeScrollDistance();
    113         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    114         mLayoutManager.expectIdleState(2);
    115         smoothScrollBy(scrollDist);
    116         mLayoutManager.waitForSnap(10);
    117 
    118         // Views have not changed
    119         View viewAfterScroll = findCenterView(mLayoutManager);
    120         assertSame("The view should NOT have scrolled", view, viewAfterScroll);
    121         assertCenterAligned(viewAfterScroll);
    122     }
    123 
    124     @Test
    125     public void snapOnScrollNextItem() throws Throwable {
    126         final Config config = (Config) mConfig.clone();
    127         setupByConfig(config);
    128         waitFirstLayout();
    129         setupSnapHelper();
    130 
    131         // Record the current center view.
    132         View view = findCenterView(mLayoutManager);
    133         assertCenterAligned(view);
    134         int scrollDistance = getViewDimension(view) + 1;
    135         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    136 
    137         smoothScrollBy(scrollDist);
    138         waitForIdleScroll(mRecyclerView);
    139         waitForIdleScroll(mRecyclerView);
    140 
    141         View viewAfterScroll = findCenterView(mLayoutManager);
    142 
    143         assertNotSame("The view should have scrolled", view, viewAfterScroll);
    144         assertCenterAligned(viewAfterScroll);
    145     }
    146 
    147     @Test
    148     public void snapOnFlingSameView() throws Throwable {
    149         final Config config = (Config) mConfig.clone();
    150         setupByConfig(config);
    151         waitFirstLayout();
    152         setupSnapHelper();
    153 
    154         // Record the current center view.
    155         View view = findCenterView(mLayoutManager);
    156         assertCenterAligned(view);
    157 
    158         // Velocity small enough to not scroll to the next view.
    159         int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
    160         int velocityDir = mReverseScroll ? -velocity : velocity;
    161         mLayoutManager.expectIdleState(2);
    162         assertTrue(fling(velocityDir, velocityDir));
    163         // Wait for two settling scrolls: the initial one and the corrective one.
    164         waitForIdleScroll(mRecyclerView);
    165         mLayoutManager.waitForSnap(100);
    166 
    167         View viewAfterFling = findCenterView(mLayoutManager);
    168 
    169         assertSame("The view should NOT have scrolled", view, viewAfterFling);
    170         assertCenterAligned(viewAfterFling);
    171     }
    172 
    173     @Test
    174     public void snapOnFlingNextView() throws Throwable {
    175         final Config config = (Config) mConfig.clone();
    176         setupByConfig(config);
    177         waitFirstLayout();
    178         setupSnapHelper();
    179 
    180         // Record the current center view.
    181         View view = findCenterView(mLayoutManager);
    182         assertCenterAligned(view);
    183 
    184         // Velocity high enough to scroll beyond the current view.
    185         int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity());
    186         int velocityDir = mReverseScroll ? -velocity : velocity;
    187 
    188         mLayoutManager.expectIdleState(1);
    189         assertTrue(fling(velocityDir, velocityDir));
    190         mLayoutManager.waitForSnap(100);
    191         getInstrumentation().waitForIdleSync();
    192 
    193         View viewAfterFling = findCenterView(mLayoutManager);
    194 
    195         assertNotSame("The view should have scrolled", view, viewAfterFling);
    196         assertCenterAligned(viewAfterFling);
    197     }
    198 
    199     private StaggeredGridLayoutManager.LayoutParams getLayoutParamsForPosition(int position) {
    200         // Only enabled fixed sizes if the config says so.
    201         if (mLayoutManager.canScrollHorizontally()) {
    202             int width = 400 + position * 70;
    203             return new StaggeredGridLayoutManager.LayoutParams(width, 300);
    204         } else {
    205             int height = 300 + position * 70;
    206             return new StaggeredGridLayoutManager.LayoutParams(300, height);
    207         }
    208     }
    209 
    210     @Nullable View findCenterView(RecyclerView.LayoutManager layoutManager) {
    211         return mLayoutManager.findFirstVisibleItemClosestToCenter();
    212     }
    213 
    214     private void setupSnapHelper() throws Throwable {
    215         SnapHelper snapHelper = new LinearSnapHelper();
    216         mLayoutManager.expectIdleState(1);
    217         snapHelper.attachToRecyclerView(mRecyclerView);
    218         mLayoutManager.waitForSnap(10);
    219 
    220         mLayoutManager.expectLayouts(1);
    221         scrollToPosition(mConfig.mItemCount / 2);
    222         mLayoutManager.waitForLayout(2);
    223 
    224         View view = findCenterView(mLayoutManager);
    225         int scrollDistance = distFromCenter(view) / 2;
    226         if (scrollDistance == 0) {
    227             return;
    228         }
    229 
    230         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    231 
    232         mLayoutManager.expectIdleState(2);
    233         smoothScrollBy(scrollDist);
    234         mLayoutManager.waitForSnap(10);
    235     }
    236 
    237     private int getViewDimension(View view) {
    238         OrientationHelper helper;
    239         if (mLayoutManager.canScrollHorizontally()) {
    240             helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
    241         } else {
    242             helper = OrientationHelper.createVerticalHelper(mLayoutManager);
    243         }
    244         return helper.getDecoratedMeasurement(view);
    245     }
    246 
    247     private void assertCenterAligned(View view) {
    248         if (mLayoutManager.canScrollHorizontally()) {
    249             assertEquals(mRecyclerView.getWidth() / 2,
    250                     mLayoutManager.getViewBounds(view).centerX());
    251         } else {
    252             assertEquals(mRecyclerView.getHeight() / 2,
    253                     mLayoutManager.getViewBounds(view).centerY());
    254         }
    255     }
    256 
    257     private int findMinSafeScrollDistance() {
    258         int minDist = Integer.MAX_VALUE;
    259         for (int i = mLayoutManager.getChildCount() - 1; i >= 0; i--) {
    260             final View child = mLayoutManager.getChildAt(i);
    261             int dist = distFromCenter(child);
    262             if (dist < minDist) {
    263                 minDist = dist;
    264             }
    265         }
    266         return minDist / 2 - 1;
    267     }
    268 
    269     private int distFromCenter(View view) {
    270         if (mLayoutManager.canScrollHorizontally()) {
    271             return Math.abs(mRecyclerView.getWidth() / 2 -
    272                     mLayoutManager.getViewBounds(view).centerX());
    273         } else {
    274             return Math.abs(mRecyclerView.getHeight() / 2 -
    275                     mLayoutManager.getViewBounds(view).centerY());
    276         }
    277     }
    278 
    279     private boolean fling(final int velocityX, final int velocityY)
    280             throws Throwable {
    281         final AtomicBoolean didStart = new AtomicBoolean(false);
    282         mActivityRule.runOnUiThread(new Runnable() {
    283             @Override
    284             public void run() {
    285                 boolean result = mRecyclerView.fling(velocityX, velocityY);
    286                 didStart.set(result);
    287             }
    288         });
    289         if (!didStart.get()) {
    290             return false;
    291         }
    292         waitForIdleScroll(mRecyclerView);
    293         return true;
    294     }
    295 }
    296