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

Commit 23fcbf2f authored by Jeremy Sim's avatar Jeremy Sim
Browse files

App Pairs: Launch animation

[App Pairs 7/?]

This patch implements the app pair launch animation from icon. Adds a new function, composeFadeInSplitLaunchAnimator(), in SplitAnimationController, that builds the combined launcher + shell animation.

Bug: 309618233
Flag: ACONFIG com.android.wm.shell.enable_app_pairs DEVELOPMENT
Test: Manual
Change-Id: I8e95f629ae2a71f1bd6cbb356f5e33233e5c2906
parent d761f5c3
Loading
Loading
Loading
Loading
+7 −6
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.folder.Folder;
@@ -998,7 +999,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
                        Toast.LENGTH_SHORT).show();
            } else {
                // Else launch the selected app pair
                launchFromTaskbarPreservingSplitIfVisible(recents, fi.contents);
                launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents);
                mControllers.uiController.onTaskbarIconLaunched(fi);
                mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
            }
@@ -1034,7 +1035,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
                                    .startShortcut(packageName, id, null, null, info.user);
                        } else {
                            launchFromTaskbarPreservingSplitIfVisible(
                                    recents, Collections.singletonList(info));
                                    recents, view, Collections.singletonList(info));
                        }

                    } catch (NullPointerException
@@ -1072,7 +1073,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
                // If we are selecting a second app for split, launch the split tasks
                taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
            } else {
                launchFromTaskbarPreservingSplitIfVisible(recents, Collections.singletonList(info));
                launchFromTaskbarPreservingSplitIfVisible(
                        recents, view, Collections.singletonList(info));
            }
            mControllers.uiController.onTaskbarIconLaunched(info);
            mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1094,7 +1096,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
     * (potentially breaking a split pair).
     */
    private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents,
            List<? extends ItemInfo> itemInfos) {
            @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
        if (recents == null) {
            return;
        }
@@ -1122,8 +1124,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
                    if (findExactPairMatch) {
                        // We did not find the app pair we were looking for, so launch one.
                        recents.getSplitSelectController().getAppPairsController().launchAppPair(
                                (WorkspaceItemInfo) itemInfos.get(0),
                                (WorkspaceItemInfo) itemInfos.get(1));
                                (AppPairIcon) launchingIconView);
                    } else {
                        startItemInfoActivity(itemInfos.get(0));
                    }
+3 −3
Original line number Diff line number Diff line
@@ -106,6 +106,7 @@ import com.android.launcher3.Workspace;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.appprediction.PredictionRowView;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -116,7 +117,6 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.statehandlers.DepthController;
@@ -1284,8 +1284,8 @@ public class QuickstepLauncher extends Launcher {
    /**
     * Launches two apps as an app pair.
     */
    public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
        mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
    public void launchAppPair(AppPairIcon appPairIcon) {
        mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon);
    }

    public boolean canStartHomeSafely() {
+18 −92
Original line number Diff line number Diff line
@@ -18,8 +18,6 @@ package com.android.quickstep;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;

import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.TOUCH_RESPONSE;
@@ -421,15 +419,11 @@ public final class TaskViewUtils {
     * Technically this case should be taken care of by
     * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether
     * it's a single task or multiple tasks results in different entry-points.
     *
     * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
     * case where launcher handles animating starting split tasks from app icon)
     */
    public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
            @NonNull StateManager stateManager, @Nullable DepthController depthController,
            int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
            SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
        if (launchingTaskView != null) {
            @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
            @NonNull Runnable finishCallback) {
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
@@ -447,80 +441,12 @@ public final class TaskViewUtils {
                RemoteAnimationTargetCompat.wrapNonApps(
                        transitionInfo, false /* wallpapers */, t, null /* leashMap */);
        final RecentsView recentsView = launchingTaskView.getRecentsView();
            composeRecentsLaunchAnimator(animatorSet, launchingTaskView,
                    appTargets, wallpaperTargets, nonAppTargets,
                    true, stateManager,
                    recentsView, depthController);
        composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets,
                nonAppTargets, /* launcherClosing */ true, stateManager, recentsView,
                depthController);

        t.apply();
        animatorSet.start();
            return;
        }

        TransitionInfo.Change splitRoot1 = null;
        TransitionInfo.Change splitRoot2 = null;
        final ArrayList<SurfaceControl> openingTargets = new ArrayList<>();
        for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
            final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
            if (change.getTaskInfo() == null) {
                continue;
            }
            final int taskId = change.getTaskInfo().taskId;
            final int mode = change.getMode();

            // Find the target tasks' root tasks since those are the split stages that need to
            // be animated (the tasks themselves are children and thus inherit animation).
            if (taskId == initialTaskId || taskId == secondTaskId) {
                if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
                    throw new IllegalStateException(
                            "Expected task to be showing, but it is " + mode);
                }
            }
            if (taskId == initialTaskId) {
                splitRoot1 = change.getParent() == null ? change :
                        transitionInfo.getChange(change.getParent());
                openingTargets.add(splitRoot1.getLeash());
            }
            if (taskId == secondTaskId) {
                splitRoot2 = change.getParent() == null ? change :
                        transitionInfo.getChange(change.getParent());
                openingTargets.add(splitRoot2.getLeash());
            }
        }

        SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction();
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
        animator.setDuration(SPLIT_LAUNCH_DURATION);
        animator.addUpdateListener(valueAnimator -> {
            float progress = valueAnimator.getAnimatedFraction();
            for (SurfaceControl leash: openingTargets) {
                animTransaction.setAlpha(leash, progress);
            }
            animTransaction.apply();
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                for (SurfaceControl leash: openingTargets) {
                    animTransaction.show(leash)
                            .setAlpha(leash, 0.0f);
                }
                animTransaction.apply();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                finishCallback.run();
            }
        });

        if (splitRoot1 != null && splitRoot1.getParent() != null) {
            // Set the highest level split root alpha; we could technically use the parent of either
            // splitRoot1 or splitRoot2
            t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f);
        }
        t.apply();
        animator.start();
    }

    /**
+9 −0
Original line number Diff line number Diff line
@@ -39,4 +39,13 @@ public class AnimUtils {
                ? SplitAnimationTimings.TABLET_SPLIT_TO_CONFIRM
                : SplitAnimationTimings.PHONE_SPLIT_TO_CONFIRM;
    }

    /**
     * Fetches device-specific timings for the app pair launch animation.
     */
    public static SplitAnimationTimings getDeviceAppPairLaunchTimings(boolean isTablet) {
        return isTablet
                ? SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH
                : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH;
    }
}
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.util

import com.android.app.animation.Interpolators

/** Timings for the app pair launch animation. */
abstract class AppPairLaunchTimings : SplitAnimationTimings {
    protected abstract val STAGED_RECT_SLIDE_DURATION: Int

    // Common timings that apply to app pair launches on any type of device
    override fun getStagedRectSlideStart() = 0
    override fun getStagedRectSlideEnd() = stagedRectSlideStart + STAGED_RECT_SLIDE_DURATION
    override fun getPlaceholderFadeInStart() = 0
    override fun getPlaceholderFadeInEnd() = 0
    override fun getPlaceholderIconFadeInStart() = 0
    override fun getPlaceholderIconFadeInEnd() = 0

    private val iconFadeStart: Int
        get() = getStagedRectSlideEnd()
    private val iconFadeEnd: Int
        get() = iconFadeStart + 83
    private val appRevealStart: Int
        get() = getStagedRectSlideEnd() + 67
    private val appRevealEnd: Int
        get() = appRevealStart + 217
    private val cellSplitStart: Int
        get() = (getStagedRectSlideEnd() * 0.83f).toInt()
    private val cellSplitEnd: Int
        get() = cellSplitStart + 500

    override fun getStagedRectXInterpolator() = Interpolators.EMPHASIZED_COMPLEMENT
    override fun getStagedRectYInterpolator() = Interpolators.EMPHASIZED
    override fun getStagedRectScaleXInterpolator() = Interpolators.EMPHASIZED
    override fun getStagedRectScaleYInterpolator() = Interpolators.EMPHASIZED
    override fun getCellSplitInterpolator() = Interpolators.EMPHASIZED
    override fun getIconFadeInterpolator() = Interpolators.LINEAR

    override fun getCellSplitStartOffset(): Float {
        return cellSplitStart.toFloat() / getDuration()
    }
    override fun getCellSplitEndOffset(): Float {
        return cellSplitEnd.toFloat() / getDuration()
    }
    override fun getIconFadeStartOffset(): Float {
        return iconFadeStart.toFloat() / getDuration()
    }
    override fun getIconFadeEndOffset(): Float {
        return iconFadeEnd.toFloat() / getDuration()
    }
    override fun getAppRevealStartOffset(): Float {
        return appRevealStart.toFloat() / getDuration()
    }
    override fun getAppRevealEndOffset(): Float {
        return appRevealEnd.toFloat() / getDuration()
    }
    abstract override fun getDuration(): Int
}
Loading