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 android.support.animation; 18 19 import android.support.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 /** 211 * @hide 212 */ 213 @Override getAcceleration(float lastDisplacement, float lastVelocity)214 public float getAcceleration(float lastDisplacement, float lastVelocity) { 215 216 lastDisplacement -= getFinalPosition(); 217 218 double k = mNaturalFreq * mNaturalFreq; 219 double c = 2 * mNaturalFreq * mDampingRatio; 220 221 return (float) (-k * lastDisplacement - c * lastVelocity); 222 } 223 224 /** 225 * @hide 226 */ 227 @Override isAtEquilibrium(float value, float velocity)228 public boolean isAtEquilibrium(float value, float velocity) { 229 if (Math.abs(velocity) < mVelocityThreshold 230 && Math.abs(value - getFinalPosition()) < mValueThreshold) { 231 return true; 232 } 233 return false; 234 } 235 236 /** 237 * Initialize the string by doing the necessary pre-calculation as well as some sanity check 238 * on the setup. 239 * 240 * @throws IllegalStateException if the final position is not yet set by the time the spring 241 * animation has started 242 */ init()243 private void init() { 244 if (mInitialized) { 245 return; 246 } 247 248 if (mFinalPosition == UNSET) { 249 throw new IllegalStateException("Error: Final position of the spring must be" 250 + " set before the animation starts"); 251 } 252 253 if (mDampingRatio > 1) { 254 // Over damping 255 mGammaPlus = -mDampingRatio * mNaturalFreq 256 + mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); 257 mGammaMinus = -mDampingRatio * mNaturalFreq 258 - mNaturalFreq * Math.sqrt(mDampingRatio * mDampingRatio - 1); 259 } else if (mDampingRatio >= 0 && mDampingRatio < 1) { 260 // Under damping 261 mDampedFreq = mNaturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio); 262 } 263 264 mInitialized = true; 265 } 266 267 /** 268 * Internal only call for Spring to calculate the spring position/velocity using 269 * an analytical approach. 270 */ updateValues(double lastDisplacement, double lastVelocity, long timeElapsed)271 DynamicAnimation.MassState updateValues(double lastDisplacement, double lastVelocity, 272 long timeElapsed) { 273 init(); 274 275 double deltaT = timeElapsed / 1000d; // unit: seconds 276 lastDisplacement -= mFinalPosition; 277 double displacement; 278 double currentVelocity; 279 if (mDampingRatio > 1) { 280 // Overdamped 281 double coeffA = lastDisplacement - (mGammaMinus * lastDisplacement - lastVelocity) 282 / (mGammaMinus - mGammaPlus); 283 double coeffB = (mGammaMinus * lastDisplacement - lastVelocity) 284 / (mGammaMinus - mGammaPlus); 285 displacement = coeffA * Math.pow(Math.E, mGammaMinus * deltaT) 286 + coeffB * Math.pow(Math.E, mGammaPlus * deltaT); 287 currentVelocity = coeffA * mGammaMinus * Math.pow(Math.E, mGammaMinus * deltaT) 288 + coeffB * mGammaPlus * Math.pow(Math.E, mGammaPlus * deltaT); 289 } else if (mDampingRatio == 1) { 290 // Critically damped 291 double coeffA = lastDisplacement; 292 double coeffB = lastVelocity + mNaturalFreq * lastDisplacement; 293 displacement = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT); 294 currentVelocity = (coeffA + coeffB * deltaT) * Math.pow(Math.E, -mNaturalFreq * deltaT) 295 * (-mNaturalFreq) + coeffB * Math.pow(Math.E, -mNaturalFreq * deltaT); 296 } else { 297 // Underdamped 298 double cosCoeff = lastDisplacement; 299 double sinCoeff = (1 / mDampedFreq) * (mDampingRatio * mNaturalFreq 300 * lastDisplacement + lastVelocity); 301 displacement = Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) 302 * (cosCoeff * Math.cos(mDampedFreq * deltaT) 303 + sinCoeff * Math.sin(mDampedFreq * deltaT)); 304 currentVelocity = displacement * (-mNaturalFreq) * mDampingRatio 305 + Math.pow(Math.E, -mDampingRatio * mNaturalFreq * deltaT) 306 * (-mDampedFreq * cosCoeff * Math.sin(mDampedFreq * deltaT) 307 + mDampedFreq * sinCoeff * Math.cos(mDampedFreq * deltaT)); 308 } 309 310 mMassState.mValue = (float) (displacement + mFinalPosition); 311 mMassState.mVelocity = (float) currentVelocity; 312 return mMassState; 313 } 314 315 /** 316 * This threshold defines how close the animation value needs to be before the animation can 317 * finish. This default value is based on the property being animated, e.g. animations on alpha, 318 * scale, translation or rotation would have different thresholds. This value should be small 319 * enough to avoid visual glitch of "jumping to the end". But it shouldn't be so small that 320 * animations take seconds to finish. 321 * 322 * @param threshold the difference between the animation value and final spring position that 323 * is allowed to end the animation when velocity is very low 324 */ setValueThreshold(double threshold)325 void setValueThreshold(double threshold) { 326 mValueThreshold = Math.abs(threshold); 327 mVelocityThreshold = mValueThreshold * VELOCITY_THRESHOLD_MULTIPLIER; 328 } 329 } 330