Add an OnFlingListener to the RecyclerView.

This enable custom fling implementations and
is used to implement expose a snapping API
to developers.

Bug: 28169210
Change-Id: I09ffcab384eb69338df1fcc5f8d1d2a0d1191a57
diff --git a/api/current.txt b/api/current.txt
index aa64e15..eb3837a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9241,7 +9241,7 @@
     field public float weight;
   }
 
-  public class LinearLayoutManager extends android.support.v7.widget.RecyclerView.LayoutManager implements android.support.v7.widget.helper.ItemTouchHelper.ViewDropHandler {
+  public class LinearLayoutManager extends android.support.v7.widget.RecyclerView.LayoutManager implements android.support.v7.widget.helper.ItemTouchHelper.ViewDropHandler android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
     ctor public LinearLayoutManager(android.content.Context);
     ctor public LinearLayoutManager(android.content.Context, int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet, int, int);
@@ -9277,7 +9277,7 @@
     field public boolean mIgnoreConsumed;
   }
 
-  public abstract class LinearSmoothScroller extends android.support.v7.widget.RecyclerView.SmoothScroller {
+  public class LinearSmoothScroller extends android.support.v7.widget.RecyclerView.SmoothScroller {
     ctor public LinearSmoothScroller(android.content.Context);
     method public int calculateDtToFit(int, int, int, int, int);
     method public int calculateDxToMakeVisible(android.view.View, int);
@@ -9285,7 +9285,7 @@
     method protected float calculateSpeedPerPixel(android.util.DisplayMetrics);
     method protected int calculateTimeForDeceleration(int);
     method protected int calculateTimeForScrolling(int);
-    method public abstract android.graphics.PointF computeScrollVectorForPosition(int);
+    method public android.graphics.PointF computeScrollVectorForPosition(int);
     method protected int getHorizontalSnapPreference();
     method protected int getVerticalSnapPreference();
     method protected void onSeekTargetStep(int, int, android.support.v7.widget.RecyclerView.State, android.support.v7.widget.RecyclerView.SmoothScroller.Action);
@@ -9303,6 +9303,13 @@
     field protected android.graphics.PointF mTargetVector;
   }
 
+  public class LinearSnapHelper extends android.support.v7.widget.SnapHelper {
+    ctor public LinearSnapHelper();
+    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 ListPopupWindow {
     ctor public ListPopupWindow(android.content.Context);
     ctor public ListPopupWindow(android.content.Context, android.util.AttributeSet);
@@ -9452,6 +9459,7 @@
     method public android.support.v7.widget.RecyclerView.LayoutManager getLayoutManager();
     method public int getMaxFlingVelocity();
     method public int getMinFlingVelocity();
+    method public android.support.v7.widget.RecyclerView.OnFlingListener getOnFlingListener();
     method public boolean getPreserveFocusAfterLayout();
     method public android.support.v7.widget.RecyclerView.RecycledViewPool getRecycledViewPool();
     method public int getScrollState();
@@ -9482,6 +9490,7 @@
     method public void setItemViewCacheSize(int);
     method public void setLayoutFrozen(boolean);
     method public void setLayoutManager(android.support.v7.widget.RecyclerView.LayoutManager);
+    method public void setOnFlingListener(android.support.v7.widget.RecyclerView.OnFlingListener);
     method public deprecated void setOnScrollListener(android.support.v7.widget.RecyclerView.OnScrollListener);
     method public void setPreserveFocusAfterLayout(boolean);
     method public void setRecycledViewPool(android.support.v7.widget.RecyclerView.RecycledViewPool);
@@ -9782,6 +9791,11 @@
     method public abstract void onChildViewDetachedFromWindow(android.view.View);
   }
 
+  public static abstract class RecyclerView.OnFlingListener {
+    ctor public RecyclerView.OnFlingListener();
+    method public abstract boolean onFling(int, int);
+  }
+
   public static abstract interface RecyclerView.OnItemTouchListener {
     method public abstract boolean onInterceptTouchEvent(android.support.v7.widget.RecyclerView, android.view.MotionEvent);
     method public abstract void onRequestDisallowInterceptTouchEvent(boolean);
@@ -9861,6 +9875,10 @@
     field public static final int UNDEFINED_DURATION = -2147483648; // 0x80000000
   }
 
+  public static abstract interface RecyclerView.SmoothScroller.ScrollVectorProvider {
+    method public abstract android.graphics.PointF computeScrollVectorForPosition(int);
+  }
+
   public static class RecyclerView.State {
     ctor public RecyclerView.State();
     method public boolean didStructureChange();
@@ -9990,15 +10008,26 @@
     method public void setSupportsChangeAnimations(boolean);
   }
 
+  public abstract class SnapHelper extends android.support.v7.widget.RecyclerView.OnFlingListener {
+    ctor public SnapHelper();
+    method public void attachToRecyclerView(android.support.v7.widget.RecyclerView) throws java.lang.IllegalStateException;
+    method public abstract int[] calculateDistanceToFinalSnap(android.support.v7.widget.RecyclerView.LayoutManager, android.view.View);
+    method public int[] calculateScrollDistance(int, int);
+    method public abstract android.view.View findSnapView(android.support.v7.widget.RecyclerView.LayoutManager);
+    method public abstract int findTargetSnapPosition(android.support.v7.widget.RecyclerView.LayoutManager, int, int);
+    method public boolean onFling(int, int);
+  }
+
   public final deprecated class Space extends android.support.v4.widget.Space {
     ctor public Space(android.content.Context);
     ctor public Space(android.content.Context, android.util.AttributeSet);
     ctor public Space(android.content.Context, android.util.AttributeSet, int);
   }
 
-  public class StaggeredGridLayoutManager extends android.support.v7.widget.RecyclerView.LayoutManager {
+  public class StaggeredGridLayoutManager extends android.support.v7.widget.RecyclerView.LayoutManager implements android.support.v7.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
     ctor public StaggeredGridLayoutManager(android.content.Context, android.util.AttributeSet, int, int);
     ctor public StaggeredGridLayoutManager(int, int);
+    method public android.graphics.PointF computeScrollVectorForPosition(int);
     method public int[] findFirstCompletelyVisibleItemPositions(int[]);
     method public int[] findFirstVisibleItemPositions(int[]);
     method public int[] findLastCompletelyVisibleItemPositions(int[]);
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index ddc9bad..848d910 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -40,7 +40,7 @@
  * similar functionality to {@link android.widget.ListView}.
  */
 public class LinearLayoutManager extends RecyclerView.LayoutManager implements
-        ItemTouchHelper.ViewDropHandler {
+        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
 
     private static final String TAG = "LinearLayoutManager";
 
@@ -424,17 +424,12 @@
     public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
             int position) {
         LinearSmoothScroller linearSmoothScroller =
-                new LinearSmoothScroller(recyclerView.getContext()) {
-                    @Override
-                    public PointF computeScrollVectorForPosition(int targetPosition) {
-                        return LinearLayoutManager.this
-                                .computeScrollVectorForPosition(targetPosition);
-                    }
-                };
+                new LinearSmoothScroller(recyclerView.getContext());
         linearSmoothScroller.setTargetPosition(position);
         startSmoothScroll(linearSmoothScroller);
     }
 
+    @Override
     public PointF computeScrollVectorForPosition(int targetPosition) {
         if (getChildCount() == 0) {
             return null;
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
index cddcf11..78250c1 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
@@ -18,6 +18,7 @@
 
 import android.content.Context;
 import android.graphics.PointF;
+import android.support.annotation.Nullable;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.View;
@@ -25,12 +26,16 @@
 import android.view.animation.LinearInterpolator;
 
 /**
- * {@link RecyclerView.SmoothScroller} implementation which uses
- * {@link android.view.animation.LinearInterpolator} until the target position becomes a child of
- * the RecyclerView and then uses
- * {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
+ * {@link RecyclerView.SmoothScroller} implementation which uses a {@link LinearInterpolator} until
+ * the target position becomes a child of the RecyclerView and then uses a
+ * {@link DecelerateInterpolator} to slowly approach to target position.
+ * <p>
+ * If the {@link RecyclerView.LayoutManager} you are using does not implement the
+ * {@link RecyclerView.SmoothScroller.ScrollVectorProvider} interface, then you must override the
+ * {@link #computeScrollVectorForPosition(int)} method. All the LayoutManagers bundled with
+ * the support library implement this interface.
  */
-abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
+public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
 
     private static final String TAG = "LinearSmoothScroller";
 
@@ -226,9 +231,6 @@
         // find an interim target position
         PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
         if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
-            Log.e(TAG, "To support smooth scrolling, you should override \n"
-                    + "LayoutManager#computeScrollVectorForPosition.\n"
-                    + "Falling back to instant scroll");
             final int target = getTargetPosition();
             action.jumpTo(target);
             stop();
@@ -335,5 +337,25 @@
         return calculateDtToFit(left, right, start, end, snapPreference);
     }
 
-    abstract public PointF computeScrollVectorForPosition(int targetPosition);
+    /**
+     * Compute the scroll vector for a given target position.
+     * <p>
+     * This method can return null if the layout manager cannot calculate a scroll vector
+     * for the given position (e.g. it has no current scroll position).
+     *
+     * @param targetPosition the position to which the scroller is scrolling
+     *
+     * @return the scroll vector for a given target position
+     */
+    @Nullable
+    public PointF computeScrollVectorForPosition(int targetPosition) {
+        RecyclerView.LayoutManager layoutManager = getLayoutManager();
+        if (layoutManager instanceof ScrollVectorProvider) {
+            return ((ScrollVectorProvider) layoutManager)
+                    .computeScrollVectorForPosition(targetPosition);
+        }
+        Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager" +
+                " does not implement " + ScrollVectorProvider.class.getCanonicalName());
+        return null;
+    }
 }
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
new file mode 100644
index 0000000..4b37c68
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
@@ -0,0 +1,286 @@
+/*
+ * 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 languag`e 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 snapping in either vertical or horizontal
+ * orientation.
+ * <p>
+ * The implementation will snap the center of the target child view to the center of
+ * the attached {@link RecyclerView}. If you intend to change this behavior then override
+ * {@link SnapHelper#calculateDistanceToFinalSnap}.
+ */
+public class LinearSnapHelper extends SnapHelper {
+
+    private static final float INVALID_DISTANCE = 1f;
+
+    // Orientation helpers are lazily created per LayoutManager.
+    @Nullable
+    private OrientationHelper mVerticalHelper;
+    @Nullable
+    private OrientationHelper mHorizontalHelper;
+
+    @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;
+    }
+
+    @Override
+    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
+            int velocityY) {
+        if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final int itemCount = layoutManager.getItemCount();
+        if (itemCount == 0) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final View currentView = findSnapView(layoutManager);
+        if (currentView == null) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        final int currentPosition = layoutManager.getPosition(currentView);
+        if (currentPosition == RecyclerView.NO_POSITION) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
+                (RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
+        // deltaJumps sign comes from the velocity which may not match the order of children in
+        // the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
+        // get the direction.
+        PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
+        if (vectorForEnd == null) {
+            // cannot get a vector for the given position.
+            return RecyclerView.NO_POSITION;
+        }
+
+        int vDeltaJump, hDeltaJump;
+        if (layoutManager.canScrollHorizontally()) {
+            hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
+                    getHorizontalHelper(layoutManager), velocityX, 0);
+            if (vectorForEnd.x < 0) {
+                hDeltaJump = -hDeltaJump;
+            }
+        } else {
+            hDeltaJump = 0;
+        }
+        if (layoutManager.canScrollVertically()) {
+            vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
+                    getVerticalHelper(layoutManager), 0, velocityY);
+            if (vectorForEnd.y < 0) {
+                vDeltaJump = -vDeltaJump;
+            }
+        } else {
+            vDeltaJump = 0;
+        }
+
+        int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
+        if (deltaJump == 0) {
+            return RecyclerView.NO_POSITION;
+        }
+
+        int targetPos = currentPosition + deltaJump;
+        if (targetPos < 0) {
+            targetPos = 0;
+        }
+        if (targetPos >= itemCount) {
+            targetPos = itemCount - 1;
+        }
+        return targetPos;
+    }
+
+    @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;
+    }
+
+    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;
+    }
+
+    /**
+     * Estimates a position to which SnapHelper will try to scroll to in response to a fling.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param helper        The {@link OrientationHelper} that is created from the LayoutManager.
+     * @param velocityX     The velocity on the x axis.
+     * @param velocityY     The velocity on the y axis.
+     *
+     * @return The diff between the target scroll position and the current position.
+     */
+    private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper, int velocityX, int velocityY) {
+        int[] distances = calculateScrollDistance(velocityX, velocityY);
+        float distancePerChild = computeDistancePerChild(layoutManager, helper);
+        if (distancePerChild <= 0) {
+            return 0;
+        }
+        int distance =
+                Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
+        return (int) Math.floor(distance / distancePerChild);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Computes an average pixel value to pass a single child.
+     * <p>
+     * Returns a negative value if it cannot be calculated.
+     *
+     * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param helper        The relevant {@link OrientationHelper} for the attached
+     *                      {@link RecyclerView.LayoutManager}.
+     *
+     * @return A float value that is the average number of pixels needed to scroll by one view in
+     * the relevant direction.
+     */
+    private float computeDistancePerChild(RecyclerView.LayoutManager layoutManager,
+            OrientationHelper helper) {
+        View minPosView = null;
+        View maxPosView = null;
+        int minPos = Integer.MAX_VALUE;
+        int maxPos = Integer.MIN_VALUE;
+        int childCount = layoutManager.getChildCount();
+        if (childCount == 0) {
+            return INVALID_DISTANCE;
+        }
+
+        for (int i = 0; i < childCount; i++) {
+            View child = layoutManager.getChildAt(i);
+            final int pos = layoutManager.getPosition(child);
+            if (pos == RecyclerView.NO_POSITION) {
+                continue;
+            }
+            if (pos < minPos) {
+                minPos = pos;
+                minPosView = child;
+            }
+            if (pos > maxPos) {
+                maxPos = pos;
+                maxPosView = child;
+            }
+        }
+        if (minPosView == null || maxPosView == null) {
+            return INVALID_DISTANCE;
+        }
+        int start = Math.min(helper.getDecoratedStart(minPosView),
+                helper.getDecoratedStart(maxPosView));
+        int end = Math.max(helper.getDecoratedEnd(minPosView),
+                helper.getDecoratedEnd(maxPosView));
+        int distance = end - start;
+        if (distance == 0) {
+            return INVALID_DISTANCE;
+        }
+        return 1f * distance / ((maxPos - minPos) + 1);
+    }
+
+    @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/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 79f3030..afde49c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -394,6 +394,7 @@
     private int mLastTouchX;
     private int mLastTouchY;
     private int mTouchSlop;
+    private OnFlingListener mOnFlingListener;
     private final int mMinFlingVelocity;
     private final int mMaxFlingVelocity;
     // This value is used when handling generic motion events.
@@ -1094,6 +1095,28 @@
         requestLayout();
     }
 
+    /**
+     * Set a {@link OnFlingListener} for this {@link RecyclerView}.
+     * <p>
+     * If the {@link OnFlingListener} is set then it will receive
+     * calls to {@link #fling(int,int)} and will be able to intercept them.
+     *
+     * @param onFlingListener The {@link OnFlingListener} instance.
+     */
+    public void setOnFlingListener(@Nullable OnFlingListener onFlingListener) {
+        mOnFlingListener = onFlingListener;
+    }
+
+    /**
+     * Get the current {@link OnFlingListener} from this {@link RecyclerView}.
+     *
+     * @return The {@link OnFlingListener} instance currently set (can be null).
+     */
+    @Nullable
+    public OnFlingListener getOnFlingListener() {
+        return mOnFlingListener;
+    }
+
     @Override
     protected Parcelable onSaveInstanceState() {
         SavedState state = new SavedState(super.onSaveInstanceState());
@@ -1912,6 +1935,10 @@
             final boolean canScroll = canScrollHorizontal || canScrollVertical;
             dispatchNestedFling(velocityX, velocityY, canScroll);
 
+            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
+                return true;
+            }
+
             if (canScroll) {
                 velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
                 velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
@@ -10375,6 +10402,30 @@
                 changed = true;
             }
         }
+
+        /**
+         * An interface which is optionally implemented by custom {@link RecyclerView.LayoutManager}
+         * to provide a hint to a {@link SmoothScroller} about the location of the target position.
+         */
+        public interface ScrollVectorProvider {
+            /**
+             * Should calculate the vector that points to the direction where the target position
+             * can be found.
+             * <p>
+             * This method is used by the {@link LinearSmoothScroller} to initiate a scroll towards
+             * the target position.
+             * <p>
+             * The magnitude of the vector is not important. It is always normalized before being
+             * used by the {@link LinearSmoothScroller}.
+             * <p>
+             * LayoutManager should not check whether the position exists in the adapter or not.
+             *
+             * @param targetPosition the target position to which the returned vector should point
+             *
+             * @return the scroll vector for a given position.
+             */
+            PointF computeScrollVectorForPosition(int targetPosition);
+        }
     }
 
     static class AdapterDataObservable extends Observable<AdapterDataObserver> {
@@ -10732,6 +10783,28 @@
     }
 
     /**
+     * This class defines the behavior of fling if the developer wishes to handle it.
+     * <p>
+     * Subclasses of {@link OnFlingListener} can be used to implement custom fling behavior.
+     *
+     * @see #setOnFlingListener(OnFlingListener)
+     */
+    public static abstract class OnFlingListener {
+
+        /**
+         * Override this to handle a fling given the velocities in both x and y directions.
+         * Note that this method will only be called if the associated {@link LayoutManager}
+         * supports scrolling and the fling is not handled by nested scrolls first.
+         *
+         * @param velocityX the fling velocity on the X axis
+         * @param velocityY the fling velocity on the Y axis
+         *
+         * @return true if the fling washandled, false otherwise.
+         */
+        public abstract boolean onFling(int velocityX, int velocityY);
+    }
+
+    /**
      * Internal listener that manages items after animations finish. This is how items are
      * retained (not recycled) during animations, but allowed to be recycled afterwards.
      * It depends on the contract with the ItemAnimator to call the appropriate dispatch*Finished()
diff --git a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
new file mode 100644
index 0000000..a2c557d
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
@@ -0,0 +1,274 @@
+/*
+ * 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.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView.LayoutManager;
+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}.
+ * <p>
+ * SnapHelper tries to handle fling as well but for this to work properly, the
+ * {@link RecyclerView.LayoutManager} must implement the {@link ScrollVectorProvider} interface or
+ * you should override {@link #onFling(int, int)} and handle fling manually.
+ */
+public abstract class SnapHelper extends RecyclerView.OnFlingListener {
+
+    private static final float MILLISECONDS_PER_INCH = 100f;
+
+    private RecyclerView mRecyclerView;
+    private Scroller mGravityScroller;
+
+    // Handles the snap on scroll case.
+    private final RecyclerView.OnScrollListener mScrollListener =
+            new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                    super.onScrollStateChanged(recyclerView, newState);
+                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                        snapToTargetExistingView();
+                    }
+                }
+            };
+
+    @Override
+    public boolean onFling(int velocityX, int velocityY) {
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null) {
+            return false;
+        }
+        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
+        if (adapter == null) {
+            return false;
+        }
+        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
+        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
+                && snapFromFling(layoutManager, velocityX, velocityY);
+    }
+
+    /**
+     * Attaches the {@link SnapHelper} to the provided RecyclerView, by calling
+     * {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
+     * You can call this method with {@code null} to detach it from the current RecyclerView.
+     *
+     * @param recyclerView The RecyclerView instance to which you want to add this helper or
+     *                     {@code null} if you want to remove SnapHelper from the current
+     *                     RecyclerView.
+     *
+     * @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
+     * attached to the provided {@link RecyclerView}.
+     *
+     */
+    public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
+            throws IllegalStateException {
+        if (mRecyclerView == recyclerView) {
+            return; // nothing to do
+        }
+        if (mRecyclerView != null) {
+            destroyCallbacks();
+        }
+        mRecyclerView = recyclerView;
+        if (mRecyclerView != null) {
+            setupCallbacks();
+            mGravityScroller = new Scroller(mRecyclerView.getContext(),
+                    new DecelerateInterpolator());
+            snapToTargetExistingView();
+        }
+    }
+
+    /**
+     * Called when an instance of a {@link RecyclerView} is attached.
+     */
+    private void setupCallbacks() throws IllegalStateException {
+        if (mRecyclerView.getOnFlingListener() != null) {
+            throw new IllegalStateException("An instance of OnFlingListener already set.");
+        }
+        mRecyclerView.addOnScrollListener(mScrollListener);
+        mRecyclerView.setOnFlingListener(this);
+    }
+
+    /**
+     * Called when the instance of a {@link RecyclerView} is detached.
+     */
+    private void destroyCallbacks() {
+        mRecyclerView.removeOnScrollListener(mScrollListener);
+        mRecyclerView.setOnFlingListener(null);
+    }
+
+    /**
+     * Calculated the estimated scroll distance in each direction given velocities on both axes.
+     *
+     * @param velocityX     Fling velocity on the horizontal axis.
+     * @param velocityY     Fling velocity on the vertical axis.
+     *
+     * @return array holding the calculated distances in x and y directions
+     * respectively.
+     */
+    public int[] calculateScrollDistance(int velocityX, int velocityY) {
+        int[] outDist = new int[2];
+        mGravityScroller.fling(0, 0, velocityX, velocityY,
+                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+        outDist[0] = mGravityScroller.getFinalX();
+        outDist[1] = mGravityScroller.getFinalY();
+        return outDist;
+    }
+
+    /**
+     * Helper method to facilitate for snapping triggered by a fling.
+     *
+     * @param layoutManager The {@link LayoutManager} associated with the attached
+     *                      {@link RecyclerView}.
+     * @param velocityX     Fling velocity on the horizontal axis.
+     * @param velocityY     Fling velocity on the vertical axis.
+     *
+     * @return true if it is handled, false otherwise.
+     */
+    private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
+            int velocityY) {
+        if (!(layoutManager instanceof ScrollVectorProvider)) {
+            return false;
+        }
+
+        RecyclerView.SmoothScroller smoothScroller = createSnapScroller(layoutManager);
+        if (smoothScroller == null) {
+            return false;
+        }
+
+        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
+        if (targetPosition == RecyclerView.NO_POSITION) {
+            return false;
+        }
+
+        smoothScroller.setTargetPosition(targetPosition);
+        layoutManager.startSmoothScroll(smoothScroller);
+        return true;
+    }
+
+    /**
+     * Snaps to a target view which currently exists in the attached {@link RecyclerView}. This
+     * method is used to snap the view when the {@link RecyclerView} is first attached; when
+     * snapping was triggered by a scroll and when the fling is at its final stages.
+     */
+    private void snapToTargetExistingView() {
+        if (mRecyclerView == null) {
+            return;
+        }
+        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+        if (layoutManager == null) {
+            return;
+        }
+        View snapView = findSnapView(layoutManager);
+        if (snapView == null) {
+            return;
+        }
+        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
+        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
+            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
+        }
+    }
+
+    /**
+     * Creates a scroller to be used in the snapping implementation.
+     *
+     * @param layoutManager     The {@link RecyclerView.LayoutManager} associated with the attached
+     *                          {@link RecyclerView}.
+     *
+     * @return a {@link LinearSmoothScroller} which will handle the scrolling.
+     */
+    @Nullable
+    private LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
+        if (!(layoutManager instanceof ScrollVectorProvider)) {
+            return null;
+        }
+        return new LinearSmoothScroller(mRecyclerView.getContext()) {
+            @Override
+            protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+                int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
+                        targetView);
+                final int dx = snapDistances[0];
+                final int dy = snapDistances[1];
+                final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
+                if (time > 0) {
+                    action.update(dx, dy, time, mDecelerateInterpolator);
+                }
+            }
+
+            @Override
+            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+                return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+            }
+        };
+    }
+
+    /**
+     * Override this method to snap to a particular point within the target view or the container
+     * view on any axis.
+     * <p>
+     * This method is called when the {@link SnapHelper} has intercepted a fling and it needs
+     * to know the exact distance required to scroll by in order to snap to the target view.
+     *
+     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}
+     * @param targetView the target view that is chosen as the view to snap
+     *
+     * @return the output coordinates the put the result into. out[0] is the distance
+     * on horizontal axis and out[1] is the distance on vertical axis.
+     */
+    @SuppressWarnings("WeakerAccess")
+    @Nullable
+    public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager,
+            @NonNull View targetView);
+
+    /**
+     * Override this method to provide a particular target view for snapping.
+     * <p>
+     * This method is called when the {@link SnapHelper} is ready to start snapping and requires
+     * a target view to snap to. It will be explicitly called when the scroll state becomes idle
+     * after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
+     * after a fling and requires a reference view from the current set of child views.
+     * <p>
+     * If this method returns {@code null}, SnapHelper will not snap to any view.
+     *
+     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}
+     *
+     * @return the target view to which to snap on fling or end of scroll
+     */
+    @SuppressWarnings("WeakerAccess")
+    @Nullable
+    public abstract View findSnapView(LayoutManager layoutManager);
+
+    /**
+     * Override to provide a particular adapter target position for snapping.
+     *
+     * @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
+     *                      {@link RecyclerView}
+     * @param velocityX fling velocity on the horizontal axis
+     * @param velocityY fling velocity on the vertical axis
+     *
+     * @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
+     *         if no snapping should happen
+     */
+    public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX,
+            int velocityY);
+}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 380c2ab..12745fe 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -52,7 +52,8 @@
  * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
  * control this behavior via {@link #setGapStrategy(int)}.
  */
-public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager {
+public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
+        RecyclerView.SmoothScroller.ScrollVectorProvider {
 
     private static final String TAG = "StaggeredGridLayoutManager";
 
@@ -1996,22 +1997,26 @@
     }
 
     @Override
+    public PointF computeScrollVectorForPosition(int targetPosition) {
+        final int direction = calculateScrollDirectionForPosition(targetPosition);
+        PointF outVector = new PointF();
+        if (direction == 0) {
+            return null;
+        }
+        if (mOrientation == HORIZONTAL) {
+            outVector.x = direction;
+            outVector.y = 0;
+        } else {
+            outVector.x = 0;
+            outVector.y = direction;
+        }
+        return outVector;
+    }
+
+    @Override
     public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
             int position) {
-        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
-            @Override
-            public PointF computeScrollVectorForPosition(int targetPosition) {
-                final int direction = calculateScrollDirectionForPosition(targetPosition);
-                if (direction == 0) {
-                    return null;
-                }
-                if (mOrientation == HORIZONTAL) {
-                    return new PointF(direction, 0);
-                } else {
-                    return new PointF(0, direction);
-                }
-            }
-        };
+        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
         scroller.setTargetPosition(position);
         startSmoothScroll(scroller);
     }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
index aa1be7b..34b6621 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -18,6 +18,7 @@
 import android.content.Context;
 import android.view.View;
 
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -67,6 +68,21 @@
         return variations;
     }
 
+    protected static List<Config> addConfigVariation(List<Config> base, String fieldName,
+            Object... variations)
+            throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
+        List<Config> newConfigs = new ArrayList<Config>();
+        Field field = Config.class.getDeclaredField(fieldName);
+        for (Config config : base) {
+            for (Object variation : variations) {
+                Config newConfig = (Config) config.clone();
+                field.set(newConfig, variation);
+                newConfigs.add(newConfig);
+            }
+        }
+        return newConfigs;
+    }
+
     public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
         mGlm.expectLayout(1);
         setRecyclerView(recyclerView);
@@ -137,6 +153,7 @@
                 mCallbacks = new ArrayList<GridLayoutManagerTest.Callback>();
 
         Boolean mFakeRTL;
+        private CountDownLatch snapLatch;
 
         public WrappedGridLayoutManager(Context context, int spanCount) {
             super(context, spanCount);
@@ -209,6 +226,35 @@
                 }
             });
         }
+
+        public void expectIdleState(int count) {
+            snapLatch = new CountDownLatch(count);
+            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                    super.onScrollStateChanged(recyclerView, newState);
+                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                        snapLatch.countDown();
+                        if (snapLatch.getCount() == 0L) {
+                            mRecyclerView.removeOnScrollListener(this);
+                        }
+                    }
+                }
+            });
+        }
+
+        public void waitForSnap(int seconds) throws Throwable {
+            snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
+            checkForMainThreadException();
+            MatcherAssert.assertThat("all scrolling should complete on time",
+                    snapLatch.getCount(), CoreMatchers.is(0L));
+            // use a runnable to ensure RV layout is finished
+            getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                }
+            });
+        }
     }
 
     class GridTestAdapter extends TestAdapter {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
index d2c4702..353e84e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
@@ -27,6 +27,7 @@
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.support.v4.util.Pair;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
@@ -258,7 +259,7 @@
 
     static class Config implements Cloneable {
 
-        static final int DEFAULT_ITEM_COUNT = 100;
+        static final int DEFAULT_ITEM_COUNT = 250;
 
         boolean mStackFromEnd;
 
@@ -342,6 +343,8 @@
 
         CountDownLatch layoutLatch;
 
+        CountDownLatch snapLatch;
+
         OrientationHelper mSecondaryOrientation;
 
         OnLayoutListener mOnLayoutListener;
@@ -367,6 +370,35 @@
             });
         }
 
+        public void expectIdleState(int count) {
+            snapLatch = new CountDownLatch(count);
+            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                    super.onScrollStateChanged(recyclerView, newState);
+                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                        snapLatch.countDown();
+                        if (snapLatch.getCount() == 0L) {
+                            mRecyclerView.removeOnScrollListener(this);
+                        }
+                    }
+                }
+            });
+        }
+
+        public void waitForSnap(int seconds) throws Throwable {
+            snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
+            checkForMainThreadException();
+            MatcherAssert.assertThat("all scrolling should complete on time",
+                    snapLatch.getCount(), CoreMatchers.is(0L));
+            // use a runnable to ensure RV layout is finished
+            getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                }
+            });
+        }
+
         @Override
         public void setOrientation(int orientation) {
             super.setOrientation(orientation);
@@ -544,7 +576,5 @@
             }
             layoutLatch.countDown();
         }
-
-
     }
 }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 7938a0d..e552f64 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -416,6 +416,25 @@
         }
     }
 
+    public void smoothScrollBy(final int dt) {
+        try {
+            runTestOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (mRecyclerView.getLayoutManager().canScrollHorizontally()) {
+                        mRecyclerView.smoothScrollBy(dt, 0);
+                    } else {
+                        mRecyclerView.smoothScrollBy(0, dt);
+                    }
+
+                }
+            });
+        } catch (Throwable throwable) {
+            Log.e(TAG, "", throwable);
+        }
+        getInstrumentation().waitForIdleSync();
+    }
+
     void scrollToPosition(final int position) throws Throwable {
         runTestOnUiThread(new Runnable() {
             @Override
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
index 0fea8d6..82f8cf0 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
@@ -475,6 +475,7 @@
         // until bug is fixed, we'll fake it.
         // public issue id: 57819
         Boolean mFakeRTL;
+        CountDownLatch snapLatch;
 
         @Override
         boolean isLayoutRTL() {
@@ -498,6 +499,35 @@
             });
         }
 
+        public void expectIdleState(int count) {
+            snapLatch = new CountDownLatch(count);
+            mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                @Override
+                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                    super.onScrollStateChanged(recyclerView, newState);
+                    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                        snapLatch.countDown();
+                        if (snapLatch.getCount() == 0L) {
+                            mRecyclerView.removeOnScrollListener(this);
+                        }
+                    }
+                }
+            });
+        }
+
+        public void waitForSnap(int seconds) throws Throwable {
+            snapLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
+            checkForMainThreadException();
+            MatcherAssert.assertThat("all scrolling should complete on time",
+                    snapLatch.getCount(), CoreMatchers.is(0L));
+            // use a runnable to ensure RV layout is finished
+            getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                }
+            });
+        }
+
         public void assertNoLayout(String msg, long timeout) throws Throwable {
             layoutLatch.await(timeout, TimeUnit.SECONDS);
             assertFalse(msg, layoutLatch.getCount() == 0);
@@ -540,6 +570,28 @@
             return 0;
         }
 
+        View findFirstVisibleItemClosestToCenter() {
+            final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
+            final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
+            final int boundsCenter = (boundsStart + boundsEnd) / 2;
+            final Rect childBounds = new Rect();
+            int minDist = Integer.MAX_VALUE;
+            View closestChild = null;
+            for (int i = getChildCount() - 1; i >= 0; i--) {
+                final View child = getChildAt(i);
+                childBounds.setEmpty();
+                getDecoratedBoundsWithMargins(child, childBounds);
+                int childCenter = canScrollHorizontally()
+                        ? childBounds.centerX() : childBounds.centerY();
+                int dist = Math.abs(boundsCenter - childCenter);
+                if (dist < minDist) {
+                    minDist = dist;
+                    closestChild = child;
+                }
+            }
+            return closestChild;
+        }
+
         public WrappedLayoutManager(int spanCount, int orientation) {
             super(spanCount, orientation);
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java
new file mode 100644
index 0000000..0ba8fed
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.support.annotation.Nullable;
+import android.test.suitebuilder.annotation.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;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class GridLayoutManagerSnappingTest extends BaseGridLayoutManagerTest {
+
+    final Config mConfig;
+    final boolean mReverseScroll;
+
+    public GridLayoutManagerSnappingTest(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 snapOnScrollSameView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        RecyclerView recyclerView = setupBasic(config);
+        waitForFirstLayout(recyclerView);
+        setupSnapHelper();
+
+        // Record the current center view.
+        View view = findCenterView(mGlm);
+        assertCenterAligned(view);
+        int scrollDistance = (getViewDimension(view) / 2) - 1;
+        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+        mGlm.expectIdleState(2);
+        smoothScrollBy(scrollDist);
+        mGlm.waitForSnap(10);
+
+        // Views have not changed
+        View viewAfterFling = findCenterView(mGlm);
+        assertSame("The view should have scrolled", view, viewAfterFling);
+        assertCenterAligned(viewAfterFling);
+    }
+
+    @Test
+    public void snapOnScrollNextItem() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        RecyclerView recyclerView = setupBasic(config);
+        waitForFirstLayout(recyclerView);
+        setupSnapHelper();
+
+        // Record the current center view.
+        View view = findCenterView(mGlm);
+        assertCenterAligned(view);
+        int scrollDistance = getViewDimension(view) + 1;
+        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+
+        smoothScrollBy(scrollDist);
+        waitForIdleScroll(mRecyclerView);
+        waitForIdleScroll(mRecyclerView);
+
+        View viewAfterScroll = findCenterView(mGlm);
+
+        assertNotSame("The view should have scrolled", view, viewAfterScroll);
+        assertCenterAligned(viewAfterScroll);
+    }
+
+    @MediumTest
+    @Test
+    public void snapOnFlingSameView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        RecyclerView recyclerView = setupBasic(config);
+        waitForFirstLayout(recyclerView);
+        setupSnapHelper();
+
+        // Record the current center view.
+        View view = findCenterView(mGlm);
+        assertCenterAligned(view);
+
+        // Velocity small enough to not scroll to the next view.
+        int velocity = (int) (1.000001 * mRecyclerView.getMinFlingVelocity());
+        int velocityDir = mReverseScroll ? -velocity : velocity;
+        mGlm.expectIdleState(2);
+        assertTrue(fling(velocityDir, velocityDir));
+        // Wait for two settling scrolls: the initial one and the corrective one.
+        waitForIdleScroll(mRecyclerView);
+        mGlm.waitForSnap(100);
+
+        View viewAfterFling = findCenterView(mGlm);
+
+        assertSame("The view should NOT have scrolled", view, viewAfterFling);
+        assertCenterAligned(viewAfterFling);
+    }
+
+
+    @MediumTest
+    @Test
+    public void snapOnFlingNextView() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        RecyclerView recyclerView = setupBasic(config);
+        waitForFirstLayout(recyclerView);
+        setupSnapHelper();
+
+        // Record the current center view.
+        View view = findCenterView(mGlm);
+        assertCenterAligned(view);
+
+        // Velocity high enough to scroll beyond the current view.
+        int velocity = (int) (0.2 * mRecyclerView.getMaxFlingVelocity());
+        int velocityDir = mReverseScroll ? -velocity : velocity;
+
+        mGlm.expectIdleState(1);
+        assertTrue(fling(velocityDir, velocityDir));
+        mGlm.waitForSnap(100);
+        getInstrumentation().waitForIdleSync();
+
+        View viewAfterFling = findCenterView(mGlm);
+
+        assertNotSame("The view should have scrolled", view, viewAfterFling);
+        assertCenterAligned(viewAfterFling);
+    }
+
+    private void setupSnapHelper() throws Throwable {
+        SnapHelper snapHelper = new LinearSnapHelper();
+        mGlm.expectIdleState(1);
+        snapHelper.attachToRecyclerView(mRecyclerView);
+        mGlm.waitForSnap(10);
+
+        mGlm.expectLayout(1);
+        scrollToPosition(mConfig.mItemCount / 2);
+        mGlm.waitForLayout(2);
+
+        View view = findCenterView(mGlm);
+        int scrollDistance = (getViewDimension(view) / 2) + 10;
+        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+
+        mGlm.expectIdleState(2);
+        smoothScrollBy(scrollDist);
+        mGlm.waitForSnap(10);
+    }
+
+    @Nullable 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 (mGlm.canScrollHorizontally()) {
+            helper = OrientationHelper.createHorizontalHelper(mGlm);
+        } else {
+            helper = OrientationHelper.createVerticalHelper(mGlm);
+        }
+        return helper.getDecoratedMeasurement(view);
+    }
+
+    private void assertCenterAligned(View view) {
+        if(mGlm.canScrollHorizontally()) {
+            assertEquals("The child should align with the center of the parent",
+                    mRecyclerView.getWidth() / 2,
+                    mGlm.getDecoratedLeft(view) +
+                            mGlm.getDecoratedMeasuredWidth(view) / 2);
+        } else {
+            assertEquals("The child should align with the center of the parent",
+                    mRecyclerView.getHeight() / 2,
+                    mGlm.getDecoratedTop(view) +
+                            mGlm.getDecoratedMeasuredHeight(view) / 2);
+        }
+    }
+
+    private boolean fling(final int velocityX, final int velocityY)
+            throws Throwable {
+        final AtomicBoolean didStart = new AtomicBoolean(false);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                boolean result = mRecyclerView.fling(velocityX, velocityY);
+                didStart.set(result);
+            }
+        });
+        if (!didStart.get()) {
+            return false;
+        }
+        waitForIdleScroll(mRecyclerView);
+        return true;
+    }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java
new file mode 100644
index 0000000..ae19fc1
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.support.annotation.Nullable;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+
+@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 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 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 = (getViewDimension(view) / 2) + 10;
+        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 boolean fling(final int velocityX, final int velocityY) throws Throwable {
+        final AtomicBoolean didStart = new AtomicBoolean(false);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                boolean result = mRecyclerView.fling(velocityX, velocityY);
+                didStart.set(result);
+            }
+        });
+        if (!didStart.get()) {
+            return false;
+        }
+        waitForIdleScroll(mRecyclerView);
+        return true;
+    }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 665236f..5398ceb 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -2193,8 +2193,11 @@
                 LinearSmoothScroller linearSmoothScroller =
                         new LinearSmoothScroller(recyclerView.getContext()) {
                             @Override
-                            public PointF computeScrollVectorForPosition(int targetPosition) {
-                                return new PointF(0, 1);
+                            public boolean computeScrollVectorForPosition(int targetPosition,
+                                    PointF endVector) {
+                                endVector.x = 0;
+                                endVector.y = 1;
+                                return true;
                             }
 
                             @Override
@@ -3972,8 +3975,9 @@
                 RecyclerView.SmoothScroller ss =
                         new LinearSmoothScroller(recyclerView.getContext()) {
                             @Override
-                            public PointF computeScrollVectorForPosition(int targetPosition) {
-                                return null;
+                            public boolean computeScrollVectorForPosition(int targetPosition,
+                                    PointF endVector) {
+                                return false;
                             }
                         };
                 ss.setTargetPosition(position);
@@ -4080,8 +4084,11 @@
                 RecyclerView.SmoothScroller ss =
                         new LinearSmoothScroller(recyclerView.getContext()) {
                             @Override
-                            public PointF computeScrollVectorForPosition(int targetPosition) {
-                                return new PointF(0, 1);
+                            public boolean computeScrollVectorForPosition(int targetPosition,
+                                    PointF endVector) {
+                                endVector.x = 0;
+                                endVector.y = 1;
+                                return true;
                             }
 
                             @Override
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerSnappingTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerSnappingTest.java
new file mode 100644
index 0000000..d4955d5
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerSnappingTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.test.suitebuilder.annotation.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.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotSame;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+
+@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;
+    }
+
+    @MediumTest
+    @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 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 = 5;
+        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 have scrolled", view, viewAfterFling);
+        assertCenterAligned(viewAfterFling);
+    }
+
+    @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);
+    }
+
+    @MediumTest
+    @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);
+    }
+
+
+    @MediumTest
+    @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 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 = (getViewDimension(view) / 2) + 10;
+        int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
+
+        mLayoutManager.expectIdleState(2);
+        smoothScrollBy(scrollDist);
+        mLayoutManager.waitForSnap(10);
+    }
+
+    @Nullable View findCenterView(RecyclerView.LayoutManager layoutManager) {
+        return mLayoutManager.findFirstVisibleItemClosestToCenter();
+    }
+
+    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 boolean fling(final int velocityX, final int velocityY)
+            throws Throwable {
+        final AtomicBoolean didStart = new AtomicBoolean(false);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                boolean result = mRecyclerView.fling(velocityX, velocityY);
+                didStart.set(result);
+            }
+        });
+        if (!didStart.get()) {
+            return false;
+        }
+        waitForIdleScroll(mRecyclerView);
+        return true;
+    }
+}