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
17package androidx.recyclerview.widget;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNotSame;
21import static org.junit.Assert.assertSame;
22import static org.junit.Assert.assertTrue;
23
24import android.support.test.filters.LargeTest;
25import android.view.View;
26import android.view.ViewGroup;
27import android.widget.TextView;
28
29import androidx.annotation.Nullable;
30
31import org.junit.Test;
32import org.junit.runner.RunWith;
33import org.junit.runners.Parameterized;
34
35import java.util.ArrayList;
36import java.util.List;
37import java.util.concurrent.atomic.AtomicBoolean;
38
39@LargeTest
40@RunWith(Parameterized.class)
41public 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