Open and close animation for trampoline task
Moving most of the logic from DesktopAppLaunchTransition to DesktopAppLaunchAnimatorHelper to make it more testable
Bug: 391548553
Flag: com.android.window.flags.enable_desktop_trampoline_close_animation_bugfix
Test: DesktopAppLaunchAnimatorHelperTest
Change-Id: I606fc3fc25250ea4128f3089b701a1ae153dc47a
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
new file mode 100644
index 0000000..adbcc75
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchAnimatorHelper.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Rect
+import android.view.Choreographer
+import android.view.SurfaceControl.Transaction
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.DesktopModeFlags
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.core.animation.addListener
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES
+import com.android.wm.shell.shared.animation.MinimizeAnimator
+import com.android.wm.shell.shared.animation.WindowAnimator
+
+/**
+ * Helper class responsible for creating and managing animators for desktop app launch and related
+ * transitions.
+ *
+ * <p>This class handles the complex logic of creating various animators, including launch,
+ * minimize, and trampoline close animations, based on the provided transition information and
+ * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank.
+ *
+ * @param context The application context.
+ * @param launchType The type of app launch, containing animation parameters.
+ * @param cujType The CUJ (Critical User Journey) type for jank monitoring.
+ */
+class DesktopAppLaunchAnimatorHelper(
+ private val context: Context,
+ private val launchType: AppLaunchType,
+ @Cuj.CujType private val cujType: Int,
+ private val transactionSupplier: Supplier<Transaction>,
+) {
+
+ private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+
+ fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> {
+ val launchChange = getLaunchChange(info)
+ requireNotNull(launchChange) { "expected an app launch Change" }
+
+ val transaction = transactionSupplier.get()
+
+ val minimizeChange = getMinimizeChange(info)
+ val trampolineCloseChange = getTrampolineCloseChange(info)
+
+ val launchAnimator =
+ createLaunchAnimator(
+ launchChange,
+ transaction,
+ finishCallback,
+ isTrampoline = trampolineCloseChange != null,
+ )
+ val animatorsList = mutableListOf(launchAnimator)
+ if (minimizeChange != null) {
+ val minimizeAnimator =
+ createMinimizeAnimator(minimizeChange, transaction, finishCallback)
+ animatorsList.add(minimizeAnimator)
+ }
+ if (trampolineCloseChange != null) {
+ val trampolineCloseAnimator =
+ createTrampolineCloseAnimator(trampolineCloseChange, transaction)
+ animatorsList.add(trampolineCloseAnimator)
+ }
+ return animatorsList
+ }
+
+ private fun getLaunchChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }
+
+ private fun getMinimizeChange(info: TransitionInfo): Change? =
+ info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
+
+ private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
+ if (
+ info.changes.size < 2 ||
+ !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue
+ ) {
+ return null
+ }
+ val openChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true
+ }
+ val closeChange =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true
+ }
+ val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName
+ val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName
+ return if (openPackage != null && closePackage != null && openPackage == closePackage) {
+ closeChange
+ } else {
+ null
+ }
+ }
+
+ private fun createLaunchAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ isTrampoline: Boolean,
+ ): Animator {
+ val boundsAnimator =
+ WindowAnimator.createBoundsAnimator(
+ context.resources.displayMetrics,
+ launchType.boundsAnimationParams,
+ change,
+ transaction,
+ )
+ val alphaAnimator =
+ ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = launchType.alphaDurationMs
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction
+ .setAlpha(change.leash, animation.animatedValue as Float)
+ .setFrameTimeline(Choreographer.getInstance().vsyncId)
+ .apply()
+ }
+ }
+ val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
+ transaction.setCrop(change.leash, clipRect)
+ transaction.setCornerRadius(
+ change.leash,
+ ScreenDecorationsUtils.getWindowCornerRadius(context),
+ )
+ return AnimatorSet().apply {
+ interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
+ if (isTrampoline) {
+ play(alphaAnimator)
+ } else {
+ playTogether(boundsAnimator, alphaAnimator)
+ }
+ addListener(
+ onEnd = { animation ->
+ onAnimFinish(animation)
+ interactionJankMonitor.end(cujType)
+ }
+ )
+ }
+ }
+
+ private fun createMinimizeAnimator(
+ change: Change,
+ transaction: Transaction,
+ onAnimFinish: (Animator) -> Unit,
+ ): Animator {
+ return MinimizeAnimator.create(
+ context.resources.displayMetrics,
+ change,
+ transaction,
+ onAnimFinish,
+ )
+ }
+
+ private fun createTrampolineCloseAnimator(change: Change, transaction: Transaction): Animator {
+ return ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 100L
+ interpolator = Interpolators.LINEAR
+ addUpdateListener { animation ->
+ transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
index 2406fb6..578bba5 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransition.kt
@@ -17,27 +17,18 @@
package com.android.launcher3.desktop
import android.animation.Animator
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.content.Context
-import android.graphics.Rect
import android.os.IBinder
-import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.IRemoteTransitionFinishedCallback
import android.window.RemoteTransitionStub
import android.window.TransitionInfo
-import android.window.TransitionInfo.Change
-import androidx.core.animation.addListener
+import androidx.core.util.Supplier
import com.android.app.animation.Interpolators
import com.android.internal.jank.Cuj
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.policy.ScreenDecorationsUtils
import com.android.quickstep.RemoteRunnable
-import com.android.wm.shell.shared.animation.MinimizeAnimator
import com.android.wm.shell.shared.animation.WindowAnimator
import java.util.concurrent.Executor
@@ -48,14 +39,18 @@
* ([android.view.WindowManager.TRANSIT_TO_BACK]) this transition will apply a minimize animation to
* that window.
*/
-class DesktopAppLaunchTransition(
- private val context: Context,
- private val mainExecutor: Executor,
- private val launchType: AppLaunchType,
+class DesktopAppLaunchTransition
+@JvmOverloads
+constructor(
+ context: Context,
+ launchType: AppLaunchType,
@Cuj.CujType private val cujType: Int,
+ private val mainExecutor: Executor,
+ transactionSupplier: Supplier<Transaction> = Supplier { Transaction() },
) : RemoteTransitionStub() {
- private val interactionJankMonitor = InteractionJankMonitor.getInstance()
+ private val animatorHelper: DesktopAppLaunchAnimatorHelper =
+ DesktopAppLaunchAnimatorHelper(context, launchType, cujType, transactionSupplier)
enum class AppLaunchType(
val boundsAnimationParams: WindowAnimator.BoundsAnimationParams,
@@ -68,7 +63,7 @@
override fun startAnimation(
token: IBinder,
info: TransitionInfo,
- t: Transaction,
+ transaction: Transaction,
transitionFinishedCallback: IRemoteTransitionFinishedCallback,
) {
val safeTransitionFinishedCallback = RemoteRunnable {
@@ -76,7 +71,7 @@
}
mainExecutor.execute {
runAnimators(info, safeTransitionFinishedCallback)
- t.apply()
+ transaction.apply()
}
}
@@ -86,77 +81,10 @@
animators -= animator
if (animators.isEmpty()) finishedCallback.run()
}
- animators += createAnimators(info, animatorFinishedCallback)
+ animators += animatorHelper.createAnimators(info, animatorFinishedCallback)
animators.forEach { it.start() }
}
- private fun createAnimators(
- info: TransitionInfo,
- finishCallback: (Animator) -> Unit,
- ): List<Animator> {
- val transaction = Transaction()
- val launchAnimator =
- createLaunchAnimator(getLaunchChange(info), transaction, finishCallback)
- val minimizeChange = getMinimizeChange(info) ?: return listOf(launchAnimator)
- val minimizeAnimator =
- MinimizeAnimator.create(
- context.resources.displayMetrics,
- minimizeChange,
- transaction,
- finishCallback,
- )
- return listOf(launchAnimator, minimizeAnimator)
- }
-
- private fun getLaunchChange(info: TransitionInfo): Change =
- requireNotNull(info.changes.firstOrNull { change -> change.mode in LAUNCH_CHANGE_MODES }) {
- "expected an app launch Change"
- }
-
- private fun getMinimizeChange(info: TransitionInfo): Change? =
- info.changes.firstOrNull { change -> change.mode == TRANSIT_TO_BACK }
-
- private fun createLaunchAnimator(
- change: Change,
- transaction: Transaction,
- onAnimFinish: (Animator) -> Unit,
- ): Animator {
- val boundsAnimator =
- WindowAnimator.createBoundsAnimator(
- context.resources.displayMetrics,
- launchType.boundsAnimationParams,
- change,
- transaction,
- )
- val alphaAnimator =
- ValueAnimator.ofFloat(0f, 1f).apply {
- duration = launchType.alphaDurationMs
- interpolator = Interpolators.LINEAR
- addUpdateListener { animation ->
- transaction
- .setAlpha(change.leash, animation.animatedValue as Float)
- .setFrameTimeline(Choreographer.getInstance().vsyncId)
- .apply()
- }
- }
- val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
- transaction.setCrop(change.leash, clipRect)
- transaction.setCornerRadius(
- change.leash,
- ScreenDecorationsUtils.getWindowCornerRadius(context),
- )
- return AnimatorSet().apply {
- interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
- playTogether(boundsAnimator, alphaAnimator)
- addListener(
- onEnd = { animation ->
- onAnimFinish(animation)
- interactionJankMonitor.end(cujType)
- }
- )
- }
- }
-
companion object {
/** Change modes that represent a task becoming visible / launching in Desktop mode. */
val LAUNCH_CHANGE_MODES = intArrayOf(TRANSIT_OPEN, TRANSIT_TO_FRONT)
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
index 36c5fba..a72b5c4 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopAppLaunchTransitionManager.kt
@@ -48,9 +48,9 @@
RemoteTransition(
DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
AppLaunchType.UNMINIMIZE,
Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ MAIN_EXECUTOR,
),
"DesktopWindowLimitUnminimize",
)
diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
index 5af7ff8..5f7a026 100644
--- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java
@@ -290,9 +290,9 @@
remoteTransition = new RemoteTransition(
new DesktopAppLaunchTransition(
context,
- MAIN_EXECUTOR,
UNMINIMIZE,
- Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH
+ Cuj.CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
+ MAIN_EXECUTOR
),
"DesktopKeyboardQuickSwitchUnminimize");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index b9d1275..8c807ee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -1533,9 +1533,9 @@
return new RemoteTransition(
new DesktopAppLaunchTransition(
this,
- getMainExecutor(),
appLaunchType,
- cujType
+ cujType,
+ getMainExecutor()
),
"TaskbarDesktopAppLaunch");
}
diff --git a/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
new file mode 100644
index 0000000..b4d9f5b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/desktop/DesktopAppLaunchAnimatorHelperTest.kt
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2025 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.desktop
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.util.DisplayMetrics
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.core.util.Supplier
+import com.android.app.animation.Interpolators
+import com.android.internal.jank.Cuj
+import com.android.launcher3.desktop.DesktopAppLaunchAnimatorHelper
+import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class DesktopAppLaunchAnimatorHelperTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val context = mock<Context>()
+ private val resources = mock<Resources>()
+ private val transaction = mock<SurfaceControl.Transaction>()
+ private val transactionSupplier = mock<Supplier<SurfaceControl.Transaction>>()
+
+ private lateinit var helper: DesktopAppLaunchAnimatorHelper
+
+ @Before
+ fun setUp() {
+ helper =
+ DesktopAppLaunchAnimatorHelper(
+ context = context,
+ launchType = AppLaunchType.LAUNCH,
+ cujType = Cuj.CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_INTENT,
+ transactionSupplier = transactionSupplier,
+ )
+ whenever(transactionSupplier.get()).thenReturn(transaction)
+ whenever(transaction.setCrop(any(), any())).thenReturn(transaction)
+ whenever(transaction.setCornerRadius(any(), any())).thenReturn(transaction)
+
+ whenever(context.resources).thenReturn(resources)
+ whenever(resources.displayMetrics).thenReturn(DisplayMetrics())
+ whenever(context.mainThreadHandler).thenReturn(MAIN_EXECUTOR.handler)
+ }
+
+ @Test
+ fun launchTransition_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ fun minimizeTransition_returnsLaunchAndMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_returnsLaunchAndCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertCloseAnimator(actual[1])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_returnsLaunchAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(1)
+ assertLaunchAnimator(actual[0])
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagEnabled_hitDesktopWindowLimit_returnsLaunchMinimizeCloseAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(3)
+ assertTrampolineLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ assertCloseAnimator(actual[2])
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX)
+ fun trampolineTransition_flagDisabled_hitDesktopWindowLimit_returnsLaunchMinimizeAnimator() {
+ val openChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_OPEN
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val minimizeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_TO_BACK
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val closeChange =
+ TransitionInfo.Change(mock(), mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ taskInfo = TASK_INFO_FREEFORM
+ }
+ val transitionInfo = TransitionInfo(WindowManager.TRANSIT_NONE, 0)
+ transitionInfo.addChange(openChange)
+ transitionInfo.addChange(minimizeChange)
+ transitionInfo.addChange(closeChange)
+
+ val actual = helper.createAnimators(transitionInfo, finishCallback = {})
+
+ assertThat(actual).hasSize(2)
+ assertLaunchAnimator(actual[0])
+ assertMinimizeAnimator(actual[1])
+ }
+
+ private fun assertLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.interpolator)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.boundsAnimationParams.durationMs)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[1].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertTrampolineLaunchAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(1)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator).isEqualTo(Interpolators.LINEAR)
+ assertThat(animator.childAnimations[0].duration)
+ .isEqualTo(AppLaunchType.LAUNCH.alphaDurationMs)
+ }
+
+ private fun assertMinimizeAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(AnimatorSet::class.java)
+ assertThat((animator as AnimatorSet).childAnimations.size).isEqualTo(2)
+ assertThat(animator.childAnimations[0]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[0].interpolator)
+ .isInstanceOf(Interpolators.STANDARD_ACCELERATE::class.java)
+ assertThat(animator.childAnimations[0].duration).isEqualTo(200)
+ assertThat(animator.childAnimations[1]).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.childAnimations[1].interpolator)
+ .isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.childAnimations[1].duration).isEqualTo(100)
+ }
+
+ private fun assertCloseAnimator(animator: Animator) {
+ assertThat(animator).isInstanceOf(ValueAnimator::class.java)
+ assertThat(animator.interpolator).isInstanceOf(Interpolators.LINEAR::class.java)
+ assertThat(animator.duration).isEqualTo(100)
+ }
+
+ private companion object {
+ val TASK_INFO_FREEFORM =
+ ActivityManager.RunningTaskInfo().apply {
+ baseIntent =
+ Intent().apply {
+ component = ComponentName("com.example.app", "com.example.app.MainActivity")
+ }
+ configuration.windowConfiguration.windowingMode =
+ WindowConfiguration.WINDOWING_MODE_FREEFORM
+ }
+ }
+}