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