1 /*
2  * Copyright (C) 2022 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 android.window;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.util.FloatProperty;
22 import android.util.TimeUtils;
23 import android.view.Choreographer;
24 
25 import com.android.internal.dynamicanimation.animation.DynamicAnimation;
26 import com.android.internal.dynamicanimation.animation.FlingAnimation;
27 import com.android.internal.dynamicanimation.animation.FloatValueHolder;
28 import com.android.internal.dynamicanimation.animation.SpringAnimation;
29 import com.android.internal.dynamicanimation.animation.SpringForce;
30 
31 /**
32  * An animator that drives the predictive back progress with a spring.
33  *
34  * The back gesture's latest touch point and committal state determines the final position of
35  * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with
36  * smoothly transitioning progress values.
37  *
38  * @hide
39  */
40 public class BackProgressAnimator implements DynamicAnimation.OnAnimationUpdateListener {
41     /**
42      *  A factor to scale the input progress by, so that it works better with the spring.
43      *  We divide the output progress by this value before sending it to apps, so that apps
44      *  always receive progress values in [0, 1].
45      */
46     private static final float SCALE_FACTOR = 100f;
47     private static final float FLING_FRICTION = 8f;
48     private final SpringAnimation mSpring;
49     private ProgressCallback mCallback;
50     private float mProgress = 0;
51     private float mVelocity = 0;
52     private BackMotionEvent mLastBackEvent;
53     private boolean mBackAnimationInProgress = false;
54     @Nullable
55     private Runnable mBackCancelledFinishRunnable;
56     @Nullable
57     private Runnable mBackInvokedFinishRunnable;
58     private FlingAnimation mBackInvokedFlingAnim;
59     private final DynamicAnimation.OnAnimationEndListener mOnAnimationEndListener =
60             (animation, canceled, value, velocity) -> {
61                 if (mBackCancelledFinishRunnable != null) invokeBackCancelledRunnable();
62                 if (mBackInvokedFinishRunnable != null) invokeBackInvokedRunnable();
63                 reset();
64             };
65     private final DynamicAnimation.OnAnimationUpdateListener mOnBackInvokedFlingUpdateListener =
66             (animation, progress, velocity) -> updateProgressValue(progress, velocity);
67 
68 
setProgress(float progress)69     private void setProgress(float progress) {
70         mProgress = progress;
71     }
72 
getProgress()73     private float getProgress() {
74         return mProgress;
75     }
76 
77     private static final FloatProperty<BackProgressAnimator> PROGRESS_PROP =
78             new FloatProperty<BackProgressAnimator>("progress") {
79                 @Override
80                 public void setValue(BackProgressAnimator animator, float value) {
81                     animator.setProgress(value);
82                 }
83 
84                 @Override
85                 public Float get(BackProgressAnimator object) {
86                     return object.getProgress();
87                 }
88             };
89 
90     @Override
onAnimationUpdate(DynamicAnimation animation, float value, float velocity)91     public void onAnimationUpdate(DynamicAnimation animation, float value, float velocity) {
92         if (mBackInvokedFinishRunnable == null) updateProgressValue(value, velocity);
93     }
94 
95 
96     /** A callback to be invoked when there's a progress value update from the animator. */
97     public interface ProgressCallback {
98         /** Called when there's a progress value update. */
onProgressUpdate(BackEvent event)99         void onProgressUpdate(BackEvent event);
100     }
101 
BackProgressAnimator()102     public BackProgressAnimator() {
103         mSpring = new SpringAnimation(this, PROGRESS_PROP);
104         mSpring.addUpdateListener(this);
105         mSpring.setSpring(new SpringForce()
106                 .setStiffness(SpringForce.STIFFNESS_MEDIUM)
107                 .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
108     }
109 
110     /**
111      * Sets a new target position for the back progress.
112      *
113      * @param event the {@link BackMotionEvent} containing the latest target progress.
114      */
onBackProgressed(BackMotionEvent event)115     public void onBackProgressed(BackMotionEvent event) {
116         if (!mBackAnimationInProgress) {
117             return;
118         }
119         mLastBackEvent = event;
120         if (mSpring == null) {
121             return;
122         }
123         mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR);
124     }
125 
126     /**
127      * Starts the back progress animation.
128      *
129      * @param event the {@link BackMotionEvent} that started the gesture.
130      * @param callback the back callback to invoke for the gesture. It will receive back progress
131      *                 dispatches as the progress animation updates.
132      */
onBackStarted(BackMotionEvent event, ProgressCallback callback)133     public void onBackStarted(BackMotionEvent event, ProgressCallback callback) {
134         mLastBackEvent = event;
135         mCallback = callback;
136         mBackAnimationInProgress = true;
137         updateProgressValue(0, 0);
138     }
139 
140     /**
141      * Resets the back progress animation. This should be called when back is invoked or cancelled.
142      */
reset()143     public void reset() {
144         if (mBackCancelledFinishRunnable != null) {
145             // Ensure that last progress value that apps see is 0
146             updateProgressValue(0, 0);
147             invokeBackCancelledRunnable();
148         } else if (mBackInvokedFinishRunnable != null) {
149             invokeBackInvokedRunnable();
150         }
151         if (mBackInvokedFlingAnim != null) {
152             mBackInvokedFlingAnim.cancel();
153             mBackInvokedFlingAnim = null;
154         }
155         mSpring.animateToFinalPosition(0);
156         if (mSpring.canSkipToEnd()) {
157             mSpring.skipToEnd();
158         } else {
159             // Should never happen.
160             mSpring.cancel();
161         }
162         mBackAnimationInProgress = false;
163         mLastBackEvent = null;
164         mCallback = null;
165         mProgress = 0;
166     }
167 
168     /**
169      * Animate the back progress animation a bit further with a high friction considering the
170      * current progress and velocity.
171      *
172      * @param finishCallback the callback to be invoked when the final destination is reached
173      */
onBackInvoked(@onNull Runnable finishCallback)174     public void onBackInvoked(@NonNull Runnable finishCallback) {
175         mBackInvokedFinishRunnable = finishCallback;
176         mSpring.animateToFinalPosition(0);
177 
178         mBackInvokedFlingAnim = new FlingAnimation(new FloatValueHolder())
179                 .setStartValue(mProgress)
180                 .setFriction(FLING_FRICTION)
181                 .setStartVelocity(mVelocity)
182                 .setMinValue(0)
183                 .setMaxValue(SCALE_FACTOR);
184         mBackInvokedFlingAnim.addUpdateListener(mOnBackInvokedFlingUpdateListener);
185         mBackInvokedFlingAnim.addEndListener(mOnAnimationEndListener);
186         mBackInvokedFlingAnim.start();
187         // do an animation-frame immediately to prevent idle frame
188         mBackInvokedFlingAnim.doAnimationFrame(
189                 Choreographer.getInstance().getLastFrameTimeNanos() / TimeUtils.NANOS_PER_MS);
190     }
191 
192     /**
193      * Animate the back progress animation from current progress to start position.
194      * This should be called when back is cancelled.
195      *
196      * @param finishCallback the callback to be invoked when the progress is reach to 0.
197      */
onBackCancelled(@onNull Runnable finishCallback)198     public void onBackCancelled(@NonNull Runnable finishCallback) {
199         mBackCancelledFinishRunnable = finishCallback;
200         mSpring.addEndListener(mOnAnimationEndListener);
201         mSpring.animateToFinalPosition(0);
202     }
203 
204     /**
205      * Removes the finishCallback passed into {@link #onBackCancelled}
206      */
removeOnBackCancelledFinishCallback()207     public void removeOnBackCancelledFinishCallback() {
208         mSpring.removeEndListener(mOnAnimationEndListener);
209         mBackCancelledFinishRunnable = null;
210     }
211 
212     /** Returns true if the back animation is in progress. */
isBackAnimationInProgress()213     boolean isBackAnimationInProgress() {
214         return mBackAnimationInProgress;
215     }
216 
217     /**
218      * @return The last recorded velocity. Unit: change in progress per second
219      */
getVelocity()220     public float getVelocity() {
221         return mVelocity / SCALE_FACTOR;
222     }
223 
updateProgressValue(float progress, float velocity)224     private void updateProgressValue(float progress, float velocity) {
225         mVelocity = velocity;
226         if (mLastBackEvent == null || mCallback == null || !mBackAnimationInProgress) {
227             return;
228         }
229         mCallback.onProgressUpdate(
230                 new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(),
231                         progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge()));
232     }
233 
invokeBackCancelledRunnable()234     private void invokeBackCancelledRunnable() {
235         mSpring.removeEndListener(mOnAnimationEndListener);
236         mBackCancelledFinishRunnable.run();
237         mBackCancelledFinishRunnable = null;
238     }
239 
invokeBackInvokedRunnable()240     private void invokeBackInvokedRunnable() {
241         mBackInvokedFlingAnim.removeUpdateListener(mOnBackInvokedFlingUpdateListener);
242         mBackInvokedFlingAnim.removeEndListener(mOnAnimationEndListener);
243         mBackInvokedFinishRunnable.run();
244         mBackInvokedFinishRunnable = null;
245     }
246 
247 }