Introduced overscrolling for the new notifications

Implemented basic support for overscrolling of the new
notifications.

Change-Id: Ie1c43a4f5efbd025614c33bcb8c03a4238fada75
This commit is contained in:
Selim Cinek
2014-05-12 15:13:04 +02:00
parent a5eaa6034d
commit 8d9ff9c2c6
4 changed files with 273 additions and 63 deletions

View File

@@ -28,6 +28,8 @@ public class AmbientState {
private int mScrollY;
private boolean mDimmed;
private View mActivatedChild;
private float mOverScrollTopAmount;
private float mOverScrollBottomAmount;
public int getScrollY() {
return mScrollY;
@@ -72,4 +74,16 @@ public class AmbientState {
public View getActivatedChild() {
return mActivatedChild;
}
public void setOverScrollAmount(float amount, boolean onTop) {
if (onTop) {
mOverScrollTopAmount = amount;
} else {
mOverScrollBottomAmount = amount;
}
}
public float getOverScrollAmount(boolean top) {
return top ? mOverScrollTopAmount : mOverScrollBottomAmount;
}
}

View File

@@ -53,6 +53,7 @@ public class NotificationStackScrollLayout extends ViewGroup
private static final String TAG = "NotificationStackScrollLayout";
private static final boolean DEBUG = false;
private static final float RUBBER_BAND_FACTOR = 0.35f;
/**
* Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
@@ -70,8 +71,8 @@ public class NotificationStackScrollLayout extends ViewGroup
private int mTouchSlop;
private int mMinimumVelocity;
private int mMaximumVelocity;
private int mOverscrollDistance;
private int mOverflingDistance;
private float mMaxOverScroll;
private boolean mIsBeingDragged;
private int mLastMotionY;
private int mActivePointerId;
@@ -107,6 +108,16 @@ public class NotificationStackScrollLayout extends ViewGroup
private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
/**
* The raw amount of the overScroll on the top, which is not rubber-banded.
*/
private float mOverScrolledTopPixels;
/**
* The raw amount of the overScroll on the bottom, which is not rubber-banded.
*/
private float mOverScrolledBottomPixels;
private OnChildLocationsChangedListener mListener;
private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
private boolean mNeedsAnimation;
@@ -175,7 +186,6 @@ public class NotificationStackScrollLayout extends ViewGroup
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mOverscrollDistance = configuration.getScaledOverscrollDistance();
mOverflingDistance = configuration.getScaledOverflingDistance();
float densityScale = getResources().getDisplayMetrics().density;
float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
@@ -544,7 +554,7 @@ public class NotificationStackScrollLayout extends ViewGroup
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
mScroller.forceFinished(true);
}
// Remember where the motion event started
@@ -572,40 +582,23 @@ public class NotificationStackScrollLayout extends ViewGroup
if (mIsBeingDragged) {
// Scroll to follow the motion event
mLastMotionY = y;
final int oldX = mScrollX;
final int oldY = mOwnScrollY;
final int range = getScrollRange();
final int overscrollMode = getOverScrollMode();
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
float scrollAmount;
if (deltaY < 0) {
scrollAmount = overScrollDown(deltaY);
} else {
scrollAmount = overScrollUp(deltaY, range);
}
// Calling overScrollBy will call onOverScrolled, which
// calls onScrollChanged if applicable.
if (overScrollBy(0, deltaY, 0, mOwnScrollY,
0, range, 0, mOverscrollDistance, true)) {
// Break our velocity if we hit a scroll barrier.
mVelocityTracker.clear();
if (scrollAmount != 0.0f) {
// The scrolling motion could not be compensated with the
// existing overScroll, we have to scroll the view
overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
0, range, 0, getHeight() / 2, true);
}
// TODO: Overscroll
// if (canOverscroll) {
// final int pulledToY = oldY + deltaY;
// if (pulledToY < 0) {
// mEdgeGlowTop.onPull((float) deltaY / getHeight());
// if (!mEdgeGlowBottom.isFinished()) {
// mEdgeGlowBottom.onRelease();
// }
// } else if (pulledToY > range) {
// mEdgeGlowBottom.onPull((float) deltaY / getHeight());
// if (!mEdgeGlowTop.isFinished()) {
// mEdgeGlowTop.onRelease();
// }
// }
// if (mEdgeGlowTop != null
// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
// postInvalidateOnAnimation();
// }
// }
}
break;
case MotionEvent.ACTION_UP:
@@ -652,6 +645,68 @@ public class NotificationStackScrollLayout extends ViewGroup
return true;
}
/**
* Perform a scroll upwards and adapt the overscroll amounts accordingly
*
* @param deltaY The amount to scroll upwards, has to be positive.
* @return The amount of scrolling to be performed by the scroller,
* not handled by the overScroll amount.
*/
private float overScrollUp(int deltaY, int range) {
deltaY = Math.max(deltaY, 0);
float currentTopAmount = getCurrentOverScrollAmount(true);
float newTopAmount = currentTopAmount - deltaY;
if (currentTopAmount > 0) {
setOverScrollAmount(newTopAmount, true /* onTop */,
false /* animate */);
}
// Top overScroll might not grab all scrolling motion,
// we have to scroll as well.
float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
float newScrollY = mOwnScrollY + scrollAmount;
if (newScrollY > range) {
float currentBottomPixels = getCurrentOverScrolledPixels(false);
// We overScroll on the top
setOverScrolledPixels(currentBottomPixels + newScrollY - range,
false /* onTop */,
false /* animate */);
mOwnScrollY = range;
scrollAmount = 0.0f;
}
return scrollAmount;
}
/**
* Perform a scroll downward and adapt the overscroll amounts accordingly
*
* @param deltaY The amount to scroll downwards, has to be negative.
* @return The amount of scrolling to be performed by the scroller,
* not handled by the overScroll amount.
*/
private float overScrollDown(int deltaY) {
deltaY = Math.min(deltaY, 0);
float currentBottomAmount = getCurrentOverScrollAmount(false);
float newBottomAmount = currentBottomAmount + deltaY;
if (currentBottomAmount > 0) {
setOverScrollAmount(newBottomAmount, false /* onTop */,
false /* animate */);
}
// Bottom overScroll might not grab all scrolling motion,
// we have to scroll as well.
float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
float newScrollY = mOwnScrollY + scrollAmount;
if (newScrollY < 0) {
float currentTopPixels = getCurrentOverScrolledPixels(true);
// We overScroll on the top
setOverScrolledPixels(currentTopPixels - newScrollY,
true /* onTop */,
false /* animate */);
mOwnScrollY = 0;
scrollAmount = 0.0f;
}
return scrollAmount;
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
@@ -701,23 +756,16 @@ public class NotificationStackScrollLayout extends ViewGroup
if (oldX != x || 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);
if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
float currVelocity = mScroller.getCurrVelocity();
if (currVelocity >= mMinimumVelocity * 20) {
mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
}
}
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
0, mOverflingDistance, false);
0, (int) (mMaxOverScroll), false);
onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
if (canOverscroll) {
// TODO: Overscroll
// if (y < 0 && oldY >= 0) {
// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
// } else if (y > range && oldY <= range) {
// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
// }
}
updateChildren();
}
// Keep on drawing until the animation has finished.
@@ -725,6 +773,81 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
@Override
protected int computeVerticalScrollRange() {
// needed for the overScroller
return mContentHeight;
}
/**
* Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
* overscroll effect based on numPixels. By default this will also cancel animations on the
* same overScroll edge.
*
* @param numPixels The amount of pixels to overScroll by. These will be scaled according to
* the rubber-banding logic.
* @param onTop Should the effect be applied on top of the scroller.
* @param animate Should an animation be performed.
*/
public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
setOverScrollAmount(numPixels * RUBBER_BAND_FACTOR, onTop, animate, true);
}
/**
* Set the effective overScroll amount which will be directly reflected in the layout.
* By default this will also cancel animations on the same overScroll edge.
*
* @param amount The amount to overScroll by.
* @param onTop Should the effect be applied on top of the scroller.
* @param animate Should an animation be performed.
*/
public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
setOverScrollAmount(amount, onTop, animate, true);
}
/**
* Set the effective overScroll amount which will be directly reflected in the layout.
*
* @param amount The amount to overScroll by.
* @param onTop Should the effect be applied on top of the scroller.
* @param animate Should an animation be performed.
* @param cancelAnimators Should running animations be cancelled.
*/
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
boolean cancelAnimators) {
if (cancelAnimators) {
mStateAnimator.cancelOverScrollAnimators(onTop);
}
setOverScrollAmountInternal(amount, onTop, animate);
}
private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) {
amount = Math.max(0, amount);
if (animate) {
mStateAnimator.animateOverScrollToAmount(amount, onTop);
} else {
setOverScrolledPixels(amount / RUBBER_BAND_FACTOR, onTop);
mAmbientState.setOverScrollAmount(amount, onTop);
requestChildrenUpdate();
}
}
public float getCurrentOverScrollAmount(boolean top) {
return mAmbientState.getOverScrollAmount(top);
}
public float getCurrentOverScrolledPixels(boolean top) {
return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
}
private void setOverScrolledPixels(float amount, boolean onTop) {
if (onTop) {
mOverScrolledTopPixels = amount;
} else {
mOverScrolledBottomPixels = amount;
}
}
private void customScrollTo(int y) {
mOwnScrollY = y;
updateChildren();
@@ -738,18 +861,41 @@ public class NotificationStackScrollLayout extends ViewGroup
final int oldY = mOwnScrollY;
mScrollX = scrollX;
mOwnScrollY = scrollY;
invalidateParentIfNeeded();
onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
if (clampedY) {
mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
springBack();
} else {
onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
invalidateParentIfNeeded();
updateChildren();
}
updateChildren();
} else {
customScrollTo(scrollY);
scrollTo(scrollX, mScrollY);
}
}
private void springBack() {
int scrollRange = getScrollRange();
boolean overScrolledTop = mOwnScrollY <= 0;
boolean overScrolledBottom = mOwnScrollY >= scrollRange;
if (overScrolledTop || overScrolledBottom) {
boolean onTop;
float newAmount;
if (overScrolledTop) {
onTop = true;
newAmount = -mOwnScrollY;
mOwnScrollY = 0;
} else {
onTop = false;
newAmount = mOwnScrollY - scrollRange;
mOwnScrollY = scrollRange;
}
setOverScrollAmount(newAmount, onTop, false);
setOverScrollAmount(0.0f, onTop, true);
mScroller.forceFinished(true);
}
}
private int getScrollRange() {
int scrollRange = 0;
ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
@@ -841,7 +987,23 @@ public class NotificationStackScrollLayout extends ViewGroup
int height = (int) getLayoutHeight();
int bottom = getContentHeight();
mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
float topAmount = getCurrentOverScrollAmount(true);
float bottomAmount = getCurrentOverScrollAmount(false);
if (velocityY < 0 && topAmount > 0) {
mOwnScrollY -= (int) topAmount;
setOverScrollAmount(0, true, false);
mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
* mOverflingDistance + topAmount;
} else if (velocityY > 0 && bottomAmount > 0) {
mOwnScrollY += bottomAmount;
setOverScrollAmount(0, false, false);
mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
* mOverflingDistance + bottomAmount;
} else {
// it will be set once we reach the boundary
mMaxOverScroll = 0.0f;
}
mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
Math.max(0, bottom - height), 0, height/2);
postInvalidateOnAnimation();
@@ -853,11 +1015,12 @@ public class NotificationStackScrollLayout extends ViewGroup
recycleVelocityTracker();
// TODO: Overscroll
// if (mEdgeGlowTop != null) {
// mEdgeGlowTop.onRelease();
// mEdgeGlowBottom.onRelease();
// }
if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
setOverScrollAmount(0, true /* onTop */, true /* animate */);
}
if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
setOverScrollAmount(0, false /* onTop */, true /* animate */);
}
}
@Override

View File

@@ -128,7 +128,10 @@ public class StackScrollAlgorithm {
algorithmState.scrolledPixelsTop = 0;
algorithmState.itemsInBottomStack = 0.0f;
algorithmState.partialInBottom = 0.0f;
algorithmState.scrollY = ambientState.getScrollY() + mCollapsedSize;
float topOverScroll = ambientState.getOverScrollAmount(true /* onTop */);
float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
algorithmState.scrollY = (int) (ambientState.getScrollY() + mCollapsedSize
+ bottomOverScroll - topOverScroll);
updateVisibleChildren(resultState, algorithmState);
@@ -215,7 +218,6 @@ public class StackScrollAlgorithm {
*
* @param resultState The result state to update if a change to the properties of a child occurs
* @param algorithmState The state in which the current pass of the algorithm is currently in
* and which will be updated
*/
private void updatePositionsForState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
@@ -295,7 +297,7 @@ public class StackScrollAlgorithm {
// The first card is always rendered.
if (i == 0) {
childViewState.alpha = 1.0f;
childViewState.yTranslation = 0;
childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0);
childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD;
}
if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) {
@@ -454,7 +456,6 @@ public class StackScrollAlgorithm {
*
* @param resultState The result state to update if a height change of an child occurs
* @param algorithmState The state in which the current pass of the algorithm is currently in
* and which will be updated
*/
private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
@@ -472,7 +473,7 @@ public class StackScrollAlgorithm {
+ childHeight
+ mPaddingBetweenElements;
if (yPositionInScrollView < algorithmState.scrollY) {
if (i == 0 && algorithmState.scrollY == mCollapsedSize) {
if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
// The starting position of the bottom stack peek
int bottomPeekStart = mInnerHeight - mBottomStackPeekSize;

View File

@@ -18,7 +18,6 @@ package com.android.systemui.statusbar.stack;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
@@ -73,6 +72,9 @@ public class StackStateAnimator {
private AnimationFilter mAnimationFilter = new AnimationFilter();
private long mCurrentLength;
private ValueAnimator mTopOverScrollAnimator;
private ValueAnimator mBottomOverScrollAnimator;
public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
mHostLayout = hostLayout;
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
@@ -510,7 +512,7 @@ public class StackStateAnimator {
ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
StackScrollState finalState) {
mNewEvents.clear();
for (NotificationStackScrollLayout.AnimationEvent event: animationEvents) {
for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
View changingView = event.changingView;
if (!mHandledEvents.contains(event)) {
if (event.animationType == NotificationStackScrollLayout.AnimationEvent
@@ -532,4 +534,34 @@ public class StackStateAnimator {
}
}
}
public void animateOverScrollToAmount(float targetAmount, final boolean onTop) {
final float startOverScrollAmount = mHostLayout.getCurrentOverScrollAmount(onTop);
cancelOverScrollAnimators(onTop);
ValueAnimator overScrollAnimator = ValueAnimator.ofFloat(startOverScrollAmount,
targetAmount);
overScrollAnimator.setDuration(ANIMATION_DURATION_STANDARD);
overScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentOverScroll = (float) animation.getAnimatedValue();
mHostLayout.setOverScrollAmount(currentOverScroll, onTop, false /* animate */,
false /* cancelAnimators */);
}
});
overScrollAnimator.setInterpolator(mFastOutSlowInInterpolator);
overScrollAnimator.start();
if (onTop) {
mTopOverScrollAnimator = overScrollAnimator;
} else {
mBottomOverScrollAnimator = overScrollAnimator;
}
}
public void cancelOverScrollAnimators(boolean onTop) {
ValueAnimator currentAnimator = onTop ? mTopOverScrollAnimator : mBottomOverScrollAnimator;
if (currentAnimator != null) {
currentAnimator.cancel();
}
}
}