blob: edefe9e3ab06b35867b14006c046a1b6e925c6d0 [file] [log] [blame]
/*
* 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 com.android.wm.shell.back;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static com.android.wm.shell.back.BackAnimationConstants.PROGRESS_COMMIT_THRESHOLD;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.RemoteException;
import android.util.FloatProperty;
import android.util.TypedValue;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackProgressAnimator;
import android.window.IOnBackInvokedCallback;
import com.android.internal.dynamicanimation.animation.SpringAnimation;
import com.android.internal.dynamicanimation.animation.SpringForce;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.annotations.ShellMainThread;
/** Class that defines cross-activity animation. */
@ShellMainThread
class CrossActivityAnimation {
/**
* Minimum scale of the entering/closing window.
*/
private static final float MIN_WINDOW_SCALE = 0.9f;
/** Duration of post animation after gesture committed. */
private static final int POST_ANIMATION_DURATION = 350;
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
private static final FloatProperty<CrossActivityAnimation> ENTER_PROGRESS_PROP =
new FloatProperty<>("enter-alpha") {
@Override
public void setValue(CrossActivityAnimation anim, float value) {
anim.setEnteringProgress(value);
}
@Override
public Float get(CrossActivityAnimation object) {
return object.getEnteringProgress();
}
};
private static final FloatProperty<CrossActivityAnimation> LEAVE_PROGRESS_PROP =
new FloatProperty<>("leave-alpha") {
@Override
public void setValue(CrossActivityAnimation anim, float value) {
anim.setLeavingProgress(value);
}
@Override
public Float get(CrossActivityAnimation object) {
return object.getLeavingProgress();
}
};
private static final float MIN_WINDOW_ALPHA = 0.01f;
private static final float WINDOW_X_SHIFT_DP = 96;
private static final int SCALE_FACTOR = 100;
// TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
private static final float TARGET_COMMIT_PROGRESS = 0.5f;
private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
private final Rect mStartTaskRect = new Rect();
private final float mCornerRadius;
// The closing window properties.
private final RectF mClosingRect = new RectF();
// The entering window properties.
private final Rect mEnteringStartRect = new Rect();
private final RectF mEnteringRect = new RectF();
private final SpringAnimation mEnteringProgressSpring;
private final SpringAnimation mLeavingProgressSpring;
// Max window x-shift in pixels.
private final float mWindowXShift;
private float mEnteringProgress = 0f;
private float mLeavingProgress = 0f;
private final PointF mInitialTouchPos = new PointF();
private final Matrix mTransformMatrix = new Matrix();
private final float[] mTmpFloat9 = new float[9];
private RemoteAnimationTarget mEnteringTarget;
private RemoteAnimationTarget mClosingTarget;
private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mBackInProgress = false;
private PointF mTouchPos = new PointF();
private IRemoteAnimationFinishedCallback mFinishCallback;
private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
final BackAnimationRunner mBackAnimationRunner;
private final BackAnimationBackground mBackground;
CrossActivityAnimation(Context context, BackAnimationBackground background) {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
mBackground = background;
mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
mEnteringProgressSpring.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_MEDIUM)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
mLeavingProgressSpring.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_MEDIUM)
.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
context.getResources().getDisplayMetrics());
}
/**
* Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
* From https://en.wikipedia.org/wiki/Smoothstep
*/
private static float smoothstep(float edge0, float edge1, float x) {
if (x < edge0) return 0;
if (x >= edge1) return 1;
x = (x - edge0) / (edge1 - edge0);
return x * x * (3 - 2 * x);
}
/**
* Linearly map x from range (a1, a2) to range (b1, b2).
*/
private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
}
/**
* Linearly map a normalized value from (0, 1) to (min, max).
*/
private static float mapRange(float value, float min, float max) {
return min + (value * (max - min));
}
private void startBackAnimation() {
if (mEnteringTarget == null || mClosingTarget == null) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
return;
}
mTransaction.setAnimationTransaction();
// Offset start rectangle to align task bounds.
mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
mStartTaskRect.offsetTo(0, 0);
// Draw background with task background color.
mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
}
private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
final float scale = targetRect.width() / mStartTaskRect.width();
mTransformMatrix.reset();
mTransformMatrix.setScale(scale, scale);
mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
mTransaction.setAlpha(leash, targetAlpha)
.setMatrix(leash, mTransformMatrix, mTmpFloat9)
.setWindowCrop(leash, mStartTaskRect)
.setCornerRadius(leash, mCornerRadius);
}
private void finishAnimation() {
if (mEnteringTarget != null) {
mEnteringTarget.leash.release();
mEnteringTarget = null;
}
if (mClosingTarget != null) {
mClosingTarget.leash.release();
mClosingTarget = null;
}
if (mBackground != null) {
mBackground.removeBackground(mTransaction);
}
mTransaction.apply();
mBackInProgress = false;
mTransformMatrix.reset();
mInitialTouchPos.set(0, 0);
if (mFinishCallback != null) {
try {
mFinishCallback.onAnimationFinished();
} catch (RemoteException e) {
e.printStackTrace();
}
mFinishCallback = null;
}
mEnteringProgressSpring.animateToFinalPosition(0);
mEnteringProgressSpring.skipToEnd();
mLeavingProgressSpring.animateToFinalPosition(0);
mLeavingProgressSpring.skipToEnd();
}
private void onGestureProgress(@NonNull BackEvent backEvent) {
if (!mBackInProgress) {
mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
mBackInProgress = true;
}
mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
float progress = backEvent.getProgress();
float springProgress = (progress > PROGRESS_COMMIT_THRESHOLD
? mapLinear(progress, 0.1f, 1, TARGET_COMMIT_PROGRESS, 1)
: mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
mLeavingProgressSpring.animateToFinalPosition(springProgress);
mEnteringProgressSpring.animateToFinalPosition(springProgress);
mBackground.onBackProgressed(progress);
}
private void onGestureCommitted() {
if (mEnteringTarget == null || mClosingTarget == null) {
finishAnimation();
return;
}
// End the fade animations
mLeavingProgressSpring.cancel();
mEnteringProgressSpring.cancel();
// We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
// coordinate of the gesture driven phase.
mEnteringRect.round(mEnteringStartRect);
mTransaction.hide(mClosingTarget.leash);
ValueAnimator valueAnimator =
ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(animation -> {
float progress = animation.getAnimatedFraction();
updatePostCommitEnteringAnimation(progress);
mTransaction.apply();
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishAnimation();
}
});
valueAnimator.start();
}
private void updatePostCommitEnteringAnimation(float progress) {
float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
float alpha = mapRange(progress, mEnteringProgress, 1.0f);
mEnteringRect.set(left, top, left + width, top + height);
applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
}
private float getEnteringProgress() {
return mEnteringProgress * SCALE_FACTOR;
}
private void setEnteringProgress(float value) {
mEnteringProgress = value / SCALE_FACTOR;
if (mEnteringTarget != null && mEnteringTarget.leash != null) {
transformWithProgress(
mEnteringProgress,
Math.max(
smoothstep(ENTER_ALPHA_THRESHOLD, 1, mEnteringProgress),
MIN_WINDOW_ALPHA), /* alpha */
mEnteringTarget.leash,
mEnteringRect,
-mWindowXShift,
0
);
}
}
private float getLeavingProgress() {
return mLeavingProgress * SCALE_FACTOR;
}
private void setLeavingProgress(float value) {
mLeavingProgress = value / SCALE_FACTOR;
if (mClosingTarget != null && mClosingTarget.leash != null) {
transformWithProgress(
mLeavingProgress,
Math.max(
1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
MIN_WINDOW_ALPHA),
mClosingTarget.leash,
mClosingRect,
0,
mWindowXShift
);
}
}
private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
RectF targetRect, float deltaXMin, float deltaXMax) {
final float touchY = mTouchPos.y;
final int width = mStartTaskRect.width();
final int height = mStartTaskRect.height();
final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
final float closingScale = MIN_WINDOW_SCALE
+ (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
final float closingWidth = closingScale * width;
final float closingHeight = (float) height / width * closingWidth;
// Move the window along the X axis.
float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
// Move the window along the Y axis.
final float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
final float closingTop = (height - closingHeight) * 0.5f;
targetRect.set(
closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
mTransaction.apply();
}
private final class Callback extends IOnBackInvokedCallback.Default {
@Override
public void onBackStarted(BackMotionEvent backEvent) {
mProgressAnimator.onBackStarted(backEvent,
CrossActivityAnimation.this::onGestureProgress);
}
@Override
public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
mProgressAnimator.onBackProgressed(backEvent);
}
@Override
public void onBackCancelled() {
mProgressAnimator.onBackCancelled(CrossActivityAnimation.this::finishAnimation);
}
@Override
public void onBackInvoked() {
mProgressAnimator.reset();
onGestureCommitted();
}
}
private final class Runner extends IRemoteAnimationRunner.Default {
@Override
public void onAnimationStart(
int transit,
RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers,
RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
for (RemoteAnimationTarget a : apps) {
if (a.mode == MODE_CLOSING) {
mClosingTarget = a;
}
if (a.mode == MODE_OPENING) {
mEnteringTarget = a;
}
}
startBackAnimation();
mFinishCallback = finishedCallback;
}
@Override
public void onAnimationCancelled() {
finishAnimation();
}
}
}