Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 083d6107 authored by Arthur Hung's avatar Arthur Hung Committed by Android (Google) Code Review
Browse files

Merge "Intruduce Cross Task Animator"

parents fc89cdfb 80419fb9
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import android.os.UserHandle;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.SparseArray;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowFocusObserver;
import android.view.InputDevice;
@@ -187,6 +188,31 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
        }
        mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                this::createExternalInterface, this);

        initBackAnimationRunners();
    }

    private void initBackAnimationRunners() {
        final IOnBackInvokedCallback dummyCallback = new IOnBackInvokedCallback.Default();
        final IRemoteAnimationRunner dummyRunner = new IRemoteAnimationRunner.Default() {
            @Override
            public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
                    RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
                    IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
                // Animation missing. Simply finish animation.
                finishedCallback.onAnimationFinished();
            }
        };

        final BackAnimationRunner dummyBackRunner =
                new BackAnimationRunner(dummyCallback, dummyRunner);
        final CrossTaskBackAnimation crossTaskAnimation = new CrossTaskBackAnimation(mContext);
        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_TASK,
                new BackAnimationRunner(crossTaskAnimation.mCallback, crossTaskAnimation.mRunner));
        // TODO (238474994): register cross activity animation when it's completed.
        mAnimationDefinition.set(BackNavigationInfo.TYPE_CROSS_ACTIVITY, dummyBackRunner);
        // TODO (236760237): register dialog close animation when it's completed.
        mAnimationDefinition.set(BackNavigationInfo.TYPE_DIALOG_CLOSE, dummyBackRunner);
    }

    private void setupAnimationDeveloperSettingsObserver(
+365 −0
Original line number Diff line number Diff line
/*
 * 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.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 float[] BACKGROUNDCOLOR = {0.263f, 0.263f, 0.227f};

    /**
     * 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 mBackgroundSurface;
    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 IOnBackInvokedCallback mCallback = new IOnBackInvokedCallback.Default() {
        @Override
        public void onBackStarted(BackEvent backEvent) {
            mProgressAnimator.onBackStarted(backEvent,
                    CrossTaskBackAnimation.this::onGestureProgress);
        }

        @Override
        public void onBackProgressed(@NonNull BackEvent backEvent) {
            mProgressAnimator.onBackProgressed(backEvent);
        }

        @Override
        public void onBackCancelled() {
            mProgressAnimator.reset();
            finishAnimation();
        }

        @Override
        public void onBackInvoked() {
            mProgressAnimator.reset();
            onGestureCommitted();
        }
    };

    final IRemoteAnimationRunner mRunner = new 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;
        }
    };

    CrossTaskBackAnimation(Context context) {
        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
    }

    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.
        mBackgroundSurface = new SurfaceControl.Builder()
                .setName("Background of Back Navigation")
                .setColorLayer()
                .setHidden(false)
                .build();
        mTransaction.setColor(mBackgroundSurface, BACKGROUNDCOLOR)
                .setLayer(mBackgroundSurface, -1);
        mTransaction.apply();
    }

    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();
    }

    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 (mBackgroundSurface != null) {
            mBackgroundSurface.release();
            mBackgroundSurface = null;
        }

        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));
    }
}
+16 −8
Original line number Diff line number Diff line
@@ -243,16 +243,22 @@ class BackNavigationController {
            } else if (currentActivity.isRootOfTask()) {
                // TODO(208789724): Create single source of truth for this, maybe in
                //  RootWindowContainer
                // TODO: Also check Task.shouldUpRecreateTaskLocked() for prevActivity logic
                prevTask = currentTask.mRootWindowContainer.getTaskBelow(currentTask);
                removedWindowContainer = currentTask;
                // If it reaches the top activity, we will check the below task from parent.
                // If it's null or multi-window, fallback the type to TYPE_CALLBACK.
                // or set the type to proper value when it's return to home or another task.
                if (prevTask == null || prevTask.inMultiWindowMode()) {
                    backType = BackNavigationInfo.TYPE_CALLBACK;
                } else {
                    prevActivity = prevTask.getTopNonFinishingActivity();
                    if (prevTask.isActivityTypeHome()) {
                        backType = BackNavigationInfo.TYPE_RETURN_TO_HOME;
                        mShowWallpaper = true;
                    } else {
                        backType = BackNavigationInfo.TYPE_CROSS_TASK;
                    }
                mShowWallpaper = true;
                }
            }
            infoBuilder.setType(backType);

@@ -263,8 +269,10 @@ class BackNavigationController {
                    removedWindowContainer,
                    BackNavigationInfo.typeToString(backType));

            // For now, we only animate when going home.
            boolean prepareAnimation = backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
            // For now, we only animate when going home and cross task.
            boolean prepareAnimation =
                    (backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
                            || backType == BackNavigationInfo.TYPE_CROSS_TASK)
                    && adapter != null;

            // Only prepare animation if no leash has been created (no animation is running).
+8 −1
Original line number Diff line number Diff line
@@ -98,12 +98,19 @@ public class BackNavigationControllerTests extends WindowTestsBase {
    @Test
    public void backTypeCrossTaskWhenBackToPreviousTask() {
        Task taskA = createTask(mDefaultDisplay);
        createActivityRecord(taskA);
        ActivityRecord recordA = createActivityRecord(taskA);
        Mockito.doNothing().when(recordA).reparentSurfaceControl(any(), any());

        withSystemCallback(createTopTaskWithActivity());
        BackNavigationInfo backNavigationInfo = startBackNavigation();
        assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull();
        assertThat(typeToString(backNavigationInfo.getType()))
                .isEqualTo(typeToString(BackNavigationInfo.TYPE_CROSS_TASK));

        // verify if back animation would start.
        verify(mBackNavigationController).scheduleAnimationLocked(
                eq(BackNavigationInfo.TYPE_CROSS_TASK), any(), eq(mBackAnimationAdapter),
                any());
    }

    @Test