diff options
| author | George Mount <mount@google.com> | 2021-03-04 23:45:44 +0000 |
|---|---|---|
| committer | George Mount <mount@google.com> | 2021-03-09 16:47:01 +0000 |
| commit | 61324dbfccf5166f355980f59a2fb950126c8410 (patch) | |
| tree | c407303458a19dd0209ecb29b0ad6dc286ab4c95 /core/java | |
| parent | af1e8b4ea981a81691c3259209efff8a8bd49898 (diff) | |
Use spring animation for stretch overscroll
Bug: 171228096
Use the spring animation from the prototype in
EdgeEffect.
Test: manual testing for fling and recede
Change-Id: I7eec03a98f5d26153512ba402d6bb5bc5dc70ffb
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/widget/EdgeEffect.java | 139 |
1 files changed, 102 insertions, 37 deletions
diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java index dc42ad583543..e74b071aeda7 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -76,6 +76,28 @@ public class EdgeEffect { */ public static final int TYPE_STRETCH = 1; + /** + * The velocity threshold before the spring animation is considered settled. + * The idea here is that velocity should be less than 1 pixel per frame (~16ms). + */ + private static final double VELOCITY_THRESHOLD = 1.0 / 0.016; + + /** + * The value threshold before the spring animation is considered close enough to + * the destination to be settled. This should be around 1 pixel. + */ + private static final double VALUE_THRESHOLD = 1; + + /** + * The natural frequency of the stretch spring. + */ + private static final double NATURAL_FREQUENCY = 14.4222; + + /** + * The damping ratio of the stretch spring. + */ + private static final double DAMPING_RATIO = 0.875; + /** @hide */ @IntDef({TYPE_GLOW, TYPE_STRETCH}) @Retention(RetentionPolicy.SOURCE) @@ -119,13 +141,12 @@ public class EdgeEffect { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private float mGlowScaleY; private float mDistance; + private float mVelocity; // only for stretch animations private float mGlowAlphaStart; private float mGlowAlphaFinish; private float mGlowScaleYStart; private float mGlowScaleYFinish; - private float mDistanceStart; - private float mDistanceFinish; private long mStartTime; private float mDuration; @@ -239,6 +260,8 @@ public class EdgeEffect { */ public void finish() { mState = STATE_IDLE; + mDistance = 0; + mVelocity = 0; } /** @@ -293,7 +316,8 @@ public class EdgeEffect { mDuration = PULL_TIME; mPullDistance += deltaDistance; - mDistanceStart = mDistanceFinish = mDistance = Math.max(0f, mPullDistance); + mDistance = Math.max(0f, mPullDistance); + mVelocity = 0; final float absdd = Math.abs(deltaDistance); mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, @@ -387,11 +411,10 @@ public class EdgeEffect { mState = STATE_RECEDE; mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; - mDistanceStart = mDistance; mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; - mDistanceFinish = 0.f; + mVelocity = 0.f; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = RECEDE_TIME; @@ -408,30 +431,36 @@ public class EdgeEffect { * @param velocity Velocity at impact in pixels per second. */ public void onAbsorb(int velocity) { - mState = STATE_ABSORB; - velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); - - mStartTime = AnimationUtils.currentAnimationTimeMillis(); - mDuration = 0.15f + (velocity * 0.02f); - - // The glow depends more on the velocity, and therefore starts out - // nearly invisible. - mGlowAlphaStart = GLOW_ALPHA_START; - mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); - mDistanceStart = mDistance; - - // Growth for the size of the glow should be quadratic to properly - // respond - // to a user's scrolling speed. The faster the scrolling speed, the more - // intense the effect should be for both the size and the saturation. - mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); - // Alpha should change for the glow as well as size. - mGlowAlphaFinish = Math.max( - mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); - mTargetDisplacement = 0.5f; - - // Use glow values to estimate the absorption for stretch distance. - mDistanceFinish = calculateDistanceFromGlowValues(mGlowScaleYFinish, mGlowAlphaFinish); + if (mEdgeEffectType == TYPE_STRETCH) { + mState = STATE_RECEDE; + mVelocity = velocity / mHeight; + mDistance = 0; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + } else { + mState = STATE_ABSORB; + mVelocity = 0; + velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY); + + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mDuration = 0.15f + (velocity * 0.02f); + + // The glow depends more on the velocity, and therefore starts out + // nearly invisible. + mGlowAlphaStart = GLOW_ALPHA_START; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); + + // Growth for the size of the glow should be quadratic to properly + // respond + // to a user's scrolling speed. The faster the scrolling speed, the more + // intense the effect should be for both the size and the saturation. + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, + 1.f); + // Alpha should change for the glow as well as size. + mGlowAlphaFinish = Math.max( + mGlowAlphaStart, + Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; + } } /** @@ -539,8 +568,8 @@ public class EdgeEffect { canvas.drawCircle(centerX, centerY, mRadius, mPaint); canvas.restoreToCount(count); } else if (canvas instanceof RecordingCanvas) { - if (mState != STATE_PULL) { - update(); + if (mState == STATE_RECEDE) { + updateSpring(); } RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; if (mTmpMatrix == null) { @@ -597,7 +626,7 @@ public class EdgeEffect { } boolean oneLastFrame = false; - if (mState == STATE_RECEDE && mDistance == 0) { + if (mState == STATE_RECEDE && mDistance == 0 && mVelocity == 0) { mState = STATE_IDLE; oneLastFrame = true; } @@ -634,7 +663,7 @@ public class EdgeEffect { mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; - mDistance = mDistanceStart + (mDistanceFinish - mDistanceStart) * interp; + mDistance = calculateDistanceFromGlowValues(mGlowScaleY, mGlowAlpha); mDisplacement = (mDisplacement + mTargetDisplacement) / 2; if (t >= 1.f - EPSILON) { @@ -646,12 +675,10 @@ public class EdgeEffect { mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; - mDistanceStart = mDistance; // After absorb, the glow should fade to nothing. mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; - mDistanceFinish = 0.f; break; case STATE_PULL: mState = STATE_PULL_DECAY; @@ -660,12 +687,10 @@ public class EdgeEffect { mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; - mDistanceStart = mDistance; // After pull, the glow should fade to nothing. mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; - mDistanceFinish = 0.f; break; case STATE_PULL_DECAY: mState = STATE_RECEDE; @@ -677,6 +702,35 @@ public class EdgeEffect { } } + private void updateSpring() { + final long time = AnimationUtils.currentAnimationTimeMillis(); + final float deltaT = (time - mStartTime) / 1000f; // Convert from millis to seconds + if (deltaT < 0.001f) { + return; // Must have at least 1 ms difference + } + final double mDampedFreq = NATURAL_FREQUENCY * Math.sqrt(1 - DAMPING_RATIO * DAMPING_RATIO); + + // We're always underdamped, so we can use only those equations: + double cosCoeff = mDistance; + double sinCoeff = (1 / mDampedFreq) * (DAMPING_RATIO * NATURAL_FREQUENCY + * mDistance + mVelocity); + double distance = Math.pow(Math.E, -DAMPING_RATIO * NATURAL_FREQUENCY * deltaT) + * (cosCoeff * Math.cos(mDampedFreq * deltaT) + + sinCoeff * Math.sin(mDampedFreq * deltaT)); + double velocity = distance * (-NATURAL_FREQUENCY) * DAMPING_RATIO + + Math.pow(Math.E, -DAMPING_RATIO * NATURAL_FREQUENCY * deltaT) + * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT) + + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); + mDistance = (float) distance; + mVelocity = (float) velocity; + mStartTime = time; + if (isAtEquilibrium()) { + mState = STATE_IDLE; + mDistance = 0; + mVelocity = 0; + } + } + /** * @return The estimated pull distance as calculated from mGlowScaleY. */ @@ -692,4 +746,15 @@ public class EdgeEffect { } return alpha / PULL_DISTANCE_ALPHA_GLOW_FACTOR; } + + /** + * @return true if the spring used for calculating the stretch animation is + * considered at rest or false if it is still animating. + */ + private boolean isAtEquilibrium() { + double velocity = mVelocity * mHeight; // in pixels/second + double displacement = mDistance * mHeight; // in pixels + return Math.abs(velocity) < VELOCITY_THRESHOLD + && Math.abs(displacement) < VALUE_THRESHOLD; + } } |
