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 android.os.Looper; 20 import android.util.AndroidRuntimeException; 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 * This creates a SpringAnimation that animates the property of the given object. 83 * Note, a spring will need to setup through {@link #setSpring(SpringForce)} before 84 * the animation starts. 85 * 86 * @param object the Object whose property will be animated 87 * @param property the property to be animated 88 * @param <K> the class on which the Property is declared 89 */ SpringAnimation(K object, FloatPropertyCompat<K> property)90 public <K> SpringAnimation(K object, FloatPropertyCompat<K> property) { 91 super(object, property); 92 } 93 94 /** 95 * This creates a SpringAnimation that animates the property of the given object. A Spring will 96 * be created with the given final position and default stiffness and damping ratio. 97 * This spring can be accessed and reconfigured through {@link #setSpring(SpringForce)}. 98 * 99 * @param object the Object whose property will be animated 100 * @param property the property to be animated 101 * @param finalPosition the final position of the spring to be created. 102 * @param <K> the class on which the Property is declared 103 */ SpringAnimation(K object, FloatPropertyCompat<K> property, float finalPosition)104 public <K> SpringAnimation(K object, FloatPropertyCompat<K> property, 105 float finalPosition) { 106 super(object, property); 107 mSpring = new SpringForce(finalPosition); 108 } 109 110 /** 111 * Returns the spring that the animation uses for animations. 112 * 113 * @return the spring that the animation uses for animations 114 */ getSpring()115 public SpringForce getSpring() { 116 return mSpring; 117 } 118 119 /** 120 * Uses the given spring as the force that drives this animation. If this spring force has its 121 * parameters re-configured during the animation, the new configuration will be reflected in the 122 * animation immediately. 123 * 124 * @param force a pre-defined spring force that drives the animation 125 * @return the animation that the spring force is set on 126 */ setSpring(SpringForce force)127 public SpringAnimation setSpring(SpringForce force) { 128 mSpring = force; 129 return this; 130 } 131 132 @Override start()133 public void start() { 134 sanityCheck(); 135 mSpring.setValueThreshold(getValueThreshold()); 136 super.start(); 137 } 138 139 /** 140 * Updates the final position of the spring. 141 * <p/> 142 * When the animation is running, calling this method would assume the position change of the 143 * spring as a continuous movement since last frame, which yields more accurate results than 144 * changing the spring position directly through {@link SpringForce#setFinalPosition(float)}. 145 * <p/> 146 * If the animation hasn't started, calling this method will change the spring position, and 147 * immediately start the animation. 148 * 149 * @param finalPosition rest position of the spring 150 */ animateToFinalPosition(float finalPosition)151 public void animateToFinalPosition(float finalPosition) { 152 if (isRunning()) { 153 mPendingPosition = finalPosition; 154 } else { 155 if (mSpring == null) { 156 mSpring = new SpringForce(finalPosition); 157 } 158 mSpring.setFinalPosition(finalPosition); 159 start(); 160 } 161 } 162 163 /** 164 * Skips to the end of the animation. If the spring is undamped, an 165 * {@link IllegalStateException} will be thrown, as the animation would never reach to an end. 166 * It is recommended to check {@link #canSkipToEnd()} before calling this method. This method 167 * should only be called on main thread. If animation is not running, no-op. 168 * 169 * @throws IllegalStateException if the spring is undamped (i.e. damping ratio = 0) 170 * @throws AndroidRuntimeException if this method is not called on the main thread 171 */ skipToEnd()172 public void skipToEnd() { 173 if (!canSkipToEnd()) { 174 throw new UnsupportedOperationException("Spring animations can only come to an end" 175 + " when there is damping"); 176 } 177 if (Looper.myLooper() != Looper.getMainLooper()) { 178 throw new AndroidRuntimeException("Animations may only be started on the main thread"); 179 } 180 if (mRunning) { 181 mEndRequested = true; 182 } 183 } 184 185 /** 186 * Queries whether the spring can eventually come to the rest position. 187 * 188 * @return {@code true} if the spring is damped, otherwise {@code false} 189 */ canSkipToEnd()190 public boolean canSkipToEnd() { 191 return mSpring.mDampingRatio > 0; 192 } 193 194 /************************ Below are private APIs *************************/ 195 sanityCheck()196 private void sanityCheck() { 197 if (mSpring == null) { 198 throw new UnsupportedOperationException("Incomplete SpringAnimation: Either final" 199 + " position or a spring force needs to be set."); 200 } 201 double finalPosition = mSpring.getFinalPosition(); 202 if (finalPosition > mMaxValue) { 203 throw new UnsupportedOperationException("Final position of the spring cannot be greater" 204 + " than the max value."); 205 } else if (finalPosition < mMinValue) { 206 throw new UnsupportedOperationException("Final position of the spring cannot be less" 207 + " than the min value."); 208 } 209 } 210 211 @Override updateValueAndVelocity(long deltaT)212 boolean updateValueAndVelocity(long deltaT) { 213 // If user had requested end, then update the value and velocity to end state and consider 214 // animation done. 215 if (mEndRequested) { 216 if (mPendingPosition != UNSET) { 217 mSpring.setFinalPosition(mPendingPosition); 218 mPendingPosition = UNSET; 219 } 220 mValue = mSpring.getFinalPosition(); 221 mVelocity = 0; 222 mEndRequested = false; 223 return true; 224 } 225 226 if (mPendingPosition != UNSET) { 227 double lastPosition = mSpring.getFinalPosition(); 228 // Approximate by considering half of the time spring position stayed at the old 229 // position, half of the time it's at the new position. 230 MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT / 2); 231 mSpring.setFinalPosition(mPendingPosition); 232 mPendingPosition = UNSET; 233 234 massState = mSpring.updateValues(massState.mValue, massState.mVelocity, deltaT / 2); 235 mValue = massState.mValue; 236 mVelocity = massState.mVelocity; 237 238 } else { 239 MassState massState = mSpring.updateValues(mValue, mVelocity, deltaT); 240 mValue = massState.mValue; 241 mVelocity = massState.mVelocity; 242 } 243 244 mValue = Math.max(mValue, mMinValue); 245 mValue = Math.min(mValue, mMaxValue); 246 247 if (isAtEquilibrium(mValue, mVelocity)) { 248 mValue = mSpring.getFinalPosition(); 249 mVelocity = 0f; 250 return true; 251 } 252 return false; 253 } 254 255 @Override getAcceleration(float value, float velocity)256 float getAcceleration(float value, float velocity) { 257 return mSpring.getAcceleration(value, velocity); 258 } 259 260 @Override isAtEquilibrium(float value, float velocity)261 boolean isAtEquilibrium(float value, float velocity) { 262 return mSpring.isAtEquilibrium(value, velocity); 263 } 264 265 @Override setValueThreshold(float threshold)266 void setValueThreshold(float threshold) { 267 } 268 } 269