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