1 /* 2 * Copyright (C) 2019 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 package com.android.launcher3.anim; 17 18 import static com.android.launcher3.anim.Interpolators.LINEAR; 19 20 import android.animation.Animator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.util.FloatProperty; 24 25 import androidx.annotation.FloatRange; 26 import androidx.dynamicanimation.animation.SpringForce; 27 28 import com.android.launcher3.util.DefaultDisplay; 29 30 /** 31 * Utility class to build an object animator which follows the same path as a spring animation for 32 * an underdamped spring. 33 */ 34 public class SpringAnimationBuilder { 35 36 private final Context mContext; 37 38 private float mStartValue; 39 private float mEndValue; 40 private float mVelocity = 0; 41 42 private float mStiffness = SpringForce.STIFFNESS_MEDIUM; 43 private float mDampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY; 44 private float mMinVisibleChange = 1; 45 46 // Multiplier to the min visible change value for value threshold 47 private static final float THRESHOLD_MULTIPLIER = 0.65f; 48 49 /** 50 * The spring equation is given as 51 * x = e^(-beta*t/2) * (a cos(gamma * t) + b sin(gamma * t) 52 * v = e^(-beta*t/2) * ((2 * a * gamma + beta * b) * sin(gamma * t) 53 * + (a * beta - 2 * b * gamma) * cos(gamma * t)) / 2 54 * 55 * a = x(0) 56 * b = beta * x(0) / (2 * gamma) + v(0) / gamma 57 */ 58 private double beta; 59 private double gamma; 60 61 private double a, b; 62 private double va, vb; 63 64 // Threshold for velocity and value to determine when it's reasonable to assume that the spring 65 // is approximately at rest. 66 private double mValueThreshold; 67 private double mVelocityThreshold; 68 69 private float mDuration = 0; 70 SpringAnimationBuilder(Context context)71 public SpringAnimationBuilder(Context context) { 72 mContext = context; 73 } 74 setEndValue(float value)75 public SpringAnimationBuilder setEndValue(float value) { 76 mEndValue = value; 77 return this; 78 } 79 setStartValue(float value)80 public SpringAnimationBuilder setStartValue(float value) { 81 mStartValue = value; 82 return this; 83 } 84 setValues(float... values)85 public SpringAnimationBuilder setValues(float... values) { 86 if (values.length > 1) { 87 mStartValue = values[0]; 88 mEndValue = values[values.length - 1]; 89 } else { 90 mEndValue = values[0]; 91 } 92 return this; 93 } 94 setStiffness( @loatRangefrom = 0.0, fromInclusive = false) float stiffness)95 public SpringAnimationBuilder setStiffness( 96 @FloatRange(from = 0.0, fromInclusive = false) float stiffness) { 97 if (stiffness <= 0) { 98 throw new IllegalArgumentException("Spring stiffness constant must be positive."); 99 } 100 mStiffness = stiffness; 101 return this; 102 } 103 setDampingRatio( @loatRangefrom = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) float dampingRatio)104 public SpringAnimationBuilder setDampingRatio( 105 @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) 106 float dampingRatio) { 107 if (dampingRatio <= 0 || dampingRatio >= 1) { 108 throw new IllegalArgumentException("Damping ratio must be between 0 and 1"); 109 } 110 mDampingRatio = dampingRatio; 111 return this; 112 } 113 setMinimumVisibleChange( @loatRangefrom = 0.0, fromInclusive = false) float minimumVisibleChange)114 public SpringAnimationBuilder setMinimumVisibleChange( 115 @FloatRange(from = 0.0, fromInclusive = false) float minimumVisibleChange) { 116 if (minimumVisibleChange <= 0) { 117 throw new IllegalArgumentException("Minimum visible change must be positive."); 118 } 119 mMinVisibleChange = minimumVisibleChange; 120 return this; 121 } 122 setStartVelocity(float startVelocity)123 public SpringAnimationBuilder setStartVelocity(float startVelocity) { 124 mVelocity = startVelocity; 125 return this; 126 } 127 getInterpolatedValue(float fraction)128 public float getInterpolatedValue(float fraction) { 129 return getValue(mDuration * fraction); 130 } 131 getValue(float time)132 private float getValue(float time) { 133 return (float) (exponentialComponent(time) * cosSinX(time)) + mEndValue; 134 } 135 computeParams()136 public SpringAnimationBuilder computeParams() { 137 int singleFrameMs = DefaultDisplay.getSingleFrameMs(mContext); 138 double naturalFreq = Math.sqrt(mStiffness); 139 double dampedFreq = naturalFreq * Math.sqrt(1 - mDampingRatio * mDampingRatio); 140 141 // All the calculations assume the stable position to be 0, shift the values accordingly. 142 beta = 2 * mDampingRatio * naturalFreq; 143 gamma = dampedFreq; 144 a = mStartValue - mEndValue; 145 b = beta * a / (2 * gamma) + mVelocity / gamma; 146 147 va = a * beta / 2 - b * gamma; 148 vb = a * gamma + beta * b / 2; 149 150 mValueThreshold = mMinVisibleChange * THRESHOLD_MULTIPLIER; 151 152 // This multiplier is used to calculate the velocity threshold given a certain value 153 // threshold. The idea is that if it takes >= 1 frame to move the value threshold amount, 154 // then the velocity is a reasonable threshold. 155 mVelocityThreshold = mValueThreshold * 1000.0 / singleFrameMs; 156 157 // Find the duration (in seconds) for the spring to reach equilibrium. 158 // equilibrium is reached when x = 0 159 double duration = Math.atan2(-a, b) / gamma; 160 161 // Keep moving ahead until the velocity reaches equilibrium. 162 double piByG = Math.PI / gamma; 163 while (duration < 0 || Math.abs(exponentialComponent(duration) * cosSinV(duration)) 164 >= mVelocityThreshold) { 165 duration += piByG; 166 } 167 168 // Find the shortest time 169 double edgeTime = Math.max(0, duration - piByG / 2); 170 double minDiff = singleFrameMs / 2000.0; // Half frame time in seconds 171 172 do { 173 if ((duration - edgeTime) < minDiff) { 174 break; 175 } 176 double mid = (edgeTime + duration) / 2; 177 if (isAtEquilibrium(mid)) { 178 duration = mid; 179 } else { 180 edgeTime = mid; 181 } 182 } while (true); 183 184 mDuration = (float) duration; 185 return this; 186 } 187 getDuration()188 public long getDuration() { 189 return (long) (1000.0 * mDuration); 190 } 191 build(T target, FloatProperty<T> property)192 public <T> ValueAnimator build(T target, FloatProperty<T> property) { 193 computeParams(); 194 195 ValueAnimator animator = ValueAnimator.ofFloat(0, mDuration); 196 animator.setDuration(getDuration()).setInterpolator(LINEAR); 197 animator.addUpdateListener(anim -> 198 property.set(target, getInterpolatedValue(anim.getAnimatedFraction()))); 199 animator.addListener(new AnimationSuccessListener() { 200 @Override 201 public void onAnimationSuccess(Animator animation) { 202 property.set(target, mEndValue); 203 } 204 }); 205 return animator; 206 } 207 isAtEquilibrium(double t)208 private boolean isAtEquilibrium(double t) { 209 double ec = exponentialComponent(t); 210 211 if (Math.abs(ec * cosSinX(t)) >= mValueThreshold) { 212 return false; 213 } 214 return Math.abs(ec * cosSinV(t)) < mVelocityThreshold; 215 } 216 exponentialComponent(double t)217 private double exponentialComponent(double t) { 218 return Math.pow(Math.E, - beta * t / 2); 219 } 220 cosSinX(double t)221 private double cosSinX(double t) { 222 return cosSin(t, a, b); 223 } 224 cosSinV(double t)225 private double cosSinV(double t) { 226 return cosSin(t, va, vb); 227 } 228 cosSin(double t, double cosFactor, double sinFactor)229 private double cosSin(double t, double cosFactor, double sinFactor) { 230 double angle = t * gamma; 231 return cosFactor * Math.cos(angle) + sinFactor * Math.sin(angle); 232 } 233 } 234