Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 28b3b51c authored by George Mount's avatar George Mount
Browse files

Support fling back in scrolling containers

Bug: 232903223

When a user flings the overscroll toward the scrolling
content, the user might think that any left-over velocity
should apply to scrolling the content. This CL makes that
happen. If the fling is less than the overscroll distance,
then the normal spring is applied.

This CL changes ListView, ScrollView, and HorizontalScrollView.

Test: manual testing and new CTS tests
Change-Id: I1a759377f5183eec805598a6b016515297191624
parent 6fd0186c
Loading
Loading
Loading
Loading
+85 −3
Original line number Diff line number Diff line
@@ -241,6 +241,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
     */
    public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;

    /**
     * When flinging the stretch towards scrolling content, it should destretch quicker than the
     * fling would normally do. The visual effect of flinging the stretch looks strange as little
     * appears to happen at first and then when the stretch disappears, the content starts
     * scrolling quickly.
     */
    private static final float FLING_DESTRETCH_FACTOR = 4f;

    /**
     * The thread that created this view.
     */
@@ -4216,9 +4224,23 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
                        // fling further.
                        boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
                        if (flingVelocity && !mEdgeGlowTop.isFinished()) {
                            if (shouldAbsorb(mEdgeGlowTop, initialVelocity)) {
                                mEdgeGlowTop.onAbsorb(initialVelocity);
                            } else {
                                if (mFlingRunnable == null) {
                                    mFlingRunnable = new FlingRunnable();
                                }
                                mFlingRunnable.start(-initialVelocity);
                            }
                        } else if (flingVelocity && !mEdgeGlowBottom.isFinished()) {
                            if (shouldAbsorb(mEdgeGlowBottom, -initialVelocity)) {
                                mEdgeGlowBottom.onAbsorb(-initialVelocity);
                            } else {
                                if (mFlingRunnable == null) {
                                    mFlingRunnable = new FlingRunnable();
                                }
                                mFlingRunnable.start(-initialVelocity);
                            }
                        } else if (flingVelocity
                                && !((mFirstPosition == 0
                                && firstChildTop == contentTop - mOverscrollDistance)
@@ -4301,6 +4323,60 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
        }
    }

    /**
     * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should
     * animate with a fling. It will animate with a fling if the velocity will remove the
     * EdgeEffect through its normal operation.
     *
     * @param edgeEffect The EdgeEffect that might absorb the velocity.
     * @param velocity The velocity of the fling motion
     * @return true if the velocity should be absorbed or false if it should be flung.
     */
    private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) {
        if (velocity > 0) {
            return true;
        }
        float distance = edgeEffect.getDistance() * getHeight();

        // This is flinging without the spring, so let's see if it will fling past the overscroll
        if (mFlingRunnable == null) {
            mFlingRunnable = new FlingRunnable();
        }
        float flingDistance = mFlingRunnable.getSplineFlingDistance(-velocity);

        return flingDistance < distance;
    }

    /**
     * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for
     * consuming deltas from EdgeEffects
     * @param unconsumed The unconsumed delta that the EdgeEffets may consume
     * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
     */
    private int consumeFlingInStretch(int unconsumed) {
        if (unconsumed < 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) {
            int size = getHeight();
            float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowTop.finish();
            }
            return unconsumed - consumed;
        }
        if (unconsumed > 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) {
            int size = getHeight();
            float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowBottom.finish();
            }
            return unconsumed - consumed;
        }
        return unconsumed;
    }

    private boolean shouldDisplayEdgeEffects() {
        return getOverScrollMode() != OVER_SCROLL_NEVER;
    }
@@ -4783,6 +4859,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
            mScroller = new OverScroller(getContext());
        }

        float getSplineFlingDistance(int velocity) {
            return (float) mScroller.getSplineFlingDistance(velocity);
        }

        // Use AbsListView#fling(int) instead
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
        void start(int initialVelocity) {
@@ -4905,6 +4985,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
                }

                if (mItemCount == 0 || getChildCount() == 0) {
                    mEdgeGlowBottom.onRelease();
                    mEdgeGlowTop.onRelease();
                    endFling();
                    return;
                }
@@ -4915,7 +4997,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te

                // Flip sign to convert finger direction to list items direction
                // (e.g. finger moving down means list is moving towards the top)
                int delta = mLastFlingY - y;
                int delta = consumeFlingInStretch(mLastFlingY - y);

                // Pretend that each frame of a fling scroll is a touch scroll
                if (delta > 0) {
+77 −5
Original line number Diff line number Diff line
@@ -77,6 +77,14 @@ public class HorizontalScrollView extends FrameLayout {

    private static final String TAG = "HorizontalScrollView";

    /**
     * When flinging the stretch towards scrolling content, it should destretch quicker than the
     * fling would normally do. The visual effect of flinging the stretch looks strange as little
     * appears to happen at first and then when the stretch disappears, the content starts
     * scrolling quickly.
     */
    private static final float FLING_DESTRETCH_FACTOR = 4f;

    private long mLastScroll;

    private final Rect mTempRect = new Rect();
@@ -1456,18 +1464,19 @@ public class HorizontalScrollView extends FrameLayout {
            int oldY = mScrollY;
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            int deltaX = consumeFlingInStretch(x - oldX);

            if (oldX != x || oldY != y) {
            if (deltaX != 0 || oldY != y) {
                final int range = getScrollRange();
                final int overscrollMode = getOverScrollMode();
                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
                overScrollBy(deltaX, y - oldY, oldX, oldY, range, 0,
                        mOverflingDistance, 0, false);
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);

                if (canOverscroll) {
                if (canOverscroll && deltaX != 0) {
                    if (x < 0 && oldX >= 0) {
                        mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
                    } else if (x > range && oldX <= range) {
@@ -1482,6 +1491,36 @@ public class HorizontalScrollView extends FrameLayout {
        }
    }

    /**
     * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for
     * consuming deltas from EdgeEffects
     * @param unconsumed The unconsumed delta that the EdgeEffets may consume
     * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
     */
    private int consumeFlingInStretch(int unconsumed) {
        if (unconsumed > 0 && mEdgeGlowLeft != null && mEdgeGlowLeft.getDistance() != 0f) {
            int size = getWidth();
            float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowLeft.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowLeft.finish();
            }
            return unconsumed - consumed;
        }
        if (unconsumed < 0 && mEdgeGlowRight != null && mEdgeGlowRight.getDistance() != 0f) {
            int size = getWidth();
            float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowRight.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowRight.finish();
            }
            return unconsumed - consumed;
        }
        return unconsumed;
    }

    /**
     * Scrolls the view to the given child.
     *
@@ -1746,11 +1785,23 @@ public class HorizontalScrollView extends FrameLayout {

            int maxScroll = Math.max(0, right - width);

            boolean shouldFling = false;
            if (mScrollX == 0 && !mEdgeGlowLeft.isFinished()) {
                if (shouldAbsorb(mEdgeGlowLeft, -velocityX)) {
                    mEdgeGlowLeft.onAbsorb(-velocityX);
                } else {
                    shouldFling = true;
                }
            } else if (mScrollX == maxScroll && !mEdgeGlowRight.isFinished()) {
                if (shouldAbsorb(mEdgeGlowRight, velocityX)) {
                    mEdgeGlowRight.onAbsorb(velocityX);
                } else {
                    shouldFling = true;
                }
            } else {
                shouldFling = true;
            }
            if (shouldFling) {
                mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
                        maxScroll, 0, 0, width / 2, 0);

@@ -1773,6 +1824,27 @@ public class HorizontalScrollView extends FrameLayout {
        }
    }

    /**
     * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should
     * animate with a fling. It will animate with a fling if the velocity will remove the
     * EdgeEffect through its normal operation.
     *
     * @param edgeEffect The EdgeEffect that might absorb the velocity.
     * @param velocity The velocity of the fling motion
     * @return true if the velocity should be absorbed or false if it should be flung.
     */
    private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) {
        if (velocity > 0) {
            return true;
        }
        float distance = edgeEffect.getDistance() * getWidth();

        // This is flinging without the spring, so let's see if it will fling past the overscroll
        float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity);

        return flingDistance < distance;
    }

    /**
     * {@inheritDoc}
     *
+4 −0
Original line number Diff line number Diff line
@@ -527,6 +527,10 @@ public class OverScroller {
                Math.signum(yvel) == Math.signum(dy);
    }

    double getSplineFlingDistance(int velocity) {
        return mScrollerY.getSplineFlingDistance(velocity);
    }

    static class SplineOverScroller {
        // Initial position
        private int mStart;
+73 −5
Original line number Diff line number Diff line
@@ -85,6 +85,14 @@ public class ScrollView extends FrameLayout {

    private static final String TAG = "ScrollView";

    /**
     * When flinging the stretch towards scrolling content, it should destretch quicker than the
     * fling would normally do. The visual effect of flinging the stretch looks strange as little
     * appears to happen at first and then when the stretch disappears, the content starts
     * scrolling quickly.
     */
    private static final float FLING_DESTRETCH_FACTOR = 4f;

    @UnsupportedAppUsage
    private long mLastScroll;

@@ -1488,18 +1496,19 @@ public class ScrollView extends FrameLayout {
            int oldY = mScrollY;
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            int deltaY = consumeFlingInStretch(y - oldY);

            if (oldX != x || oldY != y) {
            if (oldX != x || deltaY != 0) {
                final int range = getScrollRange();
                final int overscrollMode = getOverScrollMode();
                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                overScrollBy(x - oldX, deltaY, oldX, oldY, 0, range,
                        0, mOverflingDistance, false);
                onScrollChanged(mScrollX, mScrollY, oldX, oldY);

                if (canOverscroll) {
                if (canOverscroll && deltaY != 0) {
                    if (y < 0 && oldY >= 0) {
                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                    } else if (y > range && oldY <= range) {
@@ -1520,6 +1529,36 @@ public class ScrollView extends FrameLayout {
        }
    }

    /**
     * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for
     * consuming deltas from EdgeEffects
     * @param unconsumed The unconsumed delta that the EdgeEffets may consume
     * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume.
     */
    private int consumeFlingInStretch(int unconsumed) {
        if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) {
            int size = getHeight();
            float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowTop.finish();
            }
            return unconsumed - consumed;
        }
        if (unconsumed < 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) {
            int size = getHeight();
            float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size;
            int consumed = Math.round(size / FLING_DESTRETCH_FACTOR
                    * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f));
            if (consumed != unconsumed) {
                mEdgeGlowBottom.finish();
            }
            return unconsumed - consumed;
        }
        return unconsumed;
    }

    /**
     * Scrolls the view to the given child.
     *
@@ -1803,14 +1842,43 @@ public class ScrollView extends FrameLayout {
                fling(velocityY);
            } else if (!consumed) {
                if (!mEdgeGlowTop.isFinished()) {
                    if (shouldAbsorb(mEdgeGlowTop, -velocityY)) {
                        mEdgeGlowTop.onAbsorb(-velocityY);
                    } else {
                        fling(velocityY);
                    }
                } else if (!mEdgeGlowBottom.isFinished()) {
                    if (shouldAbsorb(mEdgeGlowBottom, velocityY)) {
                        mEdgeGlowBottom.onAbsorb(velocityY);
                    } else {
                        fling(velocityY);
                    }
                }
            }
        }
    }

    /**
     * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should
     * animate with a fling. It will animate with a fling if the velocity will remove the
     * EdgeEffect through its normal operation.
     *
     * @param edgeEffect The EdgeEffect that might absorb the velocity.
     * @param velocity The velocity of the fling motion
     * @return true if the velocity should be absorbed or false if it should be flung.
     */
    private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) {
        if (velocity > 0) {
            return true;
        }
        float distance = edgeEffect.getDistance() * getHeight();

        // This is flinging without the spring, so let's see if it will fling past the overscroll
        float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity);

        return flingDistance < distance;
    }

    @UnsupportedAppUsage
    private void endDrag() {
        mIsBeingDragged = false;