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
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 57b0a09..1d7c11b 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.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.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.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 @@
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) {
@@ -598,6 +607,39 @@
}
/**
+ * @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 @@
// 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 @@
// 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 @@
// 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 @@
)
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 @@
// 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 1c1e167..40937b3 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -37,17 +37,18 @@
* 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 @@
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 @@
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 @@
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 @@
* @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 e90aa13..e8d1cc1 100644
--- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -40,8 +40,8 @@
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 @@
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 @@
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 0000000..8cd997f
--- /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 68c9bf9..4bed7a0 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.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 @@
doNothing()
.whenever(spySplitAnimationController)
.composeIconSplitLaunchAnimator(any(), any(), any(), any())
+ doReturn(-1)
+ .whenever(spySplitAnimationController)
+ .hasChangesForBothAppPairs(any(), any())
spySplitAnimationController.playSplitLaunchAnimation(
null /* launchingTaskView */,
@@ -276,13 +283,45 @@
}
@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 @@
{} /* 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