[go: nahoru, domu]

Add stretch overscroll to ViewPager

Bug: 171228096

Relnote: "Adds stretch overscroll support to ViewPager"

Test: new tests. ran existing ViewPager tests.

Change-Id: I79d4e0a9dadac5b855ad2504271b2e7723479efc
diff --git a/jetifier/jetifier/migration.config b/jetifier/jetifier/migration.config
index 75d8ce3..f591f9f 100644
--- a/jetifier/jetifier/migration.config
+++ b/jetifier/jetifier/migration.config
@@ -1041,6 +1041,10 @@
       "to": "android/support/v4/widget/annotations"
     },
     {
+      "from": "androidx/viewpager/widget/annotations",
+      "to": "android/support/v4/view/annotations"
+    },
+    {
       "from": "androidx/annotation/experimental/(.*)",
       "to": "ignore"
     }
diff --git a/viewpager/viewpager/api/current.txt b/viewpager/viewpager/api/current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/current.txt
+++ b/viewpager/viewpager/api/current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/api/public_plus_experimental_current.txt b/viewpager/viewpager/api/public_plus_experimental_current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/public_plus_experimental_current.txt
+++ b/viewpager/viewpager/api/public_plus_experimental_current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/api/restricted_current.txt b/viewpager/viewpager/api/restricted_current.txt
index c0a4ddd..e8231bd 100644
--- a/viewpager/viewpager/api/restricted_current.txt
+++ b/viewpager/viewpager/api/restricted_current.txt
@@ -63,6 +63,7 @@
     method public void fakeDragBy(float);
     method public androidx.viewpager.widget.PagerAdapter? getAdapter();
     method public int getCurrentItem();
+    method public int getEdgeEffectType();
     method public int getOffscreenPageLimit();
     method public int getPageMargin();
     method public boolean isDragInGutterEnabled();
@@ -76,6 +77,7 @@
     method public void setCurrentItem(int);
     method public void setCurrentItem(int, boolean);
     method public void setDragInGutterEnabled(boolean);
+    method public void setEdgeEffectType(int);
     method public void setOffscreenPageLimit(int);
     method @Deprecated public void setOnPageChangeListener(androidx.viewpager.widget.ViewPager.OnPageChangeListener!);
     method public void setPageMargin(int);
diff --git a/viewpager/viewpager/build.gradle b/viewpager/viewpager/build.gradle
index 7194986..e10cfb8 100644
--- a/viewpager/viewpager/build.gradle
+++ b/viewpager/viewpager/build.gradle
@@ -17,7 +17,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    implementation("androidx.core:core:1.3.0-beta01")
+    implementation project(":core:core")
     api("androidx.customview:customview:1.0.0")
 
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
@@ -27,6 +27,7 @@
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation project(':internal-testutils-espresso')
 }
 
 androidx {
diff --git a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
index 15cfc63..58663e6 100644
--- a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
+++ b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/BaseViewPagerTest.java
@@ -23,6 +23,7 @@
 import static android.support.v4.testutils.TestUtilsMatchers.startAlignedToParent;
 
 import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
 import static androidx.test.espresso.action.ViewActions.pressKey;
 import static androidx.test.espresso.action.ViewActions.swipeLeft;
 import static androidx.test.espresso.action.ViewActions.swipeRight;
@@ -54,6 +55,7 @@
 
 import android.app.Activity;
 import android.graphics.Color;
+import android.os.Build;
 import android.support.v4.testutils.TestUtilsMatchers;
 import android.text.TextUtils;
 import android.util.Pair;
@@ -66,10 +68,15 @@
 
 import androidx.test.espresso.ViewAction;
 import androidx.test.espresso.action.EspressoKey;
+import androidx.test.espresso.action.GeneralLocation;
+import androidx.test.espresso.action.GeneralSwipeAction;
+import androidx.test.espresso.action.Press;
+import androidx.test.espresso.action.Swipe;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.MediumTest;
 import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.TranslatedCoordinatesProvider;
 import androidx.viewpager.test.R;
 
 import org.junit.After;
@@ -94,6 +101,13 @@
     @Rule
     public final ActivityTestRule<T> mActivityTestRule;
 
+    /**
+     * The distance of a swipe's start position from the view's edge, in terms of the view's length.
+     * We do not start the swipe exactly on the view's edge, but somewhat more inward, since swiping
+     * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer).
+     */
+    private static final float EDGE_FUZZ_FACTOR = 0.083f;
+
     private static final int DIRECTION_LEFT = -1;
     private static final int DIRECTION_RIGHT = 1;
     protected ViewPager mViewPager;
@@ -418,8 +432,13 @@
         onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeLeft()));
         assertEquals("Swipe twice left", 2, mViewPager.getCurrentItem());
 
-        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()), ViewPagerActions.wrap(swipeRight()));
-        assertEquals("Swipe left beyond last page and then right", 1, mViewPager.getCurrentItem());
+        onView(withId(R.id.pager)).perform(ViewPagerActions.wrap(swipeLeft()),
+                ViewPagerActions.wrap(slowSwipeRight()));
+        // On S and above, the swipe right will be absorbed by the EdgeEffect created during
+        // swipe left.
+        int leftRightPage = isSOrHigher() ? 2 : 1;
+        assertEquals("Swipe left beyond last page and then right", leftRightPage,
+                mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(
                 ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeRight()));
@@ -427,8 +446,47 @@
                 mViewPager.getCurrentItem());
 
         onView(withId(R.id.pager)).perform(
-                ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(swipeLeft()));
-        assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem());
+                ViewPagerActions.wrap(swipeRight()), ViewPagerActions.wrap(slowSwipeLeft()));
+        // On S and above, the swipe left will be absorbed by the EdgeEffect created during
+        // swipe right.
+        int rightLeftPage = isSOrHigher() ? 0 : 1;
+        assertEquals("Swipe right beyond first page and then left", rightLeftPage,
+                mViewPager.getCurrentItem());
+    }
+
+    /**
+     * Returns an action that performs a slow swipe left-to-right across the vertical center of the
+     * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br>
+     */
+    public static ViewAction slowSwipeRight() {
+        return actionWithAssertions(
+                new GeneralSwipeAction(
+                        Swipe.SLOW,
+                        new TranslatedCoordinatesProvider(
+                                GeneralLocation.CENTER_LEFT, EDGE_FUZZ_FACTOR, 0),
+                        GeneralLocation.CENTER_RIGHT,
+                        Press.FINGER));
+    }
+
+    /**
+     * Returns an action that performs a slow swipe left-to-right across the vertical center of the
+     * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br>
+     */
+    public static ViewAction slowSwipeLeft() {
+        return actionWithAssertions(
+                new GeneralSwipeAction(
+                        Swipe.SLOW,
+                        new TranslatedCoordinatesProvider(
+                                GeneralLocation.CENTER_RIGHT, -EDGE_FUZZ_FACTOR, 0),
+                        GeneralLocation.CENTER_LEFT,
+                        Press.FINGER));
+    }
+
+    public static boolean isSOrHigher() {
+        // TODO(b/181171227): Simplify this
+        int sdk = Build.VERSION.SDK_INT;
+        return sdk > Build.VERSION_CODES.R
+                || (sdk == Build.VERSION_CODES.R && Build.VERSION.PREVIEW_SDK_INT != 0);
     }
 
     private void verifyPageContent(boolean smoothScroll) {
diff --git a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
index 78f5a07..f72022c 100644
--- a/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
+++ b/viewpager/viewpager/src/androidTest/java/androidx/viewpager/widget/ViewPagerTest.java
@@ -16,6 +16,9 @@
 
 package androidx.viewpager.widget;
 
+import static androidx.viewpager.widget.BaseViewPagerTest.isSOrHigher;
+
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -23,12 +26,14 @@
 import android.os.Bundle;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.EdgeEffect;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
+import androidx.viewpager.test.R;
 
 import org.junit.Rule;
 import org.junit.Test;
@@ -71,6 +76,29 @@
         assertTrue(adapter.primaryCalled);
     }
 
+    @Test
+    public void testEdgeEffectType() throws Throwable {
+        activityRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                activityRule.getActivity().setContentView(R.layout.view_pager_with_stretch);
+            }
+        });
+        ViewPager viewPager = (ViewPager) activityRule.getActivity().findViewById(R.id.pager);
+        if (isSOrHigher()) {
+            // Starts out as stretch because the attribute is set
+            assertEquals(EdgeEffect.TYPE_STRETCH, viewPager.getEdgeEffectType());
+            // Set the type to glow
+            viewPager.setEdgeEffectType(EdgeEffect.TYPE_GLOW);
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+        } else {
+            // Earlier versions only support glow
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+            viewPager.setEdgeEffectType(EdgeEffect.TYPE_STRETCH);
+            assertEquals(EdgeEffect.TYPE_GLOW, viewPager.getEdgeEffectType());
+        }
+    }
+
     static final class PrimaryItemPagerAdapter extends PagerAdapter {
         public volatile int count;
         public volatile boolean primaryCalled;
diff --git a/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml b/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml
new file mode 100644
index 0000000..377370a
--- /dev/null
+++ b/viewpager/viewpager/src/androidTest/res/layout-v31/view_pager_with_stretch.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.viewpager.widget.ViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:edgeEffectType="stretch"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
+
diff --git a/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
new file mode 100644
index 0000000..15f3e79
--- /dev/null
+++ b/viewpager/viewpager/src/androidTest/res/layout/view_pager_with_stretch.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.viewpager.widget.ViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"/>
+
diff --git a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
index 76b4a6b..788f4b8 100644
--- a/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
+++ b/viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java
@@ -16,6 +16,9 @@
 
 package androidx.viewpager.widget;
 
+import static android.widget.EdgeEffect.TYPE_GLOW;
+import static android.widget.EdgeEffect.TYPE_STRETCH;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -46,15 +49,18 @@
 
 import androidx.annotation.CallSuper;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.Px;
+import androidx.annotation.RestrictTo;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.Insets;
 import androidx.core.view.AccessibilityDelegateCompat;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.WindowInsetsCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.widget.EdgeEffectCompat;
 import androidx.customview.view.AbsSavedState;
 
 import java.lang.annotation.ElementType;
@@ -125,6 +131,13 @@
         android.R.attr.layout_gravity
     };
 
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @IntDef({TYPE_GLOW, TYPE_STRETCH})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EdgeEffectType {
+    }
+
     /**
      * Used to track what the expected number of items in the adapter should be.
      * If the app changes this when we don't expect it, we'll throw a big obnoxious exception.
@@ -391,19 +404,18 @@
 
     public ViewPager(@NonNull Context context) {
         super(context);
-        initViewPager();
+        initViewPager(context, null);
     }
 
     public ViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
-        initViewPager();
+        initViewPager(context, attrs);
     }
 
-    void initViewPager() {
+    void initViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
         setWillNotDraw(false);
         setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
         setFocusable(true);
-        final Context context = getContext();
         mScroller = new Scroller(context, sInterpolator);
         final ViewConfiguration configuration = ViewConfiguration.get(context);
         final float density = context.getResources().getDisplayMetrics().density;
@@ -411,8 +423,8 @@
         mTouchSlop = configuration.getScaledPagingTouchSlop();
         mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
-        mLeftEdge = new EdgeEffect(context);
-        mRightEdge = new EdgeEffect(context);
+        mLeftEdge = EdgeEffectCompat.create(context, attrs);
+        mRightEdge = EdgeEffectCompat.create(context, attrs);
 
         mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
         mCloseEnough = (int) (CLOSE_ENOUGH * density);
@@ -477,6 +489,27 @@
                 });
     }
 
+    /**
+     * Returns the {@link EdgeEffect#getType()} for the edge effects.
+     * @return the {@link EdgeEffect#getType()} for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    @EdgeEffectType
+    public int getEdgeEffectType() {
+        return EdgeEffectCompat.getType(mLeftEdge);
+    }
+
+    /**
+     * Sets the {@link EdgeEffect#setType(int)} for the edge effects.
+     * @param type The edge effect type to use for the edge effects.
+     * @attr ref android.R.styleable#EdgeEffect_edgeEffectType
+     */
+    public void setEdgeEffectType(@EdgeEffectType int type) {
+        EdgeEffectCompat.setType(mLeftEdge, type);
+        EdgeEffectCompat.setType(mRightEdge, type);
+        invalidate();
+    }
+
     @Override
     protected void onDetachedFromWindow() {
         removeCallbacks(mEndScrollRunnable);
@@ -2107,7 +2140,7 @@
                 }
                 if (mIsBeingDragged) {
                     // Scroll to follow the motion event
-                    if (performDrag(x)) {
+                    if (performDrag(x, y)) {
                         ViewCompat.postInvalidateOnAnimation(this);
                     }
                 }
@@ -2135,6 +2168,18 @@
                     mIsBeingDragged = true;
                     requestParentDisallowInterceptTouchEvent(true);
                     setScrollState(SCROLL_STATE_DRAGGING);
+                } else if (EdgeEffectCompat.getDistance(mLeftEdge) != 0
+                        || EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                    // Caught the edge glow animation
+                    mIsBeingDragged = true;
+                    setScrollState(SCROLL_STATE_DRAGGING);
+                    if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mLeftEdge, 0f,
+                                1 - mLastMotionY / getHeight());
+                    }
+                    if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+                        EdgeEffectCompat.onPullDistance(mRightEdge, 0f, mLastMotionY / getHeight());
+                    }
                 } else {
                     completeScroll(false);
                     mIsBeingDragged = false;
@@ -2243,7 +2288,7 @@
                     // Scroll to follow the motion event
                     final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                     final float x = ev.getX(activePointerIndex);
-                    needsInvalidate |= performDrag(x);
+                    needsInvalidate |= performDrag(x, ev.getY());
                 }
                 break;
             case MotionEvent.ACTION_UP:
@@ -2310,11 +2355,42 @@
         }
     }
 
-    private boolean performDrag(float x) {
+    /**
+     * If either of the horizontal edge glows are currently active, this consumes part or all of
+     * deltaX on the edge glow.
+     *
+     * @param deltaX The pointer motion, in pixels, in the horizontal direction, positive
+     *                         for moving down and negative for moving up.
+     * @param y The vertical position of the pointer.
+     * @return The amount of <code>deltaX</code> that has been consumed by the
+     * edge glow.
+     */
+    private float releaseHorizontalGlow(float deltaX, float y) {
+        // First allow releasing existing overscroll effect:
+        float consumed = 0;
+        float displacement = y / getHeight();
+        float pullDistance = (float) deltaX / getWidth();
+        if (EdgeEffectCompat.getDistance(mLeftEdge) != 0) {
+            consumed = -EdgeEffectCompat.onPullDistance(mLeftEdge, -pullDistance, 1 - displacement);
+        } else if (EdgeEffectCompat.getDistance(mRightEdge) != 0) {
+            consumed = EdgeEffectCompat.onPullDistance(mRightEdge, pullDistance, displacement);
+        }
+        return consumed * getWidth();
+    }
+
+    private boolean performDrag(float x, float y) {
         boolean needsInvalidate = false;
 
-        final float deltaX = mLastMotionX - x;
+        final float dX = mLastMotionX - x;
         mLastMotionX = x;
+        final float releaseConsumed = releaseHorizontalGlow(dX, y);
+        final float deltaX = dX - releaseConsumed;
+        if (releaseConsumed != 0) {
+            needsInvalidate = true;
+        }
+        if (Math.abs(deltaX) < 0.0001f) { // ignore rounding errors from releaseHorizontalGlow()
+            return needsInvalidate;
+        }
 
         float oldScrollX = getScrollX();
         float scrollX = oldScrollX + deltaX;
@@ -2339,14 +2415,14 @@
         if (scrollX < leftBound) {
             if (leftAbsolute) {
                 float over = leftBound - scrollX;
-                mLeftEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mLeftEdge, over / width, 1 - y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = leftBound;
         } else if (scrollX > rightBound) {
             if (rightAbsolute) {
                 float over = scrollX - rightBound;
-                mRightEdge.onPull(Math.abs(over) / width);
+                EdgeEffectCompat.onPullDistance(mRightEdge, over / width, y / getHeight());
                 needsInvalidate = true;
             }
             scrollX = rightBound;
@@ -2407,7 +2483,9 @@
 
     private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) {
         int targetPage;
-        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
+        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity
+                && EdgeEffectCompat.getDistance(mLeftEdge) == 0 // don't fling while stretched
+                && EdgeEffectCompat.getDistance(mRightEdge) == 0) {
             targetPage = velocity > 0 ? currentPage : currentPage + 1;
         } else {
             final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f;