Implements flinging for anchor-based scrolling.

Test: atest SystemUITests, manual
Change-Id: I636dbbc4faf1dde0a97be5ac4d323fc813e2e05b
This commit is contained in:
Gus Prevas
2019-01-14 14:29:44 -05:00
parent 0fa58d6cc6
commit cdc98344d8

View File

@@ -183,6 +183,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
private VelocityTracker mVelocityTracker;
private OverScroller mScroller;
/** Last Y position reported by {@link #mScroller}, used to calculate scroll delta. */
private int mLastScrollerY;
/**
* True if the max position was set to a known position on the last call to {@link #mScroller}.
*/
private boolean mIsScrollerBoundSet;
private Runnable mFinishScrollingCallback;
private int mTouchSlop;
private int mMinimumVelocity;
@@ -425,7 +431,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
private int mStatusBarState;
private int mCachedBackgroundColor;
private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
private Runnable mAnimateScroll = this::animateScroll;
private Runnable mReflingAndAnimateScroll = () -> {
if (ANCHOR_SCROLLING) {
maybeReflingScroller();
}
animateScroll();
};
private int mCornerRadius;
private int mSidePaddings;
private final Rect mBackgroundAnimationRect = new Rect();
@@ -698,6 +709,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
int y = (int) mShelf.getTranslationY();
canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
}
canvas.drawText(Integer.toString(getMaxNegativeScrollAmount()), getWidth() - 100,
getIntrinsicPadding() + 30, mDebugPaint);
canvas.drawText(Integer.toString(getMaxPositiveScrollAmount()), getWidth() - 100,
getHeight() - 30, mDebugPaint);
}
}
@@ -1643,11 +1658,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
// TODO: once we're recycling this will need to check the adapter position of the child
ExpandableView lastRow = getLastRowNotGone();
if (!lastRow.isInShelf()) {
float distanceToMax =
Math.max(0, lastRow.getTranslationY() + lastRow.getActualHeight()
- (mMaxLayoutHeight - mFooterView.getActualHeight()));
if (distanceToMax < scrollAmount) {
if (lastRow != null && !lastRow.isInShelf()) {
float distanceToMax = Math.max(0, getMaxPositiveScrollAmount());
if (scrollAmount > distanceToMax) {
float currentBottomPixels = getCurrentOverScrolledPixels(false);
// We overScroll on the bottom
setOverScrolledPixels(currentBottomPixels + (scrollAmount - distanceToMax),
@@ -1760,7 +1773,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
private void animateScroll() {
if (mScroller.computeScrollOffset()) {
if (ANCHOR_SCROLLING) {
// TODO
int oldY = mLastScrollerY;
int y = mScroller.getCurrY();
int deltaY = y - oldY;
if (deltaY != 0) {
int maxNegativeScrollAmount = getMaxNegativeScrollAmount();
int maxPositiveScrollAmount = getMaxPositiveScrollAmount();
if ((maxNegativeScrollAmount < 0 && deltaY < maxNegativeScrollAmount)
|| (maxPositiveScrollAmount > 0 && deltaY > maxPositiveScrollAmount)) {
// This frame takes us into overscroll, so set the max overscroll based on
// the current velocity
setMaxOverScrollFromCurrentVelocity();
}
customOverScrollBy(deltaY, oldY, 0, (int) mMaxOverScroll);
mLastScrollerY = y;
}
} else {
int oldY = mOwnScrollY;
int y = mScroller.getCurrY();
@@ -1768,10 +1795,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
if (oldY != y) {
int range = getScrollRange();
if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
float currVelocity = mScroller.getCurrVelocity();
if (currVelocity >= mMinimumVelocity) {
mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
}
// This frame takes us into overscroll, so set the max overscroll based on
// the current velocity
setMaxOverScrollFromCurrentVelocity();
}
if (mDontClampNextScroll) {
@@ -1782,7 +1808,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
}
postOnAnimation(mAnimateScroll);
postOnAnimation(mReflingAndAnimateScroll);
} else {
mDontClampNextScroll = false;
if (mFinishScrollingCallback != null) {
@@ -1791,13 +1817,51 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
}
private void setMaxOverScrollFromCurrentVelocity() {
float currVelocity = mScroller.getCurrVelocity();
if (currVelocity >= mMinimumVelocity) {
mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
}
}
/**
* Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta
* would cause us to exceed the provided maximum overscroll, springs back instead.
*
* This method performs the determination of whether we're exceeding the overscroll and clamps
* the scroll amount if so. The actual scrolling/overscrolling happens in
* {@link #onCustomOverScrolled(int, boolean)} (absolute scrolling) or
* {@link #onCustomOverScrolledBy(int, boolean)} (anchor scrolling).
*
* @param deltaY The (signed) number of pixels to scroll.
* @param scrollY The current scroll position (absolute scrolling only).
* @param scrollRangeY The maximum allowable scroll position (absolute scrolling only).
* @param maxOverScrollY The current (unsigned) limit on number of pixels to overscroll by.
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private boolean customOverScrollBy(int deltaY, int scrollY, int scrollRangeY,
int maxOverScrollY) {
private void customOverScrollBy(int deltaY, int scrollY, int scrollRangeY, int maxOverScrollY) {
if (ANCHOR_SCROLLING) {
// TODO check clamped?
onCustomOverScrolledBy(deltaY);
return false;
boolean clampedY = false;
if (deltaY < 0) {
int maxScrollAmount = getMaxNegativeScrollAmount();
if (maxScrollAmount > Integer.MIN_VALUE) {
maxScrollAmount -= maxOverScrollY;
if (deltaY < maxScrollAmount) {
deltaY = maxScrollAmount;
clampedY = true;
}
}
} else {
int maxScrollAmount = getMaxPositiveScrollAmount();
if (maxScrollAmount < Integer.MAX_VALUE) {
maxScrollAmount += maxOverScrollY;
if (deltaY > maxScrollAmount) {
deltaY = maxScrollAmount;
clampedY = true;
}
}
}
onCustomOverScrolledBy(deltaY, clampedY);
} else {
int newScrollY = scrollY + deltaY;
final int top = -maxOverScrollY;
@@ -1813,8 +1877,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
onCustomOverScrolled(newScrollY, clampedY);
return clampedY;
}
}
@@ -1933,16 +1995,43 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
}
private void onCustomOverScrolledBy(int deltaY) {
/**
* Scrolls by the given delta, overscrolling if needed. If called during a fling and the delta
* would cause us to exceed the provided maximum overscroll, springs back instead.
*
* @param deltaY The (signed) number of pixels to scroll.
* @param clampedY Whether this value was clamped by the calling method, meaning we've reached
* the overscroll limit.
*/
private void onCustomOverScrolledBy(int deltaY, boolean clampedY) {
assert ANCHOR_SCROLLING;
mScrollAnchorViewY -= deltaY;
// Treat animating scrolls differently; see #computeScroll() for why.
if (!mScroller.isFinished()) {
// TODO: springback/overscroll, see #onCustomOverScrolled()
if (clampedY) {
springBack();
} else {
float overScrollTop = getCurrentOverScrollAmount(true /* top */);
if (isScrolledToTop() && mScrollAnchorViewY > 0) {
notifyOverscrollTopListener(mScrollAnchorViewY,
isRubberbanded(true /* onTop */));
} else {
notifyOverscrollTopListener(overScrollTop, isRubberbanded(true /* onTop */));
}
}
}
updateScrollAnchor();
updateOnScrollChange();
}
/**
* Scrolls to the given position, overscrolling if needed. If called during a fling and the
* position exceeds the provided maximum overscroll, springs back instead.
*
* @param scrollY The target scroll position.
* @param clampedY Whether this value was clamped by the calling method, meaning we've reached
* the overscroll limit.
*/
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private void onCustomOverScrolled(int scrollY, boolean clampedY) {
assert !ANCHOR_SCROLLING;
@@ -1964,10 +2053,30 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
}
/**
* Springs back from an overscroll by stopping the {@link #mScroller} and animating the
* overscroll amount back to zero.
*/
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private void springBack() {
if (ANCHOR_SCROLLING) {
// TODO
boolean overScrolledTop = isScrolledToTop() && mScrollAnchorViewY > 0;
int maxPositiveScrollAmount = getMaxPositiveScrollAmount();
boolean overscrolledBottom = maxPositiveScrollAmount < 0;
if (overScrolledTop || overscrolledBottom) {
float newAmount;
if (overScrolledTop) {
newAmount = mScrollAnchorViewY;
mScrollAnchorViewY = 0;
mDontReportNextOverScroll = true;
} else {
newAmount = -maxPositiveScrollAmount;
mScrollAnchorViewY -= maxPositiveScrollAmount;
}
setOverScrollAmount(newAmount, overScrolledTop, false);
setOverScrollAmount(0.0f, overScrolledTop, true);
mScroller.forceFinished(true);
}
} else {
int scrollRange = getScrollRange();
boolean overScrolledTop = mOwnScrollY <= 0;
@@ -2563,7 +2672,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
mMaxOverScroll = 0.0f;
}
if (ANCHOR_SCROLLING) {
// TODO
flingScroller(velocityY);
} else {
int scrollRange = getScrollRange();
int minScrollY = Math.max(0, scrollRange);
@@ -2578,6 +2687,121 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
}
}
/**
* Flings the overscroller with the given velocity (anchor-based scrolling).
*
* Because anchor-based scrolling can't track the current scroll position, the overscroller is
* always started at startY = 0, and we interpret the positions it computes as relative to the
* start of the scroll.
*/
private void flingScroller(int velocityY) {
assert ANCHOR_SCROLLING;
mIsScrollerBoundSet = false;
maybeFlingScroller(velocityY, true /* always fling */);
}
private void maybeFlingScroller(int velocityY, boolean alwaysFling) {
assert ANCHOR_SCROLLING;
// Attempt to determine the maximum amount to scroll before we reach the end.
// If the first view is not materialized (for an upwards scroll) or the last view is either
// not materialized or is pinned to the shade (for a downwards scroll), we don't know this
// amount, so we do an unbounded fling and rely on {@link #maybeReflingScroller()} to update
// the scroller once we approach the start/end of the list.
int minY = Integer.MIN_VALUE;
int maxY = Integer.MAX_VALUE;
if (velocityY < 0) {
minY = getMaxNegativeScrollAmount();
if (minY > Integer.MIN_VALUE) {
mIsScrollerBoundSet = true;
}
} else {
maxY = getMaxPositiveScrollAmount();
if (maxY < Integer.MAX_VALUE) {
mIsScrollerBoundSet = true;
}
}
if (mIsScrollerBoundSet || alwaysFling) {
mLastScrollerY = 0;
// x velocity is set to 1 to avoid overscroller bug
mScroller.fling(0, 0, 1, velocityY, 0, 0, minY, maxY, 0,
mExpandedInThisMotion && !isScrolledToTop() ? 0 : Integer.MAX_VALUE / 2);
}
}
/**
* Returns the maximum number of pixels we can scroll in the positive direction (downwards)
* before reaching the bottom of the list (discounting overscroll).
*
* If the return value is negative then we have overscrolled; this is a transient state which
* should immediately be handled by adjusting the anchor position and adding the extra space to
* the bottom overscroll amount.
*
* If we don't know how many pixels we have left to scroll (because the last row has not been
* materialized, or it's in the shelf so it doesn't have its "natural" position), we return
* {@link Integer#MAX_VALUE}.
*/
private int getMaxPositiveScrollAmount() {
assert ANCHOR_SCROLLING;
// TODO: once we're recycling we need to check the adapter position of the last child.
ExpandableNotificationRow lastRow = getLastRowNotGone();
if (mScrollAnchorView != null && lastRow != null && !lastRow.isInShelf()) {
// distance from bottom of last child to bottom of notifications area is:
// distance from bottom of last child
return (int) (lastRow.getTranslationY() + lastRow.getActualHeight()
// to top of anchor view
- mScrollAnchorView.getTranslationY()
// plus distance from anchor view to top of notifications area
+ mScrollAnchorViewY
// minus height of notifications area.
- (mMaxLayoutHeight - getIntrinsicPadding() - mFooterView.getActualHeight()));
} else {
return Integer.MAX_VALUE;
}
}
/**
* Returns the maximum number of pixels (as a negative number) we can scroll in the negative
* direction (upwards) before reaching the top of the list (discounting overscroll).
*
* If the return value is positive then we have overscrolled; this is a transient state which
* should immediately be handled by adjusting the anchor position and adding the extra space to
* the top overscroll amount.
*
* If we don't know how many pixels we have left to scroll (because the first row has not been
* materialized), we return {@link Integer#MIN_VALUE}.
*/
private int getMaxNegativeScrollAmount() {
assert ANCHOR_SCROLLING;
// TODO: once we're recycling we need to check the adapter position of the first child.
ExpandableView firstChild = getFirstChildNotGone();
if (mScrollAnchorView != null && firstChild != null) {
// distance from top of first child to top of notifications area is:
// distance from top of anchor view
return (int) -(mScrollAnchorView.getTranslationY()
// to top of first child
- firstChild.getTranslationY()
// minus distance from top of anchor view to top of notifications area.
- mScrollAnchorViewY);
} else {
return Integer.MIN_VALUE;
}
}
/**
* During a fling, if we were unable to set the bounds of the fling due to the top/bottom view
* not being materialized or being pinned to the shelf, we need to check on every frame if we're
* able to set the bounds. If we are, we fling the scroller again with the newly computed
* bounds.
*/
private void maybeReflingScroller() {
if (!mIsScrollerBoundSet) {
// Because mScroller is a flywheel scroller, we fling with the minimum possible
// velocity to establish direction, so as not to perceptibly affect the velocity.
maybeFlingScroller((int) Math.signum(mScroller.getCurrVelocity()),
false /* alwaysFling */);
}
}
/**
* @return Whether a fling performed on the top overscroll edge lead to the expanded
* overScroll view (i.e QS).
@@ -4100,18 +4324,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
public boolean isScrolledToBottom() {
if (ANCHOR_SCROLLING) {
// TODO: once we're recycling this will need to check the adapter position of the child
ExpandableView lastRow = getLastRowNotGone();
if (lastRow == null || mScrollAnchorView == null) {
return true;
}
if (lastRow.isInShelf()) {
return false;
}
float lastChildEnd = lastRow.getTranslationY() + lastRow.getActualHeight();
float lastChildEndDistanceFromTop =
lastChildEnd - mScrollAnchorView.getTranslationY() + mScrollAnchorViewY;
return lastChildEndDistanceFromTop > mMaxLayoutHeight - mFooterView.getActualHeight();
return getMaxPositiveScrollAmount() <= 0;
} else {
return mOwnScrollY >= getScrollRange();
}