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