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

Commit fc4420dc authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add animation runner for alt-tab desktop app launch" into main

parents 5572968c 6bb9d56d
Loading
Loading
Loading
Loading
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.launcher3.desktop

import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.os.IBinder
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.RemoteTransitionStub
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import androidx.core.animation.addListener
import com.android.app.animation.Interpolators
import com.android.quickstep.RemoteRunnable
import java.util.concurrent.Executor

/**
 * [android.window.RemoteTransition] for Desktop app launches.
 *
 * This transition supports minimize-changes, i.e. in a launch-transition, if a window is moved back
 * ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
 * that window.
 */
class DesktopAppLaunchTransition(private val context: Context, private val mainExecutor: Executor) :
    RemoteTransitionStub() {

    override fun startAnimation(
        token: IBinder,
        info: TransitionInfo,
        t: Transaction,
        transitionFinishedCallback: IRemoteTransitionFinishedCallback,
    ) {
        val safeTransitionFinishedCallback = RemoteRunnable {
            transitionFinishedCallback.onTransitionFinished(/* wct= */ null, /* sct= */ null)
        }
        mainExecutor.execute {
            runAnimators(info, safeTransitionFinishedCallback)
            t.apply()
        }
    }

    private fun runAnimators(info: TransitionInfo, finishedCallback: RemoteRunnable) {
        val animators = mutableListOf<Animator>()
        val animatorFinishedCallback: (Animator) -> Unit = { animator ->
            animators -= animator
            if (animators.isEmpty()) finishedCallback.run()
        }
        animators += createAnimators(info, animatorFinishedCallback)
        animators.forEach { it.start() }
    }

    private fun createAnimators(
        info: TransitionInfo,
        finishCallback: (Animator) -> Unit,
    ): List<Animator> {
        val transaction = Transaction()
        val launchAnimator =
            createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
        val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
        val minimizeAnimator = createMinimizeAnimator(minimizeChange, transaction, finishCallback)
        return listOf(launchAnimator, minimizeAnimator)
    }

    private fun getLaunchChange(info: TransitionInfo): Change =
        requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
            "expected an app launch Change"
        }

    private fun getMinimizeChange(info: TransitionInfo): Change? =
        info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }

    private fun createLaunchAnimator(
        change: Change,
        transaction: Transaction,
        onAnimFinish: (Animator) -> Unit,
    ): Animator {
        val boundsAnimator =
            WindowAnimator.createBoundsAnimator(
                context,
                launchBoundsAnimationDef,
                change,
                transaction,
            )
        val alphaAnimator =
            ValueAnimator.ofFloat(0f, 1f).apply {
                duration = LAUNCH_ANIM_ALPHA_DURATION_MS
                interpolator = Interpolators.LINEAR
                addUpdateListener { animation ->
                    transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
                }
            }
        return AnimatorSet().apply {
            playTogether(boundsAnimator, alphaAnimator)
            addListener(onEnd = { animation -> onAnimFinish(animation) })
        }
    }

    private fun createMinimizeAnimator(
        change: Change,
        transaction: Transaction,
        onAnimFinish: (Animator) -> Unit,
    ): Animator {
        val boundsAnimator =
            WindowAnimator.createBoundsAnimator(
                context,
                minimizeBoundsAnimationDef,
                change,
                transaction,
            )
        val alphaAnimator =
            ValueAnimator.ofFloat(1f, 0f).apply {
                duration = MINIMIZE_ANIM_ALPHA_DURATION_MS
                interpolator = Interpolators.LINEAR
                addUpdateListener { animation ->
                    transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
                }
            }
        return AnimatorSet().apply {
            playTogether(boundsAnimator, alphaAnimator)
            addListener(onEnd = { animation -> onAnimFinish(animation) })
        }
    }

    companion object {
        private val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)

        private const val LAUNCH_ANIM_ALPHA_DURATION_MS = 100L
        private const val MINIMIZE_ANIM_ALPHA_DURATION_MS = 100L

        private val launchBoundsAnimationDef =
            WindowAnimator.BoundsAnimationParams(
                durationMs = 300,
                startOffsetYDp = 12f,
                startScale = 0.97f,
                interpolator = Interpolators.STANDARD_DECELERATE,
            )

        private val minimizeBoundsAnimationDef =
            WindowAnimator.BoundsAnimationParams(
                durationMs = 200,
                endOffsetYDp = 12f,
                endScale = 0.97f,
                interpolator = Interpolators.STANDARD_ACCELERATE,
            )
    }
}
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.launcher3.desktop

import android.animation.RectEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.util.TypedValue
import android.view.SurfaceControl
import android.view.animation.Interpolator
import android.window.TransitionInfo

/** Creates animations that can be applied to windows/surfaces. */
object WindowAnimator {

    /** Parameters defining a window bounds animation. */
    data class BoundsAnimationParams(
        val durationMs: Long,
        val startOffsetYDp: Float = 0f,
        val endOffsetYDp: Float = 0f,
        val startScale: Float = 1f,
        val endScale: Float = 1f,
        val interpolator: Interpolator,
    )

    /**
     * Creates an animator to reposition and scale the bounds of the leash of the given change.
     *
     * @param boundsAnimDef the parameters for the animation itself (duration, scale, position)
     * @param change the change to which the animation should be applied
     * @param transaction the transaction to apply the animation to
     */
    fun createBoundsAnimator(
        context: Context,
        boundsAnimDef: BoundsAnimationParams,
        change: TransitionInfo.Change,
        transaction: SurfaceControl.Transaction,
    ): ValueAnimator {
        val startBounds =
            createBounds(
                context,
                change.startAbsBounds,
                boundsAnimDef.startScale,
                boundsAnimDef.startOffsetYDp,
            )
        val leash = change.leash
        val endBounds =
            createBounds(
                context,
                change.startAbsBounds,
                boundsAnimDef.endScale,
                boundsAnimDef.endOffsetYDp,
            )
        return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply {
            duration = boundsAnimDef.durationMs
            interpolator = boundsAnimDef.interpolator
            addUpdateListener { animation ->
                val animBounds = animation.animatedValue as Rect
                val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction
                transaction
                    .setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat())
                    .setScale(leash, animScale, animScale)
                    .apply()
            }
        }
    }

    private fun createBounds(context: Context, origBounds: Rect, scale: Float, offsetYDp: Float) =
        Rect(origBounds).apply {
            check(scale in 0.0..1.0)
            // Scale the  bounds down with an anchor in the center
            inset(
                (origBounds.width().toFloat() * (1 - scale) / 2).toInt(),
                (origBounds.height().toFloat() * (1 - scale) / 2).toInt(),
            )
            val offsetYPx =
                TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP,
                        offsetYDp,
                        context.resources.displayMetrics,
                    )
                    .toInt()
            offset(/* dx= */ 0, offsetYPx)
        }
}
+12 −2
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
 */
package com.android.launcher3.taskbar;

import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;

import android.animation.Animator;
@@ -31,6 +32,7 @@ import androidx.annotation.Nullable;
import com.android.internal.jank.Cuj;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.desktop.DesktopAppLaunchTransition;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
import com.android.launcher3.views.BaseDragLayer;
@@ -41,6 +43,7 @@ import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.window.flags.Flags;

import java.io.PrintWriter;
import java.util.List;
@@ -181,7 +184,7 @@ public class KeyboardQuickSwitchViewController {
        Runnable onFinishCallback = () -> InteractionJankMonitorWrapper.end(
                Cuj.CUJ_LAUNCHER_KEYBOARD_QUICK_SWITCH_APP_LAUNCH);
        TaskbarActivityContext context = mControllers.taskbarActivityContext;
        RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
        final RemoteTransition slideInTransition = new RemoteTransition(new SlideInRemoteTransition(
                Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
                context.getDeviceProfile().overviewPageSpacing,
                QuickStepContract.getWindowCornerRadius(context),
@@ -195,7 +198,7 @@ public class KeyboardQuickSwitchViewController {
                    SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
                            .showDesktopApps(
                                    mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
                                    remoteTransition));
                                    slideInTransition));
            return -1;
        }
        // Even with a valid index, this can be null if the user tries to quick switch before the
@@ -208,6 +211,13 @@ public class KeyboardQuickSwitchViewController {
            // Ignore attempts to run the selected task if it is already running.
            return -1;
        }
        RemoteTransition remoteTransition = slideInTransition;
        if (mOnDesktop && task.task1.isMinimized
                && Flags.enableDesktopAppLaunchAlttabTransitions()) {
            // This app is being unminimized - use our own transition runner.
            remoteTransition = new RemoteTransition(
                    new DesktopAppLaunchTransition(context, MAIN_EXECUTOR));
        }
        mControllers.taskbarActivityContext.handleGroupTaskLaunch(
                task,
                remoteTransition,
+16 −7
Original line number Diff line number Diff line
@@ -1240,7 +1240,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
            }
        } else if (tag instanceof TaskItemInfo info) {
            UI_HELPER_EXECUTOR.execute(() ->
                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(info.getTaskId()));
                    SystemUiProxy.INSTANCE.get(this).showDesktopApp(
                            info.getTaskId(), /* remoteTransition= */ null));
            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(
                    /* stash= */ true);
        } else if (tag instanceof WorkspaceItemInfo) {
@@ -1331,7 +1332,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
            GroupTask task,
            @Nullable RemoteTransition remoteTransition,
            boolean onDesktop) {
        handleGroupTaskLaunch(task, remoteTransition, onDesktop, null, null);
        handleGroupTaskLaunch(task, remoteTransition, onDesktop,
                /* onStartCallback= */ null, /* onFinishCallback= */ null);
    }

    /**
@@ -1355,17 +1357,24 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
            UI_HELPER_EXECUTOR.execute(() ->
                    SystemUiProxy.INSTANCE.get(this).showDesktopApps(getDisplay().getDisplayId(),
                            remoteTransition));
        } else if (onDesktop) {
            return;
        }
        if (onDesktop) {
            boolean useRemoteTransition = task.task1.isMinimized
                    && com.android.window.flags.Flags.enableDesktopAppLaunchAlttabTransitions();
            UI_HELPER_EXECUTOR.execute(() -> {
                if (onStartCallback != null) {
                    onStartCallback.run();
                }
                SystemUiProxy.INSTANCE.get(this).showDesktopApp(task.task1.key.id);
                SystemUiProxy.INSTANCE.get(this).showDesktopApp(
                        task.task1.key.id, useRemoteTransition ? remoteTransition : null);
                if (onFinishCallback != null) {
                    onFinishCallback.run();
                }
            });
        } else if (task.task2 == null) {
            return;
        }
        if (task.task2 == null) {
            UI_HELPER_EXECUTOR.execute(() -> {
                ActivityOptions activityOptions =
                        makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
@@ -1374,9 +1383,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
                ActivityManagerWrapper.getInstance().startActivityFromRecents(
                        task.task1.key, activityOptions);
            });
        } else {
            mControllers.uiController.launchSplitTasks(task, remoteTransition);
            return;
        }
        mControllers.uiController.launchSplitTasks(task, remoteTransition);
    }

    /**
+2 −2
Original line number Diff line number Diff line
@@ -1426,10 +1426,10 @@ public class SystemUiProxy implements ISystemUiProxy, NavHandle, SafeCloseable {
    /**
     * If task with the given id is on the desktop, bring it to front
     */
    public void showDesktopApp(int taskId) {
    public void showDesktopApp(int taskId, @Nullable RemoteTransition transition) {
        if (mDesktopMode != null) {
            try {
                mDesktopMode.showDesktopApp(taskId);
                mDesktopMode.showDesktopApp(taskId, transition);
            } catch (RemoteException e) {
                Log.w(TAG, "Failed call showDesktopApp", e);
            }
Loading