LinearLayoutManagerSnappingTest.java revision 3911e1c2d38e301d5ffbdf11f808fdc593dd83e9
1/*
2 * Copyright (C) 2016 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 android.support.v7.widget;
18
19import org.junit.Test;
20import org.junit.runner.RunWith;
21import org.junit.runners.Parameterized;
22
23import android.support.annotation.Nullable;
24import android.test.suitebuilder.annotation.MediumTest;
25import android.view.View;
26
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.List;
30import java.util.concurrent.atomic.AtomicBoolean;
31
32import static junit.framework.Assert.assertEquals;
33import static junit.framework.Assert.assertNotSame;
34import static junit.framework.Assert.assertSame;
35import static junit.framework.Assert.assertTrue;
36
37@RunWith(Parameterized.class)
38public class LinearLayoutManagerSnappingTest extends BaseLinearLayoutManagerTest {
39
40    final Config mConfig;
41    final boolean mReverseScroll;
42
43    public LinearLayoutManagerSnappingTest(Config config, boolean reverseScroll) {
44        mConfig = config;
45        mReverseScroll = reverseScroll;
46    }
47
48    @Parameterized.Parameters(name = "config:{0}, reverseScroll:{1}")
49    public static List<Object[]> getParams() {
50        List<Object[]> result = new ArrayList<>();
51        List<Config> configs = createBaseVariations();
52        for (Config config : configs) {
53            for (boolean reverseScroll : new boolean[] {true, false}) {
54                result.add(new Object[]{config, reverseScroll});
55            }
56        }
57        return result;
58    }
59
60    @MediumTest
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    @MediumTest
87    @Test
88    public void snapOnScrollSameView() throws Throwable {
89        final Config config = (Config) mConfig.clone();
90        setupByConfig(config, true);
91        setupSnapHelper();
92
93        // Record the current center view.
94        View view = findCenterView(mLayoutManager);
95        assertCenterAligned(view);
96
97        int scrollDistance = (getViewDimension(view) / 2) - 1;
98        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
99        mLayoutManager.expectIdleState(2);
100        smoothScrollBy(scrollDist);
101        mLayoutManager.waitForSnap(10);
102
103        // Views have not changed
104        View viewAfterFling = findCenterView(mLayoutManager);
105        assertSame("The view should NOT have scrolled", view, viewAfterFling);
106        assertCenterAligned(viewAfterFling);
107    }
108
109    @MediumTest
110    @Test
111    public void snapOnScrollNextView() throws Throwable {
112        final Config config = (Config) mConfig.clone();
113        setupByConfig(config, true);
114        setupSnapHelper();
115
116        // Record the current center view.
117        View view = findCenterView(mLayoutManager);
118        assertCenterAligned(view);
119
120        int scrollDistance = (getViewDimension(view) / 2) + 1;
121        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
122        mLayoutManager.expectIdleState(2);
123        smoothScrollBy(scrollDist);
124        mLayoutManager.waitForSnap(10);
125
126        // Views have not changed
127        View viewAfterFling = findCenterView(mLayoutManager);
128        assertNotSame("The view should have scrolled", view, viewAfterFling);
129        assertCenterAligned(viewAfterFling);
130    }
131
132    @MediumTest
133    @Test
134    public void snapOnFlingSameView() throws Throwable {
135        final Config config = (Config) mConfig.clone();
136        setupByConfig(config, true);
137        setupSnapHelper();
138
139        // Record the current center view.
140        View view = findCenterView(mLayoutManager);
141        assertCenterAligned(view);
142
143        // Velocity small enough to not scroll to the next view.
144        int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
145        int velocityDir = mReverseScroll ? -velocity : velocity;
146        mLayoutManager.expectIdleState(2);
147        assertTrue(fling(velocityDir, velocityDir));
148        // Wait for two settling scrolls: the initial one and the corrective one.
149        waitForIdleScroll(mRecyclerView);
150        mLayoutManager.waitForSnap(100);
151
152        View viewAfterFling = findCenterView(mLayoutManager);
153
154        assertSame("The view should NOT have scrolled", view, viewAfterFling);
155        assertCenterAligned(viewAfterFling);
156    }
157
158    @MediumTest
159    @Test
160    public void snapOnFlingNextView() throws Throwable {
161        final Config config = (Config) mConfig.clone();
162        setupByConfig(config, true);
163        setupSnapHelper();
164
165        // Record the current center view.
166        View view = findCenterView(mLayoutManager);
167        assertCenterAligned(view);
168
169        // Velocity high enough to scroll beyond the current view.
170        int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity());
171        int velocityDir = mReverseScroll ? -velocity : velocity;
172        mLayoutManager.expectIdleState(1);
173        assertTrue(fling(velocityDir, velocityDir));
174        mLayoutManager.waitForSnap(100);
175        getInstrumentation().waitForIdleSync();
176
177        View viewAfterFling = findCenterView(mLayoutManager);
178
179        assertNotSame("The view should have scrolled", view, viewAfterFling);
180        assertCenterAligned(viewAfterFling);
181    }
182
183    private void setupSnapHelper() throws Throwable {
184        SnapHelper snapHelper = new LinearSnapHelper();
185        mLayoutManager.expectIdleState(1);
186        snapHelper.attachToRecyclerView(mRecyclerView);
187        mLayoutManager.waitForSnap(10);
188
189        mLayoutManager.expectLayouts(1);
190        scrollToPosition(mConfig.mItemCount / 2);
191        mLayoutManager.waitForLayout(2);
192
193        View view = findCenterView(mLayoutManager);
194        int scrollDistance = distFromCenter(view) / 2;
195        if (scrollDistance == 0) {
196            return;
197        }
198
199        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
200
201        mLayoutManager.expectIdleState(2);
202        smoothScrollBy(scrollDist);
203        mLayoutManager.waitForSnap(10);
204    }
205
206    @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager) {
207        if (layoutManager.canScrollHorizontally()) {
208            return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0);
209        } else {
210            return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2);
211        }
212    }
213
214    private int getViewDimension(View view) {
215        OrientationHelper helper;
216        if (mLayoutManager.canScrollHorizontally()) {
217            helper = OrientationHelper.createHorizontalHelper(mLayoutManager);
218        } else {
219            helper = OrientationHelper.createVerticalHelper(mLayoutManager);
220        }
221        return helper.getDecoratedMeasurement(view);
222    }
223
224    private void assertCenterAligned(View view) {
225        if (mLayoutManager.canScrollHorizontally()) {
226            assertEquals(mRecyclerView.getWidth() / 2,
227                    mLayoutManager.getViewBounds(view).centerX());
228        } else {
229            assertEquals(mRecyclerView.getHeight() / 2,
230                    mLayoutManager.getViewBounds(view).centerY());
231        }
232    }
233
234    private int distFromCenter(View view) {
235        if (mLayoutManager.canScrollHorizontally()) {
236            return Math.abs(mRecyclerView.getWidth() / 2 -
237                mLayoutManager.getViewBounds(view).centerX());
238        } else {
239            return Math.abs(mRecyclerView.getHeight() / 2 -
240                mLayoutManager.getViewBounds(view).centerY());
241        }
242    }
243
244    private boolean fling(final int velocityX, final int velocityY) throws Throwable {
245        final AtomicBoolean didStart = new AtomicBoolean(false);
246        runTestOnUiThread(new Runnable() {
247            @Override
248            public void run() {
249                boolean result = mRecyclerView.fling(velocityX, velocityY);
250                didStart.set(result);
251            }
252        });
253        if (!didStart.get()) {
254            return false;
255        }
256        waitForIdleScroll(mRecyclerView);
257        return true;
258    }
259}
260