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 import android.view.ViewGroup;
     27 import android.widget.TextView;
     28 
     29 import androidx.annotation.Nullable;
     30 
     31 import org.junit.Test;
     32 import org.junit.runner.RunWith;
     33 import org.junit.runners.Parameterized;
     34 
     35 import java.util.ArrayList;
     36 import java.util.List;
     37 import java.util.concurrent.atomic.AtomicBoolean;
     38 
     39 @LargeTest
     40 @RunWith(Parameterized.class)
     41 public class PagerSnapHelperTest extends BaseLinearLayoutManagerTest {
     42 
     43     final Config mConfig;
     44     final boolean mReverseScroll;
     45 
     46     public PagerSnapHelperTest(Config config, boolean reverseScroll) {
     47         mConfig = config;
     48         mReverseScroll = reverseScroll;
     49     }
     50 
     51     @Parameterized.Parameters(name = "config:{0},reverseScroll:{1}")
     52     public static List<Object[]> getParams() {
     53         List<Object[]> result = new ArrayList<>();
     54         List<Config> configs = createBaseVariations();
     55         for (Config config : configs) {
     56             for (boolean reverseScroll : new boolean[] {false, true}) {
     57                 result.add(new Object[]{config, reverseScroll});
     58             }
     59         }
     60         return result;
     61     }
     62 
     63     @Test
     64     public void snapOnScrollSameView() throws Throwable {
     65         final Config config = (Config) mConfig.clone();
     66         setupByConfig(config, true,
     67                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     68                         ViewGroup.LayoutParams.MATCH_PARENT),
     69                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     70                         ViewGroup.LayoutParams.MATCH_PARENT));
     71         setupSnapHelper();
     72 
     73         // Record the current center view.
     74         TextView view = (TextView) findCenterView(mLayoutManager);
     75         assertCenterAligned(view);
     76 
     77         int scrollDistance = (getViewDimension(view) / 2) - 1;
     78         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
     79         mLayoutManager.expectIdleState(3);
     80         smoothScrollBy(scrollDist);
     81         mLayoutManager.waitForSnap(10);
     82 
     83         // Views have not changed
     84         View viewAfterFling = findCenterView(mLayoutManager);
     85         assertSame("The view should NOT have scrolled", view, viewAfterFling);
     86         assertCenterAligned(viewAfterFling);
     87     }
     88 
     89     @Test
     90     public void snapOnScrollNextView() throws Throwable {
     91         final Config config = (Config) mConfig.clone();
     92         setupByConfig(config, true,
     93                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     94                         ViewGroup.LayoutParams.MATCH_PARENT),
     95                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
     96                         ViewGroup.LayoutParams.MATCH_PARENT));
     97         setupSnapHelper();
     98 
     99         // Record the current center view.
    100         View view = findCenterView(mLayoutManager);
    101         assertCenterAligned(view);
    102 
    103         int scrollDistance = (getViewDimension(view) / 2) + 1;
    104         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    105         mLayoutManager.expectIdleState(3);
    106         smoothScrollBy(scrollDist);
    107         mLayoutManager.waitForSnap(10);
    108 
    109         // Views have not changed
    110         View viewAfterFling = findCenterView(mLayoutManager);
    111         assertNotSame("The view should have scrolled", view, viewAfterFling);
    112         int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
    113                 ? (mReverseScroll ? 1 : -1)
    114                 : (mReverseScroll ? -1 : 1));
    115         assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
    116         assertCenterAligned(viewAfterFling);
    117     }
    118 
    119     @Test
    120     public void snapOnFlingSameView() throws Throwable {
    121         final Config config = (Config) mConfig.clone();
    122         setupByConfig(config, true,
    123                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    124                         ViewGroup.LayoutParams.MATCH_PARENT),
    125                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    126                         ViewGroup.LayoutParams.MATCH_PARENT));
    127         setupSnapHelper();
    128 
    129         // Record the current center view.
    130         View view = findCenterView(mLayoutManager);
    131         assertCenterAligned(view);
    132 
    133         // Velocity small enough to not scroll to the next view.
    134         int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
    135         int velocityDir = mReverseScroll ? -velocity : velocity;
    136         mLayoutManager.expectIdleState(2);
    137         // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
    138         mActivityRule.runOnUiThread(new Runnable() {
    139             @Override
    140             public void run() {
    141                 mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
    142             }
    143         });
    144         waitForIdleScroll(mRecyclerView);
    145         assertTrue(fling(velocityDir, velocityDir));
    146         // Wait for two settling scrolls: the initial one and the corrective one.
    147         waitForIdleScroll(mRecyclerView);
    148         mLayoutManager.waitForSnap(100);
    149 
    150         View viewAfterFling = findCenterView(mLayoutManager);
    151 
    152         assertSame("The view should NOT have scrolled", view, viewAfterFling);
    153         assertCenterAligned(viewAfterFling);
    154     }
    155 
    156     @Test
    157     public void snapOnFlingNextView() throws Throwable {
    158         final Config config = (Config) mConfig.clone();
    159         setupByConfig(config, true,
    160                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    161                         ViewGroup.LayoutParams.MATCH_PARENT),
    162                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    163                         ViewGroup.LayoutParams.MATCH_PARENT));
    164         setupSnapHelper();
    165         runSnapOnMaxFlingNextView((int) (0.2 * mRecyclerView.getMaxFlingVelocity()));
    166     }
    167 
    168     @Test
    169     public void snapOnMaxFlingNextView() throws Throwable {
    170         final Config config = (Config) mConfig.clone();
    171         setupByConfig(config, true,
    172                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    173                         ViewGroup.LayoutParams.MATCH_PARENT),
    174                 new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    175                         ViewGroup.LayoutParams.MATCH_PARENT));
    176         setupSnapHelper();
    177         runSnapOnMaxFlingNextView(mRecyclerView.getMaxFlingVelocity());
    178     }
    179 
    180     private void runSnapOnMaxFlingNextView(int velocity) throws Throwable {
    181         // Record the current center view.
    182         View view = findCenterView(mLayoutManager);
    183         assertCenterAligned(view);
    184 
    185         int velocityDir = mReverseScroll ? -velocity : velocity;
    186         mLayoutManager.expectIdleState(1);
    187 
    188         // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
    189         mActivityRule.runOnUiThread(new Runnable() {
    190             @Override
    191             public void run() {
    192                 mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
    193             }
    194         });
    195         waitForIdleScroll(mRecyclerView);
    196         assertTrue(fling(velocityDir, velocityDir));
    197         mLayoutManager.waitForSnap(100);
    198         getInstrumentation().waitForIdleSync();
    199 
    200         View viewAfterFling = findCenterView(mLayoutManager);
    201 
    202         assertNotSame("The view should have scrolled", view, viewAfterFling);
    203         int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
    204                 ? (mReverseScroll ? 1 : -1)
    205                 : (mReverseScroll ? -1 : 1));
    206         assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
    207         assertCenterAligned(viewAfterFling);
    208     }
    209 
    210     private void setupSnapHelper() throws Throwable {
    211         SnapHelper snapHelper = new PagerSnapHelper();
    212         mLayoutManager.expectIdleState(1);
    213         snapHelper.attachToRecyclerView(mRecyclerView);
    214 
    215         mLayoutManager.expectLayouts(1);
    216         scrollToPosition(mConfig.mItemCount / 2);
    217         mLayoutManager.waitForLayout(2);
    218 
    219         View view = findCenterView(mLayoutManager);
    220         int scrollDistance = distFromCenter(view) / 2;
    221         if (scrollDistance == 0) {
    222             return;
    223         }
    224 
    225         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
    226 
    227         mLayoutManager.expectIdleState(2);
    228         smoothScrollBy(scrollDist);
    229         mLayoutManager.waitForSnap(10);
    230     }
    231 
    232     @Nullable
    233     private View findCenterView(RecyclerView.LayoutManager layoutManager) {
    234         if (layoutManager.canScrollHorizontally()) {
    235             return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0);
    236         } else {
    237             return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2);
    238         }
    239     }
    240 
    241     private int getViewDimension(View view) {
    242         OrientationHelper helper;
    243         if (mLayoutManager.canScrollHorizontally()) {
    244             helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
    245         } else {
    246             helper = OrientationHelper.createVerticalHelper(mLayoutManager);
    247         }
    248         return helper.getDecoratedMeasurement(view);
    249     }
    250 
    251     private void assertCenterAligned(View view) {
    252         if (mLayoutManager.canScrollHorizontally()) {
    253             assertEquals(mRecyclerView.getWidth() / 2,
    254                     mLayoutManager.getViewBounds(view).centerX());
    255         } else {
    256             assertEquals(mRecyclerView.getHeight() / 2,
    257                     mLayoutManager.getViewBounds(view).centerY());
    258         }
    259     }
    260 
    261     private int distFromCenter(View view) {
    262         if (mLayoutManager.canScrollHorizontally()) {
    263             return Math.abs(mRecyclerView.getWidth() / 2
    264                     - mLayoutManager.getViewBounds(view).centerX());
    265         } else {
    266             return Math.abs(mRecyclerView.getHeight() / 2
    267                     - mLayoutManager.getViewBounds(view).centerY());
    268         }
    269     }
    270 
    271     private boolean fling(final int velocityX, final int velocityY) throws Throwable {
    272         final AtomicBoolean didStart = new AtomicBoolean(false);
    273         mActivityRule.runOnUiThread(new Runnable() {
    274             @Override
    275             public void run() {
    276                 boolean result = mRecyclerView.fling(velocityX, velocityY);
    277                 didStart.set(result);
    278             }
    279         });
    280         if (!didStart.get()) {
    281             return false;
    282         }
    283         waitForIdleScroll(mRecyclerView);
    284         return true;
    285     }
    286 }
    287