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 }