diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 9470635cb1..a6b3a198e6 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -18,6 +18,7 @@ package com.android.launcher3; import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled; import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType; +import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS; import static com.android.launcher3.touch.OverScroll.OVERSCROLL_DAMP_FACTOR; import android.animation.LayoutTransition; @@ -25,6 +26,7 @@ import android.animation.TimeInterpolator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Canvas; import android.graphics.Rect; import android.os.Bundle; import android.provider.Settings; @@ -121,6 +123,8 @@ public abstract class PagedView extends ViewGrou protected boolean mIsPageInTransition = false; + protected float mSpringOverScrollX; + protected boolean mWasInOverscroll = false; protected int mUnboundedScrollX; @@ -349,6 +353,11 @@ public abstract class PagedView extends ViewGrou boolean isXBeforeFirstPage = mIsRtl ? (x > mMaxScrollX) : (x < 0); boolean isXAfterLastPage = mIsRtl ? (x < 0) : (x > mMaxScrollX); + + if (!isXBeforeFirstPage && !isXAfterLastPage) { + mSpringOverScrollX = 0; + } + if (isXBeforeFirstPage) { super.scrollTo(mIsRtl ? mMaxScrollX : 0, y); if (mAllowOverScroll) { @@ -988,12 +997,35 @@ public abstract class PagedView extends ViewGrou } } + @Override + protected void dispatchDraw(Canvas canvas) { + if (mScroller.isSpringing() && mSpringOverScrollX != 0) { + int saveCount = canvas.save(); + + canvas.translate(-mSpringOverScrollX, 0); + super.dispatchDraw(canvas); + + canvas.restoreToCount(saveCount); + } else { + super.dispatchDraw(canvas); + } + } + protected void dampedOverScroll(int amount) { - if (amount == 0) return; + mSpringOverScrollX = amount; + if (amount == 0) { + return; + } int overScrollAmount = OverScroll.dampedScroll(amount, getMeasuredWidth()); + mSpringOverScrollX = overScrollAmount; + if (mScroller.isSpringing()) { + invalidate(); + return; + } + if (amount < 0) { - super.scrollTo(overScrollAmount, getScrollY()); + super.scrollTo(amount, getScrollY()); } else { super.scrollTo(mMaxScrollX + overScrollAmount, getScrollY()); } @@ -1001,6 +1033,12 @@ public abstract class PagedView extends ViewGrou } protected void overScroll(int amount) { + mSpringOverScrollX = amount; + if (mScroller.isSpringing()) { + invalidate(); + return; + } + if (amount == 0) return; if (mFreeScroll && !mScroller.isFinished()) { @@ -1372,7 +1410,12 @@ public abstract class PagedView extends ViewGrou // interpolator at zero, ie. 5. We use 4 to make it a little slower. duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); - return snapToPage(whichPage, delta, duration); + if (QUICKSTEP_SPRINGS.get()) { + return snapToPage(whichPage, delta, duration, false, null, + velocity * Math.signum(newX - getUnboundedScrollX()), true); + } else { + return snapToPage(whichPage, delta, duration); + } } public boolean snapToPage(int whichPage) { @@ -1397,15 +1440,15 @@ public abstract class PagedView extends ViewGrou int newX = getScrollForPage(whichPage); final int delta = newX - getUnboundedScrollX(); - return snapToPage(whichPage, delta, duration, immediate, interpolator); + return snapToPage(whichPage, delta, duration, immediate, interpolator, 0, false); } protected boolean snapToPage(int whichPage, int delta, int duration) { - return snapToPage(whichPage, delta, duration, false, null); + return snapToPage(whichPage, delta, duration, false, null, 0, false); } protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate, - TimeInterpolator interpolator) { + TimeInterpolator interpolator, float velocity, boolean spring) { if (mFirstLayout) { setCurrentPage(whichPage); return false; @@ -1441,7 +1484,11 @@ public abstract class PagedView extends ViewGrou mScroller.setInterpolator(mDefaultInterpolator); } - mScroller.startScroll(getUnboundedScrollX(), delta, duration); + if (spring && QUICKSTEP_SPRINGS.get()) { + mScroller.startScrollSpring(getUnboundedScrollX(), delta, duration, velocity); + } else { + mScroller.startScroll(getUnboundedScrollX(), delta, duration); + } updatePageIndicator(); diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java index eb26961c32..3438a269fd 100644 --- a/src/com/android/launcher3/Workspace.java +++ b/src/com/android/launcher3/Workspace.java @@ -996,7 +996,7 @@ public class Workspace extends PagedView @Override protected void overScroll(int amount) { - boolean shouldScrollOverlay = mLauncherOverlay != null && + boolean shouldScrollOverlay = mLauncherOverlay != null && !mScroller.isSpringing() && ((amount <= 0 && !mIsRtl) || (amount >= 0 && mIsRtl)); boolean shouldZeroOverlay = mLauncherOverlay != null && mLastOverlayScroll != 0 && diff --git a/src/com/android/launcher3/util/OverScroller.java b/src/com/android/launcher3/util/OverScroller.java index d697eceae5..fc8a138efa 100644 --- a/src/com/android/launcher3/util/OverScroller.java +++ b/src/com/android/launcher3/util/OverScroller.java @@ -26,6 +26,11 @@ import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + /** * Based on {@link android.widget.OverScroller} supporting only 1-d scrolling and with more * customization options. @@ -196,6 +201,9 @@ public class OverScroller { switch (mMode) { case SCROLL_MODE: + if (isSpringing()) { + return true; + } long time = AnimationUtils.currentAnimationTimeMillis(); // Any scroller can be used for time, since they were started // together in scroll mode. We use X here. @@ -253,6 +261,22 @@ public class OverScroller { mScroller.startScroll(start, delta, duration); } + /** + * Start scrolling using a spring by providing a starting point and the distance to travel. + * + * @param start Starting scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param delta Distance to travel. Positive numbers will scroll the + * content to the left. + * @param duration Duration of the scroll in milliseconds. + * @param velocity The starting velocity for the spring in px per ms. + */ + public void startScrollSpring(int start, int delta, int duration, float velocity) { + mMode = SCROLL_MODE; + mScroller.mState = mScroller.SPRING; + mScroller.startScroll(start, delta, duration, velocity); + } + /** * Call this when you want to 'spring back' into a valid coordinate range. * @@ -354,6 +378,10 @@ public class OverScroller { return (int) (time - mScroller.mStartTime); } + public boolean isSpringing() { + return mScroller.mState == SplineOverScroller.SPRING && !isFinished(); + } + static class SplineOverScroller { // Initial position private int mStart; @@ -397,6 +425,8 @@ public class OverScroller { // Current state of the animation. private int mState = SPLINE; + private SpringAnimation mSpring; + // Constant gravity value, used in the deceleration phase. private static final float GRAVITY = 2000.0f; @@ -417,6 +447,20 @@ public class OverScroller { private static final int SPLINE = 0; private static final int CUBIC = 1; private static final int BALLISTIC = 2; + private static final int SPRING = 3; + + private static final FloatPropertyCompat SPRING_PROPERTY = + new FloatPropertyCompat("splineOverScrollerSpring") { + @Override + public float getValue(SplineOverScroller scroller) { + return scroller.mCurrentPosition; + } + + @Override + public void setValue(SplineOverScroller scroller, float value) { + scroller.mCurrentPosition = (int) value; + } + }; static { float x_min = 0.0f; @@ -465,6 +509,9 @@ public class OverScroller { } void updateScroll(float q) { + if (mState == SPRING) { + return; + } mCurrentPosition = mStart + Math.round(q * (mFinal - mStart)); } @@ -495,6 +542,10 @@ public class OverScroller { } void startScroll(int start, int distance, int duration) { + startScroll(start, distance, duration, 0); + } + + void startScroll(int start, int distance, int duration, float velocity) { mFinished = false; mCurrentPosition = mStart = start; @@ -503,12 +554,31 @@ public class OverScroller { mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = duration; + if (mState == SPRING) { + if (mSpring != null) { + mSpring.cancel(); + } + mSpring = new SpringAnimation(this, SPRING_PROPERTY); + + mSpring.setSpring(new SpringForce(mFinal) + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + mSpring.setStartVelocity(velocity); + mSpring.animateToFinalPosition(mFinal); + mSpring.addEndListener((animation, canceled, value, velocity1) -> { + finish(); + mState = SPLINE; + mSpring = null; + }); + } // Unused mDeceleration = 0.0f; mVelocity = 0; } void finish() { + if (mSpring != null && mSpring.isRunning()) mSpring.cancel(); + mCurrentPosition = mFinal; // Not reset since WebView relies on this value for fast fling. // TODO: restore when WebView uses the fast fling implemented in this class. @@ -518,6 +588,9 @@ public class OverScroller { void setFinalPosition(int position) { mFinal = position; + if (mState == SPRING && mSpring != null) { + mSpring.animateToFinalPosition(mFinal); + } mSplineDistance = mFinal - mStart; mFinished = false; } @@ -722,6 +795,10 @@ public class OverScroller { * reached. */ boolean update() { + if (mState == SPRING) { + return mFinished; + } + final long time = AnimationUtils.currentAnimationTimeMillis(); final long currentTime = time - mStartTime;