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; 26 27import androidx.annotation.Nullable; 28 29import org.junit.Test; 30import org.junit.runner.RunWith; 31import org.junit.runners.Parameterized; 32 33import java.util.ArrayList; 34import java.util.List; 35import java.util.concurrent.atomic.AtomicBoolean; 36 37@LargeTest 38@RunWith(Parameterized.class) 39public 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