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 LinearLayoutManagerSnappingTest extends BaseLinearLayoutManagerTest {
     40 
     41     final Config mConfig;
     42     final boolean mReverseScroll;
     43 
     44     public LinearLayoutManagerSnappingTest(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 snapOnScrollSameViewEdge() throws Throwable {
     63         final Config config = (Config) mConfig.clone();
     64         // Ensure that the views are big enough to reach the pathological case when the view closest
     65         // to the center is an edge view, but it cannot scroll further in order to snap.
     66         setupByConfig(config, true, new RecyclerView.LayoutParams(1000, 1000),
     67             new RecyclerView.LayoutParams(1500, 1500));
     68         SnapHelper snapHelper = new LinearSnapHelper();
     69         mLayoutManager.expectIdleState(1);
     70         snapHelper.attachToRecyclerView(mRecyclerView);
     71         mLayoutManager.waitForSnap(10);
     72 
     73         // Record the current center view.
     74         View view = findCenterView(mLayoutManager);
     75 
     76         int scrollDistance = (getViewDimension(view) / 2) - 1;
     77         int scrollDist = config.mStackFromEnd == config.mReverseLayout
     78             ? -scrollDistance : scrollDistance;
     79         mLayoutManager.expectIdleState(1);
     80         smoothScrollBy(scrollDist);
     81         mLayoutManager.waitForSnap(10);
     82         mLayoutManager.expectCallbacks(5);
     83         mLayoutManager.assertNoCallbacks("There should be no callbacks after some time", 3);
     84     }
     85 
     86     @Test
     87     public void snapOnScrollSameView() throws Throwable {
     88         final Config config = (Config) mConfig.clone();
     89         setupByConfig(config, true);
     90         setupSnapHelper();
     91 
     92         // Record the current center view.
     93         View view = findCenterView(mLayoutManager);
     94         assertCenterAligned(view);
     95 
     96         int scrollDistance = (getViewDimension(view) / 2) - 1;
     97         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
     98         mLayoutManager.expectIdleState(2);
     99         smoothScrollBy(scrollDist);
    100         mLayoutManager.waitForSnap(10);
    101 
    102         // Views have not changed
    103         View viewAfterFling = findCenterView(mLayoutManager);
    104         assertSame("The view should NOT have scrolled", view, viewAfterFling);
    105         assertCenterAligned(viewAfterFling);
    106     }
    107 
    108     @Test
    109     public void snapOnScrollNextView() throws Throwable {
    110         final Config config = (Config) mConfig.clone();
    111         setupByConfig(config, true);
    112         setupSnapHelper();
    113 
    114         // Record the current center view.
    115         View view = findCenterView(mLayoutManager);
    116         assertCenterAligned(view);
    117 
    118         int scrollDistance = (getViewDimension(view) / 2) + 1;
    119         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    120         mLayoutManager.expectIdleState(2);
    121         smoothScrollBy(scrollDist);
    122         mLayoutManager.waitForSnap(10);
    123 
    124         // Views have not changed
    125         View viewAfterFling = findCenterView(mLayoutManager);
    126         assertNotSame("The view should have scrolled", view, viewAfterFling);
    127         assertCenterAligned(viewAfterFling);
    128     }
    129 
    130     @Test
    131     public void snapOnFlingSameView() throws Throwable {
    132         final Config config = (Config) mConfig.clone();
    133         setupByConfig(config, true);
    134         setupSnapHelper();
    135 
    136         // Record the current center view.
    137         View view = findCenterView(mLayoutManager);
    138         assertCenterAligned(view);
    139 
    140         // Velocity small enough to not scroll to the next view.
    141         int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
    142         int velocityDir = mReverseScroll ? -velocity : velocity;
    143         mLayoutManager.expectIdleState(2);
    144         assertTrue(fling(velocityDir, velocityDir));
    145         // Wait for two settling scrolls: the initial one and the corrective one.
    146         waitForIdleScroll(mRecyclerView);
    147         mLayoutManager.waitForSnap(100);
    148 
    149         View viewAfterFling = findCenterView(mLayoutManager);
    150 
    151         assertSame("The view should NOT have scrolled", view, viewAfterFling);
    152         assertCenterAligned(viewAfterFling);
    153     }
    154 
    155     @Test
    156     public void snapOnFlingNextView() throws Throwable {
    157         final Config config = (Config) mConfig.clone();
    158         setupByConfig(config, true);
    159         setupSnapHelper();
    160 
    161         // Record the current center view.
    162         View view = findCenterView(mLayoutManager);
    163         assertCenterAligned(view);
    164 
    165         // Velocity high enough to scroll beyond the current view.
    166         int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity());
    167         int velocityDir = mReverseScroll ? -velocity : velocity;
    168         mLayoutManager.expectIdleState(1);
    169         assertTrue(fling(velocityDir, velocityDir));
    170         mLayoutManager.waitForSnap(100);
    171         getInstrumentation().waitForIdleSync();
    172 
    173         View viewAfterFling = findCenterView(mLayoutManager);
    174 
    175         assertNotSame("The view should have scrolled", view, viewAfterFling);
    176         assertCenterAligned(viewAfterFling);
    177     }
    178 
    179     private void setupSnapHelper() throws Throwable {
    180         SnapHelper snapHelper = new LinearSnapHelper();
    181         mLayoutManager.expectIdleState(1);
    182         snapHelper.attachToRecyclerView(mRecyclerView);
    183         mLayoutManager.waitForSnap(10);
    184 
    185         mLayoutManager.expectLayouts(1);
    186         scrollToPosition(mConfig.mItemCount / 2);
    187         mLayoutManager.waitForLayout(2);
    188 
    189         View view = findCenterView(mLayoutManager);
    190         int scrollDistance = distFromCenter(view) / 2;
    191         if (scrollDistance == 0) {
    192             return;
    193         }
    194 
    195         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    196 
    197         mLayoutManager.expectIdleState(2);
    198         smoothScrollBy(scrollDist);
    199         mLayoutManager.waitForSnap(10);
    200     }
    201 
    202     @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager) {
    203         if (layoutManager.canScrollHorizontally()) {
    204             return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0);
    205         } else {
    206             return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2);
    207         }
    208     }
    209 
    210     private int getViewDimension(View view) {
    211         OrientationHelper helper;
    212         if (mLayoutManager.canScrollHorizontally()) {
    213             helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
    214         } else {
    215             helper = OrientationHelper.createVerticalHelper(mLayoutManager);
    216         }
    217         return helper.getDecoratedMeasurement(view);
    218     }
    219 
    220     private void assertCenterAligned(View view) {
    221         if (mLayoutManager.canScrollHorizontally()) {
    222             assertEquals(mRecyclerView.getWidth() / 2,
    223                     mLayoutManager.getViewBounds(view).centerX());
    224         } else {
    225             assertEquals(mRecyclerView.getHeight() / 2,
    226                     mLayoutManager.getViewBounds(view).centerY());
    227         }
    228     }
    229 
    230     private int distFromCenter(View view) {
    231         if (mLayoutManager.canScrollHorizontally()) {
    232             return Math.abs(mRecyclerView.getWidth() / 2 -
    233                 mLayoutManager.getViewBounds(view).centerX());
    234         } else {
    235             return Math.abs(mRecyclerView.getHeight() / 2 -
    236                 mLayoutManager.getViewBounds(view).centerY());
    237         }
    238     }
    239 
    240     private boolean fling(final int velocityX, final int velocityY) throws Throwable {
    241         final AtomicBoolean didStart = new AtomicBoolean(false);
    242         mActivityRule.runOnUiThread(new Runnable() {
    243             @Override
    244             public void run() {
    245                 boolean result = mRecyclerView.fling(velocityX, velocityY);
    246                 didStart.set(result);
    247             }
    248         });
    249         if (!didStart.get()) {
    250             return false;
    251         }
    252         waitForIdleScroll(mRecyclerView);
    253         return true;
    254     }
    255 }
    256