From 55270a9f707508e43ef8be3cca6380f78ec659b6 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Thu, 25 Apr 2024 16:32:42 -0700 Subject: [PATCH] Allow single root candidate for app pair launch for pip edge case * Shell will launch single task if requested split apps have one of them already in Pip * Create a separate method to set animation for launching from the appPair icon on workspace * Reuse the animation method for launching an AppPair icon from taskbar by specifying which windowing mode to look for if we're launching the actual split pair vs just one in fullscreen Bug: 323089902 Test: Launches fine visually Change-Id: I415343a48e980afd7f4e511558d350cf15b97ca1 Merged-In: I415343a48e980afd7f4e511558d350cf15b97ca1 --- .../util/SplitAnimationController.kt | 312 ++++++++++++------ .../views/FloatingAppPairBackground.kt | 31 +- .../quickstep/views/FloatingAppPairView.kt | 17 +- .../FloatingFullscreenAppPairBackground.kt | 95 ++++++ .../util/SplitAnimationControllerTest.kt | 77 ++++- 5 files changed, 416 insertions(+), 116 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 57b0a09a90..1d7c11bf37 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -23,6 +23,7 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.graphics.Bitmap @@ -51,6 +52,7 @@ import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.config.FeatureFlags import com.android.launcher3.logging.StatsLogManager.EventEnum +import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.statemanager.StatefulActivity @@ -69,6 +71,7 @@ import com.android.quickstep.views.TaskThumbnailView import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskIdAttributeContainer import com.android.quickstep.views.TaskViewIcon +import com.android.wm.shell.shared.TransitionUtil import java.util.Optional import java.util.function.Supplier @@ -532,8 +535,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC check(info != null && t != null) { "trying to launch an app pair icon, but encountered an unexpected null" } - - composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info) + if (appPairLaunchingAppIndex == -1) { + // Launch split app pair animation + composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + } else { + composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t, + finishCallback, appPairLaunchingAppIndex) + } } else { // Fallback case: simple fade-in animation check(info != null && t != null) { @@ -597,6 +606,39 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ) } + /** + * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise + * the integer index corresponding to [launchingIconView]'s contents for the single app + * to be animated + */ + fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon, + transitionInfo: TransitionInfo) : Int { + val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName + val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName + var launchFullscreenAppIndex = -1 + for (change in transitionInfo.changes) { + val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + if (TransitionUtil.isOpeningType(change.mode) && + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) { + val baseIntent = taskInfo.baseIntent.component?.packageName + if (baseIntent == intent1) { + if (launchFullscreenAppIndex > -1) { + launchFullscreenAppIndex = -1 + break + } + launchFullscreenAppIndex = 0 + } else if (baseIntent == intent2) { + if (launchFullscreenAppIndex > -1) { + launchFullscreenAppIndex = -1 + break + } + launchFullscreenAppIndex = 1 + } + } + } + return launchFullscreenAppIndex + } + /** * When the user taps an app pair icon to launch split, this will play the tasks' launch * animation from the position of the icon. @@ -632,7 +674,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation if (launchingIconView.context is TaskbarActivityContext) { - composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback) + composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback, + WINDOWING_MODE_MULTI_WINDOW) return } @@ -642,11 +685,6 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Create an AnimatorSet that will run both shell and launcher transitions together val launchAnimation = AnimatorSet() - val progressUpdater = ValueAnimator.ofFloat(0f, 1f) - val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) - progressUpdater.setDuration(timings.getDuration().toLong()) - progressUpdater.interpolator = Interpolators.LINEAR - var rootCandidate: Change? = null for (change in transitionInfo.changes) { @@ -690,27 +728,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Make sure nothing weird happened, like getChange() returning null. check(rootCandidate != null) { "Failed to find a root leash" } - // Shell animation: the apps are revealed toward end of the launch animation - progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> - val progress = - Interpolators.clampToProgress( - Interpolators.LINEAR, - valueAnimator.animatedFraction, - timings.appRevealStartOffset, - timings.appRevealEndOffset - ) - - // Set the alpha of the shell layer (2 apps + divider) - t.setAlpha(rootCandidate.leash, progress) - t.apply() - } - // Create a new floating view in Launcher, positioned above the launching icon val drawableArea = launchingIconView.iconDrawableArea val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context) val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context) appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) + val floatingView = FloatingAppPairView.getFloatingAppPairView( launcher, @@ -721,84 +745,189 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ) floatingView.bringToFront() - // Launcher animation: animate the floating view, expanding to fill the display surface - progressUpdater.addUpdateListener( - object : MultiValueUpdateListener() { - var mDx = - FloatProp( - floatingView.startingPosition.left, - dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, - Interpolators.clampToProgress( - timings.getStagedRectXInterpolator(), - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mDy = - FloatProp( - floatingView.startingPosition.top, - dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mScaleX = - FloatProp( - 1f /* start */, - dp.widthPx / floatingView.startingPosition.width(), - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mScaleY = - FloatProp( - 1f /* start */, - dp.heightPx / floatingView.startingPosition.height(), - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - - override fun onUpdate(percent: Float, initOnly: Boolean) { - floatingView.progress = percent - floatingView.x = mDx.value - floatingView.y = mDy.value - floatingView.scaleX = mScaleX.value - floatingView.scaleY = mScaleY.value - floatingView.invalidate() - } - } - ) - - // When animation ends, remove the floating view and run finishCallback - progressUpdater.addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - safeRemoveViewFromDragLayer(launcher, floatingView) - finishCallback.run() - } - } - ) - - launchAnimation.play(progressUpdater) + launchAnimation.play( + getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, + rootCandidate)) launchAnimation.start() } + /** + * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate + * a single fullscreen icon + background instead of for a pair + */ + @VisibleForTesting + fun composeFullscreenIconSplitLaunchAnimator( + launchingIconView: AppPairIcon, + transitionInfo: TransitionInfo, + t: Transaction, + finishCallback: Runnable, + launchFullscreenIndex: Int + ) { + // If launching an app pair from Taskbar inside of an app context (no access to Launcher), + // use the scale-up animation + if (launchingIconView.context is TaskbarActivityContext) { + composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback, + WINDOWING_MODE_FULLSCREEN) + return + } + + // Else we are in Launcher and can launch with the full icon stretch-and-split animation. + val launcher = Launcher.getLauncher(launchingIconView.context) + val dp = launcher.deviceProfile + + // Create an AnimatorSet that will run both shell and launcher transitions together + val launchAnimation = AnimatorSet() + + val appInfo = launchingIconView.info + .getContents()[launchFullscreenIndex] as WorkspaceItemInfo + val intentToLaunch = appInfo.intent.component?.packageName + var rootCandidate: Change? = null + for (change in transitionInfo.changes) { + val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + val baseIntent = taskInfo.baseIntent.component?.packageName + if (TransitionUtil.isOpeningType(change.mode) && + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN && + baseIntent == intentToLaunch) { + rootCandidate = change + } + } + + // If we could not find a proper root candidate, something went wrong. + check(rootCandidate != null) { "Could not find a split root candidate" } + + // Recurse up the tree until parent is null, then we've found our root. + var parentToken: WindowContainerToken? = rootCandidate.parent + while (parentToken != null) { + rootCandidate = transitionInfo.getChange(parentToken) ?: break + parentToken = rootCandidate.parent + } + + // Make sure nothing weird happened, like getChange() returning null. + check(rootCandidate != null) { "Failed to find a root leash" } + + // Create a new floating view in Launcher, positioned above the launching icon + val drawableArea = launchingIconView.iconDrawableArea + val appIcon = appInfo.newIcon(launchingIconView.context) + appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) + + val floatingView = + FloatingAppPairView.getFloatingAppPairView( + launcher, + drawableArea, + appIcon, + null /*appIcon2*/, + 0 /*dividerPos*/ + ) + floatingView.bringToFront() + launchAnimation.play( + getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, + rootCandidate)) + launchAnimation.start() + } + + private fun getIconLaunchValueAnimator(t: Transaction, + dp: com.android.launcher3.DeviceProfile, + finishCallback: Runnable, + launcher: Launcher, + floatingView: FloatingAppPairView, + rootCandidate: Change) : ValueAnimator { + val progressUpdater = ValueAnimator.ofFloat(0f, 1f) + val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) + progressUpdater.setDuration(timings.getDuration().toLong()) + progressUpdater.interpolator = Interpolators.LINEAR + + // Shell animation: the apps are revealed toward end of the launch animation + progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> + val progress = + Interpolators.clampToProgress( + Interpolators.LINEAR, + valueAnimator.animatedFraction, + timings.appRevealStartOffset, + timings.appRevealEndOffset + ) + + // Set the alpha of the shell layer (2 apps + divider) + t.setAlpha(rootCandidate.leash, progress) + t.apply() + } + + progressUpdater.addUpdateListener( + object : MultiValueUpdateListener() { + var mDx = + FloatProp( + floatingView.startingPosition.left, + dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, + Interpolators.clampToProgress( + timings.getStagedRectXInterpolator(), + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mDy = + FloatProp( + floatingView.startingPosition.top, + dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mScaleX = + FloatProp( + 1f /* start */, + dp.widthPx / floatingView.startingPosition.width(), + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mScaleY = + FloatProp( + 1f /* start */, + dp.heightPx / floatingView.startingPosition.height(), + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + + override fun onUpdate(percent: Float, initOnly: Boolean) { + floatingView.progress = percent + floatingView.x = mDx.value + floatingView.y = mDy.value + floatingView.scaleX = mScaleX.value + floatingView.scaleY = mScaleY.value + floatingView.invalidate() + } + } + ) + progressUpdater.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + safeRemoveViewFromDragLayer(launcher, floatingView) + finishCallback.run() + } + } + ) + + return progressUpdater + } + /** * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when * there is no visible associated tile to expand from. + * [windowingMode] helps determine whether we are looking for a split or a single fullscreen + * [Change] */ @VisibleForTesting fun composeScaleUpLaunchAnimation( transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + windowingMode: Int ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) @@ -812,9 +941,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // TODO (b/316490565): Replace this logic when SplitBounds is available to // startAnimation() and we can know the precise taskIds of launching tasks. - // Find a change that has WINDOWING_MODE_MULTI_WINDOW. if ( - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && + taskInfo.windowingMode == windowingMode && (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) ) { // Found one! diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt index 1c1e1674e5..40937b3ed9 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt @@ -37,17 +37,18 @@ import com.android.systemui.shared.system.QuickStepContract * animation. Consists of a rectangular background that splits into two, and two app icons that * increase in size during the animation. */ -class FloatingAppPairBackground( - context: Context, - private val floatingView: FloatingAppPairView, // the view that we will draw this background on - private val appIcon1: Drawable, - private val appIcon2: Drawable, - dividerPos: Int +open class FloatingAppPairBackground( + context: Context, + // the view that we will draw this background on + protected val floatingView: FloatingAppPairView, + private val appIcon1: Drawable, + private val appIcon2: Drawable?, + dividerPos: Int ) : Drawable() { companion object { // Design specs -- app icons start small and expand during the animation - private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f) - private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f) + internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f) + internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f) // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other // API for drawing rectangles with 4 different corner radii. @@ -59,13 +60,13 @@ class FloatingAppPairBackground( private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) // Animation interpolators - private val expandXInterpolator: Interpolator - private val expandYInterpolator: Interpolator + protected val expandXInterpolator: Interpolator + protected val expandYInterpolator: Interpolator private val cellSplitInterpolator: Interpolator - private val iconFadeInterpolator: Interpolator + protected val iconFadeInterpolator: Interpolator // Device-specific measurements - private val deviceCornerRadius: Float + protected val deviceCornerRadius: Float private val deviceHalfDividerSize: Float private val desiredSplitRatio: Float @@ -217,7 +218,7 @@ class FloatingAppPairBackground( canvas.save() canvas.translate(changingIcon2Left, changingIconTop) canvas.scale(changingIconScaleX, changingIconScaleY) - appIcon2.alpha = changingIconAlpha + appIcon2!!.alpha = changingIconAlpha appIcon2.draw(canvas) canvas.restore() } @@ -317,7 +318,7 @@ class FloatingAppPairBackground( canvas.save() canvas.translate(changingIconLeft, changingIcon2Top) canvas.scale(changingIconScaleX, changingIconScaleY) - appIcon2.alpha = changingIconAlpha + appIcon2!!.alpha = changingIconAlpha appIcon2.draw(canvas) canvas.restore() } @@ -330,7 +331,7 @@ class FloatingAppPairBackground( * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top * right y, bottom right x, and so on. */ - private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) { + protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Canvas.drawDoubleRoundRect is supported from Q onward c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint) diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt index e90aa13d97..e8d1cc1e36 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt @@ -40,8 +40,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att fun getFloatingAppPairView( launcher: StatefulActivity<*>, originalView: View, - appIcon1: Drawable, - appIcon2: Drawable, + appIcon1: Drawable?, + appIcon2: Drawable?, dividerPos: Int ): FloatingAppPairView { val dragLayer: ViewGroup = launcher.getDragLayer() @@ -64,8 +64,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att fun init( launcher: StatefulActivity<*>, originalView: View, - appIcon1: Drawable, - appIcon2: Drawable, + appIcon1: Drawable?, + appIcon2: Drawable?, dividerPos: Int ) { val viewBounds = Rect(0, 0, originalView.width, originalView.height) @@ -92,7 +92,14 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att layoutParams = lp // Prepare to draw app pair icon background - background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) + background = if (appIcon1 == null || appIcon2 == null) { + val iconToAnimate = appIcon1 ?: appIcon2 + checkNotNull(iconToAnimate) + FloatingFullscreenAppPairBackground(context, this, iconToAnimate, + dividerPos) + } else { + FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) + } background.setBounds(0, 0, lp.width, lp.height) } diff --git a/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt new file mode 100644 index 0000000000..8cd997fcaa --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt @@ -0,0 +1,95 @@ +/* + * 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.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.RectF +import android.graphics.drawable.Drawable + +class FloatingFullscreenAppPairBackground( + context: Context, + floatingView: FloatingAppPairView, + private val iconToLaunch: Drawable, + dividerPos: Int) : + FloatingAppPairBackground( + context, + floatingView, + iconToLaunch, + null /*appIcon2*/, + dividerPos +) { + + /** Animates the background as if launching a fullscreen task. */ + override fun draw(canvas: Canvas) { + val progress = floatingView.progress + + // Since the entire floating app pair surface is scaling up during this animation, we + // scale down most of these drawn elements so that they appear the proper size on-screen. + val scaleFactorX = floatingView.scaleX + val scaleFactorY = floatingView.scaleY + + // Get the bounds where we will draw the background image + val width = bounds.width().toFloat() + val height = bounds.height().toFloat() + + // Get device-specific measurements + val cornerRadiusX = deviceCornerRadius / scaleFactorX + val cornerRadiusY = deviceCornerRadius / scaleFactorY + + // Draw background + drawCustomRoundedRect( + canvas, + RectF(0f, 0f, width, height), + floatArrayOf( + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + ) + ) + + // Calculate changing measurements for icon. + val changingIconSizeX = + (STARTING_ICON_SIZE_PX + + ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * + expandXInterpolator.getInterpolation(progress))) / scaleFactorX + val changingIconSizeY = + (STARTING_ICON_SIZE_PX + + ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * + expandYInterpolator.getInterpolation(progress))) / scaleFactorY + + val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f) + val changingIconTop = (height / 2f) - (changingIconSizeY / 2f) + val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width() + val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height() + val changingIconAlpha = + (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt() + + // Draw icon + canvas.save() + canvas.translate(changingIcon1Left, changingIconTop) + canvas.scale(changingIconScaleX, changingIconScaleY) + iconToLaunch.alpha = changingIconAlpha + iconToLaunch.draw(canvas) + canvas.restore() + } +} \ No newline at end of file diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index 68c9bf9799..4bed7a0bf9 100644 --- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -17,6 +17,8 @@ package com.android.quickstep.util +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.view.ContextThemeWrapper @@ -39,8 +41,10 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.kotlin.any import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.verify @@ -255,6 +259,9 @@ class SplitAnimationControllerTest { doNothing() .whenever(spySplitAnimationController) .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + doReturn(-1) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, @@ -276,13 +283,45 @@ class SplitAnimationControllerTest { } @Test - fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() { + fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() { + val spySplitAnimationController = spy(splitAnimationController) + whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper) + doNothing() + .whenever(spySplitAnimationController) + .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any()) + doReturn(0) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) + + spySplitAnimationController.playSplitLaunchAnimation( + null /* launchingTaskView */, + mockAppPairIcon, + taskId, + taskId2, + null /* apps */, + null /* wallpapers */, + null /* nonApps */, + stateManager, + depthController, + transitionInfo, + transaction, + {} /* finishCallback */ + ) + + verify(spySplitAnimationController) + .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0)) + } + + @Test + fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() { val spySplitAnimationController = spy(splitAnimationController) whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) doNothing() .whenever(spySplitAnimationController) - .composeScaleUpLaunchAnimation(any(), any(), any()) - + .composeScaleUpLaunchAnimation(any(), any(), any(), any()) + doReturn(-1) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, mockAppPairIcon, @@ -298,7 +337,37 @@ class SplitAnimationControllerTest { {} /* finishCallback */ ) - verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any()) + verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(), + eq(WINDOWING_MODE_MULTI_WINDOW)) + } + + @Test + fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() { + val spySplitAnimationController = spy(splitAnimationController) + whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) + doNothing() + .whenever(spySplitAnimationController) + .composeScaleUpLaunchAnimation(any(), any(), any(), any()) + doReturn(0) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) + spySplitAnimationController.playSplitLaunchAnimation( + null /* launchingTaskView */, + mockAppPairIcon, + taskId, + taskId2, + null /* apps */, + null /* wallpapers */, + null /* nonApps */, + stateManager, + depthController, + transitionInfo, + transaction, + {} /* finishCallback */ + ) + + verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(), + eq(WINDOWING_MODE_FULLSCREEN)) } @Test