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