summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Gus Prevas <kprevas@google.com> 2019-01-14 14:29:44 -0500
committer Gus Prevas <kprevas@google.com> 2019-01-22 11:41:48 -0500
commitcdc98344d89af0fc8b3bd7a135f68d66fbb580b3 (patch)
tree4238c0e6079d974616061baa33268fcfb3c792ad
parent0fa58d6cc6333fd5c334d215ac02f00f73c10987 (diff)
Implements flinging for anchor-based scrolling.
Test: atest SystemUITests, manual Change-Id: I636dbbc4faf1dde0a97be5ac4d323fc813e2e05b
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java283
1 files changed, 248 insertions, 35 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 99002b2edb7e..7fa152e3d7da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -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);
@@ -2579,6 +2688,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();
}