/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.window; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.FloatProperty; import com.android.internal.dynamicanimation.animation.DynamicAnimation; import com.android.internal.dynamicanimation.animation.SpringAnimation; import com.android.internal.dynamicanimation.animation.SpringForce; /** * An animator that drives the predictive back progress with a spring. * * The back gesture's latest touch point and committal state determines the final position of * the spring. The continuous movement of the spring is used to produce {@link BackEvent}s with * smoothly transitioning progress values. * * @hide */ public class BackProgressAnimator { /** * A factor to scale the input progress by, so that it works better with the spring. * We divide the output progress by this value before sending it to apps, so that apps * always receive progress values in [0, 1]. */ private static final float SCALE_FACTOR = 100f; private final SpringAnimation mSpring; private ProgressCallback mCallback; private float mProgress = 0; private BackMotionEvent mLastBackEvent; private boolean mBackAnimationInProgress = false; @Nullable private Runnable mBackCancelledFinishRunnable; private final DynamicAnimation.OnAnimationEndListener mOnAnimationEndListener = (animation, canceled, value, velocity) -> { invokeBackCancelledRunnable(); reset(); }; private void setProgress(float progress) { mProgress = progress; } private float getProgress() { return mProgress; } private static final FloatProperty PROGRESS_PROP = new FloatProperty("progress") { @Override public void setValue(BackProgressAnimator animator, float value) { animator.setProgress(value); animator.updateProgressValue(value); } @Override public Float get(BackProgressAnimator object) { return object.getProgress(); } }; /** A callback to be invoked when there's a progress value update from the animator. */ public interface ProgressCallback { /** Called when there's a progress value update. */ void onProgressUpdate(BackEvent event); } public BackProgressAnimator() { mSpring = new SpringAnimation(this, PROGRESS_PROP); mSpring.setSpring(new SpringForce() .setStiffness(SpringForce.STIFFNESS_MEDIUM) .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); } /** * Sets a new target position for the back progress. * * @param event the {@link BackMotionEvent} containing the latest target progress. */ public void onBackProgressed(BackMotionEvent event) { if (!mBackAnimationInProgress) { return; } mLastBackEvent = event; if (mSpring == null) { return; } mSpring.animateToFinalPosition(event.getProgress() * SCALE_FACTOR); } /** * Starts the back progress animation. * * @param event the {@link BackMotionEvent} that started the gesture. * @param callback the back callback to invoke for the gesture. It will receive back progress * dispatches as the progress animation updates. */ public void onBackStarted(BackMotionEvent event, ProgressCallback callback) { reset(); mLastBackEvent = event; mCallback = callback; mBackAnimationInProgress = true; updateProgressValue(0); } /** * Resets the back progress animation. This should be called when back is invoked or cancelled. */ public void reset() { if (mBackCancelledFinishRunnable != null) { // Ensure that last progress value that apps see is 0 updateProgressValue(0); invokeBackCancelledRunnable(); } mSpring.animateToFinalPosition(0); if (mSpring.canSkipToEnd()) { mSpring.skipToEnd(); } else { // Should never happen. mSpring.cancel(); } mBackAnimationInProgress = false; mLastBackEvent = null; mCallback = null; mProgress = 0; } /** * Animate the back progress animation from current progress to start position. * This should be called when back is cancelled. * * @param finishCallback the callback to be invoked when the progress is reach to 0. */ public void onBackCancelled(@NonNull Runnable finishCallback) { mBackCancelledFinishRunnable = finishCallback; mSpring.addEndListener(mOnAnimationEndListener); mSpring.animateToFinalPosition(0); } /** * Removes the finishCallback passed into {@link #onBackCancelled} */ public void removeOnBackCancelledFinishCallback() { mSpring.removeEndListener(mOnAnimationEndListener); mBackCancelledFinishRunnable = null; } /** Returns true if the back animation is in progress. */ boolean isBackAnimationInProgress() { return mBackAnimationInProgress; } private void updateProgressValue(float progress) { if (mLastBackEvent == null || mCallback == null || !mBackAnimationInProgress) { return; } mCallback.onProgressUpdate( new BackEvent(mLastBackEvent.getTouchX(), mLastBackEvent.getTouchY(), progress / SCALE_FACTOR, mLastBackEvent.getSwipeEdge())); } private void invokeBackCancelledRunnable() { mSpring.removeEndListener(mOnAnimationEndListener); mBackCancelledFinishRunnable.run(); mBackCancelledFinishRunnable = null; } }