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
This commit is contained in:
@@ -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()) {
|
||||
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<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,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}
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user