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.AnimatorSet
20 import android.graphics.Matrix
21 import android.graphics.Path
22 import android.graphics.RectF
23 import android.view.View
24 import android.view.animation.PathInterpolator
25 import androidx.core.graphics.transform
26 import com.android.app.animation.Interpolators
27 import com.android.app.animation.Interpolators.LINEAR
28 import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
29 import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE
30 import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
31 import com.android.launcher3.LauncherState
32 import com.android.launcher3.anim.AnimatorListeners
33 import com.android.launcher3.anim.PendingAnimation
34 import com.android.launcher3.anim.PropertySetter
35 import com.android.launcher3.states.StateAnimationConfig
36 import com.android.launcher3.states.StateAnimationConfig.SKIP_DEPTH_CONTROLLER
37 import com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW
38 import com.android.launcher3.states.StateAnimationConfig.SKIP_SCRIM
39 import com.android.launcher3.uioverrides.QuickstepLauncher
40 import com.android.quickstep.views.RecentsView
41 
42 /**
43  * Creates an animation where the workspace and hotseat fade in while revealing from the center of
44  * the screen outwards radially. This is used in conjunction with the swipe up to home animation.
45  */
46 class ScalingWorkspaceRevealAnim(
47     launcher: QuickstepLauncher,
48     siblingAnimation: RectFSpringAnim?,
49     windowTargetRect: RectF?
50 ) {
51     companion object {
52         private const val FADE_DURATION_MS = 200L
53         private const val SCALE_DURATION_MS = 1000L
54         private const val MAX_ALPHA = 1f
55         private const val MIN_ALPHA = 0f
56         private const val MAX_SIZE = 1f
57         private const val MIN_SIZE = 0.85f
58 
59         /**
60          * Custom interpolator for both the home and wallpaper scaling. Necessary because EMPHASIZED
61          * is too aggressive, but EMPHASIZED_DECELERATE is too soft.
62          */
63         private val SCALE_INTERPOLATOR =
64             PathInterpolator(
<lambda>null65                 Path().apply {
66                     moveTo(0f, 0f)
67                     cubicTo(0.045f, 0.0356f, 0.0975f, 0.2055f, 0.15f, 0.3952f)
68                     cubicTo(0.235f, 0.6855f, 0.235f, 1f, 1f, 1f)
69                 }
70             )
71     }
72 
73     private val animation = PendingAnimation(SCALE_DURATION_MS)
74 
75     init {
76         // Make sure the starting state is right for the animation.
77         val setupConfig = StateAnimationConfig()
78         setupConfig.animFlags = SKIP_OVERVIEW.or(SKIP_DEPTH_CONTROLLER).or(SKIP_SCRIM)
79         setupConfig.duration = 0
80         launcher.stateManager
81             .createAtomicAnimation(LauncherState.BACKGROUND_APP, LauncherState.NORMAL, setupConfig)
82             .start()
83         launcher
84             .getOverviewPanel<RecentsView<QuickstepLauncher, LauncherState>>()
85             .forceFinishScroller()
86         launcher.workspace.stateTransitionAnimation.setScrim(
87             PropertySetter.NO_ANIM_PROPERTY_SETTER,
88             LauncherState.BACKGROUND_APP,
89             setupConfig
90         )
91 
92         val workspace = launcher.workspace
93         val hotseat = launcher.hotseat
94 
95         // Scale the Workspace and Hotseat around the same pivot.
96         workspace.setPivotToScaleWithSelf(hotseat)
97         animation.addFloat(
98             workspace,
99             WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
100             MIN_SIZE,
101             MAX_SIZE,
102             SCALE_INTERPOLATOR,
103         )
104         animation.addFloat(
105             hotseat,
106             HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE],
107             MIN_SIZE,
108             MAX_SIZE,
109             SCALE_INTERPOLATOR,
110         )
111 
112         // Fade in quickly at the beginning of the animation, so the content doesn't look like it's
113         // popping into existence out of nowhere.
114         val fadeClamp = FADE_DURATION_MS.toFloat() / SCALE_DURATION_MS
115         workspace.alpha = MIN_ALPHA
116         animation.setViewAlpha(
117             workspace,
118             MAX_ALPHA,
119             Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
120         )
121         hotseat.alpha = MIN_ALPHA
122         animation.setViewAlpha(
123             hotseat,
124             MAX_ALPHA,
125             Interpolators.clampToProgress(LINEAR, 0f, fadeClamp)
126         )
127 
128         val transitionConfig = StateAnimationConfig()
129 
130         // Match the Wallpaper animation to the rest of the content.
131         val depthController = (launcher as? QuickstepLauncher)?.depthController
132         transitionConfig.setInterpolator(StateAnimationConfig.ANIM_DEPTH, SCALE_INTERPOLATOR)
133         depthController?.setStateWithAnimation(LauncherState.NORMAL, transitionConfig, animation)
134 
135         // Make sure that the contrast scrim animates correctly if needed.
136         transitionConfig.setInterpolator(StateAnimationConfig.ANIM_SCRIM_FADE, SCALE_INTERPOLATOR)
137         launcher.workspace.stateTransitionAnimation.setScrim(
138             animation,
139             LauncherState.NORMAL,
140             transitionConfig
141         )
142 
143         // To avoid awkward jumps in icon position, we want the sibling animation to always be
144         // targeting the current position. Since we can't easily access this, instead we calculate
145         // it using the animation of the whole of home.
146         // We start by caching the final target position, as this is the base for the transforms.
147         val originalTarget = RectF(windowTargetRect)
<lambda>null148         animation.addOnFrameListener {
149             val transformed = RectF(originalTarget)
150 
151             // First we scale down using the same pivot as the workspace scale, so we find the
152             // correct position AND size.
153             transformed.transform(
154                 Matrix().apply {
155                     setScale(workspace.scaleX, workspace.scaleY, workspace.pivotX, workspace.pivotY)
156                 }
157             )
158             // Then we scale back up around the center of the current position. This is because the
159             // icon animation behaves poorly if it is given a target that is smaller than the size
160             // of the icon.
161             transformed.transform(
162                 Matrix().apply {
163                     setScale(
164                         1 / workspace.scaleX,
165                         1 / workspace.scaleY,
166                         transformed.centerX(),
167                         transformed.centerY()
168                     )
169                 }
170             )
171 
172             if (transformed != windowTargetRect) {
173                 windowTargetRect?.set(transformed)
174                 siblingAnimation?.onTargetPositionChanged()
175             }
176         }
177 
178         // Needed to avoid text artefacts during the scale animation.
179         workspace.setLayerType(View.LAYER_TYPE_HARDWARE, null)
180         hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null)
181         animation.addListener(
182             AnimatorListeners.forEndCallback(
<lambda>null183                 Runnable {
184                     workspace.setLayerType(View.LAYER_TYPE_NONE, null)
185                     hotseat.setLayerType(View.LAYER_TYPE_NONE, null)
186                 }
187             )
188         )
189     }
190 
getAnimatorsnull191     fun getAnimators(): AnimatorSet {
192         return animation.buildAnim()
193     }
194 
startnull195     fun start() {
196         getAnimators().start()
197     }
198 }
199