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.util.AndroidRuntimeException; 20 import android.util.FloatProperty; 21 22 /** 23 * SpringAnimation is an animation that is driven by a {@link SpringForce}. The spring force defines 24 * the spring's stiffness, damping ratio, as well as the rest position. Once the SpringAnimation is 25 * started, on each frame the spring force will update the animation's value and velocity. 26 * The animation will continue to run until the spring force reaches equilibrium. If the spring used 27 * in the animation is undamped, the animation will never reach equilibrium. Instead, it will 28 * oscillate forever. 29 * 30 * <div class="special reference"> 31 * <h3>Developer Guides</h3> 32 * </div> 33 * 34 * <p>To create a simple {@link SpringAnimation} that uses the default {@link SpringForce}:</p> 35 * <pre class="prettyprint"> 36 * // Create an animation to animate view's X property, set the rest position of the 37 * // default spring to 0, and start the animation with a starting velocity of 5000 (pixel/s). 38 * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.X, 0) 39 * .setStartVelocity(5000); 40 * anim.start(); 41 * </pre> 42 * 43 * <p>Alternatively, a {@link SpringAnimation} can take a pre-configured {@link SpringForce}, and 44 * use that to drive the animation. </p> 45 * <pre class="prettyprint"> 46 * // Create a low stiffness, low bounce spring at position 0. 47 * SpringForce spring = new SpringForce(0) 48 * .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) 49 * .setStiffness(SpringForce.STIFFNESS_LOW); 50 * // Create an animation to animate view's scaleY property, and start the animation using 51 * // the spring above and a starting value of 0.5. Additionally, constrain the range of value for 52 * // the animation to be non-negative, effectively preventing any spring overshoot. 53 * final SpringAnimation anim = new SpringAnimation(view, DynamicAnimation.SCALE_Y) 54 * .setMinValue(0).setSpring(spring).setStartValue(1); 55 * anim.start(); 56 * </pre> 57 */ 58 public final class SpringAnimation extends DynamicAnimation<SpringAnimation> { 59 60 private SpringForce mSpring = null; 61 private float mPendingPosition = UNSET; 62 private static final float UNSET = Float.MAX_VALUE; 63 private boolean mEndRequested = false; 64 65 /** 66 * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During 67 * the animation, the {@link FloatValueHolder} instance will be updated via 68 * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date 69 * animation value via {@link FloatValueHolder#getValue()}. 70 * 71 * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via 72 * {@link FloatValueHolder#setValue(float)} outside of the animation during an 73 * animation run will not have any effect on the on-going animation. 74 * 75 * @param floatValueHolder the property to be animated 76 */ SpringAnimation(FloatValueHolder floatValueHolder)77 public SpringAnimation(FloatValueHolder floatValueHolder) { 78 super(floatValueHolder); 79 } 80 81 /** 82 * <p>This creates a SpringAnimation that animates a {@link FloatValueHolder} instance. During 83 * the animation, the {@link FloatValueHolder} instance will be updated via 84 * {@link FloatValueHolder#setValue(float)} each frame. The caller can obtain the up-to-date 85 * animation value via {@link FloatValueHolder#getValue()}. 86 * 87 * A Spring will be created with the given final position and default stiffness and damping 88 * ratio. This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}. 89 * 90 * <p><strong>Note:</strong> changing the value in the {@link FloatValueHolder} via 91 * {@link FloatValueHolder#setValue(float)} outside of the animation during an 92 * animation run will not have any effect on the on-going animation. 93 * 94 * @param floatValueHolder the property to be animated 95 * @param finalPosition the final position of the spring to be created. 96 */ SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition)97 public SpringAnimation(FloatValueHolder floatValueHolder, float finalPosition) { 98 super(floatValueHolder); 99 mSpring = new SpringForce(finalPosition); 100 } 101 102 /** 103 * This creates a SpringAnimation that animates the property of the given object. 104 * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before 105 * the animation starts. 106 * 107 * @param object the Object whose property will be animated 108 * @param property the property to be animated 109 * @param <K> the class on which the Property is declared 110 */ SpringAnimation(K object, FloatProperty<K> property)111 public <K> SpringAnimation(K object, FloatProperty<K> property) { 112 super(object, property); 113 } 114 115 /** 116 * This creates a SpringAnimation that animates the property of the given object. A Spring will 117 * be created with the given final position and default stiffness and damping ratio. 118 * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}. 119 * 120 * @param object the Object whose property will be animated 121 * @param property the property to be animated 122 * @param finalPosition the final position of the spring to be created. 123 * @param <K> the class on which the Property is declared 124 */ SpringAnimation(K object, FloatProperty<K> property, float finalPosition)125 public <K> SpringAnimation(K object, FloatProperty<K> property, 126 float finalPosition) { 127 super(object, property); 128 mSpring = new SpringForce(finalPosition); 129 } 130 131 /** 132 * Returns the spring that the animation uses for animations. 133 * 134 * @return the spring that the animation uses for animations 135 */ getSpring()136 public SpringForce getSpring() { 137 return mSpring; 138 } 139 140 /** 141 * Uses the given spring as the force that drives this animation. If this spring force has its 142 * parameters re-configured during the animation, the new configuration will be reflected in the 143 * animation immediately. 144 * 145 * @param force a pre-defined spring force that drives the animation 146 * @return the animation that the spring force is set on 147 */ setSpring(SpringForce force)148 public SpringAnimation setSpring(SpringForce force) { 149 mSpring = force; 150 return this; 151 } 152 153 @Override start()154 public void start() { 155 sanityCheck(); 156 mSpring.setValueThreshold(getValueThreshold()); 157 super.start(); 158 } 159 160 /** 161 * Updates the final position of the spring. 162 * <p/> 163 * When the animation is running, calling this method would assume the position change of the 164 * spring as a continuous movement since last frame, which yields more accurate results than 165 * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}. 166 * <p/> 167 * If the animation hasn't started, calling this method will change the spring position, and 168 * immediately start the animation. 169 * 170 * @param finalPosition rest position of the spring 171 */ animateToFinalPosition(float finalPosition)172 public void animateToFinalPosition(float finalPosition) { 173 if (isRunning()) { 174 mPendingPosition = finalPosition; 175 } else { 176 if (mSpring == null) { 177 mSpring = new SpringForce(finalPosition); 178 } 179 mSpring.setFinalPosition(finalPosition); 180 start(); 181 } 182 } 183 184 /** 185 * Cancels the on-going animation. If the animation hasn't started, no op. Note that this method 186 * should only be called on main thread. 187 * 188 * @throws AndroidRuntimeException if this method is not called on the main thread 189 */ 190 @Override cancel()191 public void cancel() { 192 super.cancel(); 193 if (mPendingPosition != UNSET) { 194 if (mSpring == null) { 195 mSpring = new SpringForce(mPendingPosition); 196 } else { 197 mSpring.setFinalPosition(mPendingPosition); 198 } 199 mPendingPosition = UNSET; 200 } 201 } 202 203 /** 204 * Skips to the end of the animation. If the spring is undamped, an 205 * {@link IllegalStateException} will be thrown, as the animation would never reach to an end. 206 * It is recommended to check {@link #canSkipToEnd()} before calling this method. If animation 207 * is not running, no-op. 208 * 209 * Unless a AnimationHandler is provided via setAnimationHandler, a default AnimationHandler 210 * is created on the same thread as the first call to start/cancel an animation. All the 211 * subsequent animation lifecycle manipulations need to be on that same thread, until the 212 * AnimationHandler is reset (using [setAnimationHandler]). 213 * 214 * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0) 215 * @throws AndroidRuntimeException if this method is not called on the same thread as the 216 * animation handler 217 */ skipToEnd()218 public void skipToEnd() { 219 if (!canSkipToEnd()) { 220 throw new UnsupportedOperationException("Spring animations can only come to an end" 221 + " when there is damping"); 222 } 223 if (!isCurrentThread()) { 224 throw new AndroidRuntimeException("Animations may only be started on the same thread " 225 + "as the animation handler"); 226 } 227 if (mRunning) { 228 mEndRequested = true; 229 } 230 } 231 232 /** 233 * Queries whether the spring can eventually come to the rest position. 234 * 235 * @return {@code true} if the spring is damped, otherwise {@code false} 236 */ canSkipToEnd()237 public boolean canSkipToEnd() { 238 return mSpring.mDampingRatio > 0; 239 } 240 241 /************************ Below are private APIs *************************/ 242 sanityCheck()243 private void sanityCheck() { 244 if (mSpring == null) { 245 throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final" 246 + " position or a spring force needs to be set."); 247 } 248 double finalPosition = mSpring.getFinalPosition(); 249 if (finalPosition > mMaxValue) { 250 throw new UnsupportedOperationException("Final position of the spring cannot be greater" 251 + " than the max value."); 252 } else if (finalPosition < mMinValue) { 253 throw new UnsupportedOperationException("Final position of the spring cannot be less" 254 + " than the min value."); 255 } 256 } 257 258 @Override updateValueAndVelocity(long deltaT)259 boolean updateValueAndVelocity(long deltaT) { 260 // If user had requested end, then update the value and velocity to end state and consider 261 // animation done. 262 if (mEndRequested) { 263 if (mPendingPosition != UNSET) { 264 mSpring.setFinalPosition(mPendingPosition); 265 mPendingPosition = UNSET; 266 } 267 mValue = mSpring.getFinalPosition(); 268 mVelocity = 0; 269 mEndRequested = false; 270 return true; 271 } 272 273 if (mPendingPosition != UNSET) { 274 // Approximate by considering half of the time spring position stayed at the old 275 // position, half of the time it's at the new position. 276 MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2); 277 mSpring.setFinalPosition(mPendingPosition); 278 mPendingPosition = UNSET; 279 280 massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2); 281 mValue = massState.mValue; 282 mVelocity = massState.mVelocity; 283 284 } else { 285 MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT); 286 mValue = massState.mValue; 287 mVelocity = massState.mVelocity; 288 } 289 290 mValue = Math.max(mValue, mMinValue); 291 mValue = Math.min(mValue, mMaxValue); 292 293 if (isAtEquilibrium(mValue, mVelocity)) { 294 mValue = mSpring.getFinalPosition(); 295 mVelocity = 0f; 296 return true; 297 } 298 return false; 299 } 300 301 @Override getAcceleration(float value, float velocity)302 float getAcceleration(float value, float velocity) { 303 return mSpring.getAcceleration(value, velocity); 304 } 305 306 @Override isAtEquilibrium(float value, float velocity)307 boolean isAtEquilibrium(float value, float velocity) { 308 return mSpring.isAtEquilibrium(value, velocity); 309 } 310 311 @Override setValueThreshold(float threshold)312 void setValueThreshold(float threshold) { 313 } 314 } 315