Adding an overloaded smoothScrollBy with duration.

Bug: 118339555
Test: RecyclerViewLayoutTest

Change-Id: I9ac51d68bc2b18071a5d5457d17be39cb0c736a8
diff --git a/recyclerview/recyclerview/api/1.1.0-alpha05.txt b/recyclerview/recyclerview/api/1.1.0-alpha05.txt
index aea607d..95bc7c3 100644
--- a/recyclerview/recyclerview/api/1.1.0-alpha05.txt
+++ b/recyclerview/recyclerview/api/1.1.0-alpha05.txt
@@ -446,6 +446,7 @@
     method public void setViewCacheExtension(androidx.recyclerview.widget.RecyclerView.ViewCacheExtension?);
     method public void smoothScrollBy(@Px int, @Px int);
     method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?);
+    method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?, int);
     method public void smoothScrollToPosition(int);
     method public boolean startNestedScroll(int, int);
     method public void stopNestedScroll(int);
@@ -461,6 +462,7 @@
     field public static final int SCROLL_STATE_SETTLING = 2; // 0x2
     field public static final int TOUCH_SLOP_DEFAULT = 0; // 0x0
     field public static final int TOUCH_SLOP_PAGING = 1; // 0x1
+    field public static final int UNDEFINED_DURATION = -2147483648; // 0x80000000
     field public static final int VERTICAL = 1; // 0x1
   }
 
diff --git a/recyclerview/recyclerview/api/1.1.0-alpha06.txt b/recyclerview/recyclerview/api/1.1.0-alpha06.txt
index aea607d..95bc7c3 100644
--- a/recyclerview/recyclerview/api/1.1.0-alpha06.txt
+++ b/recyclerview/recyclerview/api/1.1.0-alpha06.txt
@@ -446,6 +446,7 @@
     method public void setViewCacheExtension(androidx.recyclerview.widget.RecyclerView.ViewCacheExtension?);
     method public void smoothScrollBy(@Px int, @Px int);
     method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?);
+    method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?, int);
     method public void smoothScrollToPosition(int);
     method public boolean startNestedScroll(int, int);
     method public void stopNestedScroll(int);
@@ -461,6 +462,7 @@
     field public static final int SCROLL_STATE_SETTLING = 2; // 0x2
     field public static final int TOUCH_SLOP_DEFAULT = 0; // 0x0
     field public static final int TOUCH_SLOP_PAGING = 1; // 0x1
+    field public static final int UNDEFINED_DURATION = -2147483648; // 0x80000000
     field public static final int VERTICAL = 1; // 0x1
   }
 
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index aea607d..95bc7c3 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -446,6 +446,7 @@
     method public void setViewCacheExtension(androidx.recyclerview.widget.RecyclerView.ViewCacheExtension?);
     method public void smoothScrollBy(@Px int, @Px int);
     method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?);
+    method public void smoothScrollBy(@Px int, @Px int, android.view.animation.Interpolator?, int);
     method public void smoothScrollToPosition(int);
     method public boolean startNestedScroll(int, int);
     method public void stopNestedScroll(int);
@@ -461,6 +462,7 @@
     field public static final int SCROLL_STATE_SETTLING = 2; // 0x2
     field public static final int TOUCH_SLOP_DEFAULT = 0; // 0x0
     field public static final int TOUCH_SLOP_PAGING = 1; // 0x1
+    field public static final int UNDEFINED_DURATION = -2147483648; // 0x80000000
     field public static final int VERTICAL = 1; // 0x1
   }
 
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
index fd5cbcf..f6fe43c 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewLayoutTest.java
@@ -2633,6 +2633,189 @@
     }
 
     @Test
+    public void smoothScrollBy_negativeDuration_completesSynchronously() throws Throwable {
+        smoothScrollBy_completesSynchronously(-1);
+    }
+
+    @Test
+    public void smoothScrollBy_durationOf0_completesSynchronously() throws Throwable {
+        smoothScrollBy_completesSynchronously(0);
+    }
+
+    private void smoothScrollBy_completesSynchronously(final int duration) throws Throwable {
+        // Arrange
+
+        RecyclerView recyclerView = new RecyclerView(getActivity());
+        recyclerView.setAdapter(new TestAdapter(1000));
+        recyclerView.setLayoutManager(new TestLayoutManager());
+        setRecyclerView(recyclerView);
+        getInstrumentation().waitForIdleSync();
+
+        final int[] onScrolledCallCount = new int[1];
+        final int[] onScrolledTotalScrolled = new int[1];
+        final boolean[] stateChangeCalled = new boolean[1];
+
+        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+
+            @Override
+            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+                stateChangeCalled[0] = true;
+            }
+
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                onScrolledCallCount[0]++;
+                onScrolledTotalScrolled[0] += dy;
+            }
+        });
+
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Act
+                mRecyclerView.smoothScrollBy(0, 100, null, duration);
+
+                // Assert
+                assertEquals(1, onScrolledCallCount[0]);
+                assertEquals(100, onScrolledTotalScrolled[0]);
+                assertFalse(stateChangeCalled[0]);
+            }
+        });
+    }
+
+    @Test
+    public void smoothScrollBy_durationOf1_completesAsynchronously() throws Throwable {
+        smoothScrollBy_completesAsynchronously(1);
+    }
+
+    @Test
+    public void smoothScrollBy_durationOfUndefined_completesAsynchronously() throws Throwable {
+        smoothScrollBy_completesAsynchronously(RecyclerView.UNDEFINED_DURATION);
+    }
+
+    private void smoothScrollBy_completesAsynchronously(final int duration) throws Throwable {
+
+        // Arrange
+
+        RecyclerView recyclerView = new RecyclerView(getActivity());
+        recyclerView.setAdapter(new TestAdapter(1000));
+        recyclerView.setLayoutManager(new TestLayoutManager());
+        setRecyclerView(recyclerView);
+        getInstrumentation().waitForIdleSync();
+
+        final int[] onScrolledCallCount = new int[1];
+        final int[] onScrolledTotalScrolled = new int[1];
+        final ArrayList<Integer> onScrollStateChangedStates = new ArrayList<>();
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+
+            @Override
+            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+                onScrollStateChangedStates.add(newState);
+                if (newState == SCROLL_STATE_IDLE) {
+                    latch.countDown();
+                }
+            }
+
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                onScrolledTotalScrolled[0] += dy;
+                onScrolledCallCount[0]++;
+            }
+        });
+
+        // Act
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+
+                // Act
+                mRecyclerView.smoothScrollBy(0, 100, null, duration);
+
+                // Assert that only onScrollStateChange has been called with settling.
+                assertEquals(0, onScrolledCallCount[0]);
+                assertEquals(1, onScrollStateChangedStates.size());
+                assertEquals(SCROLL_STATE_SETTLING, (int) onScrollStateChangedStates.get(0));
+            }
+        });
+        latch.await(5, TimeUnit.SECONDS);
+
+        // Assert that we did indeed finish
+        assertEquals(100, onScrolledTotalScrolled[0]);
+        assertEquals(2, onScrollStateChangedStates.size());
+        assertEquals(SCROLL_STATE_IDLE, (int) onScrollStateChangedStates.get(1));
+    }
+
+    @Test
+    public void smoothScrollBy_fastDurationIsFasterThanSlowDuration() throws Throwable {
+
+        // Arrange
+
+        final RecyclerView recyclerView0 = new RecyclerView(getActivity());
+        recyclerView0.setAdapter(new TestAdapter(1000));
+        recyclerView0.setLayoutManager(new TestLayoutManager());
+
+        final RecyclerView recyclerView1 = new RecyclerView(getActivity());
+        recyclerView1.setAdapter(new TestAdapter(1000));
+        recyclerView1.setLayoutManager(new TestLayoutManager());
+
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getActivity().getContainer().addView(recyclerView0);
+                getActivity().getContainer().addView(recyclerView1);
+            }
+        });
+
+        getInstrumentation().waitForIdleSync();
+
+        final int[] totalScrolled = new int[]{0, 0};
+        final ArrayList<Integer> completionOrder = new ArrayList<>();
+        final CountDownLatch latch = new CountDownLatch(2);
+
+        recyclerView0.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                super.onScrolled(recyclerView, dx, dy);
+                totalScrolled[0] += dy;
+                if (totalScrolled[0] == 100) {
+                    completionOrder.add(0);
+                    latch.countDown();
+                }
+            }
+        });
+
+        recyclerView1.addOnScrollListener(new RecyclerView.OnScrollListener() {
+            @Override
+            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+                super.onScrolled(recyclerView, dx, dy);
+                totalScrolled[1] += dy;
+                if (totalScrolled[1] == 100) {
+                    completionOrder.add(1);
+                    latch.countDown();
+                }
+            }
+        });
+
+        // Act
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                recyclerView0.smoothScrollBy(0, 100, null, 100);
+                recyclerView1.smoothScrollBy(0, 100, null, 1000);
+            }
+        });
+        latch.await(5, TimeUnit.SECONDS);
+
+        // Assert
+        assertEquals(0, (int) completionOrder.get(0));
+        assertEquals(1, (int) completionOrder.get(1));
+    }
+
+    @Test
     public void scrollStateForSmoothScroll() throws Throwable {
         TestAdapter testAdapter = new TestAdapter(10);
         TestLayoutManager tlm = new TestLayoutManager();
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 51374b5..85eab5f 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -290,7 +290,10 @@
      */
     public static final int TOUCH_SLOP_PAGING = 1;
 
-    static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
+    /**
+     * Constant that represents that a duration has not been defined.
+     */
+    public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
 
     static final int MAX_SCROLL_DURATION = 2000;
 
@@ -2314,9 +2317,27 @@
      * @param dx Pixels to scroll horizontally
      * @param dy Pixels to scroll vertically
      * @param interpolator {@link Interpolator} to be used for scrolling. If it is
-     *                     {@code null}, RecyclerView is going to use the default interpolator.
+     *                     {@code null}, RecyclerView will use an internal default interpolator.
      */
     public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator) {
+        smoothScrollBy(dx, dy, interpolator, UNDEFINED_DURATION);
+    }
+
+    /**
+     * Smooth scrolls the RecyclerView by a given distance.
+     *
+     * @param dx x distance in pixels.
+     * @param dy y distance in pixels.
+     * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null},
+     *                     RecyclerView will use an internal default interpolator.
+     * @param duration Duration of the animation in milliseconds. Set to {@link #UNDEFINED_DURATION}
+     *                 to have the duration be automatically calculated based on an internally
+     *                 defined standard initial velocity. A duration less than 1 (that does not
+     *                 equal UNDEFINED_DURATION), will result in a call to
+     *                 {@link #scrollBy(int, int)}.
+     */
+    public void smoothScrollBy(@Px int dx, @Px int dy, @Nullable Interpolator interpolator,
+            int duration) {
         if (mLayout == null) {
             Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                     + "Call setLayoutManager with a non-null argument.");
@@ -2332,7 +2353,12 @@
             dy = 0;
         }
         if (dx != 0 || dy != 0) {
-            mViewFlinger.smoothScrollBy(dx, dy, UNDEFINED_DURATION, interpolator);
+            boolean durationSuggestsAnimation = duration == UNDEFINED_DURATION || duration > 0;
+            if (durationSuggestsAnimation) {
+                mViewFlinger.smoothScrollBy(dx, dy, duration, interpolator);
+            } else {
+                scrollBy(dx, dy);
+            }
         }
     }
 
@@ -5134,6 +5160,7 @@
                 || mAdapterHelper.hasPendingUpdates();
     }
 
+    // Effectively private.  Set to default to avoid synthetic accessor.
     class ViewFlinger implements Runnable {
         private int mLastFlingX;
         private int mLastFlingY;
@@ -5330,6 +5357,17 @@
             postOnAnimation();
         }
 
+        /**
+         * Smooth scrolls the RecyclerView by a given distance.
+         *
+         * @param dx x distance in pixels.
+         * @param dy y distance in pixels.
+         * @param duration Duration of the animation in milliseconds. Set to
+         *                 {@link #UNDEFINED_DURATION} to have the duration automatically calculated
+         *                 based on an internally defined standard velocity.
+         * @param interpolator {@link Interpolator} to be used for scrolling. If it is {@code null},
+         *                     RecyclerView will use an internal default interpolator.
+         */
         public void smoothScrollBy(int dx, int dy, int duration,
                 @Nullable Interpolator interpolator) {
 
diff --git a/samples/Support7Demos/src/main/AndroidManifest.xml b/samples/Support7Demos/src/main/AndroidManifest.xml
index 769c2fe..cd51a31 100644
--- a/samples/Support7Demos/src/main/AndroidManifest.xml
+++ b/samples/Support7Demos/src/main/AndroidManifest.xml
@@ -444,6 +444,15 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".widget.RecyclerViewSmoothScrollByActivity"
+            android:label="@string/recycler_view_smooth_scroll_by"
+            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.RvInNestedScrollViewActivity"
                   android:label="@string/rv_in_nestedScrollView"
                   android:theme="@style/Theme.AppCompat">
diff --git a/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewSmoothScrollByActivity.java b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewSmoothScrollByActivity.java
new file mode 100644
index 0000000..7e84c3f
--- /dev/null
+++ b/samples/Support7Demos/src/main/java/com/example/android/supportv7/widget/RecyclerViewSmoothScrollByActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019 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.os.Bundle;
+import android.text.Editable;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.EditText;
+
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.example.android.supportv7.Cheeses;
+import com.example.android.supportv7.R;
+import com.example.android.supportv7.widget.adapter.SimpleStringAdapter;
+
+/**
+ * Simple activity to test {@link RecyclerView#smoothScrollBy(int, int, Interpolator, int)}
+ * functionality.
+ */
+public class RecyclerViewSmoothScrollByActivity extends Activity {
+
+    private RecyclerView mRecyclerView;
+    private EditText mEditText;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.activity_rv_smoothscrollby);
+
+        mRecyclerView = findViewById(R.id.recyclerView);
+        mEditText = findViewById(R.id.editTextDuration);
+
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
+        mRecyclerView.setAdapter(new SimpleStringAdapter(this, Cheeses.sCheeseStrings) {
+            @Override
+            public ViewHolder onCreateViewHolder(ViewGroup parent,
+                    int viewType) {
+                final ViewHolder vh = super
+                        .onCreateViewHolder(parent, viewType);
+                return vh;
+            }
+        });
+        mRecyclerView
+                .addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
+
+        findViewById(R.id.buttonUp).setOnClickListener(v -> scroll(false));
+        findViewById(R.id.buttonDown).setOnClickListener(v -> scroll(true));
+    }
+
+    private void scroll(boolean down) {
+        int duration = 100;
+        Editable editable = mEditText.getText();
+        if (editable != null) {
+            duration = Integer.parseInt(editable.toString());
+        }
+        mRecyclerView.smoothScrollBy(0, down ? 1000 : -1000, null, duration);
+    }
+}
diff --git a/samples/Support7Demos/src/main/res/layout/activity_rv_smoothscrollby.xml b/samples/Support7Demos/src/main/res/layout/activity_rv_smoothscrollby.xml
new file mode 100644
index 0000000..311d3ad
--- /dev/null
+++ b/samples/Support7Demos/src/main/res/layout/activity_rv_smoothscrollby.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2019 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.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <EditText
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/editTextDuration"
+        android:text="100"/>
+
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="down"
+        android:id="@+id/buttonDown"/>
+
+    <Button
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="up"
+        android:id="@+id/buttonUp"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:id="@+id/recyclerView" />
+
+
+</LinearLayout>
\ 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 cb961ec..b87603e 100644
--- a/samples/Support7Demos/src/main/res/values/strings.xml
+++ b/samples/Support7Demos/src/main/res/values/strings.xml
@@ -148,6 +148,7 @@
     <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string>
 
     <string name="recycler_view">RecyclerView/RecyclerViewActivity</string>
+    <string name="recycler_view_smooth_scroll_by">RecyclerView/RecyclerView SmoothScrollBy Activity</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>