Add PagerSnapHelper to enable ViewPager-like behavior.

- Adds PagerSnapHelper.
- Adds a simple PagerRecyclerView example
- Adds snap toggle to LinearLayoutManagerActivity.

Test: Added PagerSnapHelperTest
Change-Id: Ia0f3b59b9c40fdbc4646d89feb5337d43d570916
diff --git a/api/current.txt b/api/current.txt
index 7b2416c5..1597af1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9857,6 +9857,13 @@
     field protected final android.support.v7.widget.RecyclerView.LayoutManager mLayoutManager;
   }
 
+  public class PagerSnapHelper extends android.support.v7.widget.SnapHelper {
+    ctor public PagerSnapHelper();
+    method public int[] calculateDistanceToFinalSnap(android.support.v7.widget.RecyclerView.LayoutManager, android.view.View);
+    method public android.view.View findSnapView(android.support.v7.widget.RecyclerView.LayoutManager);
+    method public int findTargetSnapPosition(android.support.v7.widget.RecyclerView.LayoutManager, int, int);
+  }
+
   public class PopupMenu {
     ctor public PopupMenu(android.content.Context, android.view.View);
     ctor public PopupMenu(android.content.Context, android.view.View, int);
diff --git a/samples/Support7Demos/AndroidManifest.xml b/samples/Support7Demos/AndroidManifest.xml
index aac5a6d..0e9005a 100644
--- a/samples/Support7Demos/AndroidManifest.xml
+++ b/samples/Support7Demos/AndroidManifest.xml
@@ -370,6 +370,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.PagerRecyclerViewActivity"
+                  android:label="@string/pager_recycler_view"
+                  android:theme="@style/Theme.AppCompat">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv7.SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
         <activity android:name=".widget.AnimatedRecyclerView"
                   android:label="@string/animated_recycler_view"
                   android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/res/values/strings.xml b/samples/Support7Demos/res/values/strings.xml
index afb37b0..10f6761 100644
--- a/samples/Support7Demos/res/values/strings.xml
+++ b/samples/Support7Demos/res/values/strings.xml
@@ -132,6 +132,7 @@
     <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
 
     <string name="recycler_view">RecyclerView/RecyclerViewActivity</string>
+    <string name="pager_recycler_view">RecyclerView/PagerRecyclerViewActivity</string>
     <string name="animated_recycler_view">RecyclerView/Animated RecyclerView</string>
     <string name="nested_recycler_view">RecyclerView/Nested RecyclerView</string>
     <string name="linear_layout_manager">RecyclerView/Linear Layout Manager</string>
@@ -143,6 +144,7 @@
     <string name="checkbox_reverse">Rev.</string>
     <string name="checkbox_layout_dir">Layout Dir</string>
     <string name="checkbox_stack_from_end">Stack From End</string>
+    <string name="checkbox_snap">Snap</string>
     <string name="enableAnimations">Animate</string>
     <string name="enablePredictiveAnimations">Predictive</string>
     <string name="enableInPlaceChange">In Place Change</string>
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
index 31d53cc..184beaa 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/LinearLayoutManagerActivity.java
@@ -19,6 +19,7 @@
 import android.support.v4.view.ViewCompat;
 import android.support.v7.widget.DividerItemDecoration;
 import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.LinearSnapHelper;
 import android.support.v7.widget.RecyclerView;
 
 import com.example.android.supportv7.R;
@@ -29,6 +30,8 @@
  */
 public class LinearLayoutManagerActivity extends BaseLayoutManagerActivity<LinearLayoutManager> {
     private DividerItemDecoration mDividerItemDecoration;
+    private LinearSnapHelper mLinearSnapHelper;
+    private boolean mSnapHelperAttached;
 
     @Override
     protected LinearLayoutManager createLayoutManager() {
@@ -39,6 +42,7 @@
     protected void onRecyclerViewInit(RecyclerView recyclerView) {
         mDividerItemDecoration = new DividerItemDecoration(this, mLayoutManager.getOrientation());
         recyclerView.addItemDecoration(mDividerItemDecoration);
+        mLinearSnapHelper = new LinearSnapHelper();
     }
 
     @Override
@@ -94,6 +98,18 @@
                     public void onChange(boolean newValue) {
                         mLayoutManager.setStackFromEnd(newValue);
                     }
+                },
+                new ConfigToggle(this, R.string.checkbox_snap) {
+                    @Override
+                    public boolean isChecked() {
+                        return mSnapHelperAttached;
+                    }
+
+                    @Override
+                    public void onChange(boolean newValue) {
+                        mLinearSnapHelper.attachToRecyclerView(newValue ? mRecyclerView : null);
+                        mSnapHelperAttached = newValue;
+                    }
                 }
         };
     }
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java
new file mode 100644
index 0000000..bb0c248
--- /dev/null
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/PagerRecyclerViewActivity.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.example.android.supportv7.widget;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.PagerSnapHelper;
+import android.support.v7.widget.RecyclerView;
+import android.view.ViewGroup;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.widget.adapter.SimpleStringAdapter;
+
+/**
+ * Example activity that uses LinearLayoutManager, RecyclerView, and PagerSnapHelper.
+ */
+public class PagerRecyclerViewActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final RecyclerView rv = new RecyclerView(this);
+        final LinearLayoutManager manager =
+                new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
+        rv.setLayoutManager(manager);
+        rv.setHasFixedSize(true);
+        rv.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT));
+        rv.setAdapter(new SimpleStringAdapter(this, Cheeses.sCheeseStrings) {
+            @Override
+            public RecyclerView.LayoutParams getLayoutParams() {
+                return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT);
+            }
+        });
+        PagerSnapHelper snapHelper = new PagerSnapHelper();
+        snapHelper.attachToRecyclerView(rv);
+        setContentView(rv);
+    }
+}
diff --git a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
index 04161ea..f9c4ae4 100644
--- a/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
+++ b/samples/Support7Demos/src/com/example/android/supportv7/widget/adapter/SimpleStringAdapter.java
@@ -27,6 +27,9 @@
 import java.util.Collections;
 import java.util.List;
 
+/**
+ * A simple RecyclerView adapter that displays every string passed in a constructor as an item.
+ */
 public class SimpleStringAdapter extends RecyclerView.Adapter<SimpleStringAdapter.ViewHolder> {
 
     private int mBackground;
@@ -78,13 +81,7 @@
         h.mTextView.setPadding(20, 0, 20, 0);
         h.mTextView.setFocusable(true);
         h.mTextView.setBackgroundResource(mBackground);
-        RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
-                ViewGroup.LayoutParams.WRAP_CONTENT,
-                ViewGroup.LayoutParams.WRAP_CONTENT);
-        lp.leftMargin = 10;
-        lp.rightMargin = 5;
-        lp.topMargin = 20;
-        lp.bottomMargin = 15;
+        RecyclerView.LayoutParams lp = getLayoutParams();
         h.mTextView.setLayoutParams(lp);
         return h;
     }
@@ -97,6 +94,23 @@
         holder.mTextView.setBackgroundColor(getBackgroundColor(position));
     }
 
+
+    /**
+     * Returns LayoutParams to be used for each item in this adapter. It can be overridden
+     * to provide different LayoutParams.
+     * @return LayoutParams to be used for each item in this adapter.
+     */
+    public RecyclerView.LayoutParams getLayoutParams() {
+        RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+        lp.leftMargin = 10;
+        lp.rightMargin = 5;
+        lp.topMargin = 20;
+        lp.bottomMargin = 15;
+        return lp;
+    }
+
     private int getBackgroundColor(int position) {
         switch (position % 4) {
             case 0: return Color.BLACK;
diff --git a/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
new file mode 100644
index 0000000..169a1b1
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/PagerSnapHelper.java
@@ -0,0 +1,213 @@
+/*
+ * 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 android.graphics.PointF;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+
+/**
+ * Implementation of the {@link SnapHelper} supporting pager style snapping in either vertical or
+ * horizontal orientation.
+ */
+public class PagerSnapHelper extends SnapHelper {
+    // Orientation helpers are lazily created per LayoutManager.
+    @Nullable
+    private OrientationHelper mVerticalHelper;
+    @Nullable
+    private OrientationHelper mHorizontalHelper;
+
+    @Nullable
+    @Override
+    public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
+            @NonNull View targetView) {
+        int[] out = new int[2];
+        if (layoutManager.canScrollHorizontally()) {
+            out[0] = distanceToCenter(layoutManager, targetView,
+                    getHorizontalHelper(layoutManager));
+        } else {
+            out[0] = 0;
+        }
+
+        if (layoutManager.canScrollVertically()) {
+            out[1] = distanceToCenter(layoutManager, targetView,
+                    getVerticalHelper(layoutManager));
+        } else {
+            out[1] = 0;
+        }
+        return out;
+    }
+
+    @Nullable
+    @Override
+    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
+        if (layoutManager.canScrollVertically()) {
+            return findCenterView(layoutManager, getVerticalHelper(layoutManager));
+        } else if (layoutManager.canScrollHorizontally()) {
+            return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
+        }
+        return null;
+    }
+
+    @Override
+    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
+            int velocityY) {
+        final int itemCount = layoutManager.getItemCount();
+        if (itemCount == 0) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        View mStartMostChildView = null;
+        if (layoutManager.canScrollVertically()) {
+            mStartMostChildView = findStartView(layoutManager, getVerticalHelper(layoutManager));
+        } else if (layoutManager.canScrollHorizontally()) {
+            mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
+        }
+
+        if (mStartMostChildView == null) {
+            return RecyclerView.NO_POSITION;
+        }
+        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
+        if (centerPosition == RecyclerView.NO_POSITION) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final boolean forwardDirection;
+        if (layoutManager.canScrollHorizontally()) {
+            forwardDirection = velocityX > 0;
+        } else {
+            forwardDirection = velocityY > 0;
+        }
+        boolean reverseLayout = false;
+        if ((layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
+            RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
+                    (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
+            PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
+            if (vectorForEnd != null) {
+                reverseLayout = vectorForEnd.x < 0 || vectorForEnd.y < 0;
+            }
+        }
+        return reverseLayout
+                ? (forwardDirection ? centerPosition - 1 : centerPosition)
+                : (forwardDirection ? centerPosition + 1 : centerPosition);
+    }
+
+    private int distanceToCenter(@NonNull RecyclerView.LayoutManager layoutManager,
+            @NonNull View targetView, OrientationHelper helper) {
+        final int childCenter = helper.getDecoratedStart(targetView)
+                + (helper.getDecoratedMeasurement(targetView) / 2);
+        final int containerCenter;
+        if (layoutManager.getClipToPadding()) {
+            containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
+        } else {
+            containerCenter = helper.getEnd() / 2;
+        }
+        return childCenter - containerCenter;
+    }
+
+    /**
+     * Return the child view that is currently closest to the center of this parent.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
+     *
+     * @return the child view that is currently closest to the center of this parent.
+     */
+    @Nullable
+    private View findCenterView(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+
+        View closestChild = null;
+        final int center;
+        if (layoutManager.getClipToPadding()) {
+            center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
+        } else {
+            center = helper.getEnd() / 2;
+        }
+        int absClosest = Integer.MAX_VALUE;
+
+        for (int i = 0; i < childCount; i++) {
+            final View child = layoutManager.getChildAt(i);
+            int childCenter = helper.getDecoratedStart(child)
+                    + (helper.getDecoratedMeasurement(child) / 2);
+            int absDistance = Math.abs(childCenter - center);
+
+            /** if child center is closer than previous closest, set it as closest  **/
+            if (absDistance < absClosest) {
+                absClosest = absDistance;
+                closestChild = child;
+            }
+        }
+        return closestChild;
+    }
+
+    /**
+     * Return the child view that is currently closest to the start of this parent.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param helper The relevant {@link OrientationHelper} for the attached {@link RecyclerView}.
+     *
+     * @return the child view that is currently closest to the start of this parent.
+     */
+    @Nullable
+    private View findStartView(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper) {
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return null;
+        }
+
+        View closestChild = null;
+        int startest = Integer.MAX_VALUE;
+
+        for (int i = 0; i < childCount; i++) {
+            final View child = layoutManager.getChildAt(i);
+            int childStart = helper.getDecoratedStart(child);
+
+            /** if child is more to start than previous closest, set it as closest  **/
+            if (childStart < startest) {
+                startest = childStart;
+                closestChild = child;
+            }
+        }
+        return closestChild;
+    }
+
+    @NonNull
+    private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+        if (mVerticalHelper == null || mVerticalHelper.mLayoutManager != layoutManager) {
+            mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+        }
+        return mVerticalHelper;
+    }
+
+    @NonNull
+    private OrientationHelper getHorizontalHelper(
+            @NonNull RecyclerView.LayoutManager layoutManager) {
+        if (mHorizontalHelper == null || mHorizontalHelper.mLayoutManager != layoutManager) {
+            mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+        }
+        return mHorizontalHelper;
+    }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
index 37197e4..0d15d91 100644
--- a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
@@ -19,11 +19,11 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
 import android.util.DisplayMetrics;
 import android.view.View;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.Scroller;
-import android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider;
 
 /**
  * Class intended to support snapping for a {@link RecyclerView}.
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java
new file mode 100644
index 0000000..0799d3d7
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/PagerSnapHelperTest.java
@@ -0,0 +1,289 @@
+/*
+ * 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 android.view.ViewGroup;
+import android.widget.TextView;
+
+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 PagerSnapHelperTest extends BaseLinearLayoutManagerTest {
+
+    final Config mConfig;
+    final boolean mReverseScroll;
+
+    public PagerSnapHelperTest(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[] {false, true}) {
+                result.add(new Object[]{config, reverseScroll});
+            }
+        }
+        return result;
+    }
+
+    @MediumTest
+    @Test
+    public void snapOnScrollSameView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        setupByConfig(config, true,
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT),
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        setupSnapHelper();
+
+        // Record the current center view.
+        TextView view = (TextView) findCenterView(mLayoutManager);
+        assertCenterAligned(view);
+
+        int scrollDistance = (getViewDimension(view) / 2) - 1;
+        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+        mLayoutManager.expectIdleState(3);
+        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,
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT),
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        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(3);
+        smoothScrollBy(scrollDist);
+        mLayoutManager.waitForSnap(10);
+
+        // Views have not changed
+        View viewAfterFling = findCenterView(mLayoutManager);
+        assertNotSame("The view should have scrolled", view, viewAfterFling);
+        int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
+                ? (mReverseScroll ? 1 : -1)
+                : (mReverseScroll ? -1 : 1));
+        assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
+        assertCenterAligned(viewAfterFling);
+    }
+
+    @MediumTest
+    @Test
+    public void snapOnFlingSameView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        setupByConfig(config, true,
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT),
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        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);
+        // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
+            }
+        });
+        waitForIdleScroll(mRecyclerView);
+        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,
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT),
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        setupSnapHelper();
+        runSnapOnMaxFlingNextView((int) (0.2 * mRecyclerView.getMaxFlingVelocity()));
+    }
+
+    @MediumTest
+    @Test
+    public void snapOnMaxFlingNextView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        setupByConfig(config, true,
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT),
+                new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        setupSnapHelper();
+        runSnapOnMaxFlingNextView(mRecyclerView.getMaxFlingVelocity());
+    }
+
+    private void runSnapOnMaxFlingNextView(int velocity) throws Throwable {
+        // Record the current center view.
+        View view = findCenterView(mLayoutManager);
+        assertCenterAligned(view);
+
+        int velocityDir = mReverseScroll ? -velocity : velocity;
+        mLayoutManager.expectIdleState(1);
+
+        // Scroll at one pixel in the correct direction to allow fling snapping to the next view.
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mRecyclerView.scrollBy(mReverseScroll ? -1 : 1, mReverseScroll ? -1 : 1);
+            }
+        });
+        waitForIdleScroll(mRecyclerView);
+        assertTrue(fling(velocityDir, velocityDir));
+        mLayoutManager.waitForSnap(100);
+        getInstrumentation().waitForIdleSync();
+
+        View viewAfterFling = findCenterView(mLayoutManager);
+
+        assertNotSame("The view should have scrolled", view, viewAfterFling);
+        int expectedPosition = mConfig.mItemCount / 2 + (mConfig.mReverseLayout
+                ? (mReverseScroll ? 1 : -1)
+                : (mReverseScroll ? -1 : 1));
+        assertEquals(expectedPosition, mLayoutManager.getPosition(viewAfterFling));
+        assertCenterAligned(viewAfterFling);
+    }
+
+    private void setupSnapHelper() throws Throwable {
+        SnapHelper snapHelper = new PagerSnapHelper();
+        mLayoutManager.expectIdleState(1);
+        snapHelper.attachToRecyclerView(mRecyclerView);
+
+        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;
+    }
+}