blob: 3979444499613db20e0effe302a0a126c7c22262 [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.quickstep;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.animation.AnimationUtils;
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.view.AppearanceRegion;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.systemui.shared.system.QuickStepContract;
/**
* Controls the animation of swiping back and returning to launcher.
*
* This is a two part animation. The first part is an animation that tracks gesture location to
* scale and move the leaving app window. Once the gesture is committed, the second part takes over
* the app window and plays the rest of app close transitions in one go.
*
* 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.
*
* Apps using the legacy back dispatching will keep triggering the WALLPAPER_OPEN remote
* transition registered in {@link QuickstepTransitionManager}.
*
*/
public class LauncherBackAnimationController {
private static final int CANCEL_TRANSITION_DURATION = 233;
private static final int SCRIM_FADE_DURATION = 233;
private static final float MIN_WINDOW_SCALE = 0.85f;
private static final float MAX_SCRIM_ALPHA_DARK = 0.8f;
private static final float MAX_SCRIM_ALPHA_LIGHT = 0.2f;
private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.20f;
private final QuickstepTransitionManager mQuickstepTransitionManager;
private final Matrix mTransformMatrix = new Matrix();
/** The window position at the beginning of the back animation. */
private final Rect mStartRect = new Rect();
/** The window position when the back gesture is cancelled. */
private final RectF mCancelRect = new RectF();
/** The current window position. */
private final RectF mCurrentRect = new RectF();
private final QuickstepLauncher mLauncher;
private final int mWindowScaleMarginX;
/** Max window translation in the Y axis. */
private final int mWindowMaxDeltaY;
private final float mWindowScaleEndCornerRadius;
private final float mWindowScaleStartCornerRadius;
private final Interpolator mCancelInterpolator;
private final PointF mInitialTouchPos = new PointF();
private RemoteAnimationTarget mBackTarget;
private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private boolean mSpringAnimationInProgress = false;
private boolean mAnimatorSetInProgress = false;
private float mBackProgress = 0;
private boolean mBackInProgress = false;
private IOnBackInvokedCallback mBackCallback;
private IRemoteAnimationFinishedCallback mAnimationFinishedCallback;
private BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
private SurfaceControl mScrimLayer;
private ValueAnimator mScrimAlphaAnimator;
private float mScrimAlpha;
private boolean mOverridingStatusBarFlags;
public LauncherBackAnimationController(
QuickstepLauncher launcher,
QuickstepTransitionManager quickstepTransitionManager) {
mLauncher = launcher;
mQuickstepTransitionManager = quickstepTransitionManager;
mWindowScaleEndCornerRadius = QuickStepContract.supportsRoundedCornersOnWindows(
mLauncher.getResources())
? mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_corner_radius)
: 0;
mWindowScaleStartCornerRadius = QuickStepContract.getWindowCornerRadius(mLauncher);
mWindowScaleMarginX = mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_scale_x_margin);
mWindowMaxDeltaY = mLauncher.getResources().getDimensionPixelSize(
R.dimen.swipe_back_window_max_delta_y);
mCancelInterpolator =
AnimationUtils.loadInterpolator(mLauncher, R.interpolator.back_cancel);
}
/**
* Registers {@link IOnBackInvokedCallback} to receive back dispatches from shell.
* @param handler Handler to the thread to run the animations on.
*/
public void registerBackCallbacks(Handler handler) {
mBackCallback = new IOnBackInvokedCallback.Stub() {
@Override
public void onBackCancelled() {
handler.post(() -> {
resetPositionAnimated();
mProgressAnimator.reset();
});
}
@Override
public void onBackInvoked() {
handler.post(() -> {
startTransition();
mProgressAnimator.reset();
});
}
@Override
public void onBackProgressed(BackMotionEvent backEvent) {
handler.post(() -> {
mProgressAnimator.onBackProgressed(backEvent);
});
}
@Override
public void onBackStarted(BackMotionEvent backEvent) {
handler.post(() -> {
startBack(backEvent);
mProgressAnimator.onBackStarted(backEvent, event -> {
mBackProgress = event.getProgress();
// TODO: Update once the interpolation curve spec is finalized.
mBackProgress =
1 - (1 - mBackProgress) * (1 - mBackProgress) * (1
- mBackProgress);
updateBackProgress(mBackProgress, event);
});
});
}
};
final IRemoteAnimationRunner runner = new IRemoteAnimationRunner.Stub() {
@Override
public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
IRemoteAnimationFinishedCallback finishedCallback) {
for (final RemoteAnimationTarget target : apps) {
if (MODE_CLOSING == target.mode) {
mBackTarget = target;
break;
}
}
mAnimationFinishedCallback = finishedCallback;
}
@Override
public void onAnimationCancelled(boolean isKeyguardOccluded) {}
};
SystemUiProxy.INSTANCE.get(mLauncher).setBackToLauncherCallback(mBackCallback, runner);
}
private void resetPositionAnimated() {
ValueAnimator cancelAnimator = ValueAnimator.ofFloat(0, 1);
mCancelRect.set(mCurrentRect);
cancelAnimator.setDuration(CANCEL_TRANSITION_DURATION);
cancelAnimator.setInterpolator(mCancelInterpolator);
cancelAnimator.addUpdateListener(
animation -> {
updateCancelProgress((float) animation.getAnimatedValue());
});
cancelAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Refresh the status bar appearance to the original one.
customizeStatusBarAppearance(false);
finishAnimation();
}
});
cancelAnimator.start();
}
/** Unregisters the back to launcher callback in shell. */
public void unregisterBackCallbacks() {
if (mBackCallback != null) {
SystemUiProxy.INSTANCE.get(mLauncher).clearBackToLauncherCallback(mBackCallback);
}
mProgressAnimator.reset();
mBackCallback = null;
}
private void startBack(BackMotionEvent backEvent) {
mBackInProgress = true;
RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
if (appTarget == null) {
return;
}
mTransaction
.show(appTarget.leash)
.setAnimationTransaction();
mBackTarget = appTarget;
mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
// TODO(b/218916755): Offset start rectangle in multiwindow mode.
mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
mCurrentRect.set(mStartRect);
addScrimLayer();
mTransaction.apply();
}
void addScrimLayer() {
ViewRootImpl viewRootImpl = mLauncher.getDragLayer().getViewRootImpl();
SurfaceControl parent = viewRootImpl != null
? viewRootImpl.getSurfaceControl()
: null;
boolean isDarkTheme = Utilities.isDarkTheme(mLauncher);
mScrimLayer = new SurfaceControl.Builder()
.setName("Back to launcher background scrim")
.setCallsite("LauncherBackAnimationController")
.setColorLayer()
.setParent(parent)
.setOpaque(false)
.setHidden(false)
.build();
final float[] colorComponents = new float[] { 0f, 0f, 0f };
mScrimAlpha = (isDarkTheme)
? MAX_SCRIM_ALPHA_DARK : MAX_SCRIM_ALPHA_LIGHT;
mTransaction
.setColor(mScrimLayer, colorComponents)
.setAlpha(mScrimLayer, mScrimAlpha)
.show(mScrimLayer);
}
void removeScrimLayer() {
if (mScrimLayer == null) {
return;
}
if (mScrimLayer.isValid()) {
mTransaction.remove(mScrimLayer).apply();
}
mScrimLayer = null;
}
private void updateBackProgress(float progress, BackEvent event) {
if (!mBackInProgress || mBackTarget == null) {
return;
}
float screenWidth = mStartRect.width();
float screenHeight = mStartRect.height();
float width = Utilities.mapRange(progress, 1, MIN_WINDOW_SCALE) * screenWidth;
float height = screenHeight / screenWidth * width;
float deltaYRatio = (event.getTouchY() - mInitialTouchPos.y) / screenHeight;
// 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) * mWindowMaxDeltaY * progress;
// Move the window along the Y axis.
float top = (screenHeight - height) * 0.5f + deltaY;
// Move the window along the X axis.
float left = event.getSwipeEdge() == BackEvent.EDGE_RIGHT
? progress * mWindowScaleMarginX
: screenWidth - progress * mWindowScaleMarginX - width;
mCurrentRect.set(left, top, left + width, top + height);
float cornerRadius = Utilities.mapRange(
progress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
applyTransform(mCurrentRect, cornerRadius);
customizeStatusBarAppearance(progress > UPDATE_SYSUI_FLAGS_THRESHOLD);
}
private void updateCancelProgress(float progress) {
if (mBackTarget == null) {
return;
}
mCurrentRect.set(
Utilities.mapRange(progress, mCancelRect.left, mStartRect.left),
Utilities.mapRange(progress, mCancelRect.top, mStartRect.top),
Utilities.mapRange(progress, mCancelRect.right, mStartRect.right),
Utilities.mapRange(progress, mCancelRect.bottom, mStartRect.bottom));
float endCornerRadius = Utilities.mapRange(
mBackProgress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
float cornerRadius = Utilities.mapRange(
progress, endCornerRadius, mWindowScaleStartCornerRadius);
applyTransform(mCurrentRect, cornerRadius);
}
/** Transform the target window to match the target rect. */
private void applyTransform(RectF targetRect, float cornerRadius) {
final float scale = targetRect.width() / mStartRect.width();
mTransformMatrix.reset();
mTransformMatrix.setScale(scale, scale);
mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
if (mBackTarget.leash.isValid()) {
mTransaction.setMatrix(mBackTarget.leash, mTransformMatrix, new float[9]);
mTransaction.setWindowCrop(mBackTarget.leash, mStartRect);
mTransaction.setCornerRadius(mBackTarget.leash, cornerRadius);
}
mTransaction.apply();
}
private void startTransition() {
if (mBackTarget == null) {
// Trigger transition system instead of custom transition animation.
finishAnimation();
return;
}
if (mLauncher.isDestroyed()) {
return;
}
// TODO: Catch the moment when launcher becomes visible after the top app un-occludes
// launcher and start animating afterwards. Currently we occasionally get a flicker from
// animating when launcher is still invisible.
if (mLauncher.hasSomeInvisibleFlag(PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION)) {
mLauncher.addForceInvisibleFlag(INVISIBLE_BY_PENDING_FLAGS);
mLauncher.getStateManager().moveToRestState();
}
// Explicitly close opened floating views (which is typically called from
// Launcher#onResumed, but in the predictive back flow launcher is not resumed until
// the transition is fully finished.)
AbstractFloatingView.closeAllOpenViewsExcept(mLauncher, false, TYPE_REBIND_SAFE);
float cornerRadius = Utilities.mapRange(
mBackProgress, mWindowScaleStartCornerRadius, mWindowScaleEndCornerRadius);
Pair<RectFSpringAnim, AnimatorSet> pair =
mQuickstepTransitionManager.createWallpaperOpenAnimations(
new RemoteAnimationTarget[]{mBackTarget},
new RemoteAnimationTarget[0],
false /* fromUnlock */,
mCurrentRect,
cornerRadius,
mBackInProgress /* fromPredictiveBack */);
startTransitionAnimations(pair.first, pair.second);
mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL);
}
private void finishAnimation() {
mBackTarget = null;
mBackInProgress = false;
mBackProgress = 0;
mTransformMatrix.reset();
mCancelRect.setEmpty();
mCurrentRect.setEmpty();
mStartRect.setEmpty();
mInitialTouchPos.set(0, 0);
mAnimatorSetInProgress = false;
mSpringAnimationInProgress = false;
// We don't call customizeStatusBarAppearance here to prevent the status bar update with
// the legacy appearance. It should be refreshed after the transition done.
mOverridingStatusBarFlags = false;
if (mAnimationFinishedCallback != null) {
try {
mAnimationFinishedCallback.onAnimationFinished();
} catch (RemoteException e) {
Log.w("ShellBackPreview", "Failed call onBackAnimationFinished", e);
}
mAnimationFinishedCallback = null;
}
if (mScrimAlphaAnimator != null && mScrimAlphaAnimator.isRunning()) {
mScrimAlphaAnimator.cancel();
mScrimAlphaAnimator = null;
}
}
private void startTransitionAnimations(RectFSpringAnim springAnim, AnimatorSet anim) {
mAnimatorSetInProgress = anim != null;
mSpringAnimationInProgress = springAnim != null;
if (springAnim != null) {
springAnim.addAnimatorListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mSpringAnimationInProgress = false;
tryFinishBackAnimation();
}
}
);
}
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mAnimatorSetInProgress = false;
tryFinishBackAnimation();
}
});
mScrimAlphaAnimator = new ValueAnimator().ofFloat(1, 0);
mScrimAlphaAnimator.addUpdateListener(animation -> {
float value = (Float) animation.getAnimatedValue();
if (mScrimLayer.isValid()) {
mTransaction.setAlpha(mScrimLayer, value * mScrimAlpha);
mTransaction.apply();
}
});
mScrimAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
resetScrim();
}
});
mScrimAlphaAnimator.setDuration(SCRIM_FADE_DURATION).start();
anim.start();
}
private void resetScrim() {
removeScrimLayer();
mScrimAlpha = 0;
}
private void tryFinishBackAnimation() {
if (!mSpringAnimationInProgress && !mAnimatorSetInProgress) {
finishAnimation();
}
}
private void customizeStatusBarAppearance(boolean overridingStatusBarFlags) {
if (mOverridingStatusBarFlags == overridingStatusBarFlags) {
return;
}
mOverridingStatusBarFlags = overridingStatusBarFlags;
final boolean isBackgroundDark =
(mLauncher.getWindow().getDecorView().getSystemUiVisibility()
& View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) == 0;
final AppearanceRegion region = mOverridingStatusBarFlags
? new AppearanceRegion(!isBackgroundDark ? APPEARANCE_LIGHT_STATUS_BARS : 0,
mBackTarget.windowConfiguration.getBounds())
: null;
SystemUiProxy.INSTANCE.get(mLauncher).customizeStatusBarAppearance(region);
}
}