1 /* 2 * Copyright (C) 2024 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.quickstep.util 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.AnimatorSet 22 import android.graphics.Matrix 23 import android.graphics.Path 24 import android.graphics.RectF 25 import android.util.Log 26 import android.view.View 27 import android.view.animation.PathInterpolator 28 import androidx.core.graphics.transform 29 import com.android.app.animation.Animations 30 import com.android.app.animation.Interpolators 31 import com.android.app.animation.Interpolators.LINEAR 32 import com.android.launcher3.Flags 33 import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY 34 import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE 35 import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY 36 import com.android.launcher3.LauncherState 37 import com.android.launcher3.anim.AnimatorListeners 38 import com.android.launcher3.anim.PendingAnimation 39 import com.android.launcher3.anim.PropertySetter 40 import com.android.launcher3.states.StateAnimationConfig 41 import com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER 42 import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW 43 import com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM 44 import com.android.launcher3.uioverrides.QuickstepLauncher 45 import com.android.quickstep.views.RecentsView 46 47 const val TAG = "ScalingWorkspaceRevealAnim" 48 49 /** 50 * Creates an animation where the workspace and hotseat fade in while revealing from the center of 51 * the screen outwards radially. This is used in conjunction with the swipe up to home animation. 52 */ 53 class ScalingWorkspaceRevealAnim( 54 private val launcher: QuickstepLauncher, 55 siblingAnimation: RectFSpringAnim?, 56 windowTargetRect: RectF?, 57 ) { 58 companion object { 59 private const val FADE_DURATION_MS = 200L 60 private const val SCALE_DURATION_MS = 1000L 61 private const val MAX_ALPHA = 1f 62 private const val MIN_ALPHA = 0f 63 private const val MAX_SIZE = 1f 64 private const val MIN_SIZE = 0.85f 65 66 /** 67 * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED 68 * is too aggressive, but EMPHASIZED_DECELERATE is too soft. 69 */ 70 @JvmField 71 val SCALE_INTERPOLATOR = 72 PathInterpolator( <lambda>null73 Path().apply { 74 moveTo(0f, 0f) 75 cubicTo(0.045f, 0.0356f, 0.0975f, 0.2055f, 0.15f, 0.3952f) 76 cubicTo(0.235f, 0.6855f, 0.235f, 1f, 1f, 1f) 77 } 78 ) 79 } 80 81 private val animation = PendingAnimation(SCALE_DURATION_MS) 82 83 init { 84 // Make sure the starting state is right for the animation. 85 val setupConfig = StateAnimationConfig() 86 setupConfig.animFlags = SKIP_OVERVIEW.or(SKIP_DEPTH_CONTROLLER).or(SKIP_SCRIM) 87 setupConfig.duration = 0 88 launcher.stateManager 89 .createAtomicAnimation(LauncherState.BACKGROUND_APP, LauncherState.NORMAL, setupConfig) 90 .start() 91 launcher 92 .getOverviewPanel<RecentsView<QuickstepLauncher, LauncherState>>() 93 .forceFinishScroller() 94 launcher.workspace.stateTransitionAnimation.setScrim( 95 PropertySetter.NO_ANIM_PROPERTY_SETTER, 96 LauncherState.BACKGROUND_APP, 97 setupConfig, 98 ) 99 100 val workspace = launcher.workspace 101 val hotseat = launcher.hotseat 102 103 var fromSize = 104 if (Flags.coordinateWorkspaceScale()) { 105 // Interrupt the current animation, if any. 106 Animations.cancelOngoingAnimation(workspace) 107 Animations.cancelOngoingAnimation(hotseat) 108 109 if (workspace.scaleX != MAX_SIZE) { 110 workspace.scaleX 111 } else { 112 MIN_SIZE 113 } 114 } else { 115 MIN_SIZE 116 } 117 118 // Scale the Workspace and Hotseat around the same pivot. 119 workspace.setPivotToScaleWithSelf(hotseat) 120 animation.addFloat( 121 workspace, 122 WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], 123 fromSize, 124 MAX_SIZE, 125 SCALE_INTERPOLATOR, 126 ) 127 animation.addFloat( 128 hotseat, 129 HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], 130 fromSize, 131 MAX_SIZE, 132 SCALE_INTERPOLATOR, 133 ) 134 135 // Fade in quickly at the beginning of the animation, so the content doesn't look like it's 136 // popping into existence out of nowhere. 137 val fadeClamp = FADE_DURATION_MS.toFloat() / SCALE_DURATION_MS 138 workspace.alpha = MIN_ALPHA 139 animation.setViewAlpha( 140 workspace, 141 MAX_ALPHA, 142 Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), 143 ) 144 hotseat.alpha = MIN_ALPHA 145 animation.setViewAlpha( 146 hotseat, 147 MAX_ALPHA, 148 Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), 149 ) 150 151 val transitionConfig = StateAnimationConfig() 152 153 // Match the Wallpaper animation to the rest of the content. 154 val depthController = (launcher as? QuickstepLauncher)?.depthController 155 transitionConfig.setInterpolator(StateAnimationConfig.ANIM_DEPTH, SCALE_INTERPOLATOR) 156 depthController?.setStateWithAnimation(LauncherState.NORMAL, transitionConfig, animation) 157 158 // Make sure that the contrast scrim animates correctly if needed. 159 transitionConfig.setInterpolator(StateAnimationConfig.ANIM_SCRIM_FADE, SCALE_INTERPOLATOR) 160 launcher.workspace.stateTransitionAnimation.setScrim( 161 animation, 162 LauncherState.NORMAL, 163 transitionConfig, 164 ) 165 166 // To avoid awkward jumps in icon position, we want the sibling animation to always be 167 // targeting the current position. Since we can't easily access this, instead we calculate 168 // it using the animation of the whole of home. 169 // We start by caching the final target position, as this is the base for the transforms. 170 val originalTarget = RectF(windowTargetRect) <lambda>null171 animation.addOnFrameListener { 172 val transformed = RectF(originalTarget) 173 174 // First we scale down using the same pivot as the workspace scale, so we find the 175 // correct position AND size. 176 transformed.transform( 177 Matrix().apply { 178 setScale(workspace.scaleX, workspace.scaleY, workspace.pivotX, workspace.pivotY) 179 } 180 ) 181 // Then we scale back up around the center of the current position. This is because the 182 // icon animation behaves poorly if it is given a target that is smaller than the size 183 // of the icon. 184 transformed.transform( 185 Matrix().apply { 186 setScale( 187 1 / workspace.scaleX, 188 1 / workspace.scaleY, 189 transformed.centerX(), 190 transformed.centerY(), 191 ) 192 } 193 ) 194 195 if (transformed != windowTargetRect) { 196 windowTargetRect?.set(transformed) 197 siblingAnimation?.onTargetPositionChanged() 198 } 199 } 200 201 // Needed to avoid text artefacts during the scale animation. 202 workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null) 203 hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null) 204 animation.addListener( 205 object : AnimatorListenerAdapter() { onAnimationCancelnull206 override fun onAnimationCancel(animation: Animator) { 207 super.onAnimationCancel(animation) 208 Log.d(TAG, "onAnimationCancel") 209 } 210 onAnimationPausenull211 override fun onAnimationPause(animation: Animator) { 212 super.onAnimationPause(animation) 213 Log.d(TAG, "onAnimationPause") 214 } 215 } 216 ) 217 animation.addListener( 218 AnimatorListeners.forEndCallback( <lambda>null219 Runnable { 220 // The workspace might stay at a transparent state when the animation is 221 // cancelled, and the alpha will not be recovered (this doesn't apply to scales 222 // somehow). Resetting the alpha for the workspace here. 223 workspace.alpha = 1.0F 224 225 workspace.setLayerType(View.LAYER_TYPE_NONE, null) 226 hotseat.setLayerType(View.LAYER_TYPE_NONE, null) 227 228 if (Flags.coordinateWorkspaceScale()) { 229 // Reset the cached animations. 230 Animations.setOngoingAnimation(workspace, animation = null) 231 Animations.setOngoingAnimation(hotseat, animation = null) 232 } 233 234 Log.d(TAG, "alpha of workspace at the end of animation: ${workspace.alpha}") 235 } 236 ) 237 ) 238 } 239 getAnimatorsnull240 fun getAnimators(): AnimatorSet { 241 return animation.buildAnim() 242 } 243 startnull244 fun start() { 245 val animators = getAnimators() 246 if (Flags.coordinateWorkspaceScale()) { 247 // Make sure to cache the current animation, so it can be properly interrupted. 248 // TODO(b/367591368): ideally these animations would be refactored to be controlled 249 // centrally so each instances doesn't need to care about this coordination. 250 Animations.setOngoingAnimation(launcher.workspace, animators) 251 Animations.setOngoingAnimation(launcher.hotseat, animators) 252 } 253 animators.start() 254 } 255 } 256