/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v7.widget; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertSame; import static junit.framework.Assert.assertTrue; import android.support.annotation.Nullable; import android.support.test.filters.MediumTest; import android.view.View; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @RunWith(Parameterized.class) public class LinearLayoutManagerSnappingTest extends BaseLinearLayoutManagerTest { final Config mConfig; final boolean mReverseScroll; public LinearLayoutManagerSnappingTest(Config config, boolean reverseScroll) { mConfig = config; mReverseScroll = reverseScroll; } @Parameterized.Parameters(name = "config:{0},reverseScroll:{1}") public static List<Object[]> getParams() { List<Object[]> result = new ArrayList<>(); List<Config> configs = createBaseVariations(); for (Config config : configs) { for (boolean reverseScroll : new boolean[] {true, false}) { result.add(new Object[]{config, reverseScroll}); } } return result; } @MediumTest @Test public void snapOnScrollSameViewEdge() throws Throwable { final Config config = (Config) mConfig.clone(); // Ensure that the views are big enough to reach the pathological case when the view closest // to the center is an edge view, but it cannot scroll further in order to snap. setupByConfig(config, true, new RecyclerView.LayoutParams(1000, 1000), new RecyclerView.LayoutParams(1500, 1500)); SnapHelper snapHelper = new LinearSnapHelper(); mLayoutManager.expectIdleState(1); snapHelper.attachToRecyclerView(mRecyclerView); mLayoutManager.waitForSnap(10); // Record the current center view. View view = findCenterView(mLayoutManager); int scrollDistance = (getViewDimension(view) / 2) - 1; int scrollDist = config.mStackFromEnd == config.mReverseLayout ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(1); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); mLayoutManager.expectCallbacks(5); mLayoutManager.assertNoCallbacks("There should be no callbacks after some time", 3); } @MediumTest @Test public void snapOnScrollSameView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config, true); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); int scrollDistance = (getViewDimension(view) / 2) - 1; int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(2); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); // Views have not changed View viewAfterFling = findCenterView(mLayoutManager); assertSame("The view should NOT have scrolled", view, viewAfterFling); assertCenterAligned(viewAfterFling); } @MediumTest @Test public void snapOnScrollNextView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config, true); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); int scrollDistance = (getViewDimension(view) / 2) + 1; int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(2); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); // Views have not changed View viewAfterFling = findCenterView(mLayoutManager); assertNotSame("The view should have scrolled", view, viewAfterFling); assertCenterAligned(viewAfterFling); } @MediumTest @Test public void snapOnFlingSameView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config, true); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); // Velocity small enough to not scroll to the next view. int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity()); int velocityDir = mReverseScroll ? -velocity : velocity; mLayoutManager.expectIdleState(2); assertTrue(fling(velocityDir, velocityDir)); // Wait for two settling scrolls: the initial one and the corrective one. waitForIdleScroll(mRecyclerView); mLayoutManager.waitForSnap(100); View viewAfterFling = findCenterView(mLayoutManager); assertSame("The view should NOT have scrolled", view, viewAfterFling); assertCenterAligned(viewAfterFling); } @MediumTest @Test public void snapOnFlingNextView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config, true); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); // Velocity high enough to scroll beyond the current view. int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity()); int velocityDir = mReverseScroll ? -velocity : velocity; mLayoutManager.expectIdleState(1); assertTrue(fling(velocityDir, velocityDir)); mLayoutManager.waitForSnap(100); getInstrumentation().waitForIdleSync(); View viewAfterFling = findCenterView(mLayoutManager); assertNotSame("The view should have scrolled", view, viewAfterFling); assertCenterAligned(viewAfterFling); } private void setupSnapHelper() throws Throwable { SnapHelper snapHelper = new LinearSnapHelper(); mLayoutManager.expectIdleState(1); snapHelper.attachToRecyclerView(mRecyclerView); mLayoutManager.waitForSnap(10); mLayoutManager.expectLayouts(1); scrollToPosition(mConfig.mItemCount / 2); mLayoutManager.waitForLayout(2); View view = findCenterView(mLayoutManager); int scrollDistance = distFromCenter(view) / 2; if (scrollDistance == 0) { return; } int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(2); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); } @Nullable private View findCenterView(RecyclerView.LayoutManager layoutManager) { if (layoutManager.canScrollHorizontally()) { return mRecyclerView.findChildViewUnder(mRecyclerView.getWidth() / 2, 0); } else { return mRecyclerView.findChildViewUnder(0, mRecyclerView.getHeight() / 2); } } private int getViewDimension(View view) { OrientationHelper helper; if (mLayoutManager.canScrollHorizontally()) { helper = OrientationHelper.createHorizontalHelper(mLayoutManager); } else { helper = OrientationHelper.createVerticalHelper(mLayoutManager); } return helper.getDecoratedMeasurement(view); } private void assertCenterAligned(View view) { if (mLayoutManager.canScrollHorizontally()) { assertEquals(mRecyclerView.getWidth() / 2, mLayoutManager.getViewBounds(view).centerX()); } else { assertEquals(mRecyclerView.getHeight() / 2, mLayoutManager.getViewBounds(view).centerY()); } } private int distFromCenter(View view) { if (mLayoutManager.canScrollHorizontally()) { return Math.abs(mRecyclerView.getWidth() / 2 - mLayoutManager.getViewBounds(view).centerX()); } else { return Math.abs(mRecyclerView.getHeight() / 2 - mLayoutManager.getViewBounds(view).centerY()); } } private boolean fling(final int velocityX, final int velocityY) throws Throwable { final AtomicBoolean didStart = new AtomicBoolean(false); mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { boolean result = mRecyclerView.fling(velocityX, velocityY); didStart.set(result); } }); if (!didStart.get()) { return false; } waitForIdleScroll(mRecyclerView); return true; } }