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