/* * 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; @MediumTest @RunWith(Parameterized.class) public class StaggeredGridLayoutManagerSnappingTest extends BaseStaggeredGridLayoutManagerTest { final Config mConfig; final boolean mReverseScroll; public StaggeredGridLayoutManagerSnappingTest(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; } @Test public void snapOnScrollSameViewFixedSize() throws Throwable { // This test is a special case for fixed sized children. final Config config = ((Config) mConfig.clone()).itemCount(10); setupByConfig(config); RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(1000, 950); mRecyclerView.setLayoutParams(lp); mAdapter.mOnBindCallback = new OnBindCallback() { @Override void onBoundItem(TestViewHolder vh, int position) { StaggeredGridLayoutManager.LayoutParams slp = getLayoutParamsForPosition(position); vh.itemView.setLayoutParams(slp); } @Override boolean assignRandomSize() { return false; } }; waitFirstLayout(); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); // This number comes from the sizes of the fixed views that are created for this config/ // See getLayoutParamsForPosition(int) below. Obtained manually. int scrollDistance = mLayoutManager.canScrollHorizontally() ? 52 : 52; int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(2); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); // Views have not changed View viewAfterScroll = findCenterView(mLayoutManager); assertSame("The view should NOT have scrolled", view, viewAfterScroll); assertCenterAligned(viewAfterScroll); } @Test public void snapOnScrollSameView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config); waitFirstLayout(); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); // For a staggered grid layout manager with unknown item size we need to keep the distance // small enough to ensure we do not scroll over to an offset view in a different span. int scrollDistance = findMinSafeScrollDistance(); int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; mLayoutManager.expectIdleState(2); smoothScrollBy(scrollDist); mLayoutManager.waitForSnap(10); // Views have not changed View viewAfterScroll = findCenterView(mLayoutManager); assertSame("The view should NOT have scrolled", view, viewAfterScroll); assertCenterAligned(viewAfterScroll); } @Test public void snapOnScrollNextItem() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config); waitFirstLayout(); setupSnapHelper(); // Record the current center view. View view = findCenterView(mLayoutManager); assertCenterAligned(view); int scrollDistance = getViewDimension(view) + 1; int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance; smoothScrollBy(scrollDist); waitForIdleScroll(mRecyclerView); waitForIdleScroll(mRecyclerView); View viewAfterScroll = findCenterView(mLayoutManager); assertNotSame("The view should have scrolled", view, viewAfterScroll); assertCenterAligned(viewAfterScroll); } @Test public void snapOnFlingSameView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config); waitFirstLayout(); 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); } @Test public void snapOnFlingNextView() throws Throwable { final Config config = (Config) mConfig.clone(); setupByConfig(config); waitFirstLayout(); 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 StaggeredGridLayoutManager.LayoutParams getLayoutParamsForPosition(int position) { // Only enabled fixed sizes if the config says so. if (mLayoutManager.canScrollHorizontally()) { int width = 400 + position * 70; return new StaggeredGridLayoutManager.LayoutParams(width, 300); } else { int height = 300 + position * 70; return new StaggeredGridLayoutManager.LayoutParams(300, height); } } @Nullable View findCenterView(RecyclerView.LayoutManager layoutManager) { return mLayoutManager.findFirstVisibleItemClosestToCenter(); } 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); } 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 findMinSafeScrollDistance() { int minDist = Integer.MAX_VALUE; for (int i = mLayoutManager.getChildCount() - 1; i >= 0; i--) { final View child = mLayoutManager.getChildAt(i); int dist = distFromCenter(child); if (dist < minDist) { minDist = dist; } } return minDist / 2 - 1; } 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; } }