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 static junit.framework.Assert.assertEquals; 20import static junit.framework.Assert.assertNotSame; 21import static junit.framework.Assert.assertSame; 22import static junit.framework.Assert.assertTrue; 23 24import android.support.annotation.Nullable; 25import android.support.test.filters.LargeTest; 26import android.view.View; 27import android.widget.TextView; 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 GridLayoutManagerSnappingTest extends BaseGridLayoutManagerTest { 40 41 final Config mConfig; 42 final boolean mReverseScroll; 43 44 public GridLayoutManagerSnappingTest(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 snapOnScrollSameView() throws Throwable { 63 final Config config = (Config) mConfig.clone(); 64 RecyclerView recyclerView = setupBasic(config); 65 waitForFirstLayout(recyclerView); 66 setupSnapHelper(); 67 68 // Record the current center view. 69 View view = findCenterView(mGlm); 70 assertCenterAligned(view); 71 int scrollDistance = (getViewDimension(view) / 2) - 1; 72 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 73 mGlm.expectIdleState(2); 74 smoothScrollBy(scrollDist); 75 mGlm.waitForSnap(10); 76 77 // Views have not changed 78 View viewAfterFling = findCenterView(mGlm); 79 assertSame("The view should have scrolled", view, viewAfterFling); 80 assertCenterAligned(viewAfterFling); 81 } 82 83 @Test 84 public void snapOnScrollNextItem() throws Throwable { 85 final Config config = (Config) mConfig.clone(); 86 RecyclerView recyclerView = setupBasic(config); 87 waitForFirstLayout(recyclerView); 88 setupSnapHelper(); 89 90 // Record the current center view. 91 View view = findCenterView(mGlm); 92 assertCenterAligned(view); 93 int scrollDistance = getViewDimension(view) + 1; 94 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 95 96 smoothScrollBy(scrollDist); 97 waitForIdleScroll(mRecyclerView); 98 waitForIdleScroll(mRecyclerView); 99 100 View viewAfterScroll = findCenterView(mGlm); 101 102 assertNotSame("The view should have scrolled", view, viewAfterScroll); 103 assertCenterAligned(viewAfterScroll); 104 } 105 106 @Test 107 public void snapOnFlingSameView() throws Throwable { 108 final Config config = (Config) mConfig.clone(); 109 RecyclerView recyclerView = setupBasic(config); 110 waitForFirstLayout(recyclerView); 111 setupSnapHelper(); 112 113 // Record the current center view. 114 View view = findCenterView(mGlm); 115 assertCenterAligned(view); 116 117 // Velocity small enough to not scroll to the next view. 118 int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity()); 119 int velocityDir = mReverseScroll ? -velocity : velocity; 120 mGlm.expectIdleState(2); 121 assertTrue(fling(velocityDir, velocityDir)); 122 // Wait for two settling scrolls: the initial one and the corrective one. 123 waitForIdleScroll(mRecyclerView); 124 mGlm.waitForSnap(100); 125 126 View viewAfterFling = findCenterView(mGlm); 127 128 assertSame("The view should NOT have scrolled", view, viewAfterFling); 129 assertCenterAligned(viewAfterFling); 130 } 131 132 @Test 133 public void snapOnFlingNextView() throws Throwable { 134 final Config config = (Config) mConfig.clone(); 135 RecyclerView recyclerView = setupBasic(config); 136 waitForFirstLayout(recyclerView); 137 setupSnapHelper(); 138 139 // Record the current center view. 140 View view = findCenterView(mGlm); 141 assertCenterAligned(view); 142 143 // Velocity high enough to scroll beyond the current view. 144 int velocity = (int) (0.25 * mRecyclerView.getMaxFlingVelocity()); 145 int velocityDir = mReverseScroll ? -velocity : velocity; 146 147 mGlm.expectIdleState(1); 148 assertTrue(fling(velocityDir, velocityDir)); 149 mGlm.waitForSnap(100); 150 getInstrumentation().waitForIdleSync(); 151 152 View viewAfterFling = findCenterView(mGlm); 153 154 assertNotSame("The view should have scrolled!", 155 ((TextView) view).getText(),((TextView) viewAfterFling).getText()); 156 assertCenterAligned(viewAfterFling); 157 } 158 159 private void setupSnapHelper() throws Throwable { 160 SnapHelper snapHelper = new LinearSnapHelper(); 161 mGlm.expectIdleState(1); 162 snapHelper.attachToRecyclerView(mRecyclerView); 163 mGlm.waitForSnap(10); 164 165 mGlm.expectLayout(1); 166 scrollToPosition(mConfig.mItemCount / 2); 167 mGlm.waitForLayout(2); 168 169 View view = findCenterView(mGlm); 170 int scrollDistance = distFromCenter(view) / 2; 171 if (scrollDistance == 0) { 172 return; 173 } 174 175 int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; 176 177 mGlm.expectIdleState(2); 178 smoothScrollBy(scrollDist); 179 mGlm.waitForSnap(10); 180 } 181 182 @Nullable View findCenterView(RecyclerView.LayoutManager layoutManager) { 183 if (layoutManager.canScrollHorizontally()) { 184 return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0); 185 } else { 186 return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2); 187 } 188 } 189 190 private int getViewDimension(View view) { 191 OrientationHelper helper; 192 if (mGlm.canScrollHorizontally()) { 193 helper = OrientationHelper.createHorizontalHelper(mGlm); 194 } else { 195 helper = OrientationHelper.createVerticalHelper(mGlm); 196 } 197 return helper.getDecoratedMeasurement(view); 198 } 199 200 private void assertCenterAligned(View view) { 201 if(mGlm.canScrollHorizontally()) { 202 assertEquals("The child should align with the center of the parent", 203 mRecyclerView.getWidth() / 2, 204 mGlm.getDecoratedLeft(view) + 205 mGlm.getDecoratedMeasuredWidth(view) / 2, 1); 206 } else { 207 assertEquals("The child should align with the center of the parent", 208 mRecyclerView.getHeight() / 2, 209 mGlm.getDecoratedTop(view) + 210 mGlm.getDecoratedMeasuredHeight(view) / 2, 1); 211 } 212 } 213 214 private int distFromCenter(View view) { 215 if (mGlm.canScrollHorizontally()) { 216 return Math.abs(mRecyclerView.getWidth() / 2 - mGlm.getViewBounds(view).centerX()); 217 } else { 218 return Math.abs(mRecyclerView.getHeight() / 2 - mGlm.getViewBounds(view).centerY()); 219 } 220 } 221 222 private boolean fling(final int velocityX, final int velocityY) 223 throws Throwable { 224 final AtomicBoolean didStart = new AtomicBoolean(false); 225 mActivityRule.runOnUiThread(new Runnable() { 226 @Override 227 public void run() { 228 boolean result = mRecyclerView.fling(velocityX, velocityY); 229 didStart.set(result); 230 } 231 }); 232 if (!didStart.get()) { 233 return false; 234 } 235 waitForIdleScroll(mRecyclerView); 236 return true; 237 } 238} 239