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 }