diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 184e7bca963b7..1cb96b15b8ef2 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -241,6 +241,14 @@ public abstract class AbsListView extends AdapterView 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 implements Te // fling further. boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity; if (flingVelocity && !mEdgeGlowTop.isFinished()) { - mEdgeGlowTop.onAbsorb(initialVelocity); + if (shouldAbsorb(mEdgeGlowTop, initialVelocity)) { + mEdgeGlowTop.onAbsorb(initialVelocity); + } else { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } + mFlingRunnable.start(-initialVelocity); + } } else if (flingVelocity && !mEdgeGlowBottom.isFinished()) { - mEdgeGlowBottom.onAbsorb(-initialVelocity); + 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 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 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 implements Te } if (mItemCount == 0 || getChildCount() == 0) { + mEdgeGlowBottom.onRelease(); + mEdgeGlowTop.onRelease(); endFling(); return; } @@ -4915,7 +4997,7 @@ public abstract class AbsListView extends AdapterView 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) { diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 2dbfd7e5b2e26..1d6778b8a4a96 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -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()) { - mEdgeGlowLeft.onAbsorb(-velocityX); + if (shouldAbsorb(mEdgeGlowLeft, -velocityX)) { + mEdgeGlowLeft.onAbsorb(-velocityX); + } else { + shouldFling = true; + } } else if (mScrollX == maxScroll && !mEdgeGlowRight.isFinished()) { - mEdgeGlowRight.onAbsorb(velocityX); + 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} * diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index 1683878cd8b23..d00fc1c89d8fb 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -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; diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 2acd50c9e169b..84b6f65ffea63 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -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()) { - mEdgeGlowTop.onAbsorb(-velocityY); + if (shouldAbsorb(mEdgeGlowTop, -velocityY)) { + mEdgeGlowTop.onAbsorb(-velocityY); + } else { + fling(velocityY); + } } else if (!mEdgeGlowBottom.isFinished()) { - mEdgeGlowBottom.onAbsorb(velocityY); + 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;