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