RecyclerView nested scrolling 3 implementaiton.

Also moved and refactored a simple MotionEvent
generating utility so that both NestedScrollView
and RecyclerView tests can make use of it, and
refactored NestedScrollView tests as necessary.

Bug: 69326291
Bug: 65129787
Bug: 67345200
Test: androidx.recyclerview.widget.
        RecyclerViewScrollingTest
	RecyclerViewNestedScrollingChildTest

Change-Id: I2137ccf29bc938d8c2e00c28cff1e424ce1760ba
diff --git a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java b/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
index 22ba721..ca5422b 100644
--- a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
+++ b/core/src/androidTest/java/androidx/core/widget/NestedScrollViewNestedScrollingChildTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.SystemClock;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
@@ -42,6 +43,10 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
+import androidx.testutils.Direction;
+import androidx.testutils.FlingData;
+import androidx.testutils.MotionEventData;
+import androidx.testutils.SimpleGestureGeneratorKt;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,6 +56,8 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import java.util.List;
+
 /**
  * Small integration tests that verifies that {@link NestedScrollView} interacts with the latest
  * version of the nested scroll parents correctly.
@@ -212,8 +219,9 @@
     public void uiFling_parentHasNestedScrollingChildWithTypeFling() {
         when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
                 .thenReturn(true);
-        NestedScrollViewTestUtils
-                .simulateFlingDown(InstrumentationRegistry.getContext(), mNestedScrollView);
+        long startTime = SystemClock.uptimeMillis();
+        SimpleGestureGeneratorKt
+                .simulateFling(mNestedScrollView, startTime, 500, 500, Direction.UP);
 
         assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_NONE));
         assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_VERTICAL));
@@ -223,8 +231,9 @@
     public void uiFling_callsNestedFlingsCorrectly() {
         when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
                 .thenReturn(true);
-        NestedScrollViewTestUtils
-                .simulateFlingDown(InstrumentationRegistry.getContext(), mNestedScrollView);
+        long startTime = SystemClock.uptimeMillis();
+        SimpleGestureGeneratorKt
+                .simulateFling(mNestedScrollView, startTime, 500, 500, Direction.UP);
 
         InOrder inOrder = Mockito.inOrder(mParentSpy);
         inOrder.verify(mParentSpy).onNestedPreFling(
@@ -240,56 +249,72 @@
 
     @Test
     public void uiDown_duringFling_stopsNestedScrolling() {
+
+        // Arrange
+
         when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
                 .thenReturn(true);
+
         final Context context = InstrumentationRegistry.getContext();
-        final int[] targetFlingTimeAndDistance =
-                NestedScrollViewTestUtils.getTargetFlingVelocityTimeAndDistance(context);
-        final int targetTimePassed = targetFlingTimeAndDistance[1];
-        final MotionEvent[] motionEvents =
-                NestedScrollViewTestUtils.generateMotionEvents(targetFlingTimeAndDistance);
-        NestedScrollViewTestUtils.dispatchMotionEventsToView(mNestedScrollView, motionEvents);
+        FlingData flingData = SimpleGestureGeneratorKt.generateFlingData(context);
+
+        final long firstDownTime = SystemClock.uptimeMillis();
+        // Should be after fling events occurred.
+        final long secondDownTime = firstDownTime + flingData.getTime() + 100;
+
+        List<MotionEventData> motionEventData = SimpleGestureGeneratorKt
+                .generateFlingMotionEventData(flingData, 500, 500, Direction.UP);
+        SimpleGestureGeneratorKt
+                .dispatchTouchEvents(mNestedScrollView, firstDownTime, motionEventData);
+
         // Sanity check that onStopNestedScroll has not yet been called of type TYPE_NON_TOUCH.
         verify(mParentSpy, never())
                 .onStopNestedScroll(mNestedScrollView, ViewCompat.TYPE_NON_TOUCH);
 
+        // Act
+
         MotionEvent down = MotionEvent.obtain(
-                0,
-                targetTimePassed + 100, // Should be after fling events occurred.
+                secondDownTime,
+                secondDownTime,
                 MotionEvent.ACTION_DOWN,
                 500,
                 500,
                 0);
         mNestedScrollView.dispatchTouchEvent(down);
 
+        // Assert
+
         verify(mParentSpy).onStopNestedScroll(mNestedScrollView, ViewCompat.TYPE_NON_TOUCH);
     }
 
     @Test
-    public void uiFlings_parentReturnsTrueForOnNestedFling_dispatchNestedFlingCalled() {
-        when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
-                .thenReturn(true);
-        when(mParentSpy.onNestedPreFling(eq(mNestedScrollView), anyFloat(), anyFloat()))
-                .thenReturn(false);
-
-        NestedScrollViewTestUtils
-                .simulateFlingDown(InstrumentationRegistry.getContext(), mNestedScrollView);
-
-        verify(mParentSpy).onNestedFling(eq(mNestedScrollView), anyFloat(), anyFloat(), eq(true));
+    public void uiFlings_parentReturnsFalseForOnNestedPreFling_onNestedFlingCalled() {
+        uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(false, true);
     }
 
     @Test
-    public void uiFlings_parentReturnsFalseForOnNestedFling_dispatchNestedFlingNotCalled() {
+    public void uiFlings_parentReturnsTrueForOnNestedPreFling_onNestedFlingNotCalled() {
+        uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(true, false);
+    }
+
+    private void uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(
+            boolean returnValue, boolean onNestedFlingCalled) {
         when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
                 .thenReturn(true);
         when(mParentSpy.onNestedPreFling(eq(mNestedScrollView), anyFloat(), anyFloat()))
-                .thenReturn(true);
+                .thenReturn(returnValue);
 
-        NestedScrollViewTestUtils
-                .simulateFlingDown(InstrumentationRegistry.getContext(), mNestedScrollView);
+        long startTime = SystemClock.uptimeMillis();
+        SimpleGestureGeneratorKt
+                .simulateFling(mNestedScrollView, startTime, 500, 500, Direction.UP);
 
-        verify(mParentSpy, never())
-                .onNestedFling(any(View.class), anyFloat(), anyFloat(), anyBoolean());
+        if (onNestedFlingCalled) {
+            verify(mParentSpy).onNestedFling(eq(mNestedScrollView), anyFloat(), anyFloat(),
+                    eq(true));
+        } else {
+            verify(mParentSpy, never()).onNestedFling(any(View.class), anyFloat(), anyFloat(),
+                    anyBoolean());
+        }
     }
 
     @Test
diff --git a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewScrollingTest.java b/core/src/androidTest/java/androidx/core/widget/NestedScrollViewScrollingTest.java
index d28612e..dc8d752 100644
--- a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewScrollingTest.java
+++ b/core/src/androidTest/java/androidx/core/widget/NestedScrollViewScrollingTest.java
@@ -31,6 +31,7 @@
 
 import android.content.Context;
 import android.graphics.drawable.GradientDrawable;
+import android.os.SystemClock;
 import android.support.v4.BaseInstrumentationTestCase;
 import android.view.View;
 import android.view.ViewGroup;
@@ -45,6 +46,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
+import androidx.testutils.Direction;
+import androidx.testutils.SimpleGestureGeneratorKt;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -53,8 +56,8 @@
 import java.util.concurrent.TimeUnit;
 
 /**
-* Large integration tests that verify that a {@link NestedScrollView} interacts with it's child
-* correctly.
+ * Large integration tests that verify correct {@link NestedScrollView} scrolling behavior,
+ * including interaction with nested scrolling.
 */
 @RunWith(AndroidJUnit4.class)
 @LargeTest
@@ -65,6 +68,7 @@
     private static final int NSV_HEIGHT = 400;
     private static final int PARENT_HEIGHT = 400;
     private static final int WIDTH = 400;
+    private static final int ORIGIN_X_Y = 200;
     private static final int TOTAL_SCROLL_DISTANCE = CHILD_HEIGHT - NSV_HEIGHT;
     private static final int PARTIAL_SCROLL_DISTANCE = TOTAL_SCROLL_DISTANCE / 10;
 
@@ -76,8 +80,6 @@
         super(TestContentViewActivity.class);
     }
 
-    // Large
-
     @Test
     public void onNestedFling_consumedIsFalse_animatesScroll() throws Throwable {
         onNestedFling_consumeParamDeterminesScroll(false, true);
@@ -94,9 +96,8 @@
         attachToActivity();
 
         final Context context = InstrumentationRegistry.getContext();
-        final int targetVelocity =
-                NestedScrollViewTestUtils.getTargetFlingVelocityTimeAndDistance(context)[0];
-
+        final int targetVelocity = (int)
+                Math.ceil(SimpleGestureGeneratorKt.generateFlingData(context).getVelocity() * 1000);
         final CountDownLatch countDownLatch = new CountDownLatch(1);
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
@@ -119,22 +120,20 @@
     }
 
     @Test
-    public void uiFlings_dispatchNestedPreFlingReturnsFalse_scrolls() throws Throwable {
-        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsScroll(false, true);
+    public void uiFlings_dispatchNestedPreFlingReturnsFalse_flings() throws Throwable {
+        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(false, true);
     }
 
     @Test
-    public void uiFlings_dispatchNestedPreFlingReturnsTrue_doesNotScroll() throws Throwable {
-        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsScroll(true, false);
+    public void uiFlings_dispatchNestedPreFlingReturnsTrue_doesNotFling() throws Throwable {
+        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(true, false);
     }
 
-    private void uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsScroll(
+    private void uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(
             final boolean returnValue, final boolean scrolls) throws Throwable {
         setup();
         attachToActivity();
 
-        final Context context = InstrumentationRegistry.getContext();
-
         final CountDownLatch countDownLatch = new CountDownLatch(1);
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
@@ -149,13 +148,23 @@
                             @Override
                             public void onScrollChange(NestedScrollView v, int scrollX, int scrollY,
                                     int oldScrollX, int oldScrollY) {
-                                if (scrollY > PARTIAL_SCROLL_DISTANCE) {
-                                    countDownLatch.countDown();
+                                // If we have a TYPE_NON_TOUCH nested scrolling parent, we are
+                                // animated a fling.
+                                if (mNestedScrollView
+                                        .hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
+                                    if (scrollY > PARTIAL_SCROLL_DISTANCE) {
+                                        countDownLatch.countDown();
+                                    }
                                 }
                             }
                         });
 
-                NestedScrollViewTestUtils.simulateFlingDown(context, mNestedScrollView);
+                SimpleGestureGeneratorKt.simulateFling(
+                        mNestedScrollView,
+                        SystemClock.uptimeMillis(),
+                        ORIGIN_X_Y,
+                        ORIGIN_X_Y,
+                        Direction.UP);
             }
         });
         assertThat(countDownLatch.await(1, TimeUnit.SECONDS), is(scrolls));
@@ -166,15 +175,23 @@
         setup();
         attachToActivity();
 
-        final Context context = InstrumentationRegistry.getContext();
-
-        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final CountDownLatch countDownLatch = new CountDownLatch(2);
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
                 doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
                         anyInt(), anyInt());
 
+                mParent.mOnStopNestedScrollListener =
+                        new NestedScrollingSpyView.OnStopNestedScrollListener() {
+                            @Override
+                            public void onStopNestedScroll(int type) {
+                                if (type == ViewCompat.TYPE_NON_TOUCH) {
+                                    countDownLatch.countDown();
+                                }
+                            }
+                        };
+
                 mNestedScrollView.setOnScrollChangeListener(
                         new NestedScrollView.OnScrollChangeListener() {
                             @Override
@@ -186,7 +203,12 @@
                             }
                         });
 
-                NestedScrollViewTestUtils.simulateFlingDown(context, mNestedScrollView);
+                SimpleGestureGeneratorKt.simulateFling(
+                        mNestedScrollView,
+                        SystemClock.uptimeMillis(),
+                        ORIGIN_X_Y,
+                        ORIGIN_X_Y,
+                        Direction.UP);
             }
         });
         assertThat(countDownLatch.await(2, TimeUnit.SECONDS), is(true));
@@ -224,18 +246,27 @@
     public void fling_fullyParticipatesInNestedScrolling() throws Throwable {
         setup();
         attachToActivity();
-
         final Context context = InstrumentationRegistry.getContext();
-        final int targetVelocity =
-                NestedScrollViewTestUtils.getTargetFlingVelocityTimeAndDistance(context)[0];
+        final int targetVelocity = (int)
+                Math.ceil(SimpleGestureGeneratorKt.generateFlingData(context).getVelocity() * 1000);
 
-        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        final CountDownLatch countDownLatch = new CountDownLatch(2);
         mActivityTestRule.runOnUiThread(new Runnable() {
             @Override
             public void run() {
                 doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
                         anyInt(), anyInt());
 
+                mParent.mOnStopNestedScrollListener =
+                        new NestedScrollingSpyView.OnStopNestedScrollListener() {
+                            @Override
+                            public void onStopNestedScroll(int type) {
+                                if (type == ViewCompat.TYPE_NON_TOUCH) {
+                                    countDownLatch.countDown();
+                                }
+                            }
+                        };
+
                 mNestedScrollView.setOnScrollChangeListener(
                         new NestedScrollView.OnScrollChangeListener() {
                             @Override
@@ -355,9 +386,11 @@
         testContentView.awaitLayouts(2);
     }
 
-    public class NestedScrollingSpyView extends FrameLayout implements NestedScrollingChild3,
+    public static class NestedScrollingSpyView extends FrameLayout implements NestedScrollingChild3,
             NestedScrollingParent3 {
 
+        public OnStopNestedScrollListener mOnStopNestedScrollListener;
+
         public NestedScrollingSpyView(Context context) {
             super(context);
         }
@@ -376,7 +409,9 @@
 
         @Override
         public void onStopNestedScroll(@NonNull View target, int type) {
-
+            if (mOnStopNestedScrollListener != null) {
+                mOnStopNestedScrollListener.onStopNestedScroll(type);
+            }
         }
 
         @Override
@@ -517,5 +552,9 @@
         public int getNestedScrollAxes() {
             return 0;
         }
+
+        interface OnStopNestedScrollListener {
+            void onStopNestedScroll(int type);
+        }
     }
 }
diff --git a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTestUtils.java b/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTestUtils.java
deleted file mode 100644
index 01e8aac..0000000
--- a/core/src/androidTest/java/androidx/core/widget/NestedScrollViewTestUtils.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright 2018 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 androidx.core.widget;
-
-import android.content.Context;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-
-public class NestedScrollViewTestUtils {
-
-    public static int[] getTargetFlingVelocityTimeAndDistance(Context context) {
-        ViewConfiguration configuration =
-                ViewConfiguration.get(context);
-        int touchSlop = configuration.getScaledTouchSlop();
-        int mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
-        int mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
-
-        int targetVelocitySeconds = ((mMaximumVelocity - mMinimumVelocity) / 2) + mMinimumVelocity;
-        int targetDistanceTraveled = touchSlop * 2;
-        int targetTimePassed = (targetDistanceTraveled * 1000) / targetVelocitySeconds;
-
-        return new int[]{targetVelocitySeconds, targetTimePassed, targetDistanceTraveled};
-    }
-
-    public static MotionEvent[] generateMotionEvents(int[] targetFlingVelocityTimeAndDistance) {
-        int targetTimePassed = targetFlingVelocityTimeAndDistance[1];
-        int targetDistanceTraveled = targetFlingVelocityTimeAndDistance[2];
-        targetDistanceTraveled *= -1;
-
-        MotionEvent down = MotionEvent.obtain(
-                0,
-                0,
-                MotionEvent.ACTION_DOWN,
-                500,
-                500,
-                0);
-        MotionEvent move = MotionEvent.obtain(
-                0,
-                targetTimePassed,
-                MotionEvent.ACTION_MOVE,
-                500,
-                500 + targetDistanceTraveled,
-                0);
-        MotionEvent up = MotionEvent.obtain(
-                0,
-                targetTimePassed,
-                MotionEvent.ACTION_UP,
-                500,
-                500 + targetDistanceTraveled,
-                0);
-
-        return new MotionEvent[]{down, move, up};
-    }
-
-    public static void dispatchMotionEventsToView(View view, MotionEvent[] motionEvents) {
-        for (MotionEvent motionEvent : motionEvents) {
-            view.dispatchTouchEvent(motionEvent);
-        }
-    }
-
-    public static void simulateFlingDown(Context context, View view) {
-        int[] targetFlingTimeAndDistance =
-                NestedScrollViewTestUtils.getTargetFlingVelocityTimeAndDistance(context);
-        MotionEvent[] motionEvents =
-                NestedScrollViewTestUtils.generateMotionEvents(targetFlingTimeAndDistance);
-        NestedScrollViewTestUtils.dispatchMotionEventsToView(view, motionEvents);
-    }
-}
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index 50ef133..fb04788 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -350,7 +350,7 @@
     method public int findTargetSnapPosition(androidx.recyclerview.widget.RecyclerView.LayoutManager, int, int);
   }
 
-  public class RecyclerView extends android.view.ViewGroup implements androidx.core.view.NestedScrollingChild2 androidx.core.view.ScrollingView {
+  public class RecyclerView extends android.view.ViewGroup implements androidx.core.view.NestedScrollingChild2 androidx.core.view.NestedScrollingChild3 androidx.core.view.ScrollingView {
     ctor public RecyclerView(android.content.Context);
     ctor public RecyclerView(android.content.Context, android.util.AttributeSet);
     ctor public RecyclerView(android.content.Context, android.util.AttributeSet, int);
@@ -369,6 +369,7 @@
     method public int computeVerticalScrollRange();
     method public boolean dispatchNestedPreScroll(int, int, int[], int[], int);
     method public boolean dispatchNestedScroll(int, int, int, int, int[], int);
+    method public final void dispatchNestedScroll(int, int, int, int, int[], int, int[]);
     method public boolean drawChild(android.graphics.Canvas, android.view.View, long);
     method public android.view.View findChildViewUnder(float, float);
     method public android.view.View findContainingItemView(android.view.View);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java
new file mode 100644
index 0000000..eb5aaad
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingChildTest.java
@@ -0,0 +1,660 @@
+/*
+ * Copyright 2018 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 androidx.recyclerview.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.NestedScrollingChild3;
+import androidx.core.view.NestedScrollingParent3;
+import androidx.core.view.ViewCompat;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.testutils.Direction;
+import androidx.testutils.FlingData;
+import androidx.testutils.MotionEventData;
+import androidx.testutils.SimpleGestureGeneratorKt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+
+/**
+ * Small integration tests that verifies that {@link RecyclerView} interacts with the latest
+ * version of the nested scroll parents correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RecyclerViewNestedScrollingChildTest {
+
+    private NestedScrollingSpyView mParentSpy;
+    private RecyclerView mRecyclerView;
+
+    private void setup(boolean vertical, int scrollDistance, boolean parentAccepts) {
+
+        Context context = InstrumentationRegistry.getContext();
+
+        // Create views
+
+        mRecyclerView = new RecyclerView(context);
+        mRecyclerView.setMinimumWidth(1000);
+        mRecyclerView.setMinimumHeight(1000);
+
+        mParentSpy = Mockito.spy(new NestedScrollingSpyView(context));
+        mParentSpy.setMinimumWidth(1000);
+        mParentSpy.setMinimumHeight(1000);
+
+        // Setup RecyclerView
+        int orientation = vertical ? RecyclerView.VERTICAL : RecyclerView.HORIZONTAL;
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(context, orientation, false));
+        mRecyclerView.setAdapter(new TestAdapter(context, 1000 + scrollDistance, 1, vertical));
+
+        // Create view hierarchy
+        mParentSpy.addView(mRecyclerView);
+
+        //  Measure and layout
+        int measureSpecWidth = View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY);
+        int measureSpecHeight = View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY);
+        mParentSpy.measure(measureSpecWidth, measureSpecHeight);
+        mParentSpy.layout(0, 0, 1000, 1000);
+
+        when(mParentSpy.onStartNestedScroll(any(View.class), any(View.class), anyInt(), anyInt()))
+                .thenReturn(parentAccepts);
+    }
+
+    @Test
+    public void uiFingerDown_vertical_parentHasNestedScrollingChildWithTypeTouch() {
+        setup(true, 100, true);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_VERTICAL));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+    }
+
+    @Test
+    public void uiFingerDown_horizontal_parentHasNestedScrollingChildWithTypeTouch() {
+        setup(false, 100, true);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_HORIZONTAL));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+    }
+
+    @Test
+    public void uiFingerDown_parentRejects_parentDoesNotHaveNestedScrollingChild() {
+        setup(true, 100, false);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+    }
+
+    @Test
+    public void uiFingerUp_afterFingerDown_parentDoesNotHaveNestedScrollingChild() {
+        setup(true, 100, true);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent up = MotionEvent.obtain(0, 100, MotionEvent.ACTION_UP, 500, 500, 0);
+        mRecyclerView.dispatchTouchEvent(down);
+
+        mRecyclerView.dispatchTouchEvent(up);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+    }
+
+    @Test
+    public void uiFingerScroll_vertical_parentOnNestedPreScrollCalledCorrectly() {
+        setup(true, 100, true);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 500, 300, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+        mRecyclerView.dispatchTouchEvent(move);
+
+        // Can't verify 'consumed' parameter values due to mutation, so instead capturing actual
+        // values manually in the the NestedScrollingSpyView object.
+        verify(mParentSpy).onNestedPreScroll(eq(mRecyclerView), eq(0), eq(200), any(int[].class),
+                eq(ViewCompat.TYPE_TOUCH));
+        assertThat(mParentSpy.onNestedPreScrollConsumedX, is(0));
+        assertThat(mParentSpy.onNestedPreScrollConsumedY, is(0));
+    }
+
+    @Test
+    public void uiFingerScroll_horizontal_parentOnNestedPreScrollCalledCorrectly() {
+        setup(false, 100, true);
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 300, 500, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+        mRecyclerView.dispatchTouchEvent(move);
+
+        // Can't verify 'consumed' parameter values due to mutation, so instead capturing actual
+        // values manually in the the NestedScrollingSpyView object.
+        verify(mParentSpy).onNestedPreScroll(eq(mRecyclerView), eq(200), eq(0), any(int[].class),
+                eq(ViewCompat.TYPE_TOUCH));
+        assertThat(mParentSpy.onNestedPreScrollConsumedX, is(0));
+        assertThat(mParentSpy.onNestedPreScrollConsumedY, is(0));
+    }
+
+    @Test
+    public void uiFingerScroll_scrollsBeyondLimitVertical_parentOnNestedScrollCalledCorrectly() {
+        setup(true, 100, true);
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 500, 300 - touchSlop, 0);
+
+        mParentSpy.dispatchTouchEvent(down);
+        mParentSpy.dispatchTouchEvent(move);
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 0, 100, 0, 100, ViewCompat.TYPE_TOUCH,
+                new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFingerScroll_scrollsBeyondLimitHorizontal_parentOnNestedScrollCalledCorrectly() {
+        setup(false, 100, true);
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 300 - touchSlop, 500, 0);
+
+        mParentSpy.dispatchTouchEvent(down);
+        mParentSpy.dispatchTouchEvent(move);
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 100, 0, 100, 0, ViewCompat.TYPE_TOUCH,
+                new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFingerScroll_scrollsWithinLimitVertical_parentOnNestedScrollCalledCorrectly() {
+        setup(true, 100, true);
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 500, 450 - touchSlop, 0);
+
+        mParentSpy.dispatchTouchEvent(down);
+        mParentSpy.dispatchTouchEvent(move);
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 0, 50, 0, 0, ViewCompat.TYPE_TOUCH,
+                new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFingerScroll_scrollsWithinLimitHorizontal_parentOnNestedScrollCalledCorrectly() {
+        setup(false, 100, true);
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 450 - touchSlop, 500, 0);
+
+        mParentSpy.dispatchTouchEvent(down);
+        mParentSpy.dispatchTouchEvent(move);
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 50, 0, 0, 0, ViewCompat.TYPE_TOUCH,
+                new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFingerScroll_vertical_preSelfPostChainWorks() {
+        setup(true, 100, true);
+        doAnswer(new Answer() {
+            public Object answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                ((int[]) args[3])[1] = 50;
+                return null;
+            }
+        }).when(mParentSpy)
+                .onNestedPreScroll(any(View.class), anyInt(), anyInt(), any(int[].class), anyInt());
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 500, 300, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+        mRecyclerView.dispatchTouchEvent(move);
+
+        // Can't verify 'consumed' parameter values due to mutation, so instead capturing actual
+        // values manually in the the NestedScrollingSpyView object.
+        verify(mParentSpy).onNestedPreScroll(eq(mRecyclerView), eq(0), eq(200), any(int[].class),
+                eq(ViewCompat.TYPE_TOUCH));
+        assertThat(mParentSpy.onNestedPreScrollConsumedX, is(0));
+        assertThat(mParentSpy.onNestedPreScrollConsumedY, is(0));
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 0, 100, 0, 50 - touchSlop,
+                ViewCompat.TYPE_TOUCH, new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFingerScroll_horizontal_preSelfPostChainWorks() {
+        setup(false, 100, true);
+        doAnswer(new Answer() {
+            public Object answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                ((int[]) args[3])[0] = 50;
+                return null;
+            }
+        }).when(mParentSpy)
+                .onNestedPreScroll(any(View.class), anyInt(), anyInt(), any(int[].class), anyInt());
+        int touchSlop =
+                ViewConfiguration.get(InstrumentationRegistry.getContext()).getScaledTouchSlop();
+        MotionEvent down = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 500, 500, 0);
+        MotionEvent move =
+                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, 300, 500, 0);
+
+        mRecyclerView.dispatchTouchEvent(down);
+        mRecyclerView.dispatchTouchEvent(move);
+
+        // Can't verify 'consumed' parameter values due to mutation, so instead capturing actual
+        // values manually in the the NestedScrollingSpyView object.
+        verify(mParentSpy).onNestedPreScroll(eq(mRecyclerView), eq(200), eq(0), any(int[].class),
+                eq(ViewCompat.TYPE_TOUCH));
+        assertThat(mParentSpy.onNestedPreScrollConsumedX, is(0));
+        assertThat(mParentSpy.onNestedPreScrollConsumedY, is(0));
+
+        verify(mParentSpy).onNestedScroll(mRecyclerView, 100, 0, 50 - touchSlop, 0,
+                ViewCompat.TYPE_TOUCH, new int[]{0, 0});
+    }
+
+    @Test
+    public void uiFling_vertical_parentHasNestedScrollingChildWithTypeFling() {
+        setup(true, 100, true);
+        long startTime = SystemClock.uptimeMillis();
+
+        SimpleGestureGeneratorKt
+                .simulateFling(mRecyclerView, startTime, 500, 500, Direction.UP);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_VERTICAL));
+    }
+
+    @Test
+    public void uiFling_horizontal_parentHasNestedScrollingChildWithTypeFling() {
+        setup(false, 100, true);
+        long startTime = SystemClock.uptimeMillis();
+
+        SimpleGestureGeneratorKt
+                .simulateFling(mRecyclerView, startTime, 500, 500, Direction.LEFT);
+
+        assertThat(mParentSpy.axesForTypeTouch, is(ViewCompat.SCROLL_AXIS_NONE));
+        assertThat(mParentSpy.axesForTypeNonTouch, is(ViewCompat.SCROLL_AXIS_HORIZONTAL));
+    }
+
+    @Test
+    public void uiFling_callsNestedFlingsCorrectly() {
+        setup(true, 100, true);
+        long startTime = SystemClock.uptimeMillis();
+
+        SimpleGestureGeneratorKt
+                .simulateFling(mRecyclerView, startTime, 500, 500, Direction.UP);
+
+        InOrder inOrder = Mockito.inOrder(mParentSpy);
+        inOrder.verify(mParentSpy).onNestedPreFling(
+                eq(mRecyclerView),
+                eq(0f),
+                anyFloat());
+        inOrder.verify(mParentSpy).onNestedFling(
+                eq(mRecyclerView),
+                eq(0f),
+                anyFloat(),
+                eq(true));
+    }
+
+    @Test
+    public void uiDown_duringFling_stopsNestedScrolling() {
+
+        // Arrange
+
+        setup(true, 1000, true);
+
+        final Context context = InstrumentationRegistry.getContext();
+        FlingData flingData = SimpleGestureGeneratorKt.generateFlingData(context);
+
+        final long firstDownTime = SystemClock.uptimeMillis();
+        // Should be after fling events occurred.
+        final long secondDownTime = firstDownTime + flingData.getTime() + 100;
+
+        List<MotionEventData> motionEventData = SimpleGestureGeneratorKt
+                .generateFlingMotionEventData(flingData, 500, 500, Direction.UP);
+        SimpleGestureGeneratorKt
+                .dispatchTouchEvents(mRecyclerView, firstDownTime, motionEventData);
+
+        // Sanity check that onStopNestedScroll has not yet been called of type TYPE_NON_TOUCH.
+        verify(mParentSpy, never())
+                .onStopNestedScroll(mRecyclerView, ViewCompat.TYPE_NON_TOUCH);
+
+        // Act
+
+        MotionEvent down = MotionEvent.obtain(
+                secondDownTime,
+                secondDownTime,
+                MotionEvent.ACTION_DOWN,
+                500,
+                500,
+                0);
+        mRecyclerView.dispatchTouchEvent(down);
+
+        // Assert
+
+        verify(mParentSpy).onStopNestedScroll(mRecyclerView, ViewCompat.TYPE_NON_TOUCH);
+    }
+
+    @Test
+    public void uiFlings_parentReturnsFalseForOnNestedPreFling_onNestedFlingCalled() {
+        uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(false, true);
+    }
+
+    @Test
+    public void uiFlings_parentReturnsTrueForOnNestedPreFling_onNestedFlingNotCalled() {
+        uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(true, false);
+    }
+
+    private void uiFlings_returnValueOfOnNestedPreFlingDeterminesCallToOnNestedFling(
+            boolean returnValue, boolean onNestedFlingCalled) {
+        setup(true, 100, true);
+        when(mParentSpy.onNestedPreFling(eq(mRecyclerView), anyFloat(), anyFloat()))
+                .thenReturn(returnValue);
+
+        long startTime = SystemClock.uptimeMillis();
+        SimpleGestureGeneratorKt
+                .simulateFling(mRecyclerView, startTime, 500, 500, Direction.UP);
+
+        if (onNestedFlingCalled) {
+            verify(mParentSpy).onNestedFling(eq(mRecyclerView), anyFloat(), anyFloat(), eq(true));
+        } else {
+            verify(mParentSpy, never()).onNestedFling(any(View.class), anyFloat(), anyFloat(),
+                    anyBoolean());
+        }
+    }
+
+    @Test
+    public void smoothScrollBy_doesNotStartNestedScrolling() {
+        setup(true, 100, true);
+        mRecyclerView.smoothScrollBy(0, 100);
+        verify(mParentSpy, never()).onStartNestedScroll(
+                any(View.class), any(View.class), anyInt(), anyInt());
+    }
+
+    public class NestedScrollingSpyView extends FrameLayout implements NestedScrollingChild3,
+            NestedScrollingParent3 {
+
+        public int axesForTypeTouch;
+        public int axesForTypeNonTouch;
+        public int onNestedPreScrollConsumedX;
+        public int onNestedPreScrollConsumedY;
+
+        public NestedScrollingSpyView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
+                int type) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
+                int type) {
+            if (type == ViewCompat.TYPE_NON_TOUCH) {
+                axesForTypeNonTouch = axes;
+            } else {
+                axesForTypeTouch = axes;
+            }
+        }
+
+        @Override
+        public void onStopNestedScroll(@NonNull View target, int type) {
+            if (type == ViewCompat.TYPE_NON_TOUCH) {
+                axesForTypeNonTouch = ViewCompat.SCROLL_AXIS_NONE;
+            } else {
+                axesForTypeTouch = ViewCompat.SCROLL_AXIS_NONE;
+            }
+        }
+
+        @Override
+        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+                int dxUnconsumed, int dyUnconsumed, int type) {
+
+        }
+
+        @Override
+        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+                int type) {
+            onNestedPreScrollConsumedX = consumed[0];
+            onNestedPreScrollConsumedY = consumed[1];
+        }
+
+        @Override
+        public boolean startNestedScroll(int axes, int type) {
+            return false;
+        }
+
+        @Override
+        public void stopNestedScroll(int type) {
+
+        }
+
+        @Override
+        public boolean hasNestedScrollingParent(int type) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
+                @Nullable int[] offsetInWindow, int type) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+                int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
+        }
+
+        @Override
+        public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
+                @NonNull int[] consumed) {
+        }
+
+        @Override
+        public void setNestedScrollingEnabled(boolean enabled) {
+
+        }
+
+        @Override
+        public boolean isNestedScrollingEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean startNestedScroll(int axes) {
+            return false;
+        }
+
+        @Override
+        public void stopNestedScroll() {
+
+        }
+
+        @Override
+        public boolean hasNestedScrollingParent() {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, int[] offsetInWindow) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
+                int[] offsetInWindow) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+            return false;
+        }
+
+        @Override
+        public boolean onStartNestedScroll(View child, View target, int axes) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScrollAccepted(View child, View target, int axes) {
+
+        }
+
+        @Override
+        public void onStopNestedScroll(View target) {
+
+        }
+
+        @Override
+        public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed) {
+
+        }
+
+        @Override
+        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+
+        }
+
+        @Override
+        public boolean onNestedFling(View target, float velocityX, float velocityY,
+                boolean consumed) {
+            return false;
+        }
+
+        @Override
+        public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+            return false;
+        }
+
+        @Override
+        public int getNestedScrollAxes() {
+            return 0;
+        }
+    }
+
+    private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+
+        private Context mContext;
+        private int mOrientationSize;
+        private int mItemCount;
+        private boolean mVertical;
+
+        TestAdapter(Context context, float orientationSize, int itemCount, boolean vertical) {
+            mContext = context;
+            mOrientationSize = (int) Math.floor(orientationSize / itemCount);
+            mItemCount = itemCount;
+            mVertical = vertical;
+        }
+
+        @Override
+        public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                int viewType) {
+            View view = new View(mContext);
+
+            int width;
+            int height;
+            if (mVertical) {
+                width = ViewGroup.LayoutParams.MATCH_PARENT;
+                height = mOrientationSize;
+            } else {
+                width = mOrientationSize;
+                height = ViewGroup.LayoutParams.MATCH_PARENT;
+            }
+
+            view.setLayoutParams(new ViewGroup.LayoutParams(width, height));
+            view.setMinimumHeight(mOrientationSize);
+            return new TestViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(TestViewHolder holder, int position) {
+
+        }
+
+        @Override
+        public int getItemCount() {
+            return mItemCount;
+        }
+    }
+
+    private class TestViewHolder extends RecyclerView.ViewHolder {
+
+        TestViewHolder(View itemView) {
+            super(itemView);
+        }
+    }
+}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingTest.java
new file mode 100644
index 0000000..41f6777
--- /dev/null
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingTest.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright 2018 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 androidx.recyclerview.widget;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.NestedScrollingChild3;
+import androidx.core.view.NestedScrollingParent3;
+import androidx.core.view.ViewCompat;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.testutils.Direction;
+import androidx.testutils.SimpleGestureGeneratorKt;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Large integration tests that verify that a {@link RecyclerView} interacts with nested scrolling
+ * correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class RecyclerViewNestedScrollingTest {
+
+    private static final int CHILD_HEIGHT = 800;
+    private static final int NSV_HEIGHT = 400;
+    private static final int PARENT_HEIGHT = 400;
+    private static final int WIDTH = 400;
+    private static final int ORIGIN_X_Y = 200;
+    private static final int TOTAL_SCROLL_DISTANCE = CHILD_HEIGHT - NSV_HEIGHT;
+    private static final int PARTIAL_SCROLL_DISTANCE = TOTAL_SCROLL_DISTANCE / 10;
+
+    private RecyclerView mRecyclerView;
+    private NestedScrollingSpyView mParent;
+
+    @Rule
+    public final ActivityTestRule<TestContentViewActivity> mActivityTestRule;
+
+    public RecyclerViewNestedScrollingTest() {
+        mActivityTestRule = new ActivityTestRule<>(TestContentViewActivity.class);
+    }
+
+    @Test
+    public void uiFlings_dispatchNestedPreFlingReturnsFalse_flings() throws Throwable {
+        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(false, true);
+    }
+
+    @Test
+    public void uiFlings_dispatchNestedPreFlingReturnsTrue_doesNotFling() throws Throwable {
+        uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(true, false);
+    }
+
+    private void uiFlings_parentPreFlingReturnDeterminesNestedScrollViewsFling(
+            final boolean returnValue, final boolean scrolls) throws Throwable {
+        setup();
+        attachToActivity();
+
+        final CountDownLatch countDownLatch = new CountDownLatch(1);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
+                        anyInt(), anyInt());
+                doReturn(returnValue).when(mParent).onNestedPreFling(any(View.class), anyFloat(),
+                        anyFloat());
+
+                mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                    int mTotalScrolled = 0;
+                    @Override
+                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+
+                    }
+
+                    @Override
+                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                        // If we have a TYPE_NON_TOUCH nested scrolling parent, we are animated a
+                        // fling.
+                        if (mRecyclerView.hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
+                            mTotalScrolled += dy;
+                            if (mTotalScrolled > PARTIAL_SCROLL_DISTANCE) {
+                                countDownLatch.countDown();
+                            }
+                        }
+                    }
+                });
+
+                SimpleGestureGeneratorKt.simulateFling(
+                        mRecyclerView,
+                        SystemClock.uptimeMillis(),
+                        ORIGIN_X_Y,
+                        ORIGIN_X_Y,
+                        Direction.UP);
+            }
+        });
+        assertThat(countDownLatch.await(1, TimeUnit.SECONDS), is(scrolls));
+    }
+
+    @Test
+    public void uiFling_fullyParticipatesInNestedScrolling() throws Throwable {
+        setup();
+        attachToActivity();
+
+        final CountDownLatch countDownLatch = new CountDownLatch(2);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
+                        anyInt(), anyInt());
+
+                mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                    int mTotalScrolled = 0;
+                    @Override
+                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                            countDownLatch.countDown();
+                        }
+                    }
+
+                    @Override
+                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                        mTotalScrolled += dy;
+                        if (mTotalScrolled == TOTAL_SCROLL_DISTANCE) {
+                            countDownLatch.countDown();
+                        }
+                    }
+                });
+
+                SimpleGestureGeneratorKt.simulateFling(
+                        mRecyclerView,
+                        SystemClock.uptimeMillis(),
+                        ORIGIN_X_Y,
+                        ORIGIN_X_Y,
+                        Direction.UP);
+            }
+        });
+        assertThat(countDownLatch.await(2, TimeUnit.SECONDS), is(true));
+
+        // Verify all of the following TYPE_TOUCH nested scrolling methods are called.
+        verify(mParent, atLeastOnce()).onStartNestedScroll(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedScrollAccepted(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedPreScroll(eq(mRecyclerView), anyInt(), anyInt(),
+                any(int[].class), eq(ViewCompat.TYPE_TOUCH));
+        verify(mParent, atLeastOnce()).onNestedScroll(eq(mRecyclerView),  anyInt(), anyInt(),
+                anyInt(), anyInt(), eq(ViewCompat.TYPE_TOUCH), any(int[].class));
+        verify(mParent, atLeastOnce()).onNestedPreFling(eq(mRecyclerView),  anyFloat(),
+                anyFloat());
+        verify(mParent, atLeastOnce()).onNestedFling(eq(mRecyclerView),  anyFloat(), anyFloat(),
+                eq(true));
+        verify(mParent, atLeastOnce()).onStopNestedScroll(mRecyclerView, ViewCompat.TYPE_TOUCH);
+
+        // Verify all of the following TYPE_NON_TOUCH nested scrolling methods are called
+        verify(mParent, atLeastOnce()).onStartNestedScroll(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedScrollAccepted(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedPreScroll(eq(mRecyclerView), anyInt(), anyInt(),
+                any(int[].class), eq(ViewCompat.TYPE_NON_TOUCH));
+        verify(mParent, atLeastOnce()).onNestedScroll(eq(mRecyclerView),  anyInt(), anyInt(),
+                anyInt(),
+                anyInt(), eq(ViewCompat.TYPE_NON_TOUCH), any(int[].class));
+        verify(mParent, atLeastOnce()).onStopNestedScroll(mRecyclerView,
+                ViewCompat.TYPE_NON_TOUCH);
+    }
+
+    @Test
+    public void fling_fullyParticipatesInNestedScrolling() throws Throwable {
+        setup();
+        attachToActivity();
+        final Context context = InstrumentationRegistry.getContext();
+        final int targetVelocity = (int) Math.ceil(
+                SimpleGestureGeneratorKt.generateFlingData(context).getVelocity() * 1000);
+
+        final CountDownLatch countDownLatch = new CountDownLatch(2);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
+                        anyInt(), anyInt());
+
+                mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+                    int mTotalScrolled = 0;
+                    @Override
+                    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+                            countDownLatch.countDown();
+                        }
+                    }
+
+                    @Override
+                    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+                        mTotalScrolled += dy;
+                        if (mTotalScrolled == TOTAL_SCROLL_DISTANCE) {
+                            countDownLatch.countDown();
+                        }
+                    }
+                });
+
+                mRecyclerView.fling(0, targetVelocity);
+            }
+        });
+        assertThat(countDownLatch.await(1, TimeUnit.SECONDS), is(true));
+
+        // Verify all of the following TYPE_NON_TOUCH nested scrolling methods are called
+        verify(mParent, atLeastOnce()).onStartNestedScroll(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedScrollAccepted(mRecyclerView, mRecyclerView,
+                ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH);
+        verify(mParent, atLeastOnce()).onNestedPreScroll(eq(mRecyclerView), anyInt(), anyInt(),
+                any(int[].class), eq(ViewCompat.TYPE_NON_TOUCH));
+        verify(mParent, atLeastOnce()).onNestedScroll(eq(mRecyclerView),  anyInt(), anyInt(),
+                anyInt(),
+                anyInt(), eq(ViewCompat.TYPE_NON_TOUCH), any(int[].class));
+        verify(mParent, atLeastOnce()).onStopNestedScroll(mRecyclerView,
+                ViewCompat.TYPE_NON_TOUCH);
+
+        // Verify all of the following TYPE_TOUCH nested scrolling methods are not called.
+        verify(mParent, never()).onStartNestedScroll(any(View.class), any(View.class),
+                anyInt(), eq(ViewCompat.TYPE_TOUCH));
+        verify(mParent, never()).onNestedScrollAccepted(any(View.class), any(View.class),
+                anyInt(), eq(ViewCompat.TYPE_TOUCH));
+        verify(mParent, never()).onNestedPreScroll(any(View.class), anyInt(), anyInt(),
+                any(int[].class), eq(ViewCompat.TYPE_TOUCH));
+        verify(mParent, never()).onNestedScroll(any(View.class),  anyInt(), anyInt(), anyInt(),
+                anyInt(), eq(ViewCompat.TYPE_TOUCH), any(int[].class));
+        verify(mParent, never()).onNestedPreFling(any(View.class),  anyFloat(), anyFloat());
+        verify(mParent, never()).onNestedFling(any(View.class),  anyFloat(), anyFloat(),
+                anyBoolean());
+        verify(mParent, never()).onStopNestedScroll(any(View.class), eq(ViewCompat.TYPE_TOUCH));
+    }
+
+    private void setup() {
+        Context context = mActivityTestRule.getActivity();
+
+        mRecyclerView = new RecyclerView(context);
+        mRecyclerView.setLayoutParams(new ViewGroup.LayoutParams(WIDTH, NSV_HEIGHT));
+        mRecyclerView.setBackgroundColor(0xFF0000FF);
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+        mRecyclerView.setAdapter(new TestAdapter(context, CHILD_HEIGHT, 100, true));
+
+        mParent = spy(new NestedScrollingSpyView(context));
+        mParent.setLayoutParams(new ViewGroup.LayoutParams(WIDTH, PARENT_HEIGHT));
+        mParent.setBackgroundColor(0xFF0000FF);
+        mParent.addView(mRecyclerView);
+    }
+
+    private void attachToActivity() throws Throwable {
+        final TestContentView testContentView = mActivityTestRule.getActivity().getContentView();
+        testContentView.expectLayouts(1);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                testContentView.addView(mParent);
+            }
+        });
+        testContentView.awaitLayouts(2);
+    }
+
+    public static class NestedScrollingSpyView extends FrameLayout implements NestedScrollingChild3,
+            NestedScrollingParent3 {
+
+        public OnStopNestedScrollListener mOnStopNestedScrollListener;
+
+        public NestedScrollingSpyView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes,
+                int type) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes,
+                int type) {
+
+        }
+
+        @Override
+        public void onStopNestedScroll(@NonNull View target, int type) {
+            if (mOnStopNestedScrollListener != null) {
+                mOnStopNestedScrollListener.onStopNestedScroll(type);
+            }
+        }
+
+        @Override
+        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+                int dxUnconsumed, int dyUnconsumed, int type) {
+
+        }
+
+        @Override
+        public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
+                int type) {
+
+        }
+
+        @Override
+        public boolean startNestedScroll(int axes, int type) {
+            return false;
+        }
+
+        @Override
+        public void stopNestedScroll(int type) {
+
+        }
+
+        @Override
+        public boolean hasNestedScrollingParent(int type) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
+                @Nullable int[] offsetInWindow, int type) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
+                int dxUnconsumed, int dyUnconsumed, int type, @Nullable int[] consumed) {
+        }
+
+        @Override
+        public void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, @Nullable int[] offsetInWindow, int type,
+                @Nullable int[] consumed) {
+        }
+
+        @Override
+        public void setNestedScrollingEnabled(boolean enabled) {
+
+        }
+
+        @Override
+        public boolean isNestedScrollingEnabled() {
+            return false;
+        }
+
+        @Override
+        public boolean startNestedScroll(int axes) {
+            return false;
+        }
+
+        @Override
+        public void stopNestedScroll() {
+
+        }
+
+        @Override
+        public boolean hasNestedScrollingParent() {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed, int[] offsetInWindow) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed,
+                int[] offsetInWindow) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+            return false;
+        }
+
+        @Override
+        public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+            return false;
+        }
+
+        @Override
+        public boolean onStartNestedScroll(View child, View target, int axes) {
+            return false;
+        }
+
+        @Override
+        public void onNestedScrollAccepted(View child, View target, int axes) {
+
+        }
+
+        @Override
+        public void onStopNestedScroll(View target) {
+
+        }
+
+        @Override
+        public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
+                int dyUnconsumed) {
+
+        }
+
+        @Override
+        public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+
+        }
+
+        @Override
+        public boolean onNestedFling(View target, float velocityX, float velocityY,
+                boolean consumed) {
+            return false;
+        }
+
+        @Override
+        public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+            return false;
+        }
+
+        @Override
+        public int getNestedScrollAxes() {
+            return 0;
+        }
+
+        interface OnStopNestedScrollListener {
+            void onStopNestedScroll(int type);
+        }
+    }
+
+    private class TestAdapter extends RecyclerView.Adapter<TestViewHolder> {
+
+        private Context mContext;
+        private int mOrientationSize;
+        private int mItemCount;
+        private boolean mVertical;
+
+        TestAdapter(Context context, int orientationSize, int itemCount, boolean vertical) {
+            mContext = context;
+            mOrientationSize = orientationSize / itemCount;
+            mItemCount = itemCount;
+            mVertical = vertical;
+        }
+
+        @Override
+        public TestViewHolder onCreateViewHolder(ViewGroup parent,
+                int viewType) {
+            View view = new View(mContext);
+
+            int width;
+            int height;
+            if (mVertical) {
+                width = ViewGroup.LayoutParams.MATCH_PARENT;
+                height = mOrientationSize;
+            } else {
+                width = mOrientationSize;
+                height = ViewGroup.LayoutParams.MATCH_PARENT;
+            }
+
+            view.setLayoutParams(new ViewGroup.LayoutParams(width, height));
+            view.setMinimumHeight(mOrientationSize);
+            return new TestViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(TestViewHolder holder, int position) {
+
+        }
+
+        @Override
+        public int getItemCount() {
+            return mItemCount;
+        }
+    }
+
+    private class TestViewHolder extends RecyclerView.ViewHolder {
+
+        TestViewHolder(View itemView) {
+            super(itemView);
+        }
+    }
+
+}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index a1db707..d2baf5c 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -69,6 +69,7 @@
 import androidx.core.view.InputDeviceCompat;
 import androidx.core.view.MotionEventCompat;
 import androidx.core.view.NestedScrollingChild2;
+import androidx.core.view.NestedScrollingChild3;
 import androidx.core.view.NestedScrollingChildHelper;
 import androidx.core.view.ScrollingView;
 import androidx.core.view.ViewCompat;
@@ -204,7 +205,8 @@
  *
  * @attr ref androidx.recyclerview.R.styleable#RecyclerView_layoutManager
  */
-public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
+public class RecyclerView extends ViewGroup implements ScrollingView,
+        NestedScrollingChild2, NestedScrollingChild3 {
 
     static final String TAG = "RecyclerView";
 
@@ -565,14 +567,10 @@
 
     private NestedScrollingChildHelper mScrollingChildHelper;
     private final int[] mScrollOffset = new int[2];
-    final int[] mScrollConsumed = new int[2];
     private final int[] mNestedOffsets = new int[2];
 
-    /**
-     * Reusable int array for use in calls to {@link #scrollStep(int, int, int[])} so that the
-     * method may mutate it to "return" 2 ints.
-     */
-    final int[] mScrollStepConsumed = new int[2];
+    // Reusable int array to be passed to method calls that mutate it in order to "return" two ints.
+    final int[] mReusableIntPair = new int[2];
 
     /**
      * These are views that had their a11y importance changed during a layout. We defer these events
@@ -1919,14 +1917,18 @@
      * @return Whether any scroll was consumed in either direction.
      */
     boolean scrollByInternal(int x, int y, MotionEvent ev) {
-        int unconsumedX = 0, unconsumedY = 0;
-        int consumedX = 0, consumedY = 0;
+        int unconsumedX = 0;
+        int unconsumedY = 0;
+        int consumedX = 0;
+        int consumedY = 0;
 
         consumePendingUpdateOperations();
         if (mAdapter != null) {
-            scrollStep(x, y, mScrollStepConsumed);
-            consumedX = mScrollStepConsumed[0];
-            consumedY = mScrollStepConsumed[1];
+            mReusableIntPair[0] = 0;
+            mReusableIntPair[1] = 0;
+            scrollStep(x, y, mReusableIntPair);
+            consumedX = mReusableIntPair[0];
+            consumedY = mReusableIntPair[1];
             unconsumedX = x - consumedX;
             unconsumedY = y - consumedY;
         }
@@ -1934,17 +1936,23 @@
             invalidate();
         }
 
-        if (dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
-                TYPE_TOUCH)) {
-            // Update the last touch co-ords, taking any scroll offset into account
-            mLastTouchX -= mScrollOffset[0];
-            mLastTouchY -= mScrollOffset[1];
-            if (ev != null) {
-                ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
-            }
-            mNestedOffsets[0] += mScrollOffset[0];
-            mNestedOffsets[1] += mScrollOffset[1];
-        } else if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+        mReusableIntPair[0] = 0;
+        mReusableIntPair[1] = 0;
+        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
+                TYPE_TOUCH, mReusableIntPair);
+        unconsumedX -= mReusableIntPair[0];
+        unconsumedY -= mReusableIntPair[1];
+
+        // Update the last touch co-ords, taking any scroll offset into account
+        mLastTouchX -= mScrollOffset[0];
+        mLastTouchY -= mScrollOffset[1];
+        if (ev != null) {
+            ev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
+        }
+        mNestedOffsets[0] += mScrollOffset[0];
+        mNestedOffsets[1] += mScrollOffset[1];
+
+        if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
             if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {
                 pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);
             }
@@ -2439,18 +2447,26 @@
     void absorbGlows(int velocityX, int velocityY) {
         if (velocityX < 0) {
             ensureLeftGlow();
-            mLeftGlow.onAbsorb(-velocityX);
+            if (mLeftGlow.isFinished()) {
+                mLeftGlow.onAbsorb(-velocityX);
+            }
         } else if (velocityX > 0) {
             ensureRightGlow();
-            mRightGlow.onAbsorb(velocityX);
+            if (mRightGlow.isFinished()) {
+                mRightGlow.onAbsorb(velocityX);
+            }
         }
 
         if (velocityY < 0) {
             ensureTopGlow();
-            mTopGlow.onAbsorb(-velocityY);
+            if (mTopGlow.isFinished()) {
+                mTopGlow.onAbsorb(-velocityY);
+            }
         } else if (velocityY > 0) {
             ensureBottomGlow();
-            mBottomGlow.onAbsorb(velocityY);
+            if (mBottomGlow.isFinished()) {
+                mBottomGlow.onAbsorb(velocityY);
+            }
         }
 
         if (velocityX != 0 || velocityY != 0) {
@@ -3027,6 +3043,7 @@
                 if (mScrollState == SCROLL_STATE_SETTLING) {
                     getParent().requestDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
+                    stopNestedScroll(TYPE_NON_TOUCH);
                 }
 
                 // Clear the nested offsets
@@ -3168,9 +3185,11 @@
                 int dx = mLastTouchX - x;
                 int dy = mLastTouchY - y;
 
-                if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
-                    dx -= mScrollConsumed[0];
-                    dy -= mScrollConsumed[1];
+                mReusableIntPair[0] = 0;
+                mReusableIntPair[1] = 0;
+                if (dispatchNestedPreScroll(dx, dy, mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {
+                    dx -= mReusableIntPair[0];
+                    dy -= mReusableIntPair[1];
                     vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
                     // Updated the nested offsets
                     mNestedOffsets[0] += mScrollOffset[0];
@@ -5075,29 +5094,42 @@
             // cause unexpected behaviors
             final OverScroller scroller = mScroller;
             if (scroller.computeScrollOffset()) {
-                final int[] scrollConsumed = mScrollConsumed;
                 final int x = scroller.getCurrX();
                 final int y = scroller.getCurrY();
-                int dx = x - mLastFlingX;
-                int dy = y - mLastFlingY;
-                int hresult = 0;
-                int vresult = 0;
+                int unconsumedX = x - mLastFlingX;
+                int unconsumedY = y - mLastFlingY;
                 mLastFlingX = x;
                 mLastFlingY = y;
-                int overscrollX = 0, overscrollY = 0;
+                int consumedX = 0;
+                int consumedY = 0;
 
-                if (dispatchNestedPreScroll(dx, dy, scrollConsumed, null, TYPE_NON_TOUCH)) {
-                    dx -= scrollConsumed[0];
-                    dy -= scrollConsumed[1];
+                // Nested Pre Scroll
+                mReusableIntPair[0] = 0;
+                mReusableIntPair[1] = 0;
+                if (dispatchNestedPreScroll(unconsumedX, unconsumedY, mReusableIntPair, null,
+                        TYPE_NON_TOUCH)) {
+                    unconsumedX -= mReusableIntPair[0];
+                    unconsumedY -= mReusableIntPair[1];
                 }
 
-                if (mAdapter != null) {
-                    scrollStep(dx, dy, mScrollStepConsumed);
-                    hresult = mScrollStepConsumed[0];
-                    vresult = mScrollStepConsumed[1];
-                    overscrollX = dx - hresult;
-                    overscrollY = dy - vresult;
+                // Based on movement, we may want to trigger the hiding of existing over scroll
+                // glows.
+                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
+                    considerReleasingGlowsOnScroll(unconsumedX, unconsumedY);
+                }
 
+                // Local Scroll
+                if (mAdapter != null) {
+                    mReusableIntPair[0] = 0;
+                    mReusableIntPair[1] = 0;
+                    scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
+                    consumedX = mReusableIntPair[0];
+                    consumedY = mReusableIntPair[1];
+                    unconsumedX -= consumedX;
+                    unconsumedY -= consumedY;
+
+                    // If SmoothScroller exists, this ViewFlinger was started by it, so we must
+                    // report back to SmoothScroller.
                     SmoothScroller smoothScroller = mLayout.mSmoothScroller;
                     if (smoothScroller != null && !smoothScroller.isPendingInitialRun()
                             && smoothScroller.isRunning()) {
@@ -5106,62 +5138,57 @@
                             smoothScroller.stop();
                         } else if (smoothScroller.getTargetPosition() >= adapterSize) {
                             smoothScroller.setTargetPosition(adapterSize - 1);
-                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+                            smoothScroller.onAnimation(consumedX, consumedY);
                         } else {
-                            smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+                            smoothScroller.onAnimation(consumedX, consumedY);
                         }
                     }
                 }
+
                 if (!mItemDecorations.isEmpty()) {
                     invalidate();
                 }
-                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
-                    considerReleasingGlowsOnScroll(dx, dy);
-                }
 
-                if (!dispatchNestedScroll(hresult, vresult, overscrollX, overscrollY, null,
-                        TYPE_NON_TOUCH)
-                        && (overscrollX != 0 || overscrollY != 0)) {
-                    final int vel = (int) scroller.getCurrVelocity();
+                // Nested Post Scroll
+                mReusableIntPair[0] = 0;
+                mReusableIntPair[1] = 0;
+                dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
+                        TYPE_NON_TOUCH, mReusableIntPair);
+                unconsumedX -= mReusableIntPair[0];
+                unconsumedY -= mReusableIntPair[1];
 
-                    int velX = 0;
-                    if (overscrollX != x) {
-                        velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
-                    }
-
-                    int velY = 0;
-                    if (overscrollY != y) {
-                        velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
-                    }
-
-                    if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
-                        absorbGlows(velX, velY);
-                    }
-                    if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0)
-                            && (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
-                        scroller.abortAnimation();
-                    }
-                }
-                if (hresult != 0 || vresult != 0) {
-                    dispatchOnScrolled(hresult, vresult);
+                if (consumedX != 0 || consumedY != 0) {
+                    dispatchOnScrolled(consumedX, consumedY);
                 }
 
                 if (!awakenScrollBars()) {
                     invalidate();
                 }
 
-                final boolean fullyConsumedVertical = dy != 0 && mLayout.canScrollVertically()
-                        && vresult == dy;
-                final boolean fullyConsumedHorizontal = dx != 0 && mLayout.canScrollHorizontally()
-                        && hresult == dx;
-                final boolean fullyConsumedAny = (dx == 0 && dy == 0) || fullyConsumedHorizontal
-                        || fullyConsumedVertical;
+                // We are done scrolling if scroller is finished, or for both the x and y dimension,
+                // we are done scrolling or we can't scroll further (we know we can't scroll further
+                // when we have unconsumed scroll distance).  It's possible that we don't need
+                // to also check for scroller.isFinished() at all, but no harm in doing so in case
+                // of old bugs in Overscroller.
+                boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX();
+                boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY();
+                final boolean doneScrolling = scroller.isFinished()
+                        || ((scrollerFinishedX || unconsumedX != 0)
+                            && (scrollerFinishedY || unconsumedY != 0));
 
                 SmoothScroller smoothScroller = mLayout.mSmoothScroller;
-                boolean smoothScrollerPending = smoothScroller != null
-                        && smoothScroller.isPendingInitialRun();
-                if (!smoothScrollerPending && (scroller.isFinished() || (!fullyConsumedAny
-                        && !hasNestedScrollingParent(TYPE_NON_TOUCH)))) {
+                boolean smoothScrollerPending =
+                        smoothScroller != null && smoothScroller.isPendingInitialRun();
+
+                if (!smoothScrollerPending && doneScrolling) {
+                    // If we are done scrolling and the layout's SmoothScroller is not pending,
+                    // stop the scroll.
+
+                    final int vel = (int) scroller.getCurrVelocity();
+                    int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0;
+                    int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0;
+                    absorbGlows(velX, velY);
+
                     // setting state to idle will stop this.
                     setScrollState(SCROLL_STATE_IDLE);
                     if (ALLOW_THREAD_GAP_WORK) {
@@ -5169,9 +5196,11 @@
                     }
                     stopNestedScroll(TYPE_NON_TOUCH);
                 } else {
+                    // Otherwise continue the scroll.
+
                     postOnAnimation();
                     if (mGapWorker != null) {
-                        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
+                        mGapWorker.postFromTraversal(RecyclerView.this, unconsumedX, unconsumedY);
                     }
                 }
             }
@@ -11345,6 +11374,13 @@
     }
 
     @Override
+    public final void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
+            int dyUnconsumed, int[] offsetInWindow, int type, int[] consumed) {
+        getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
+                dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed);
+    }
+
+    @Override
     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
         return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
     }
diff --git a/samples/Support7Demos/src/main/AndroidManifest.xml b/samples/Support7Demos/src/main/AndroidManifest.xml
index 692ebf2..d7f1513 100644
--- a/samples/Support7Demos/src/main/AndroidManifest.xml
+++ b/samples/Support7Demos/src/main/AndroidManifest.xml
@@ -396,6 +396,24 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.RvInNestedScrollViewActivity"
+                  android:label="@string/rv_in_nestedScrollView"
+                  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.RvIn2NestedScrollViewsActivity"
+                  android:label="@string/rv_in_2_nestedScrollViews"
+                  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.PagerRecyclerViewActivity"
                   android:label="@string/pager_recycler_view"
                   android:theme="@style/Theme.AppCompat">
@@ -452,7 +470,7 @@
         </activity>
 
         <activity android:name=".widget.StableIdActivity"
-                  android:label="@string/recycler_view"
+                  android:label="@string/recycler_view_stableid"
                   android:theme="@style/Theme.AppCompat">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvIn2NestedScrollViewsActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvIn2NestedScrollViewsActivity.java
new file mode 100644
index 0000000..66a112e
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvIn2NestedScrollViewsActivity.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 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.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.android.supportv7.R;
+
+/**
+ * A sample nested RecyclerView activity.
+ */
+public class RvIn2NestedScrollViewsActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_rv_in_two_nestedscrollviews);
+
+        RecyclerView recyclerView = findViewById(R.id.rv);
+        recyclerView.setLayoutManager(new LinearLayoutManager(this));
+        recyclerView.setAdapter(new MyAdapter(this));
+    }
+
+    private static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
+
+        Context mContext;
+        int mItemMinHeight;
+
+        MyAdapter(Context context) {
+            mContext = context;
+            mItemMinHeight = (int) TypedValue.applyDimension(
+                    TypedValue.COMPLEX_UNIT_DIP, 96,
+                    context.getResources().getDisplayMetrics());
+        }
+
+        @Override
+        public MyViewHolder onCreateViewHolder(ViewGroup parent,
+                int viewType) {
+            TextView textView = new TextView(mContext);
+            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+            textView.setLayoutParams(layoutParams);
+            textView.setMinHeight(mItemMinHeight);
+            return new MyViewHolder(textView);
+        }
+
+        @Override
+        public void onBindViewHolder(MyViewHolder holder, int position) {
+            TextView textView = ((TextView) holder.itemView);
+            textView.setText("Position: " + position);
+            textView.setOnClickListener(
+                    v -> Toast.makeText(v.getContext(), "CLICK!", Toast.LENGTH_SHORT).show());
+        }
+
+        @Override
+        public int getItemCount() {
+            return 10;
+        }
+    }
+
+    private static class MyViewHolder extends RecyclerView.ViewHolder {
+
+        MyViewHolder(View itemView) {
+            super(itemView);
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvInNestedScrollViewActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvInNestedScrollViewActivity.java
new file mode 100644
index 0000000..6671131
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RvInNestedScrollViewActivity.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2018 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.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.android.supportv7.R;
+
+/**
+ * A sample nested RecyclerView activity.
+ */
+public class RvInNestedScrollViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_rv_in_nestedscrollview);
+
+        RecyclerView recyclerView = findViewById(R.id.rv);
+        recyclerView.setLayoutManager(new LinearLayoutManager(this));
+        recyclerView.setAdapter(new MyAdapter(this));
+    }
+
+    private static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
+
+        Context mContext;
+        int mItemMinHeight;
+
+        MyAdapter(Context context) {
+            mContext = context;
+            mItemMinHeight = (int) TypedValue.applyDimension(
+                    TypedValue.COMPLEX_UNIT_DIP, 96,
+                    context.getResources().getDisplayMetrics());
+        }
+
+        @Override
+        public MyViewHolder onCreateViewHolder(ViewGroup parent,
+                int viewType) {
+            TextView textView = new TextView(mContext);
+            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+            textView.setLayoutParams(layoutParams);
+            textView.setMinHeight(mItemMinHeight);
+            return new MyViewHolder(textView);
+        }
+
+        @Override
+        public void onBindViewHolder(MyViewHolder holder, int position) {
+            TextView textView = ((TextView) holder.itemView);
+            textView.setText("Position: " + position);
+            textView.setOnClickListener(
+                    v -> Toast.makeText(v.getContext(), "CLICK!", Toast.LENGTH_SHORT).show());
+        }
+
+        @Override
+        public int getItemCount() {
+            return 50;
+        }
+    }
+
+    private static class MyViewHolder extends RecyclerView.ViewHolder {
+
+        MyViewHolder(View itemView) {
+            super(itemView);
+        }
+    }
+}
diff --git a/samples/Support7Demos/src/main/res/layout/activity_rv_in_nestedscrollview.xml b/samples/Support7Demos/src/main/res/layout/activity_rv_in_nestedscrollview.xml
new file mode 100644
index 0000000..964849e
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/activity_rv_in_nestedscrollview.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 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.
+  -->
+
+<androidx.core.widget.NestedScrollView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+    <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv"
+            android:layout_width="match_parent"
+            android:layout_height="2000dp"/>
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/samples/Support7Demos/src/main/res/layout/activity_rv_in_two_nestedscrollviews.xml b/samples/Support7Demos/src/main/res/layout/activity_rv_in_two_nestedscrollviews.xml
new file mode 100644
index 0000000..eef496a
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/activity_rv_in_two_nestedscrollviews.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 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.
+  -->
+<androidx.core.widget.NestedScrollView
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:tag="outer">
+
+    <LinearLayout android:orientation="vertical"
+                  android:layout_width="match_parent"
+                  android:layout_height="wrap_content"
+                  android:background="#700">
+
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="100dp"
+                android:text="ONE"/>
+
+        <androidx.core.widget.NestedScrollView
+                android:layout_width="match_parent"
+                android:layout_height="500dp"
+                android:tag="inner">
+
+            <LinearLayout android:orientation="vertical"
+                          android:layout_width="match_parent"
+                          android:layout_height="wrap_content"
+                          android:background="#070">
+
+                <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="100dp"
+                        android:text="TWO"/>
+
+                <androidx.recyclerview.widget.RecyclerView
+                        android:background="#007"
+                        android:id="@+id/rv"
+                        android:layout_width="match_parent"
+                        android:layout_height="400dp"/>
+
+                <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="100dp"
+                        android:text="FOUR"/>
+
+            </LinearLayout>
+
+        </androidx.core.widget.NestedScrollView>
+
+        <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="100dp"
+                android:text="FIVE"/>
+
+    </LinearLayout>
+
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/samples/Support7Demos/src/main/res/values/strings.xml b/samples/Support7Demos/src/main/res/values/strings.xml
index c116600..9934d27 100644
--- a/samples/Support7Demos/src/main/res/values/strings.xml
+++ b/samples/Support7Demos/src/main/res/values/strings.xml
@@ -137,9 +137,12 @@
     <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
 
     <string name="recycler_view">RecyclerView/RecyclerViewActivity</string>
+    <string name="recycler_view_stableid">RecyclerView/Stable Ids</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="rv_in_nestedScrollView">RecyclerView/RV in NestedScrollView</string>
+    <string name="rv_in_2_nestedScrollViews">RecyclerView/RV in 2 NestedScrollViews</string>
     <string name="linear_layout_manager">RecyclerView/Linear Layout Manager</string>
     <string name="linear_layout_manager_jank">RecyclerView/Janky Linear Layout Manager</string>
     <string name="grid_layout_manager">RecyclerView/Grid Layout Manager</string>
diff --git a/testutils/build.gradle b/testutils/build.gradle
index be56003..f1fe241 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -18,6 +18,7 @@
 
 plugins {
     id("SupportAndroidLibraryPlugin")
+    id("kotlin-android")
 }
 
 dependencies {
@@ -30,6 +31,7 @@
     implementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     implementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     implementation(JUNIT)
+    implementation(KOTLIN_STDLIB)
 }
 
 android {
diff --git a/testutils/src/main/java/androidx/testutils/SimpleGestureGenerator.kt b/testutils/src/main/java/androidx/testutils/SimpleGestureGenerator.kt
new file mode 100644
index 0000000..72e7cf3
--- /dev/null
+++ b/testutils/src/main/java/androidx/testutils/SimpleGestureGenerator.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2018 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 androidx.testutils
+
+import android.content.Context
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewConfiguration
+import java.util.ArrayList
+import kotlin.math.ceil
+import kotlin.math.floor
+
+/** One [MotionEvent] approximately every 10 milliseconds. We care about this frequency because a
+ * standard touchscreen operates at 100 Hz and therefore produces about one touch event every
+ * 10 milliseconds.  We want to produce a similar frequency to emulate real world input events.*/
+const val MOTION_EVENT_INTERVAL_MILLIS: Int = 10
+
+/**
+ * Distance and time span necessary to produce a fling.
+ *
+ * Distance and time necessary to produce a fling for [MotionEvent]
+ *
+ * @property distance Distance between [MotionEvent]s in pixels for a fling.
+ * @property time Time between [MotionEvent]s in milliseconds for a fling.
+ */
+data class FlingData(val distance: Float, val time: Int) {
+
+    /**
+     * @property velocity Velocity of fling in pixels per millisecond.
+     */
+    val velocity: Float = distance / time
+}
+
+data class MotionEventData(
+    val eventTimeDelta: Int,
+    val action: Int,
+    val x: Float,
+    val y: Float,
+    val metaState: Int
+)
+
+enum class Direction {
+    UP, DOWN, LEFT, RIGHT
+}
+
+fun MotionEventData.toMotionEvent(downTime: Long): MotionEvent = MotionEvent.obtain(
+        downTime,
+        this.eventTimeDelta + downTime,
+        this.action,
+        this.x,
+        this.y,
+        this.metaState)
+
+/**
+ * Constructs a [FlingData] from a [Context].
+ */
+fun generateFlingData(context: Context): FlingData {
+    val configuration = ViewConfiguration.get(context)
+    val touchSlop = configuration.scaledTouchSlop
+    val minimumVelocity = configuration.scaledMinimumFlingVelocity
+    val maximumVelocity = configuration.scaledMaximumFlingVelocity
+
+    val targetPixelsPerMilli = ((maximumVelocity + minimumVelocity) / 2) / 1000f
+    val targetDistancePixels = touchSlop * 2
+    val targetMillisPassed = floor(targetDistancePixels / targetPixelsPerMilli).toInt()
+
+    if (targetMillisPassed < 1) {
+        throw IllegalArgumentException("Flings must require some time")
+    }
+
+    return FlingData(targetDistancePixels.toFloat(), targetMillisPassed)
+}
+
+/**
+ *  Returns [value] rounded up to the closest [interval] * N, where N is a Integer.
+ */
+private fun ceilToInterval(value: Int, interval: Int): Int =
+        ceil(value.toFloat() / interval).toInt() * interval
+
+/**
+ * Generates a [List] of [MotionEventData] starting from ([originX], [originY]) that will cause a
+ * fling in the finger direction [Direction].
+ */
+fun FlingData.generateFlingMotionEventData(
+    originX: Float,
+    originY: Float,
+    fingerDirection: Direction
+):
+    List<MotionEventData> {
+
+    // Ceiling the time and distance to match up with motion event intervals.
+    val time: Int = ceilToInterval(this.time, MOTION_EVENT_INTERVAL_MILLIS)
+    val distance: Float = velocity * time
+
+    val dx: Float = when (fingerDirection) {
+        Direction.LEFT -> -distance
+        Direction.RIGHT -> distance
+        else -> 0f
+    }
+    val dy: Float = when (fingerDirection) {
+        Direction.UP -> -distance
+        Direction.DOWN -> distance
+        else -> 0f
+    }
+    val toX = originX + dx
+    val toY = originY + dy
+
+    val numberOfInnerEvents = (time / MOTION_EVENT_INTERVAL_MILLIS) - 1
+    val dxIncrement = dx / (numberOfInnerEvents + 1)
+    val dyIncrement = dy / (numberOfInnerEvents + 1)
+
+    val motionEventData = ArrayList<MotionEventData>()
+    motionEventData.add(MotionEventData(0, MotionEvent.ACTION_DOWN, originX, originY, 0))
+    for (i in 1..(numberOfInnerEvents)) {
+        val timeDelta = i * MOTION_EVENT_INTERVAL_MILLIS
+        val x = i * dxIncrement
+        val y = i * dyIncrement
+        motionEventData.add(MotionEventData(timeDelta, MotionEvent.ACTION_MOVE, x, y, 0))
+    }
+    motionEventData.add(MotionEventData(time, MotionEvent.ACTION_MOVE, toX, toY, 0))
+    motionEventData.add(MotionEventData(time, MotionEvent.ACTION_UP, toX, toY, 0))
+
+    return motionEventData
+}
+
+/**
+ * Dispatches an array of [MotionEvent] to a [View].
+ *
+ * The MotionEvents will start at [downTime] and will be generated from the [motionEventData].
+ * The MotionEvents will be dispatched synchronously, one after the other, with no gaps of time
+ * in between each [MotionEvent].
+ *
+ */
+fun View.dispatchTouchEvents(downTime: Long, motionEventData: List<MotionEventData>) {
+    for (motionEventDataItem in motionEventData) {
+        dispatchTouchEvent(motionEventDataItem.toMotionEvent(downTime))
+    }
+}
+
+/**
+ * Simulates a fling on a [View].
+ *
+ * Convenience method that calls other public api.  See documentation of those functions for more
+ * detail.
+ *
+ * @see [generateFlingData]
+ * @see [generateFlingMotionEventData]
+ * @see [dispatchTouchEvents]
+ */
+fun View.simulateFling(
+    downTime: Long,
+    originX: Float,
+    originY: Float,
+    direction: Direction
+) {
+    dispatchTouchEvents(downTime,
+            generateFlingData(context).generateFlingMotionEventData(originX, originY, direction))
+}