From cdc98344d89af0fc8b3bd7a135f68d66fbb580b3 Mon Sep 17 00:00:00 2001 From: Gus Prevas Date: Mon, 14 Jan 2019 14:29:44 -0500 Subject: Implements flinging for anchor-based scrolling. Test: atest SystemUITests, manual Change-Id: I636dbbc4faf1dde0a97be5ac4d323fc813e2e05b --- .../stack/NotificationStackScrollLayout.java | 283 ++++++++++++++++++--- 1 file 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); @@ -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(); } -- cgit v1.2.3-59-g8ed1b