Merge "RecyclerView: add APIs to query Scroller progress" into oc-support-26.0-dev
diff --git a/api/26.0.0-SNAPSHOT.txt b/api/26.0.0-SNAPSHOT.txt
index e173069..657fba6 100644
--- a/api/26.0.0-SNAPSHOT.txt
+++ b/api/26.0.0-SNAPSHOT.txt
@@ -12508,6 +12508,8 @@
     method public boolean didStructureChange();
     method public <T> T get(int);
     method public int getItemCount();
+    method public int getRemainingScrollHorizontal();
+    method public int getRemainingScrollVertical();
     method public int getTargetScrollPosition();
     method public boolean hasTargetScrollPosition();
     method public boolean isMeasuring();
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index cb57d08..8d2d7bd 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -1738,6 +1738,7 @@
             eatRequestLayout();
             onEnterLayoutOrScroll();
             TraceCompat.beginSection(TRACE_SCROLL_TAG);
+            fillRemainingScrollValues(mState);
             if (x != 0) {
                 consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                 unconsumedX = x - consumedX;
@@ -3548,6 +3549,17 @@
         return lastKnownId;
     }
 
+    final void fillRemainingScrollValues(State state) {
+        if (getScrollState() == SCROLL_STATE_SETTLING) {
+            final OverScroller scroller = mViewFlinger.mScroller;
+            state.mRemainingScrollHorizontal = scroller.getFinalX() - scroller.getCurrX();
+            state.mRemainingScrollVertical = scroller.getFinalY() - scroller.getCurrY();
+        } else {
+            state.mRemainingScrollHorizontal = 0;
+            state.mRemainingScrollVertical = 0;
+        }
+    }
+
     /**
      * The first step of a layout where we;
      * - process adapter updates
@@ -3557,6 +3569,7 @@
      */
     private void dispatchLayoutStep1() {
         mState.assertLayoutStep(State.STEP_START);
+        fillRemainingScrollValues(mState);
         mState.mIsMeasuring = false;
         eatRequestLayout();
         mViewInfoStore.clear();
@@ -4793,6 +4806,7 @@
                     eatRequestLayout();
                     onEnterLayoutOrScroll();
                     TraceCompat.beginSection(TRACE_SCROLL_TAG);
+                    fillRemainingScrollValues(mState);
                     if (dx != 0) {
                         hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
                         overscrollX = dx - hresult;
@@ -11584,6 +11598,7 @@
             }
         };
     }
+
     /**
      * <p>Contains useful information about the current RecyclerView state like target scroll
      * position or view focus. State object can also keep arbitrary data, identified by resource
@@ -11673,6 +11688,9 @@
         // that one instead
         int mFocusedSubChildId;
 
+        int mRemainingScrollHorizontal;
+        int mRemainingScrollVertical;
+
         ////////////////////////////////////////////////////////////////////////////////////////////
 
         State reset() {
@@ -11850,6 +11868,28 @@
                     : mItemCount;
         }
 
+        /**
+         * Returns remaining horizontal scroll distance of an ongoing scroll animation(fling/
+         * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is
+         * other than {@link #SCROLL_STATE_SETTLING}.
+         *
+         * @return Remaining horizontal scroll distance
+         */
+        public int getRemainingScrollHorizontal() {
+            return mRemainingScrollHorizontal;
+        }
+
+        /**
+         * Returns remaining vertical scroll distance of an ongoing scroll animation(fling/
+         * smoothScrollTo/SmoothScroller) in pixels. Returns zero if {@link #getScrollState()} is
+         * other than {@link #SCROLL_STATE_SETTLING}.
+         *
+         * @return Remaining vertical scroll distance
+         */
+        public int getRemainingScrollVertical() {
+            return mRemainingScrollVertical;
+        }
+
         @Override
         public String toString() {
             return "State{"
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 38b5345..be687c3 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -4571,4 +4571,74 @@
         }
     }
 
+    @Test
+    public void testRemainingScrollInLayout() throws Throwable {
+        final RecyclerView recyclerView = new RecyclerView(getActivity());
+        final TestAdapter adapter = new TestAdapter(100);
+
+        final CountDownLatch firstScrollDone = new CountDownLatch(1);
+        final CountDownLatch scrollFinished = new CountDownLatch(1);
+        final int[] totalScrollDistance = new int[] {0};
+        recyclerView.setLayoutManager(new TestLayoutManager() {
+            @Override
+            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+                if (firstScrollDone.getCount() < 1 && scrollFinished.getCount() == 1) {
+                    try {
+                        assertTrue("layout pass has remaining scroll",
+                                state.getRemainingScrollVertical() != 0);
+                        assertEquals("layout pass has remaining scroll",
+                                1000 - totalScrollDistance[0], state.getRemainingScrollVertical());
+                    } catch (Throwable throwable) {
+                        postExceptionToInstrumentation(throwable);
+                    }
+                }
+                super.onLayoutChildren(recycler, state);
+            }
+
+            @Override
+            public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+                    RecyclerView.State state) {
+                firstScrollDone.countDown();
+                totalScrollDistance[0] += dy;
+                if (state.getRemainingScrollVertical() == 0) {
+                    // the last scroll pass will have remaining 0
+                    scrollFinished.countDown();
+                }
+                return super.scrollVerticallyBy(dy, recycler, state);
+            }
+
+            @Override
+            public boolean canScrollVertically() {
+                return true;
+            }
+        });
+        recyclerView.setAdapter(adapter);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    setRecyclerView(recyclerView);
+                    recyclerView.smoothScrollBy(0, 1000);
+                } catch (Throwable throwable) {
+                    postExceptionToInstrumentation(throwable);
+                }
+            }
+        });
+
+        firstScrollDone.await(1, TimeUnit.SECONDS);
+        mActivityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    recyclerView.requestLayout();
+                } catch (Throwable throwable) {
+                    postExceptionToInstrumentation(throwable);
+                }
+            }
+        });
+        waitForIdleScroll(recyclerView);
+        assertTrue(scrollFinished.getCount() < 1);
+        assertEquals(totalScrollDistance[0], 1000);
+    }
+
 }