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 27 import androidx.annotation.Nullable; 28 29 import org.junit.Test; 30 import org.junit.runner.RunWith; 31 import org.junit.runners.Parameterized; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.concurrent.atomic.AtomicBoolean; 36 37 @LargeTest 38 @RunWith(Parameterized.class) 39 public class StaggeredGridLayoutManagerSnappingTest extends BaseStaggeredGridLayoutManagerTest { 40 41 final Config mConfig; 42 final boolean mReverseScroll; 43 44 public StaggeredGridLayoutManagerSnappingTest(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 snapOnScrollSameViewFixedSize() throws Throwable { 63 // This test is a special case for fixed sized children. 64 final Config config = ((Config) mConfig.clone()).itemCount(10); 65 setupByConfig(config); 66 RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(1000, 950); 67 mRecyclerView.setLayoutParams(lp); 68 mAdapter.mOnBindCallback = new OnBindCallback() { 69 @Override 70 void onBoundItem(TestViewHolder vh, int position) { 71 StaggeredGridLayoutManager.LayoutParams slp = getLayoutParamsForPosition(position); 72 vh.itemView.setLayoutParams(slp); 73 } 74 75 @Override 76 boolean assignRandomSize() { 77 return false; 78 } 79 }; 80 waitFirstLayout(); 81 setupSnapHelper(); 82 83 // Record the current center view. 84 View view = findCenterView(mLayoutManager); 85 assertCenterAligned(view); 86 // This number comes from the sizes of the fixed views that are created for this config/ 87 // See getLayoutParamsForPosition(int) below. Obtained manually. 88 int scrollDistance = mLayoutManager.canScrollHorizontally() ? 52 : 52; 89 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 90 mLayoutManager.expectIdleState(2); 91 smoothScrollBy(scrollDist); 92 mLayoutManager.waitForSnap(10); 93 94 // Views have not changed 95 View viewAfterScroll = findCenterView(mLayoutManager); 96 assertSame("The view should NOT have scrolled", view, viewAfterScroll); 97 assertCenterAligned(viewAfterScroll); 98 } 99 100 @Test 101 public void snapOnScrollSameView() throws Throwable { 102 final Config config = (Config) mConfig.clone(); 103 setupByConfig(config); 104 waitFirstLayout(); 105 setupSnapHelper(); 106 107 // Record the current center view. 108 View view = findCenterView(mLayoutManager); 109 assertCenterAligned(view); 110 // For a staggered grid layout manager with unknown item size we need to keep the distance 111 // small enough to ensure we do not scroll over to an offset view in a different span. 112 int scrollDistance = findMinSafeScrollDistance(); 113 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 114 mLayoutManager.expectIdleState(2); 115 smoothScrollBy(scrollDist); 116 mLayoutManager.waitForSnap(10); 117 118 // Views have not changed 119 View viewAfterScroll = findCenterView(mLayoutManager); 120 assertSame("The view should NOT have scrolled", view, viewAfterScroll); 121 assertCenterAligned(viewAfterScroll); 122 } 123 124 @Test 125 public void snapOnScrollNextItem() throws Throwable { 126 final Config config = (Config) mConfig.clone(); 127 setupByConfig(config); 128 waitFirstLayout(); 129 setupSnapHelper(); 130 131 // Record the current center view. 132 View view = findCenterView(mLayoutManager); 133 assertCenterAligned(view); 134 int scrollDistance = getViewDimension(view) + 1; 135 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 136 137 smoothScrollBy(scrollDist); 138 waitForIdleScroll(mRecyclerView); 139 waitForIdleScroll(mRecyclerView); 140 141 View viewAfterScroll = findCenterView(mLayoutManager); 142 143 assertNotSame("The view should have scrolled", view, viewAfterScroll); 144 assertCenterAligned(viewAfterScroll); 145 } 146 147 @Test 148 public void snapOnFlingSameView() throws Throwable { 149 final Config config = (Config) mConfig.clone(); 150 setupByConfig(config); 151 waitFirstLayout(); 152 setupSnapHelper(); 153 154 // Record the current center view. 155 View view = findCenterView(mLayoutManager); 156 assertCenterAligned(view); 157 158 // Velocity small enough to not scroll to the next view. 159 int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity()); 160 int velocityDir = mReverseScroll ? -velocity : velocity; 161 mLayoutManager.expectIdleState(2); 162 assertTrue(fling(velocityDir, velocityDir)); 163 // Wait for two settling scrolls: the initial one and the corrective one. 164 waitForIdleScroll(mRecyclerView); 165 mLayoutManager.waitForSnap(100); 166 167 View viewAfterFling = findCenterView(mLayoutManager); 168 169 assertSame("The view should NOT have scrolled", view, viewAfterFling); 170 assertCenterAligned(viewAfterFling); 171 } 172 173 @Test 174 public void snapOnFlingNextView() throws Throwable { 175 final Config config = (Config) mConfig.clone(); 176 setupByConfig(config); 177 waitFirstLayout(); 178 setupSnapHelper(); 179 180 // Record the current center view. 181 View view = findCenterView(mLayoutManager); 182 assertCenterAligned(view); 183 184 // Velocity high enough to scroll beyond the current view. 185 int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity()); 186 int velocityDir = mReverseScroll ? -velocity : velocity; 187 188 mLayoutManager.expectIdleState(1); 189 assertTrue(fling(velocityDir, velocityDir)); 190 mLayoutManager.waitForSnap(100); 191 getInstrumentation().waitForIdleSync(); 192 193 View viewAfterFling = findCenterView(mLayoutManager); 194 195 assertNotSame("The view should have scrolled", view, viewAfterFling); 196 assertCenterAligned(viewAfterFling); 197 } 198 199 private StaggeredGridLayoutManager.LayoutParams getLayoutParamsForPosition(int position) { 200 // Only enabled fixed sizes if the config says so. 201 if (mLayoutManager.canScrollHorizontally()) { 202 int width = 400 + position * 70; 203 return new StaggeredGridLayoutManager.LayoutParams(width, 300); 204 } else { 205 int height = 300 + position * 70; 206 return new StaggeredGridLayoutManager.LayoutParams(300, height); 207 } 208 } 209 210 @Nullable View findCenterView(RecyclerView.LayoutManager layoutManager) { 211 return mLayoutManager.findFirstVisibleItemClosestToCenter(); 212 } 213 214 private void setupSnapHelper() throws Throwable { 215 SnapHelper snapHelper = new LinearSnapHelper(); 216 mLayoutManager.expectIdleState(1); 217 snapHelper.attachToRecyclerView(mRecyclerView); 218 mLayoutManager.waitForSnap(10); 219 220 mLayoutManager.expectLayouts(1); 221 scrollToPosition(mConfig.mItemCount / 2); 222 mLayoutManager.waitForLayout(2); 223 224 View view = findCenterView(mLayoutManager); 225 int scrollDistance = distFromCenter(view) / 2; 226 if (scrollDistance == 0) { 227 return; 228 } 229 230 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 231 232 mLayoutManager.expectIdleState(2); 233 smoothScrollBy(scrollDist); 234 mLayoutManager.waitForSnap(10); 235 } 236 237 private int getViewDimension(View view) { 238 OrientationHelper helper; 239 if (mLayoutManager.canScrollHorizontally()) { 240 helper = OrientationHelper.createHorizontalHelper(mLayoutManager); 241 } else { 242 helper = OrientationHelper.createVerticalHelper(mLayoutManager); 243 } 244 return helper.getDecoratedMeasurement(view); 245 } 246 247 private void assertCenterAligned(View view) { 248 if (mLayoutManager.canScrollHorizontally()) { 249 assertEquals(mRecyclerView.getWidth() / 2, 250 mLayoutManager.getViewBounds(view).centerX()); 251 } else { 252 assertEquals(mRecyclerView.getHeight() / 2, 253 mLayoutManager.getViewBounds(view).centerY()); 254 } 255 } 256 257 private int findMinSafeScrollDistance() { 258 int minDist = Integer.MAX_VALUE; 259 for (int i = mLayoutManager.getChildCount() - 1; i >= 0; i--) { 260 final View child = mLayoutManager.getChildAt(i); 261 int dist = distFromCenter(child); 262 if (dist < minDist) { 263 minDist = dist; 264 } 265 } 266 return minDist / 2 - 1; 267 } 268 269 private int distFromCenter(View view) { 270 if (mLayoutManager.canScrollHorizontally()) { 271 return Math.abs(mRecyclerView.getWidth() / 2 - 272 mLayoutManager.getViewBounds(view).centerX()); 273 } else { 274 return Math.abs(mRecyclerView.getHeight() / 2 - 275 mLayoutManager.getViewBounds(view).centerY()); 276 } 277 } 278 279 private boolean fling(final int velocityX, final int velocityY) 280 throws Throwable { 281 final AtomicBoolean didStart = new AtomicBoolean(false); 282 mActivityRule.runOnUiThread(new Runnable() { 283 @Override 284 public void run() { 285 boolean result = mRecyclerView.fling(velocityX, velocityY); 286 didStart.set(result); 287 } 288 }); 289 if (!didStart.get()) { 290 return false; 291 } 292 waitForIdleScroll(mRecyclerView); 293 return true; 294 } 295 } 296