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