1 /*
2  * Copyright (C) 2020 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.constraintlayout.core.motion.utils;
18 
19 /**
20  * This contains the class to provide the logic for an animation to come to a stop.
21  * The setup defines a series of velocity gradients that gets to the desired position
22  * ending at 0 velocity.
23  * The path is computed such that the velocities are continuous
24  *
25  *
26  */
27 public class StopLogicEngine implements StopEngine {
28     // the velocity at the start of each period
29     private float mStage1Velocity, mStage2Velocity, mStage3Velocity;
30     private float mStage1Duration, mStage2Duration, mStage3Duration; // the time for each period
31     private float mStage1EndPosition, mStage2EndPosition, mStage3EndPosition; // ending position
32     private int mNumberOfStages;
33     private String mType;
34     private boolean mBackwards = false;
35     private float mStartPosition;
36     private float mLastPosition;
37     private float mLastTime;
38     @SuppressWarnings("unused")
39     private boolean mDone = false;
40     private static final float EPSILON = 0.00001f;
41 
42     /**
43      * Debugging logic to log the state.
44      *
45      * @param desc Description to pre append
46      * @param time Time during animation
47      * @return string useful for debugging the state of the StopLogic
48      */
49     @Override
debug(String desc, float time)50     public String debug(String desc, float time) {
51         String ret = desc + " ===== " + mType + "\n";
52         ret += desc + (mBackwards ? "backwards" : "forward ")
53                 + " time = " + time + "  stages " + mNumberOfStages + "\n";
54         ret += desc + " dur " + mStage1Duration + " vel "
55                 + mStage1Velocity + " pos " + mStage1EndPosition + "\n";
56 
57         if (mNumberOfStages > 1) {
58             ret += desc + " dur " + mStage2Duration + " vel "
59                     + mStage2Velocity + " pos " + mStage2EndPosition + "\n";
60 
61         }
62         if (mNumberOfStages > 2) {
63             ret += desc + " dur " + mStage3Duration + " vel "
64                     + mStage3Velocity + " pos " + mStage3EndPosition + "\n";
65         }
66 
67         if (time <= mStage1Duration) {
68             ret += desc + "stage 0" + "\n";
69             return ret;
70         }
71         if (mNumberOfStages == 1) {
72             ret += desc + "end stage 0" + "\n";
73             return ret;
74         }
75         time -= mStage1Duration;
76         if (time < mStage2Duration) {
77 
78             ret += desc + " stage 1" + "\n";
79             return ret;
80         }
81         if (mNumberOfStages == 2) {
82             ret += desc + "end stage 1" + "\n";
83             return ret;
84         }
85         time -= mStage2Duration;
86         if (time < mStage3Duration) {
87 
88             ret += desc + " stage 2" + "\n";
89             return ret;
90         }
91         ret += desc + " end stage 2" + "\n";
92         return ret;
93     }
94 
95     // @TODO: add description
96     @Override
getVelocity(float x)97     public float getVelocity(float x) {
98         if (x <= mStage1Duration) {
99             return mStage1Velocity + (mStage2Velocity - mStage1Velocity) * x / mStage1Duration;
100         }
101         if (mNumberOfStages == 1) {
102             return 0;
103         }
104         x -= mStage1Duration;
105         if (x < mStage2Duration) {
106 
107             return mStage2Velocity + (mStage3Velocity - mStage2Velocity) * x / mStage2Duration;
108         }
109         if (mNumberOfStages == 2) {
110             return 0;
111         }
112         x -= mStage2Duration;
113         if (x < mStage3Duration) {
114 
115             return mStage3Velocity - mStage3Velocity * x / mStage3Duration;
116         }
117         return 0;
118     }
119 
calcY(float time)120     private float calcY(float time) {
121         mDone = false;
122         if (time <= mStage1Duration) {
123             return mStage1Velocity * time + (mStage2Velocity - mStage1Velocity)
124                     * time * time / (2 * mStage1Duration);
125         }
126         if (mNumberOfStages == 1) {
127             return mStage1EndPosition;
128         }
129         time -= mStage1Duration;
130         if (time < mStage2Duration) {
131 
132             return mStage1EndPosition + mStage2Velocity * time
133                     + (mStage3Velocity - mStage2Velocity) * time * time / (2 * mStage2Duration);
134         }
135         if (mNumberOfStages == 2) {
136             return mStage2EndPosition;
137         }
138         time -= mStage2Duration;
139         if (time <= mStage3Duration) {
140 
141             return mStage2EndPosition + mStage3Velocity
142                     * time - mStage3Velocity * time * time / (2 * mStage3Duration);
143         }
144         mDone = true;
145         return mStage3EndPosition;
146     }
147 
148     // @TODO: add description
config(float currentPos, float destination, float currentVelocity, float maxTime, float maxAcceleration, float maxVelocity)149     public void config(float currentPos, float destination, float currentVelocity,
150             float maxTime, float maxAcceleration, float maxVelocity) {
151         mDone = false;
152         mStartPosition = currentPos;
153         mBackwards = (currentPos > destination);
154         if (mBackwards) {
155             setup(-currentVelocity, currentPos - destination,
156                     maxAcceleration, maxVelocity, maxTime);
157         } else {
158             setup(currentVelocity, destination - currentPos, maxAcceleration, maxVelocity, maxTime);
159         }
160     }
161 
162     // @TODO: add description
163     @Override
getInterpolation(float v)164     public float getInterpolation(float v) {
165         float y = calcY(v);
166         mLastPosition = y;
167         mLastTime = v;
168         return mBackwards ? mStartPosition - y : mStartPosition + y;
169     }
170 
171     @Override
getVelocity()172     public float getVelocity() {
173         return mBackwards ? -getVelocity(mLastTime) : getVelocity(mLastTime);
174     }
175 
176     @Override
isStopped()177     public boolean isStopped() {
178         return getVelocity() < EPSILON && Math.abs(mStage3EndPosition - mLastPosition) < EPSILON;
179     }
180 
setup(float velocity, float distance, float maxAcceleration, float maxVelocity, float maxTime)181     private void setup(float velocity, float distance, float maxAcceleration, float maxVelocity,
182             float maxTime) {
183         mDone = false;
184         mStage3EndPosition = distance;
185         if (velocity == 0) {
186             velocity = 0.0001f;
187         }
188         float min_time_to_stop = velocity / maxAcceleration;
189         float stopDistance = min_time_to_stop * velocity / 2;
190 
191         if (velocity < 0) { // backward
192             float timeToZeroVelocity = -velocity / maxAcceleration;
193             float reversDistanceTraveled = timeToZeroVelocity * velocity / 2;
194             float totalDistance = distance - reversDistanceTraveled;
195             float peak_v = (float) Math.sqrt(maxAcceleration * totalDistance);
196             if (peak_v < maxVelocity) { // accelerate then decelerate
197                 mType = "backward accelerate, decelerate";
198                 this.mNumberOfStages = 2;
199                 this.mStage1Velocity = velocity;
200                 this.mStage2Velocity = peak_v;
201                 this.mStage3Velocity = 0;
202                 this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
203                 this.mStage2Duration = peak_v / maxAcceleration;
204                 this.mStage1EndPosition = (velocity + peak_v) * this.mStage1Duration / 2;
205                 this.mStage2EndPosition = distance;
206                 this.mStage3EndPosition = distance;
207                 return;
208             }
209             mType = "backward accelerate cruse decelerate";
210             this.mNumberOfStages = 3;
211             this.mStage1Velocity = velocity;
212             this.mStage2Velocity = maxVelocity;
213             this.mStage3Velocity = maxVelocity;
214 
215             this.mStage1Duration = (maxVelocity - velocity) / maxAcceleration;
216             this.mStage3Duration = maxVelocity / maxAcceleration;
217             float accDist = (velocity + maxVelocity) * this.mStage1Duration / 2;
218             float decDist = (maxVelocity * this.mStage3Duration) / 2;
219             this.mStage2Duration = (distance - accDist - decDist) / maxVelocity;
220             this.mStage1EndPosition = accDist;
221             this.mStage2EndPosition = (distance - decDist);
222             this.mStage3EndPosition = distance;
223             return;
224         }
225 
226         if (stopDistance >= distance) { // we cannot make it hit the breaks.
227             // we do a force hard stop
228             mType = "hard stop";
229             float time = 2 * distance / velocity;
230             this.mNumberOfStages = 1;
231             this.mStage1Velocity = velocity;
232             this.mStage2Velocity = 0;
233             this.mStage1EndPosition = distance;
234             this.mStage1Duration = time;
235             return;
236         }
237 
238         float distance_before_break = distance - stopDistance;
239         float cruseTime = distance_before_break / velocity; // do we just Cruse then stop?
240         if (cruseTime + min_time_to_stop < maxTime) { // close enough maintain v then break
241             mType = "cruse decelerate";
242             this.mNumberOfStages = 2;
243             this.mStage1Velocity = velocity;
244             this.mStage2Velocity = velocity;
245             this.mStage3Velocity = 0;
246             this.mStage1EndPosition = distance_before_break;
247             this.mStage2EndPosition = distance;
248             this.mStage1Duration = cruseTime;
249             this.mStage2Duration = velocity / maxAcceleration;
250             return;
251         }
252 
253         float peak_v = (float) Math.sqrt(maxAcceleration * distance + velocity * velocity / 2);
254         this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
255         this.mStage2Duration = peak_v / maxAcceleration;
256         if (peak_v < maxVelocity) { // accelerate then decelerate
257             mType = "accelerate decelerate";
258             this.mNumberOfStages = 2;
259             this.mStage1Velocity = velocity;
260             this.mStage2Velocity = peak_v;
261             this.mStage3Velocity = 0;
262             this.mStage1Duration = (peak_v - velocity) / maxAcceleration;
263             this.mStage2Duration = peak_v / maxAcceleration;
264             this.mStage1EndPosition = (velocity + peak_v) * this.mStage1Duration / 2;
265             this.mStage2EndPosition = distance;
266 
267             return;
268         }
269         mType = "accelerate cruse decelerate";
270         // accelerate, cruse then decelerate
271         this.mNumberOfStages = 3;
272         this.mStage1Velocity = velocity;
273         this.mStage2Velocity = maxVelocity;
274         this.mStage3Velocity = maxVelocity;
275 
276         this.mStage1Duration = (maxVelocity - velocity) / maxAcceleration;
277         this.mStage3Duration = maxVelocity / maxAcceleration;
278         float accDist = (velocity + maxVelocity) * this.mStage1Duration / 2;
279         float decDist = (maxVelocity * this.mStage3Duration) / 2;
280 
281         this.mStage2Duration = (distance - accDist - decDist) / maxVelocity;
282         this.mStage1EndPosition = accDist;
283         this.mStage2EndPosition = (distance - decDist);
284         this.mStage3EndPosition = distance;
285     }
286 
287     // Support the simple Decelerate use case
288     public static class Decelerate implements StopEngine {
289         private float mDestination;
290         private float mInitialVelocity;
291         private float mAcceleration;
292         private float mLastVelocity;
293         private float mDuration;
294         private float mInitialPos;
295         private boolean mDone = false;
296 
297         @Override
debug(String desc, float time)298         public String debug(String desc, float time) {
299             return mDuration + " " + mLastVelocity;
300         }
301 
302         @Override
getVelocity(float time)303         public float getVelocity(float time) {
304             if (time > mDuration) {
305                 return 0;
306             }
307             return mLastVelocity = mInitialVelocity + mAcceleration * time;
308         }
309 
310         @Override
getInterpolation(float time)311         public float getInterpolation(float time) {
312             if (time > mDuration) {
313                 mDone = true;
314                 return mDestination;
315             }
316             getVelocity(time);
317             return mInitialPos + (mInitialVelocity + mAcceleration * time / 2) * time;
318         }
319 
320         @Override
getVelocity()321         public float getVelocity() {
322             return mLastVelocity;
323         }
324 
325         @Override
isStopped()326         public boolean isStopped() {
327             return mDone;
328         }
329 
330         /**
331          * Configure simple deceleration controller
332          *
333          * @param currentPos      the current position
334          * @param destination     the destination position
335          * @param currentVelocity the currentVelocity change in pos / second
336          */
config(float currentPos, float destination, float currentVelocity)337         public void config(float currentPos, float destination, float currentVelocity) {
338             mDone = false;
339             mDestination = destination;
340             mInitialVelocity = currentVelocity;
341             mInitialPos = currentPos;
342             float distance = mDestination - currentPos;
343             mDuration = distance / (currentVelocity / 2);
344             mAcceleration = -currentVelocity / mDuration;
345         }
346     }
347 }
348