1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.dynamicanimation.animation; 18 19 import android.annotation.FloatRange; 20 21 /** 22 * Spring Force defines the characteristics of the spring being used in the animation. 23 * <p> 24 * By configuring the stiffness and damping ratio, callers can create a spring with the look and 25 * feel suits their use case. Stiffness corresponds to the spring constant. The stiffer the spring 26 * is, the harder it is to stretch it, the faster it undergoes dampening. 27 * <p> 28 * Spring damping ratio describes how oscillations in a system decay after a disturbance. 29 * When damping ratio > 1* (i.e. over-damped), the object will quickly return to the rest position 30 * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will 31 * return to equilibrium within the shortest amount of time. When damping ratio is less than 1 32 * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without any 33 * damping (i.e. damping ratio = 0), the mass will oscillate forever. 34 */ 35 public final class SpringForce implements Force { 36 /** 37 * Stiffness constant for extremely stiff spring. 38 */ 39 public static final float STIFFNESS_HIGH = 10_000f; 40 /** 41 * Stiffness constant for medium stiff spring. This is the default stiffness for spring force. 42 */ 43 public static final float STIFFNESS_MEDIUM = 1500f; 44 /** 45 * Stiffness constant for a spring with low stiffness. 46 */ 47 public static final float STIFFNESS_LOW = 200f; 48 /** 49 * Stiffness constant for a spring with very low stiffness. 50 */ 51 public static final float STIFFNESS_VERY_LOW = 50f; 52 53 /** 54 * Damping ratio for a very bouncy spring. Note for under-damped springs 55 * (i.e. damping ratio < 1), the lower the damping ratio, the more bouncy the spring. 56 */ 57 public static final float DAMPING_RATIO_HIGH_BOUNCY = 0.2f; 58 /** 59 * Damping ratio for a medium bouncy spring. This is also the default damping ratio for spring 60 * force. Note for under-damped springs (i.e. damping ratio < 1), the lower the damping ratio, 61 * the more bouncy the spring. 62 */ 63 public static final float DAMPING_RATIO_MEDIUM_BOUNCY = 0.5f; 64 /** 65 * Damping ratio for a spring with low bounciness. Note for under-damped springs 66 * (i.e. damping ratio < 1), the lower the damping ratio, the higher the bounciness. 67 */ 68 public static final float DAMPING_RATIO_LOW_BOUNCY = 0.75f; 69 /** 70 * Damping ratio for a spring with no bounciness. This damping ratio will create a critically 71 * damped spring that returns to equilibrium within the shortest amount of time without 72 * oscillating. 73 */ 74 public static final float DAMPING_RATIO_NO_BOUNCY = 1f; 75 76 // This multiplier is used to calculate the velocity threshold given a certain value threshold. 77 // The idea is that if it takes >= 1 frame to move the value threshold amount, then the velocity 78 // is a reasonable threshold. 79 private static final double VELOCITY_THRESHOLD_MULTIPLIER = 1000.0 / 16.0; 80 81 // Natural frequency 82 double mNaturalFreq = Math.sqrt(STIFFNESS_MEDIUM); 83 // Damping ratio. 84 double mDampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY; 85 86 // Value to indicate an unset state. 87 private static final double UNSET = Double.MAX_VALUE; 88 89 // Indicates whether the spring has been initialized 90 private boolean mInitialized = false; 91 92 // Threshold for velocity and value to determine when it's reasonable to assume that the spring 93 // is approximately at rest. 94 private double mValueThreshold; 95 private double mVelocityThreshold; 96 97 // Intermediate values to simplify the spring function calculation per frame. 98 private double mGammaPlus; 99 private double mGammaMinus; 100 private double mDampedFreq; 101 102 // Final position of the spring. This must be set before the start of the animation. 103 private double mFinalPosition = UNSET; 104 105 // Internal state to hold a value/velocity pair. 106 private final DynamicAnimation.MassState mMassState = new DynamicAnimation.MassState(); 107 108 /** 109 * Creates a spring force. Note that final position of the spring must be set through 110 * {@link #setFinalPosition(float)} before the spring animation starts. 111 */ SpringForce()112 public SpringForce() { 113 // No op. 114 } 115 116 /** 117 * Creates a spring with a given final rest position. 118 * 119 * @param finalPosition final position of the spring when it reaches equilibrium 120 */ SpringForce(float finalPosition)121 public SpringForce(float finalPosition) { 122 mFinalPosition = finalPosition; 123 } 124 125 /** 126 * Sets the stiffness of a spring. The more stiff a spring is, the more force it applies to 127 * the object attached when the spring is not at the final position. Default stiffness is 128 * {@link #STIFFNESS_MEDIUM}. 129 * 130 * @param stiffness non-negative stiffness constant of a spring 131 * @return the spring force that the given stiffness is set on 132 * @throws IllegalArgumentException if the given spring stiffness is not positive 133 */ setStiffness( @loatRangefrom = 0.0, fromInclusive = false) float stiffness)134 public SpringForce setStiffness( 135 @FloatRange(from = 0.0, fromInclusive = false) float stiffness) { 136 if (stiffness <= 0) { 137 throw new IllegalArgumentException("Spring stiffness constant must be positive."); 138 } 139 mNaturalFreq = Math.sqrt(stiffness); 140 // All the intermediate values need to be recalculated. 141 mInitialized = false; 142 return this; 143 } 144 145 /** 146 * Gets the stiffness of the spring. 147 * 148 * @return the stiffness of the spring 149 */ getStiffness()150 public float getStiffness() { 151 return (float) (mNaturalFreq * mNaturalFreq); 152 } 153 154 /** 155 * Spring damping ratio describes how oscillations in a system decay after a disturbance. 156 * <p> 157 * When damping ratio > 1 (over-damped), the object will quickly return to the rest position 158 * without overshooting. If damping ratio equals to 1 (i.e. critically damped), the object will 159 * return to equilibrium within the shortest amount of time. When damping ratio is less than 1 160 * (i.e. under-damped), the mass tends to overshoot, and return, and overshoot again. Without 161 * any damping (i.e. damping ratio = 0), the mass will oscillate forever. 162 * <p> 163 * Default damping ratio is {@link #DAMPING_RATIO_MEDIUM_BOUNCY}. 164 * 165 * @param dampingRatio damping ratio of the spring, it should be non-negative 166 * @return the spring force that the given damping ratio is set on 167 * @throws IllegalArgumentException if the {@param dampingRatio} is negative. 168 */ setDampingRatio(@loatRangefrom = 0.0) float dampingRatio)169 public SpringForce setDampingRatio(@FloatRange(from = 0.0) float dampingRatio) { 170 if (dampingRatio < 0) { 171 throw new IllegalArgumentException("Damping ratio must be non-negative"); 172 } 173 mDampingRatio = dampingRatio; 174 // All the intermediate values need to be recalculated. 175 mInitialized = false; 176 return this; 177 } 178 179 /** 180 * Returns the damping ratio of the spring. 181 * 182 * @return damping ratio of the spring 183 */ getDampingRatio()184 public float getDampingRatio() { 185 return (float) mDampingRatio; 186 } 187 188 /** 189 * Sets the rest position of the spring. 190 * 191 * @param finalPosition rest position of the spring 192 * @return the spring force that the given final position is set on 193 */ setFinalPosition(float finalPosition)194 public SpringForce setFinalPosition(float finalPosition) { 195 mFinalPosition = finalPosition; 196 return this; 197 } 198 199 /** 200 * Returns the rest position of the spring. 201 * 202 * @return rest position of the spring 203 */ getFinalPosition()204 public float getFinalPosition() { 205 return (float) mFinalPosition; 206 } 207 208 /*********************** Below are private APIs *********************/ 209 210 @Override getAcceleration(float lastDisplacement, float lastVelocity)211 public float getAcceleration(float lastDisplacement, float lastVelocity) { 212 213 lastDisplacement -= getFinalPosition(); 214 215 double k = mNaturalFreq * mNaturalFreq; 216 double c = 2 * mNaturalFreq * mDampingRatio; 217 218 return (float) (-k * lastDisplacement - c * lastVelocity); 219 } 220 221 @Override isAtEquilibrium(float value, float velocity)222 public boolean isAtEquilibrium(float value, float velocity) { 223 if (Math.abs(velocity) < mVelocityThreshold 224 && Math.abs(value - getFinalPosition()) < mValueThreshold) { 225 return true; 226 } 227 return false; 228 } 229 230 /** 231 * Initialize the string by doing the necessary pre-calculation as well as some sanity check 232 * on the setup. 233 * 234 * @throws IllegalStateException if the final position is not yet set by the time the spring 235 * animation has started 236 */ init()237 private void init() { 238 if (mInitialized) { 239 return; 240 } 241 242 if (mFinalPosition == UNSET) { 243 throw new IllegalStateException("Error: Final position of the spring must be" 244 + " set before the animation starts"); 245 } 246 247 if (mDampingRatio > 1) { 248 // Over damping 249 mGammaPlus = -mDampingRatio * mNaturalFreq 250 + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); 251 mGammaMinus = -mDampingRatio * mNaturalFreq 252 - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); 253 } else if (mDampingRatio >= 0 && mDampingRatio < 1) { 254 // Under damping 255 mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio); 256 } 257 258 mInitialized = true; 259 } 260 261 /** 262 * Internal only call for Spring to calculate the spring position/velocity using 263 * an analytical approach. 264 */ updateValues(double lastDisplacement, double lastVelocity, long timeElapsed)265 DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity, 266 long timeElapsed) { 267 init(); 268 269 double deltaT = timeElapsed / 1000d; // unit: seconds 270 lastDisplacement -= mFinalPosition; 271 double displacement; 272 double currentVelocity; 273 if (mDampingRatio > 1) { 274 // Overdamped 275 double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity) 276 / (mGammaMinus - mGammaPlus); 277 double coeffB = (mGammaMinus * lastDisplacement - lastVelocity) 278 / (mGammaMinus - mGammaPlus); 279 displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT) 280 + coeffB * Math.pow(Math.E, mGammaPlus * deltaT); 281 currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT) 282 + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT); 283 } else if (mDampingRatio == 1) { 284 // Critically damped 285 double coeffA = lastDisplacement; 286 double coeffB = lastVelocity + mNaturalFreq * lastDisplacement; 287 displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT); 288 currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT) 289 * -mNaturalFreq + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT); 290 } else { 291 // Underdamped 292 double cosCoeff = lastDisplacement; 293 double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq 294 * lastDisplacement + lastVelocity); 295 displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) 296 * (cosCoeff * Math.cos(mDampedFreq * deltaT) 297 + sinCoeff * Math.sin(mDampedFreq * deltaT)); 298 currentVelocity = displacement * -mNaturalFreq * mDampingRatio 299 + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) 300 * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT) 301 + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); 302 } 303 304 mMassState.mValue = (float) (displacement + mFinalPosition); 305 mMassState.mVelocity = (float) currentVelocity; 306 return mMassState; 307 } 308 309 /** 310 * This threshold defines how close the animation value needs to be before the animation can 311 * finish. This default value is based on the property being animated, e.g. animations on alpha, 312 * scale, translation or rotation would have different thresholds. This value should be small 313 * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that 314 * animations take seconds to finish. 315 * 316 * @param threshold the difference between the animation value and final spring position that 317 * is allowed to end the animation when velocity is very low 318 */ setValueThreshold(double threshold)319 void setValueThreshold(double threshold) { 320 mValueThreshold = Math.abs(threshold); 321 mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER; 322 } 323 } 324