/* * Copyright (C) 2019 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.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.app.animation.Interpolators.TOUCH_RESPONSE; import static com.android.app.animation.Interpolators.clampToProgress; import static com.android.launcher3.Flags.enableDesktopExplodedView; import static com.android.launcher3.Flags.enableGridOnlyOverview; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_DELAY_NAV_FADE_IN; import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_NAV_FADE_IN_DURATION; import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_NAV_FADE_OUT_DURATION; import static com.android.launcher3.QuickstepTransitionManager.NAV_FADE_IN_INTERPOLATOR; import static com.android.launcher3.QuickstepTransitionManager.NAV_FADE_OUT_INTERPOLATOR; import static com.android.launcher3.QuickstepTransitionManager.RECENTS_LAUNCH_DURATION; import static com.android.launcher3.QuickstepTransitionManager.SPLIT_DIVIDER_ANIM_DURATION; import static com.android.launcher3.QuickstepTransitionManager.SPLIT_LAUNCH_DURATION; import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.quickstep.BaseContainerInterface.getTaskDimension; import static com.android.quickstep.util.AnimUtils.clampToDuration; import static com.android.wm.shell.shared.TransitionUtil.TYPE_SPLIT_SCREEN_DIM_LAYER; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.ComponentName; import android.content.Context; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.util.Pair; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.View; import android.window.TransitionInfo; import android.window.WindowAnimationState; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.internal.jank.Cuj; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.statehandlers.DepthController; import com.android.launcher3.statemanager.StateManager; import com.android.launcher3.statemanager.StatefulContainer; import com.android.launcher3.taskbar.TaskbarUIController; import com.android.launcher3.util.DisplayController; import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; import com.android.quickstep.util.MultiValueUpdateListener; import com.android.quickstep.util.SurfaceTransaction; import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties; import com.android.quickstep.util.SurfaceTransactionApplier; import com.android.quickstep.util.TaskViewSimulator; import com.android.quickstep.util.TransformParams; import com.android.quickstep.views.DesktopTaskView; import com.android.quickstep.views.GroupedTaskView; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.RecentsViewContainer; import com.android.quickstep.views.TaskView; import com.android.systemui.animation.RemoteAnimationTargetCompat; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.InteractionJankMonitorWrapper; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * Utility class for helpful methods related to {@link TaskView} objects and their tasks. */ public final class TaskViewUtils { private TaskViewUtils() {} private static final Rect TEMP_THUMBNAIL_BOUNDS = new Rect(); private static final Rect TEMP_FULLSCREEN_BOUNDS = new Rect(); private static final PointF TEMP_TASK_DIMENSION = new PointF(); private static final PointF TEMP_PIVOT = new PointF(); /** * Try to find a TaskView that corresponds with the component of the launched view. * * If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation. * Otherwise, we will assume we are using a normal app transition, but it's possible that the * opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView. */ public static TaskView findTaskViewToLaunch( RecentsView recentsView, View v, RemoteAnimationTarget[] targets) { if (v instanceof TaskView) { TaskView taskView = (TaskView) v; return recentsView.isTaskViewVisible(taskView) ? taskView : null; } // It's possible that the launched view can still be resolved to a visible task view, check // the task id of the opening task and see if we can find a match. if (v.getTag() instanceof ItemInfo) { ItemInfo itemInfo = (ItemInfo) v.getTag(); ComponentName componentName = itemInfo.getTargetComponent(); int userId = itemInfo.user.getIdentifier(); if (componentName != null) { for (TaskView taskView : recentsView.getTaskViews()) { Task firstTask = taskView.getFirstTask(); if (firstTask != null && recentsView.isTaskViewVisible(taskView)) { Task.TaskKey key = firstTask.key; if (componentName.equals(key.getComponent()) && userId == key.userId) { return taskView; } } } } } if (targets == null) { return null; } // Resolve the opening task id int openingTaskId = -1; for (RemoteAnimationTarget target : targets) { if (target.mode == MODE_OPENING) { openingTaskId = target.taskId; break; } } // If there is no opening task id, fall back to the normal app icon launch animation if (openingTaskId == -1) { return null; } // If the opening task id is not currently visible in overview, then fall back to normal app // icon launch animation TaskView taskView = recentsView.getTaskViewByTaskId(openingTaskId); if (taskView == null || !recentsView.isTaskViewVisible(taskView)) { return null; } return taskView; } public static > void createRecentsWindowAnimator( @NonNull RecentsView recentsView, @NonNull TaskView taskView, boolean skipViewChanges, @NonNull RemoteAnimationTarget[] appTargets, @NonNull RemoteAnimationTarget[] wallpaperTargets, @NonNull RemoteAnimationTarget[] nonAppTargets, @Nullable DepthController depthController, @Nullable TransitionInfo transitionInfo, PendingAnimation out) { boolean isQuickSwitch = taskView.isEndQuickSwitchCuj(); taskView.setEndQuickSwitchCuj(false); final RemoteAnimationTargets targets = new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets, MODE_OPENING); final RemoteAnimationTarget navBarTarget = targets.getNavBarRemoteAnimationTarget(); SurfaceTransactionApplier applier = new SurfaceTransactionApplier(taskView); targets.addReleaseCheck(applier); RemoteTargetHandle[] remoteTargetHandles; RemoteTargetHandle[] recentsViewHandles = recentsView.getRemoteTargetHandles(); if (taskView.isRunningTask() && recentsViewHandles != null) { // Re-use existing handles remoteTargetHandles = recentsViewHandles; } else { boolean forDesktop = taskView instanceof DesktopTaskView; RemoteTargetGluer gluer = new RemoteTargetGluer(taskView.getContext(), recentsView.getSizeStrategy(), targets, forDesktop); if (forDesktop) { remoteTargetHandles = gluer.assignTargetsForDesktop(targets, transitionInfo); if (enableDesktopExplodedView()) { ((DesktopTaskView) taskView).setRemoteTargetHandles(remoteTargetHandles); } } else if (taskView.containsMultipleTasks()) { remoteTargetHandles = gluer.assignTargetsForSplitScreen(targets, ((GroupedTaskView) taskView).getSplitBoundsConfig()); } else { remoteTargetHandles = gluer.assignTargets(targets); } } final int recentsActivityRotation = recentsView.getPagedViewOrientedState().getRecentsActivityRotation(); for (RemoteTargetHandle remoteTargetHandle : remoteTargetHandles) { remoteTargetHandle.getTaskViewSimulator().getOrientationState() .setRecentsRotation(recentsActivityRotation); remoteTargetHandle.getTransformParams().setSyncTransactionApplier(applier); } int taskIndex = recentsView.indexOfChild(taskView); Context context = taskView.getContext(); T container = RecentsViewContainer.containerFromContext(context); DeviceProfile dp = container.getDeviceProfile(); boolean showAsGrid = dp.isTablet; boolean parallaxCenterAndAdjacentTask = !showAsGrid && taskIndex != recentsView.getCurrentPage(); int taskRectTranslationPrimary = recentsView.getScrollOffset(taskIndex); int taskRectTranslationSecondary = showAsGrid ? (int) taskView.getGridTranslationY() : 0; RemoteTargetHandle[] topMostSimulators = null; if (!taskView.isRunningTask()) { // TVSs already initialized from the running task, no need to re-init for (RemoteTargetHandle targetHandle : remoteTargetHandles) { TaskViewSimulator tvsLocal = targetHandle.getTaskViewSimulator(); tvsLocal.setDp(dp); // RecentsView never updates the display rotation until swipe-up so the value may // be stale. Use the display value instead. int displayRotation = DisplayController.INSTANCE.get(context).getInfo().rotation; tvsLocal.getOrientationState().update(displayRotation, displayRotation); tvsLocal.fullScreenProgress.value = 0; tvsLocal.recentsViewScale.value = 1; if (!enableGridOnlyOverview()) { tvsLocal.setIsGridTask(taskView.isGridTask()); } tvsLocal.getOrientationState().getOrientationHandler().set(tvsLocal, TaskViewSimulator::setTaskRectTranslation, taskRectTranslationPrimary, taskRectTranslationSecondary); if (taskView instanceof DesktopTaskView) { targetHandle.getTransformParams().setTargetAlpha(1f); } else { // Fade in the task during the initial 20% of the animation out.addFloat(targetHandle.getTransformParams(), TransformParams.TARGET_ALPHA, 0, 1, clampToProgress(LINEAR, 0, 0.2f)); } } } for (RemoteTargetHandle targetHandle : remoteTargetHandles) { TaskViewSimulator tvsLocal = targetHandle.getTaskViewSimulator(); out.setFloat(tvsLocal.fullScreenProgress, AnimatedFloat.VALUE, 1, TOUCH_RESPONSE); out.setFloat(tvsLocal.recentsViewScale, AnimatedFloat.VALUE, tvsLocal.getFullScreenScale(), TOUCH_RESPONSE); if (!enableGridOnlyOverview()) { out.setFloat(tvsLocal.recentsViewScroll, AnimatedFloat.VALUE, 0, TOUCH_RESPONSE); } out.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { super.onAnimationStart(animation); final SurfaceTransaction showTransaction = new SurfaceTransaction(); for (int i = targets.apps.length - 1; i >= 0; --i) { showTransaction.getTransaction().show(targets.apps[i].leash); } applier.scheduleApply(showTransaction); if (enableGridOnlyOverview()) { taskView.getThumbnailBounds(TEMP_THUMBNAIL_BOUNDS, /*relativeToDragLayer=*/ true); getTaskDimension(context, container.getDeviceProfile(), TEMP_TASK_DIMENSION); TEMP_FULLSCREEN_BOUNDS.set(0, 0, (int) TEMP_TASK_DIMENSION.x, (int) TEMP_TASK_DIMENSION.y); Utilities.getPivotsForScalingRectToRect(TEMP_THUMBNAIL_BOUNDS, TEMP_FULLSCREEN_BOUNDS, TEMP_PIVOT); tvsLocal.setPivotOverride(TEMP_PIVOT); } } }); out.addOnFrameCallback(() -> { for (RemoteTargetHandle handle : remoteTargetHandles) { handle.getTaskViewSimulator().apply(handle.getTransformParams()); } }); if (navBarTarget != null) { final Rect cropRect = new Rect(); out.addOnFrameListener(new MultiValueUpdateListener() { FloatProp mNavFadeOut = new FloatProp(1f, 0f, clampToDuration( NAV_FADE_OUT_INTERPOLATOR, 0, ANIMATION_NAV_FADE_OUT_DURATION, out.getDuration())); FloatProp mNavFadeIn = new FloatProp(0f, 1f, clampToDuration( NAV_FADE_IN_INTERPOLATOR, ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_NAV_FADE_IN_DURATION, out.getDuration())); @Override public void onUpdate(float percent, boolean initOnly) { // TODO Do we need to operate over multiple TVSs for the navbar leash? for (RemoteTargetHandle handle : remoteTargetHandles) { SurfaceTransaction transaction = new SurfaceTransaction(); SurfaceProperties navBuilder = transaction.forSurface(navBarTarget.leash); if (mNavFadeIn.value > mNavFadeIn.getStartValue()) { TaskViewSimulator taskViewSimulator = handle.getTaskViewSimulator(); taskViewSimulator.getCurrentCropRect().round(cropRect); navBuilder.setMatrix(taskViewSimulator.getCurrentMatrix()) .setWindowCrop(cropRect) .setAlpha(mNavFadeIn.value); } else { navBuilder.setAlpha(mNavFadeOut.value); } handle.getTransformParams().applySurfaceParams(transaction); } } }); } topMostSimulators = remoteTargetHandles; } if (!skipViewChanges && parallaxCenterAndAdjacentTask && topMostSimulators != null && topMostSimulators.length > 0) { out.addFloat(taskView, VIEW_ALPHA, 1, 0, clampToProgress(LINEAR, 0.2f, 0.4f)); RemoteTargetHandle[] simulatorCopies = topMostSimulators; for (RemoteTargetHandle handle : simulatorCopies) { handle.getTaskViewSimulator().apply(handle.getTransformParams()); } // Mt represents the overall transformation on the thumbnailView relative to the // Launcher's rootView // K(t) represents transformation on the running window by the taskViewSimulator at // any time t. // at t = 0, we know that the simulator matches the thumbnailView. So if we apply K(0)` // on the Launcher's rootView, the thumbnailView would match the full running task // window. If we apply "K(0)` K(t)" thumbnailView will match the final transformed // window at any time t. This gives the overall matrix on thumbnailView to be: // Mt K(0)` K(t) // During animation we apply transformation on the thumbnailView (and not the rootView) // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is: // Mt K(0)` K(t) Mt` View[] thumbnails = taskView.getSnapshotViews(); // In case simulator copies and thumbnail size do no match, ensure we get the lesser. // This ensures we do not create arrays with empty elements or attempt to references // indexes out of array bounds. final int matrixSize = Math.min(simulatorCopies.length, thumbnails.length); Matrix[] mt = new Matrix[matrixSize]; Matrix[] mti = new Matrix[matrixSize]; for (int i = 0; i < matrixSize; i++) { View ttv = thumbnails[i]; RectF localBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight()); float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()}; getDescendantCoordRelativeToAncestor(ttv, ttv.getRootView(), tvBoundsMapped, false); RectF localBoundsInRoot = new RectF( tvBoundsMapped[0], tvBoundsMapped[1], tvBoundsMapped[2], tvBoundsMapped[3]); Matrix localMt = new Matrix(); localMt.setRectToRect(localBounds, localBoundsInRoot, ScaleToFit.FILL); mt[i] = localMt; Matrix localMti = new Matrix(); localMt.invert(localMti); mti[i] = localMti; // Translations for child thumbnails also get scaled as the parent taskView scales // Add inverse scaling to keep translations the same float translationY = ttv.getTranslationY(); float translationX = ttv.getTranslationX(); float fullScreenScale = topMostSimulators[i].getTaskViewSimulator().getFullScreenScale(); out.addFloat(ttv, VIEW_TRANSLATE_Y, translationY, translationY / fullScreenScale, TOUCH_RESPONSE); out.addFloat(ttv, VIEW_TRANSLATE_X, translationX, translationX / fullScreenScale, TOUCH_RESPONSE); } Matrix[] k0i = new Matrix[matrixSize]; for (int i = 0; i < matrixSize; i++) { k0i[i] = new Matrix(); simulatorCopies[i].getTaskViewSimulator().getCurrentMatrix().invert(k0i[i]); } Matrix animationMatrix = new Matrix(); out.addOnFrameCallback(() -> { for (int i = 0; i < matrixSize; i++) { animationMatrix.set(mt[i]); animationMatrix.postConcat(k0i[i]); animationMatrix.postConcat(simulatorCopies[i] .getTaskViewSimulator().getCurrentMatrix()); animationMatrix.postConcat(mti[i]); thumbnails[i].setAnimationMatrix(animationMatrix); } }); out.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { for (View ttv : thumbnails) { ttv.setAnimationMatrix(null); } } }); } out.addListener(new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { recentsView.setDrawBelowRecents(false, remoteTargetHandles); } @Override public void onAnimationSuccess(Animator animator) { if (isQuickSwitch) { InteractionJankMonitorWrapper.end(Cuj.CUJ_LAUNCHER_QUICK_SWITCH); } } @Override public void onAnimationEnd(Animator animation) { targets.release(); super.onAnimationEnd(animation); } }); if (depthController != null) { out.setFloat(depthController.stateDepth, MULTI_PROPERTY_VALUE, BACKGROUND_APP.getDepth(container), TOUCH_RESPONSE); } } /** * If {@param launchingTaskView} is not null, then this will play the tasks launch animation * from the position of the GroupedTaskView (when user taps on the TaskView to start it). * 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. */ public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView, @NonNull StateManager stateManager, @Nullable DepthController depthController, @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finishCallback.run(); } }); final RemoteAnimationTarget[] appTargets = RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */); final RemoteAnimationTarget[] wallpaperTargets = RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, true /* wallpapers */, t, null /* leashMap */); final RemoteAnimationTarget[] nonAppTargets = RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, false /* wallpapers */, t, null /* leashMap */); final RecentsView recentsView = launchingTaskView.getRecentsView(); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, /* launcherClosing */ true, stateManager, recentsView, depthController, /* transitionInfo= */ null); t.apply(); animatorSet.start(); } /** * Legacy version (until shell transitions are enabled) * * If {@param launchingTaskView} is not null, then this will play the tasks launch animation * from the position of the GroupedTaskView (when user taps on the TaskView to start it). * 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) * @deprecated with shell transitions */ public static void composeRecentsSplitLaunchAnimatorLegacy( @Nullable GroupedTaskView launchingTaskView, int initialTaskId, int secondTaskId, @NonNull RemoteAnimationTarget[] appTargets, @NonNull RemoteAnimationTarget[] wallpaperTargets, @NonNull RemoteAnimationTarget[] nonAppTargets, @NonNull StateManager stateManager, @Nullable DepthController depthController, @NonNull Runnable finishCallback) { if (launchingTaskView != null) { AnimatorSet animatorSet = new AnimatorSet(); RecentsView recentsView = launchingTaskView.getRecentsView(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finishCallback.run(); } }); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, true, stateManager, recentsView, depthController, /* transitionInfo= */ null); animatorSet.start(); return; } final ArrayList openingTargets = new ArrayList<>(); final ArrayList closingTargets = new ArrayList<>(); for (RemoteAnimationTarget appTarget : appTargets) { final int taskId = appTarget.taskInfo != null ? appTarget.taskInfo.taskId : -1; final int mode = appTarget.mode; final SurfaceControl leash = appTarget.leash; if (leash == null) { continue; } if (mode == MODE_OPENING) { openingTargets.add(leash); } else if (taskId == initialTaskId || taskId == secondTaskId) { throw new IllegalStateException("Expected task to be opening, but it is " + mode); } else if (mode == MODE_CLOSING) { closingTargets.add(leash); } } for (int i = 0; i < nonAppTargets.length; ++i) { final SurfaceControl leash = nonAppTargets[i].leash; if (nonAppTargets[i].windowType == TYPE_DOCK_DIVIDER && leash != null) { openingTargets.add(leash); } } final SurfaceControl.Transaction t = 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) { t.setAlpha(leash, progress); } t.apply(); }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { for (SurfaceControl leash: openingTargets) { t.show(leash).setAlpha(leash, 0.0f); } t.apply(); } @Override public void onAnimationEnd(Animator animation) { for (SurfaceControl leash: closingTargets) { t.hide(leash); } finishCallback.run(); } }); animator.start(); } /** * Start recents to desktop animation */ public static AnimatorSet composeRecentsDesktopLaunchAnimator( @NonNull TaskView launchingTaskView, @NonNull StateManager stateManager, @Nullable DepthController depthController, @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { t.apply(); } @Override public void onAnimationEnd(Animator animation) { finishCallback.run(); } }); final RemoteAnimationTarget[] apps = RemoteAnimationTargetCompat.wrapApps( transitionInfo, t, null /* leashMap */); final RemoteAnimationTarget[] wallpaper = RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, true /* wallpapers */, t, null /* leashMap */); final RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, false /* wallpapers */, t, null /* leashMap */); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, apps, wallpaper, nonApps, true /* launcherClosing */, stateManager, launchingTaskView.getRecentsView(), depthController, transitionInfo); return animatorSet; } public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v, @NonNull RemoteAnimationTarget[] appTargets, @NonNull RemoteAnimationTarget[] wallpaperTargets, @NonNull RemoteAnimationTarget[] nonAppTargets, boolean launcherClosing, @NonNull StateManager stateManager, @NonNull RecentsView recentsView, @Nullable DepthController depthController, @Nullable TransitionInfo transitionInfo) { boolean skipLauncherChanges = !launcherClosing; TaskView taskView = findTaskViewToLaunch(recentsView, v, appTargets); PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION); createRecentsWindowAnimator(recentsView, taskView, skipLauncherChanges, appTargets, wallpaperTargets, nonAppTargets, depthController, transitionInfo, pa); if (launcherClosing) { // TODO(b/182592057): differentiate between "restore split" vs "launch fullscreen app" TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets, true /*shown*/, (dividerAnimator) -> { // If split apps are launching, we want to delay showing the divider bar // until the very end once the apps are mostly in place. This is because we // aren't moving the divider leash in the relative position with the // launching apps. dividerAnimator.setStartDelay(pa.getDuration() - SPLIT_DIVIDER_ANIM_DURATION); pa.add(dividerAnimator); }); } Animator childStateAnimation = null; // Found a visible recents task that matches the opening app, lets launch the app from there Animator launcherAnim; final AnimatorListenerAdapter windowAnimEndListener; if (launcherClosing) { // Since Overview is in launcher, just opening overview sets willFinishToHome to true. // Now that we are closing the launcher, we need to (re)set willFinishToHome back to // false. Otherwise, RecentsAnimationController can't differentiate between closing // overview to 3p home vs closing overview to app. final RecentsAnimationController raController = recentsView.getRecentsAnimationController(); if (raController != null) { raController.setWillFinishToHome(false); } launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView); launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE); launcherAnim.setDuration(RECENTS_LAUNCH_DURATION); windowAnimEndListener = new AnimationSuccessListener() { @Override public void onAnimationStart(Animator animation) { recentsView.onTaskLaunchedInLiveTileMode(); } // Make sure recents gets fixed up by resetting task alphas and scales, etc. // This should only be run onAnimationSuccess, otherwise finishRecentsAnimation will // interfere with a rapid swipe up to home in the live tile + running task case. @Override public void onAnimationSuccess(Animator animation) { recentsView.finishRecentsAnimation(false /* toRecents */, () -> { recentsView.post(() -> { stateManager.moveToRestState(); stateManager.reapplyState(); // We may have notified launcher is not visible so that taskbar can // stash immediately. Now that the animation is over, we can update // that launcher is still visible. TaskbarUIController controller = recentsView.getSizeStrategy() .getTaskbarController(); if (controller != null) { boolean launcherVisible = true; for (RemoteAnimationTarget target : appTargets) { launcherVisible &= target.isTranslucent; } if (launcherVisible) { controller.onLauncherVisibilityChanged(true); } } }); }); } @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); recentsView.onTaskLaunchedInLiveTileModeCancelled(); } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); recentsView.setTaskLaunchCancelledRunnable(null); } }; } else { AnimatorPlaybackController controller = stateManager.createAnimationToNewWorkspace(NORMAL, RECENTS_LAUNCH_DURATION); controller.dispatchOnStart(); childStateAnimation = controller.getTarget(); launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION); windowAnimEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { recentsView.finishRecentsAnimation(false /* toRecents */, () -> stateManager.goToState(NORMAL, false)); } }; } pa.add(launcherAnim); if (recentsView.getRunningTaskIndex() != -1) { pa.addOnFrameCallback(recentsView::redrawLiveTile); } anim.play(pa.buildAnim()); // Set the current animation first, before adding windowAnimEndListener. Setting current // animation adds some listeners which need to be called before windowAnimEndListener // (the ordering of listeners matter in this case). stateManager.setCurrentAnimation(anim, childStateAnimation); anim.addListener(windowAnimEndListener); } /** * Creates an animation to show/hide the auxiliary surfaces (aka. divider bar), only calling * {@param animatorHandler} if there are valid surfaces to animate. * Passing null handler to apply the visibility immediately. * * @return the animator animating the surfaces */ public static ValueAnimator createSplitAuxiliarySurfacesAnimator( @Nullable RemoteAnimationTarget[] nonApps, boolean shown, @Nullable Consumer animatorHandler) { if (nonApps == null || nonApps.length == 0) { return null; } List auxiliarySurfaces = new ArrayList<>(); for (RemoteAnimationTarget target : nonApps) { final SurfaceControl leash = target.leash; if ((target.windowType == TYPE_DOCK_DIVIDER || target.windowType == TYPE_SPLIT_SCREEN_DIM_LAYER) && leash != null && leash.isValid()) { auxiliarySurfaces.add(leash); } } if (auxiliarySurfaces.isEmpty()) { return null; } SurfaceControl.Transaction t = new SurfaceControl.Transaction(); if (animatorHandler == null) { // Apply the visibility directly without fade animation. for (SurfaceControl leash : auxiliarySurfaces) { t.setVisibility(leash, shown); } t.apply(); t.close(); return null; } ValueAnimator dockFadeAnimator = ValueAnimator.ofFloat(0f, 1f); dockFadeAnimator.addUpdateListener(valueAnimator -> { float progress = valueAnimator.getAnimatedFraction(); for (SurfaceControl leash : auxiliarySurfaces) { if (leash != null && leash.isValid()) { t.setAlpha(leash, shown ? progress : 1 - progress); } } t.apply(); }); dockFadeAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { if (shown) { for (SurfaceControl leash : auxiliarySurfaces) { t.setLayer(leash, Integer.MAX_VALUE); t.setAlpha(leash, 0); t.show(leash); } t.apply(); } } @Override public void onAnimationEnd(Animator animation) { if (!shown) { for (SurfaceControl leash : auxiliarySurfaces) { if (leash != null && leash.isValid()) { t.hide(leash); } } t.apply(); } t.close(); } }); dockFadeAnimator.setDuration(SPLIT_DIVIDER_ANIM_DURATION); animatorHandler.accept(dockFadeAnimator); return dockFadeAnimator; } /** * Creates an array of {@link RemoteAnimationTarget}s and a matching array of * {@link WindowAnimationState}s from the provided handles. * Important: the ordering of the two arrays is the same, so the state at each index of the * second applies to the target in the same index of the first. * * @param handles The handles wrapping each target. * @param timestamp The start time of the current frame. * @param velocityPxPerMs The current velocity of the target animations. */ @NonNull public static Pair extractTargetsAndStates( @NonNull RemoteTargetHandle[] handles, long timestamp, @NonNull PointF velocityPxPerMs) { RemoteAnimationTarget[] targets = new RemoteAnimationTarget[handles.length]; WindowAnimationState[] animationStates = new WindowAnimationState[handles.length]; for (int i = 0; i < handles.length; i++) { targets[i] = handles[i].getTransformParams().getTargetSet().apps[i]; TaskViewSimulator taskViewSimulator = handles[i].getTaskViewSimulator(); RectF startRect = taskViewSimulator.getCurrentRect(); float cornerRadius = taskViewSimulator.getCurrentCornerRadius(); WindowAnimationState state = new WindowAnimationState(); state.timestamp = timestamp; state.bounds = new RectF( startRect.left, startRect.top, startRect.right, startRect.bottom); state.topLeftRadius = cornerRadius; state.topRightRadius = cornerRadius; state.bottomRightRadius = cornerRadius; state.bottomLeftRadius = cornerRadius; state.velocityPxPerMs = velocityPxPerMs; animationStates[i] = state; } return new Pair<>(targets, animationStates); } }