blob: a7dd27a0784f0062ca47f578ee75f894f275a680 [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 android.window.BackEvent.EDGE_RIGHT;
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.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.animation.AccelerateDecelerateInterpolator;
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.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.annotations.ShellMainThread;
/**
* Controls the animation of swiping back and returning to another task.
*
* This is a two part animation. The first part is an animation that tracks gesture location to
* scale and move the closing and entering app windows.
* Once the gesture is committed, the second part remains the closing window in place.
* The entering window plays the rest of app opening transition to enter full screen.
*
* This animation is used only for apps that enable back dispatching via
* {@link android.window.OnBackInvokedDispatcher}. The controller registers
* an {@link IOnBackInvokedCallback} with WM Shell and receives back dispatches when a back
* navigation to launcher starts.
*/
@ShellMainThread
class CrossTaskBackAnimation {
private static final int BACKGROUNDCOLOR = 0x43433A;
/**
* Minimum scale of the entering window.
*/
private static final float ENTERING_MIN_WINDOW_SCALE = 0.85f;
/**
* Minimum scale of the closing window.
*/
private static final float CLOSING_MIN_WINDOW_SCALE = 0.75f;
/**
* Minimum color scale of the closing window.
*/
private static final float CLOSING_MIN_WINDOW_COLOR_SCALE = 0.1f;
/**
* The margin between the entering window and the closing window
*/
private static final int WINDOW_MARGIN = 35;
/** Max window translation in the Y axis. */
private static final int WINDOW_MAX_DELTA_Y = 160;
private final Rect mStartTaskRect = new Rect();
private final float mCornerRadius;
// The closing window properties.
private final RectF mClosingCurrentRect = new RectF();
// The entering window properties.
private final Rect mEnteringStartRect = new Rect();
private final RectF mEnteringCurrentRect = new RectF();
private final PointF mInitialTouchPos = new PointF();
private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
private final Matrix mTransformMatrix = new Matrix();
private final float[] mTmpFloat9 = new float[9];
private final float[] mTmpTranslate = {0, 0, 0};
private RemoteAnimationTarget mEnteringTarget;
private RemoteAnimationTarget mClosingTarget;
private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mBackInProgress = false;
private boolean mIsRightEdge;
private float mProgress = 0;
private PointF mTouchPos = new PointF();
private IRemoteAnimationFinishedCallback mFinishCallback;
private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
final BackAnimationRunner mBackAnimationRunner;
private final BackAnimationBackground mBackground;
CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
mBackAnimationRunner = new BackAnimationRunner(new Callback(), new Runner());
mBackground = background;
}
private float getInterpolatedProgress(float backProgress) {
return 1 - (1 - backProgress) * (1 - backProgress) * (1 - backProgress);
}
private void startBackAnimation() {
if (mEnteringTarget == null || mClosingTarget == null) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
return;
}
// Offset start rectangle to align task bounds.
mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
mStartTaskRect.offsetTo(0, 0);
// Draw background.
mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
BACKGROUNDCOLOR, mTransaction);
}
private void updateGestureBackProgress(float progress, BackEvent event) {
if (mEnteringTarget == null || mClosingTarget == null) {
return;
}
float touchX = event.getTouchX();
float touchY = event.getTouchY();
float dX = Math.abs(touchX - mInitialTouchPos.x);
// The 'follow width' is the width of the window if it completely matches
// the gesture displacement.
final int width = mStartTaskRect.width();
final int height = mStartTaskRect.height();
// The 'progress width' is the width of the window if it strictly linearly interpolates
// to minimum scale base on progress.
float enteringScale = mapRange(progress, 1, ENTERING_MIN_WINDOW_SCALE);
float closingScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_SCALE);
float closingColorScale = mapRange(progress, 1, CLOSING_MIN_WINDOW_COLOR_SCALE);
// The final width is derived from interpolating between the follow with and progress width
// using gesture progress.
float enteringWidth = enteringScale * width;
float closingWidth = closingScale * width;
float enteringHeight = (float) height / width * enteringWidth;
float closingHeight = (float) height / width * closingWidth;
float deltaYRatio = (touchY - mInitialTouchPos.y) / height;
// Base the window movement in the Y axis on the touch movement in the Y axis.
float deltaY = (float) Math.sin(deltaYRatio * Math.PI * 0.5f) * WINDOW_MAX_DELTA_Y;
// Move the window along the Y axis.
float closingTop = (height - closingHeight) * 0.5f + deltaY;
float enteringTop = (height - enteringHeight) * 0.5f + deltaY;
// Move the window along the X axis.
float right = width - (progress * WINDOW_MARGIN);
float left = right - closingWidth;
mClosingCurrentRect.set(left, closingTop, right, closingTop + closingHeight);
mEnteringCurrentRect.set(left - enteringWidth - WINDOW_MARGIN, enteringTop,
left - WINDOW_MARGIN, enteringTop + enteringHeight);
applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
applyColorTransform(mClosingTarget.leash, closingColorScale);
applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
mTransaction.apply();
mBackground.onBackProgressed(progress);
}
private void updatePostCommitClosingAnimation(float progress) {
mTransaction.setLayer(mClosingTarget.leash, 0);
float alpha = mapRange(progress, 1, 0);
mTransaction.setAlpha(mClosingTarget.leash, alpha);
}
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());
mEnteringCurrentRect.set(left, top, left + width, top + height);
applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
}
/** Transform the target window to match the target rect. */
private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) {
if (leash == null) {
return;
}
final float scale = targetRect.width() / mStartTaskRect.width();
mTransformMatrix.reset();
mTransformMatrix.setScale(scale, scale);
mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
mTransaction.setMatrix(leash, mTransformMatrix, mTmpFloat9)
.setWindowCrop(leash, mStartTaskRect)
.setCornerRadius(leash, cornerRadius);
}
private void applyColorTransform(SurfaceControl leash, float colorScale) {
if (leash == null) {
return;
}
computeScaleTransformMatrix(colorScale, mTmpFloat9);
mTransaction.setColorTransform(leash, mTmpFloat9, mTmpTranslate);
}
static void computeScaleTransformMatrix(float scale, float[] matrix) {
matrix[0] = scale;
matrix[1] = 0;
matrix[2] = 0;
matrix[3] = 0;
matrix[4] = scale;
matrix[5] = 0;
matrix[6] = 0;
matrix[7] = 0;
matrix[8] = scale;
}
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();
mClosingCurrentRect.setEmpty();
mInitialTouchPos.set(0, 0);
if (mFinishCallback != null) {
try {
mFinishCallback.onAnimationFinished();
} catch (RemoteException e) {
e.printStackTrace();
}
mFinishCallback = null;
}
}
private void onGestureProgress(@NonNull BackEvent backEvent) {
if (!mBackInProgress) {
mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
mBackInProgress = true;
}
mProgress = backEvent.getProgress();
mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
updateGestureBackProgress(getInterpolatedProgress(mProgress), backEvent);
}
private void onGestureCommitted() {
if (mEnteringTarget == null || mClosingTarget == null) {
finishAnimation();
return;
}
// We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
// coordinate of the gesture driven phase.
mEnteringCurrentRect.round(mEnteringStartRect);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(300);
valueAnimator.setInterpolator(mInterpolator);
valueAnimator.addUpdateListener(animation -> {
float progress = animation.getAnimatedFraction();
updatePostCommitEnteringAnimation(progress);
updatePostCommitClosingAnimation(progress);
mTransaction.apply();
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishAnimation();
}
});
valueAnimator.start();
}
private static float mapRange(float value, float min, float max) {
return min + (value * (max - min));
}
private final class Callback extends IOnBackInvokedCallback.Default {
@Override
public void onBackStarted(BackMotionEvent backEvent) {
mProgressAnimator.onBackStarted(backEvent,
CrossTaskBackAnimation.this::onGestureProgress);
}
@Override
public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
mProgressAnimator.onBackProgressed(backEvent);
}
@Override
public void onBackCancelled() {
mProgressAnimator.onBackCancelled(CrossTaskBackAnimation.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 task animation.");
for (RemoteAnimationTarget a : apps) {
if (a.mode == MODE_CLOSING) {
mClosingTarget = a;
}
if (a.mode == MODE_OPENING) {
mEnteringTarget = a;
}
}
startBackAnimation();
mFinishCallback = finishedCallback;
}
};
}