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