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 package com.android.launcher3.uioverrides 17 18 import com.android.app.animation.Interpolators.ACCELERATE_DECELERATE 19 import com.android.app.animation.Interpolators.AGGRESSIVE_EASE_IN_OUT 20 import com.android.app.animation.Interpolators.FINAL_FRAME 21 import com.android.app.animation.Interpolators.INSTANT 22 import com.android.app.animation.Interpolators.LINEAR 23 import com.android.launcher3.Flags.enableDesktopExplodedView 24 import com.android.launcher3.Flags.enableGridOnlyOverview 25 import com.android.launcher3.Flags.enableLargeDesktopWindowingTile 26 import com.android.launcher3.LauncherState 27 import com.android.launcher3.anim.AnimatedFloat 28 import com.android.launcher3.anim.AnimatorListeners.forSuccessCallback 29 import com.android.launcher3.anim.PendingAnimation 30 import com.android.launcher3.anim.PropertySetter 31 import com.android.launcher3.statemanager.StateManager.StateHandler 32 import com.android.launcher3.states.StateAnimationConfig 33 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_ACTIONS_FADE 34 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE 35 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL 36 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE 37 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X 38 import com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y 39 import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW 40 import com.android.quickstep.util.AnimUtils 41 import com.android.quickstep.views.AddDesktopButton 42 import com.android.quickstep.views.ClearAllButton 43 import com.android.quickstep.views.RecentsView 44 import com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET 45 import com.android.quickstep.views.RecentsView.CONTENT_ALPHA 46 import com.android.quickstep.views.RecentsView.DESKTOP_CAROUSEL_DETACH_PROGRESS 47 import com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS 48 import com.android.quickstep.views.RecentsView.RECENTS_GRID_PROGRESS 49 import com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY 50 import com.android.quickstep.views.RecentsView.TASK_MODALNESS 51 import com.android.quickstep.views.RecentsView.TASK_PRIMARY_SPLIT_TRANSLATION 52 import com.android.quickstep.views.RecentsView.TASK_SECONDARY_SPLIT_TRANSLATION 53 import com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION 54 import com.android.quickstep.views.RecentsView.TASK_THUMBNAIL_SPLASH_ALPHA 55 import com.android.quickstep.views.RecentsViewUtils.Companion.DESK_EXPLODE_PROGRESS 56 import com.android.quickstep.views.TaskView.Companion.FLAG_UPDATE_ALL 57 58 /** 59 * State handler for handling UI changes for [com.android.quickstep.views.LauncherRecentsView]. In 60 * addition to managing the basic view properties, this class also manages changes in the task 61 * visuals. 62 */ 63 class RecentsViewStateController(private val launcher: QuickstepLauncher) : 64 StateHandler<LauncherState> { 65 private val recentsView: RecentsView<*, *> = launcher.getOverviewPanel() 66 67 override fun setState(state: LauncherState) { 68 val scaleAndOffset = state.getOverviewScaleAndOffset(launcher) 69 RECENTS_SCALE_PROPERTY.set(recentsView, scaleAndOffset[0]) 70 ADJACENT_PAGE_HORIZONTAL_OFFSET.set(recentsView, scaleAndOffset[1]) 71 TASK_SECONDARY_TRANSLATION.set(recentsView, 0f) 72 73 CONTENT_ALPHA.set(recentsView, if (state.isRecentsViewVisible) 1f else 0f) 74 TASK_MODALNESS.set(recentsView, state.overviewModalness) 75 RECENTS_GRID_PROGRESS.set( 76 recentsView, 77 if (state.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f, 78 ) 79 if (enableDesktopExplodedView()) { 80 DESK_EXPLODE_PROGRESS.set(recentsView, if (state.showExplodedDesktopView()) 1f else 0f) 81 } 82 83 TASK_THUMBNAIL_SPLASH_ALPHA.set( 84 recentsView, 85 if (state.showTaskThumbnailSplash()) 1f else 0f, 86 ) 87 if (enableLargeDesktopWindowingTile()) { 88 DESKTOP_CAROUSEL_DETACH_PROGRESS.set( 89 recentsView, 90 if (state.detachDesktopCarousel()) 1f else 0f, 91 ) 92 } 93 94 if (state.isRecentsViewVisible) { 95 recentsView.updateEmptyMessage() 96 } else { 97 recentsView.resetTaskVisuals() 98 } 99 setAlphas(PropertySetter.NO_ANIM_PROPERTY_SETTER, StateAnimationConfig(), state) 100 recentsView.setFullscreenProgress(state.overviewFullscreenProgress) 101 // In Overview, we may be layering app surfaces behind Launcher, so we need to notify 102 // DepthController to prevent optimizations which might occlude the layers behind 103 launcher.depthController.setHasContentBehindLauncher(state.isRecentsViewVisible) 104 105 val builder = PendingAnimation(state.getTransitionDuration(launcher, true).toLong()) 106 handleSplitSelectionState(state, builder, animate = false) 107 } 108 109 override fun setStateWithAnimation( 110 toState: LauncherState, 111 config: StateAnimationConfig, 112 builder: PendingAnimation, 113 ) { 114 if (config.hasAnimationFlag(SKIP_OVERVIEW)) return 115 116 val scaleAndOffset = toState.getOverviewScaleAndOffset(launcher) 117 builder.setFloat( 118 recentsView, 119 RECENTS_SCALE_PROPERTY, 120 scaleAndOffset[0], 121 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR), 122 ) 123 builder.setFloat( 124 recentsView, 125 ADJACENT_PAGE_HORIZONTAL_OFFSET, 126 scaleAndOffset[1], 127 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR), 128 ) 129 builder.setFloat( 130 recentsView, 131 TASK_SECONDARY_TRANSLATION, 132 0f, 133 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR), 134 ) 135 136 builder.setFloat( 137 recentsView, 138 CONTENT_ALPHA, 139 if (toState.isRecentsViewVisible) 1f else 0f, 140 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT), 141 ) 142 143 builder.setFloat( 144 recentsView, 145 TASK_MODALNESS, 146 toState.overviewModalness, 147 config.getInterpolator( 148 ANIM_OVERVIEW_MODAL, 149 if (enableGridOnlyOverview() && !toState.isRecentsViewVisible) FINAL_FRAME 150 else LINEAR, 151 ), 152 ) 153 154 val fromState = launcher.stateManager.state 155 builder.setFloat( 156 recentsView, 157 TASK_THUMBNAIL_SPLASH_ALPHA, 158 if (toState.showTaskThumbnailSplash()) 1f else 0f, 159 getOverviewInterpolator(fromState, toState), 160 ) 161 162 builder.setFloat( 163 recentsView, 164 RECENTS_GRID_PROGRESS, 165 if (toState.displayOverviewTasksAsGrid(launcher.deviceProfile)) 1f else 0f, 166 getOverviewInterpolator(fromState, toState), 167 ) 168 169 if (enableDesktopExplodedView()) { 170 builder.setFloat( 171 recentsView, 172 DESK_EXPLODE_PROGRESS, 173 if (toState.showExplodedDesktopView()) 1f else 0f, 174 getOverviewInterpolator(fromState, toState), 175 ) 176 } 177 178 if (enableLargeDesktopWindowingTile()) { 179 builder.setFloat( 180 recentsView, 181 DESKTOP_CAROUSEL_DETACH_PROGRESS, 182 if (toState.detachDesktopCarousel()) 1f else 0f, 183 getOverviewInterpolator(fromState, toState), 184 ) 185 } 186 187 if (toState.isRecentsViewVisible) { 188 // While animating into recents, update the visible task data as needed 189 builder.addOnFrameCallback { recentsView.loadVisibleTaskData(FLAG_UPDATE_ALL) } 190 recentsView.updateEmptyMessage() 191 } else { 192 builder.addListener(forSuccessCallback { recentsView.resetTaskVisuals() }) 193 } 194 // In Overview, we may be layering app surfaces behind Launcher, so we need to notify 195 // DepthController to prevent optimizations which might occlude the layers behind 196 builder.addListener( 197 forSuccessCallback { 198 launcher.depthController.setHasContentBehindLauncher(toState.isRecentsViewVisible) 199 } 200 ) 201 202 handleSplitSelectionState(toState, builder, animate = true) 203 204 setAlphas(builder, config, toState) 205 builder.setFloat( 206 recentsView, 207 FULLSCREEN_PROGRESS, 208 toState.overviewFullscreenProgress, 209 LINEAR, 210 ) 211 212 builder.addEndListener { success: Boolean -> 213 if (!success && !toState.isRecentsViewVisible) { 214 recentsView.reset() 215 } 216 } 217 } 218 219 /** 220 * Create or dismiss split screen select animations. 221 * 222 * @param builder if null then this will run the split select animations right away, otherwise 223 * will add animations to builder. 224 */ 225 private fun handleSplitSelectionState( 226 toState: LauncherState, 227 builder: PendingAnimation, 228 animate: Boolean, 229 ) { 230 val goingToOverviewFromWorkspaceContextual = 231 toState == LauncherState.OVERVIEW && launcher.isSplitSelectionActive 232 if ( 233 toState != LauncherState.OVERVIEW_SPLIT_SELECT && 234 !goingToOverviewFromWorkspaceContextual 235 ) { 236 // Not going to split 237 return 238 } 239 240 // Create transition animations to split select 241 val orientationHandler = recentsView.pagedOrientationHandler 242 val taskViewsFloat = 243 orientationHandler.getSplitSelectTaskOffset( 244 TASK_PRIMARY_SPLIT_TRANSLATION, 245 TASK_SECONDARY_SPLIT_TRANSLATION, 246 launcher.deviceProfile, 247 ) 248 249 val timings = AnimUtils.getDeviceOverviewToSplitTimings(launcher.deviceProfile.isTablet) 250 if (!goingToOverviewFromWorkspaceContextual) { 251 // This animation is already done for the contextual case, don't redo it 252 recentsView.createSplitSelectInitAnimation( 253 builder, 254 toState.getTransitionDuration(launcher, true), 255 ) 256 } 257 // Shift tasks vertically downward to get out of placeholder view 258 builder.setFloat( 259 recentsView, 260 taskViewsFloat.first, 261 toState.getSplitSelectTranslation(launcher), 262 timings.gridSlidePrimaryInterpolator, 263 ) 264 // Zero out horizontal translation 265 builder.setFloat( 266 recentsView, 267 taskViewsFloat.second, 268 0f, 269 timings.gridSlideSecondaryInterpolator, 270 ) 271 272 recentsView.handleDesktopTaskInSplitSelectState( 273 builder, 274 timings.desktopTaskFadeInterpolator, 275 ) 276 277 if (!animate) { 278 builder.buildAnim().apply { 279 start() 280 end() 281 } 282 } 283 } 284 285 private fun setAlphas( 286 propertySetter: PropertySetter, 287 config: StateAnimationConfig, 288 state: LauncherState, 289 ) { 290 val clearAllButtonAlpha = 291 if (state.areElementsVisible(launcher, LauncherState.CLEAR_ALL_BUTTON)) 1f else 0f 292 propertySetter.setFloat( 293 recentsView.clearAllButton, 294 ClearAllButton.VISIBILITY_ALPHA, 295 clearAllButtonAlpha, 296 LINEAR, 297 ) 298 val overviewButtonAlpha = 299 if (state.areElementsVisible(launcher, LauncherState.OVERVIEW_ACTIONS)) 1f else 0f 300 propertySetter.setFloat( 301 launcher.actionsView.visibilityAlpha, 302 AnimatedFloat.VALUE, 303 overviewButtonAlpha, 304 config.getInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, LINEAR), 305 ) 306 recentsView.addDeskButton?.let { 307 propertySetter.setFloat( 308 it, 309 AddDesktopButton.VISIBILITY_ALPHA, 310 if (state.areElementsVisible(launcher, LauncherState.ADD_DESK_BUTTON)) 1f else 0f, 311 LINEAR, 312 ) 313 } 314 } 315 316 private fun getOverviewInterpolator(fromState: LauncherState, toState: LauncherState) = 317 when { 318 fromState == LauncherState.QUICK_SWITCH_FROM_HOME -> ACCELERATE_DECELERATE 319 toState.isRecentsViewVisible -> INSTANT 320 else -> FINAL_FRAME 321 } 322 } 323