From 22a9f9c83e937e1ae72442d6906f1339756b143e Mon Sep 17 00:00:00 2001 From: George Mount Date: Wed, 23 Jun 2021 18:29:41 +0000 Subject: [PATCH] Linearly complete overscroll stretch when close Fixes: 190475978 Damped spring animations asymptotically approach their destinations. The final pixels can take many frames to complete, but stopping them means that we get a disruptive jump in those final pixels. This CL detects when the edge effect animation is close to the finish and completes the animation with a linear velocity that matches a common velocity at 8 pixels distant. This means that the animation terminates quicker so that touch events directed at the contents after the animation completes (e.g. taps) can interact with the contents soon. The CL also adjusts the delta for detecting zero in the stretch animation as it was jumping by a pixel or more at the end of an otherwise smooth animation. Test: manual testing for the visual effect Change-Id: Ie249b0265c5c5939b597668d5afe4f76d0430821 --- core/java/android/widget/EdgeEffect.java | 112 +++++++++++++++-------- libs/hwui/effects/StretchEffect.h | 16 +++- 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java index f2827f3fec73e..472e3e72ab2fa 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -97,12 +97,28 @@ public class EdgeEffect { */ private static final double VELOCITY_THRESHOLD = 0.01; + /** + * The speed at which we should start linearly interpolating to the destination. + * When using a spring, as it gets closer to the destination, the speed drops off exponentially. + * Instead of landing very slowly, a better experience is achieved if the final + * destination is arrived at quicker. + */ + private static final float LINEAR_VELOCITY_TAKE_OVER = 200f; + /** * The value threshold before the spring animation is considered close enough to * the destination to be settled. This should be around 0.01 pixel. */ private static final double VALUE_THRESHOLD = 0.001; + /** + * The maximum distance at which we should start linearly interpolating to the destination. + * When using a spring, as it gets closer to the destination, the speed drops off exponentially. + * Instead of landing very slowly, a better experience is achieved if the final + * destination is arrived at quicker. + */ + private static final double LINEAR_DISTANCE_TAKE_OVER = 8.0; + /** * The natural frequency of the stretch spring. */ @@ -587,55 +603,57 @@ public class EdgeEffect { if (mState == STATE_RECEDE) { updateSpring(); } - RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; - if (mTmpMatrix == null) { - mTmpMatrix = new Matrix(); - mTmpPoints = new float[12]; - } - //noinspection deprecation - recordingCanvas.getMatrix(mTmpMatrix); + if (mDistance != 0f) { + RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; + if (mTmpMatrix == null) { + mTmpMatrix = new Matrix(); + mTmpPoints = new float[12]; + } + //noinspection deprecation + recordingCanvas.getMatrix(mTmpMatrix); - mTmpPoints[0] = 0; - mTmpPoints[1] = 0; // top-left - mTmpPoints[2] = mWidth; - mTmpPoints[3] = 0; // top-right - mTmpPoints[4] = mWidth; - mTmpPoints[5] = mHeight; // bottom-right - mTmpPoints[6] = 0; - mTmpPoints[7] = mHeight; // bottom-left - mTmpPoints[8] = mWidth * mDisplacement; - mTmpPoints[9] = 0; // drag start point - mTmpPoints[10] = mWidth * mDisplacement; - mTmpPoints[11] = mHeight * mDistance; // drag point - mTmpMatrix.mapPoints(mTmpPoints); + mTmpPoints[0] = 0; + mTmpPoints[1] = 0; // top-left + mTmpPoints[2] = mWidth; + mTmpPoints[3] = 0; // top-right + mTmpPoints[4] = mWidth; + mTmpPoints[5] = mHeight; // bottom-right + mTmpPoints[6] = 0; + mTmpPoints[7] = mHeight; // bottom-left + mTmpPoints[8] = mWidth * mDisplacement; + mTmpPoints[9] = 0; // drag start point + mTmpPoints[10] = mWidth * mDisplacement; + mTmpPoints[11] = mHeight * mDistance; // drag point + mTmpMatrix.mapPoints(mTmpPoints); - RenderNode renderNode = recordingCanvas.mNode; + RenderNode renderNode = recordingCanvas.mNode; - float left = renderNode.getLeft() + float left = renderNode.getLeft() + min(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]); - float top = renderNode.getTop() + float top = renderNode.getTop() + min(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]); - float right = renderNode.getLeft() + float right = renderNode.getLeft() + max(mTmpPoints[0], mTmpPoints[2], mTmpPoints[4], mTmpPoints[6]); - float bottom = renderNode.getTop() + float bottom = renderNode.getTop() + max(mTmpPoints[1], mTmpPoints[3], mTmpPoints[5], mTmpPoints[7]); - // assume rotations of increments of 90 degrees - float x = mTmpPoints[10] - mTmpPoints[8]; - float width = right - left; - float vecX = dampStretchVector(Math.max(-1f, Math.min(1f, x / width))); + // assume rotations of increments of 90 degrees + float x = mTmpPoints[10] - mTmpPoints[8]; + float width = right - left; + float vecX = dampStretchVector(Math.max(-1f, Math.min(1f, x / width))); - float y = mTmpPoints[11] - mTmpPoints[9]; - float height = bottom - top; - float vecY = dampStretchVector(Math.max(-1f, Math.min(1f, y / height))); + float y = mTmpPoints[11] - mTmpPoints[9]; + float height = bottom - top; + float vecY = dampStretchVector(Math.max(-1f, Math.min(1f, y / height))); - boolean hasValidVectors = Float.isFinite(vecX) && Float.isFinite(vecY); - if (right > left && bottom > top && mWidth > 0 && mHeight > 0 && hasValidVectors) { - renderNode.stretch( + boolean hasValidVectors = Float.isFinite(vecX) && Float.isFinite(vecY); + if (right > left && bottom > top && mWidth > 0 && mHeight > 0 && hasValidVectors) { + renderNode.stretch( vecX, // horizontal stretch intensity vecY, // vertical stretch intensity mWidth, // max horizontal stretch in pixels mHeight // max vertical stretch in pixels - ); + ); + } } } else { // Animations have been disabled or this is TYPE_STRETCH and drawing into a Canvas @@ -730,6 +748,26 @@ public class EdgeEffect { if (deltaT < 0.001f) { return; // Must have at least 1 ms difference } + mStartTime = time; + + if (Math.abs(mVelocity) <= LINEAR_VELOCITY_TAKE_OVER + && Math.abs(mDistance * mHeight) < LINEAR_DISTANCE_TAKE_OVER + && Math.signum(mVelocity) == -Math.signum(mDistance) + ) { + // This is close. The spring will slowly reach the destination. Instead, we + // will interpolate linearly so that it arrives at its destination quicker. + mVelocity = Math.signum(mVelocity) * LINEAR_VELOCITY_TAKE_OVER; + + float targetDistance = mDistance + (mVelocity * deltaT / mHeight); + if (Math.signum(targetDistance) != Math.signum(mDistance)) { + // We have arrived + mDistance = 0; + mVelocity = 0; + } else { + mDistance = targetDistance; + } + return; + } final double mDampedFreq = NATURAL_FREQUENCY * Math.sqrt(1 - DAMPING_RATIO * DAMPING_RATIO); // We're always underdamped, so we can use only those equations: @@ -745,9 +783,7 @@ public class EdgeEffect { + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); mDistance = (float) distance / mHeight; mVelocity = (float) velocity; - mStartTime = time; if (isAtEquilibrium()) { - mState = STATE_IDLE; mDistance = 0; mVelocity = 0; } diff --git a/libs/hwui/effects/StretchEffect.h b/libs/hwui/effects/StretchEffect.h index 25777c278a118..fcbb70739aa55 100644 --- a/libs/hwui/effects/StretchEffect.h +++ b/libs/hwui/effects/StretchEffect.h @@ -40,9 +40,7 @@ public: StretchEffect() {} - bool isEmpty() const { - return MathUtils::isZero(mStretchDirection.x()) && MathUtils::isZero(mStretchDirection.y()); - } + bool isEmpty() const { return isZero(mStretchDirection.x()) && isZero(mStretchDirection.y()); } void setEmpty() { *this = StretchEffect{}; @@ -114,6 +112,18 @@ public: } private: + // The epsilon for StretchEffect is less than in MathUtils because + // the range is 0-1 for an entire screen and should be significantly + // less than 1 pixel for a smooth stretch animation. + inline static bool isZero(float value) { + // Using fabsf is more performant as ARM computes + // fabsf in a single instruction. + return fabsf(value) <= NON_ZERO_EPSILON; + } + // This should be good for 1/25,000 of a screen and should be good for + // screens with less than ~8000 pixels in one dimension with only 1/4 pixel + // cut-off. + static constexpr float NON_ZERO_EPSILON = 0.00004f; static sk_sp getStretchEffect(); mutable SkVector mStretchDirection{0, 0}; mutable std::unique_ptr mBuilder;