1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.desktop 18 19 import android.animation.Animator 20 import android.animation.AnimatorSet 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.Rect 24 import android.util.Log 25 import android.view.Choreographer 26 import android.view.SurfaceControl.Transaction 27 import android.view.WindowManager.TRANSIT_CLOSE 28 import android.view.WindowManager.TRANSIT_OPEN 29 import android.view.WindowManager.TRANSIT_TO_BACK 30 import android.window.DesktopModeFlags 31 import android.window.TransitionInfo 32 import android.window.TransitionInfo.Change 33 import androidx.core.animation.addListener 34 import androidx.core.util.Supplier 35 import com.android.app.animation.Interpolators 36 import com.android.internal.jank.Cuj 37 import com.android.internal.jank.InteractionJankMonitor 38 import com.android.internal.policy.ScreenDecorationsUtils 39 import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType 40 import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES 41 import com.android.wm.shell.shared.animation.MinimizeAnimator 42 import com.android.wm.shell.shared.animation.WindowAnimator 43 44 /** 45 * Helper class responsible for creating and managing animators for desktop app launch and related 46 * transitions. 47 * 48 * <p>This class handles the complex logic of creating various animators, including launch, 49 * minimize, and trampoline close animations, based on the provided transition information and 50 * launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank. 51 * 52 * @param context The application context. 53 * @param launchType The type of app launch, containing animation parameters. 54 * @param cujType The CUJ (Critical User Journey) type for jank monitoring. 55 */ 56 class DesktopAppLaunchAnimatorHelper( 57 private val context: Context, 58 private val launchType: AppLaunchType, 59 @Cuj.CujType private val cujType: Int, 60 private val transactionSupplier: Supplier<Transaction>, 61 ) { 62 63 private val interactionJankMonitor = InteractionJankMonitor.getInstance() 64 65 fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List<Animator> { 66 val launchChange = getLaunchChange(info) 67 if (launchChange == null) { 68 val tasksInfo = 69 info.changes.joinToString(", ") { change -> 70 "${change.taskInfo?.taskId}:${change.taskInfo?.isFreeform}" 71 } 72 Log.e(TAG, "No launch change found: Transition info=$info, tasks state=$tasksInfo") 73 return emptyList() 74 } 75 76 val transaction = transactionSupplier.get() 77 78 val minimizeChange = getMinimizeChange(info) 79 val trampolineCloseChange = getTrampolineCloseChange(info) 80 81 val launchAnimator = 82 createLaunchAnimator( 83 launchChange, 84 transaction, 85 finishCallback, 86 isTrampoline = trampolineCloseChange != null, 87 ) 88 val animatorsList = mutableListOf(launchAnimator) 89 if (minimizeChange != null) { 90 val minimizeAnimator = 91 createMinimizeAnimator(minimizeChange, transaction, finishCallback) 92 animatorsList.add(minimizeAnimator) 93 } 94 if (trampolineCloseChange != null) { 95 val trampolineCloseAnimator = 96 createTrampolineCloseAnimator(trampolineCloseChange, transaction, finishCallback) 97 animatorsList.add(trampolineCloseAnimator) 98 } 99 return animatorsList 100 } 101 102 private fun getLaunchChange(info: TransitionInfo): Change? = 103 info.changes.firstOrNull { change -> 104 change.mode in LAUNCH_CHANGE_MODES && change.taskInfo?.isFreeform == true 105 } 106 107 private fun getMinimizeChange(info: TransitionInfo): Change? = 108 info.changes.firstOrNull { change -> 109 change.mode == TRANSIT_TO_BACK && change.taskInfo?.isFreeform == true 110 } 111 112 private fun getTrampolineCloseChange(info: TransitionInfo): Change? { 113 if ( 114 info.changes.size < 2 || 115 !DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue 116 ) { 117 return null 118 } 119 val openChange = 120 info.changes.firstOrNull { change -> 121 change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true 122 } 123 val closeChange = 124 info.changes.firstOrNull { change -> 125 change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true 126 } 127 val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName 128 val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName 129 return if (openPackage != null && closePackage != null && openPackage == closePackage) { 130 closeChange 131 } else { 132 null 133 } 134 } 135 136 private fun createLaunchAnimator( 137 change: Change, 138 transaction: Transaction, 139 onAnimFinish: (Animator) -> Unit, 140 isTrampoline: Boolean, 141 ): Animator { 142 val boundsAnimator = 143 WindowAnimator.createBoundsAnimator( 144 context.resources.displayMetrics, 145 launchType.boundsAnimationParams, 146 change, 147 transaction, 148 ) 149 val alphaAnimator = 150 ValueAnimator.ofFloat(0f, 1f).apply { 151 duration = launchType.alphaDurationMs 152 interpolator = Interpolators.LINEAR 153 addUpdateListener { animation -> 154 transaction 155 .setAlpha(change.leash, animation.animatedValue as Float) 156 .setFrameTimeline(Choreographer.getInstance().vsyncId) 157 .apply() 158 } 159 } 160 val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) } 161 transaction.setCrop(change.leash, clipRect) 162 transaction.setCornerRadius( 163 change.leash, 164 ScreenDecorationsUtils.getWindowCornerRadius(context), 165 ) 166 return AnimatorSet().apply { 167 interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType) 168 if (isTrampoline) { 169 play(alphaAnimator) 170 } else { 171 playTogether(boundsAnimator, alphaAnimator) 172 } 173 addListener( 174 onEnd = { animation -> 175 onAnimFinish(animation) 176 interactionJankMonitor.end(cujType) 177 } 178 ) 179 } 180 } 181 182 private fun createMinimizeAnimator( 183 change: Change, 184 transaction: Transaction, 185 onAnimFinish: (Animator) -> Unit, 186 ): Animator { 187 return MinimizeAnimator.create( 188 context, 189 change, 190 transaction, 191 onAnimFinish, 192 interactionJankMonitor, 193 context.mainThreadHandler, 194 ) 195 } 196 197 private fun createTrampolineCloseAnimator( 198 change: Change, 199 transaction: Transaction, 200 onAnimFinish: (Animator) -> Unit, 201 ): Animator { 202 return ValueAnimator.ofFloat(1f, 0f).apply { 203 duration = 100L 204 interpolator = Interpolators.LINEAR 205 addUpdateListener { animation -> 206 transaction.setAlpha(change.leash, animation.animatedValue as Float).apply() 207 } 208 addListener( 209 onEnd = { animation -> 210 onAnimFinish(animation) 211 } 212 ) 213 } 214 } 215 216 private companion object { 217 const val TAG = "DesktopAppLaunchAnimatorHelper" 218 } 219 } 220