1 /* 2 * Copyright (C) 2021 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.systemui.animation 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.graphics.PorterDuff 24 import android.graphics.PorterDuffXfermode 25 import android.graphics.drawable.GradientDrawable 26 import android.util.Log 27 import android.util.MathUtils 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.animation.Interpolator 31 import com.android.systemui.animation.Interpolators.LINEAR 32 import kotlin.math.roundToInt 33 34 private const val TAG = "LaunchAnimator" 35 36 /** A base class to animate a window launch (activity or dialog) from a view . */ 37 class LaunchAnimator( 38 private val timings: Timings, 39 private val interpolators: Interpolators 40 ) { 41 companion object { 42 internal const val DEBUG = false 43 private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) 44 45 /** 46 * Given the [linearProgress] of a launch animation, return the linear progress of the 47 * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. 48 */ 49 @JvmStatic 50 fun getProgress( 51 timings: Timings, 52 linearProgress: Float, 53 delay: Long, 54 duration: Long 55 ): Float { 56 return MathUtils.constrain( 57 (linearProgress * timings.totalDuration - delay) / duration, 58 0.0f, 59 1.0f 60 ) 61 } 62 } 63 64 private val launchContainerLocation = IntArray(2) 65 private val cornerRadii = FloatArray(8) 66 67 /** 68 * A controller that takes care of applying the animation to an expanding view. 69 * 70 * Note that all callbacks (onXXX methods) are all called on the main thread. 71 */ 72 interface Controller { 73 /** 74 * The container in which the view that started the animation will be animating together 75 * with the opening window. 76 * 77 * This will be used to: 78 * - Get the associated [Context]. 79 * - Compute whether we are expanding fully above the launch container. 80 * - Apply surface transactions in sync with RenderThread when animating an activity 81 * launch. 82 * 83 * This container can be changed to force this [Controller] to animate the expanding view 84 * inside a different location, for instance to ensure correct layering during the 85 * animation. 86 */ 87 var launchContainer: ViewGroup 88 89 /** 90 * Return the [State] of the view that will be animated. We will animate from this state to 91 * the final window state. 92 * 93 * Note: This state will be mutated and passed to [onLaunchAnimationProgress] during the 94 * animation. 95 */ 96 fun createAnimatorState(): State 97 98 /** 99 * The animation started. This is typically used to initialize any additional resource 100 * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding 101 * fully above the [launchContainer]. 102 */ 103 fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {} 104 105 /** The animation made progress and the expandable view [state] should be updated. */ 106 fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {} 107 108 /** 109 * The animation ended. This will be called *if and only if* [onLaunchAnimationStart] was 110 * called previously. This is typically used to clean up the resources initialized when the 111 * animation was started. 112 */ 113 fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {} 114 } 115 116 /** The state of an expandable view during a [LaunchAnimator] animation. */ 117 open class State( 118 /** The position of the view in screen space coordinates. */ 119 var top: Int = 0, 120 var bottom: Int = 0, 121 var left: Int = 0, 122 var right: Int = 0, 123 124 var topCornerRadius: Float = 0f, 125 var bottomCornerRadius: Float = 0f 126 ) { 127 private val startTop = top 128 129 val width: Int 130 get() = right - left 131 132 val height: Int 133 get() = bottom - top 134 135 open val topChange: Int 136 get() = top - startTop 137 138 val centerX: Float 139 get() = left + width / 2f 140 141 val centerY: Float 142 get() = top + height / 2f 143 144 /** Whether the expanding view should be visible or hidden. */ 145 var visible: Boolean = true 146 } 147 148 interface Animation { 149 /** Cancel the animation. */ 150 fun cancel() 151 } 152 153 /** The timings (durations and delays) used by this animator. */ 154 class Timings( 155 /** The total duration of the animation. */ 156 val totalDuration: Long, 157 158 /** The time to wait before fading out the expanding content. */ 159 val contentBeforeFadeOutDelay: Long, 160 161 /** The duration of the expanding content fade out. */ 162 val contentBeforeFadeOutDuration: Long, 163 164 /** 165 * The time to wait before fading in the expanded content (usually an activity or dialog 166 * window). 167 */ 168 val contentAfterFadeInDelay: Long, 169 170 /** The duration of the expanded content fade in. */ 171 val contentAfterFadeInDuration: Long 172 ) 173 174 /** The interpolators used by this animator. */ 175 data class Interpolators( 176 /** The interpolator used for the Y position, width, height and corner radius. */ 177 val positionInterpolator: Interpolator, 178 179 /** 180 * The interpolator used for the X position. This can be different than 181 * [positionInterpolator] to create an arc-path during the animation. 182 */ 183 val positionXInterpolator: Interpolator = positionInterpolator, 184 185 /** The interpolator used when fading out the expanding content. */ 186 val contentBeforeFadeOutInterpolator: Interpolator, 187 188 /** The interpolator used when fading in the expanded content. */ 189 val contentAfterFadeInInterpolator: Interpolator 190 ) 191 192 /** 193 * Start a launch animation controlled by [controller] towards [endState]. An intermediary 194 * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and 195 * should be the same background color as the opening (or closing) window. If [drawHole] is 196 * true, then this intermediary layer will be drawn with SRC blending mode while it fades out. 197 * 198 * TODO(b/184121838): Remove [drawHole] and instead make the StatusBar draw this hole instead. 199 */ 200 fun startAnimation( 201 controller: Controller, 202 endState: State, 203 windowBackgroundColor: Int, 204 drawHole: Boolean = false 205 ): Animation { 206 val state = controller.createAnimatorState() 207 208 // Start state. 209 val startTop = state.top 210 val startBottom = state.bottom 211 val startLeft = state.left 212 val startRight = state.right 213 val startCenterX = (startLeft + startRight) / 2f 214 val startWidth = startRight - startLeft 215 val startTopCornerRadius = state.topCornerRadius 216 val startBottomCornerRadius = state.bottomCornerRadius 217 218 // End state. 219 var endTop = endState.top 220 var endBottom = endState.bottom 221 var endLeft = endState.left 222 var endRight = endState.right 223 var endCenterX = (endLeft + endRight) / 2f 224 var endWidth = endRight - endLeft 225 val endTopCornerRadius = endState.topCornerRadius 226 val endBottomCornerRadius = endState.bottomCornerRadius 227 228 fun maybeUpdateEndState() { 229 if (endTop != endState.top || endBottom != endState.bottom || 230 endLeft != endState.left || endRight != endState.right) { 231 endTop = endState.top 232 endBottom = endState.bottom 233 endLeft = endState.left 234 endRight = endState.right 235 endCenterX = (endLeft + endRight) / 2f 236 endWidth = endRight - endLeft 237 } 238 } 239 240 val launchContainer = controller.launchContainer 241 val isExpandingFullyAbove = isExpandingFullyAbove(launchContainer, endState) 242 243 // We add an extra layer with the same color as the dialog/app splash screen background 244 // color, which is usually the same color of the app background. We first fade in this layer 245 // to hide the expanding view, then we fade it out with SRC mode to draw a hole in the 246 // launch container and reveal the opening window. 247 val windowBackgroundLayer = GradientDrawable().apply { 248 setColor(windowBackgroundColor) 249 alpha = 0 250 } 251 252 // Update state. 253 val animator = ValueAnimator.ofFloat(0f, 1f) 254 animator.duration = timings.totalDuration 255 animator.interpolator = LINEAR 256 257 val launchContainerOverlay = launchContainer.overlay 258 var cancelled = false 259 animator.addListener(object : AnimatorListenerAdapter() { 260 override fun onAnimationStart(animation: Animator?, isReverse: Boolean) { 261 if (DEBUG) { 262 Log.d(TAG, "Animation started") 263 } 264 controller.onLaunchAnimationStart(isExpandingFullyAbove) 265 266 // Add the drawable to the launch container overlay. Overlays always draw 267 // drawables after views, so we know that it will be drawn above any view added 268 // by the controller. 269 launchContainerOverlay.add(windowBackgroundLayer) 270 } 271 272 override fun onAnimationEnd(animation: Animator?) { 273 if (DEBUG) { 274 Log.d(TAG, "Animation ended") 275 } 276 controller.onLaunchAnimationEnd(isExpandingFullyAbove) 277 launchContainerOverlay.remove(windowBackgroundLayer) 278 } 279 }) 280 281 animator.addUpdateListener { animation -> 282 if (cancelled) { 283 // TODO(b/184121838): Cancel the animator directly instead of just skipping the 284 // update. 285 return@addUpdateListener 286 } 287 288 maybeUpdateEndState() 289 290 // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non 291 // reversed animation. 292 val linearProgress = animation.animatedFraction 293 val progress = interpolators.positionInterpolator.getInterpolation(linearProgress) 294 val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress) 295 296 val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) 297 val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f 298 299 state.top = MathUtils.lerp(startTop, endTop, progress).roundToInt() 300 state.bottom = MathUtils.lerp(startBottom, endBottom, progress).roundToInt() 301 state.left = (xCenter - halfWidth).roundToInt() 302 state.right = (xCenter + halfWidth).roundToInt() 303 304 state.topCornerRadius = 305 MathUtils.lerp(startTopCornerRadius, endTopCornerRadius, progress) 306 state.bottomCornerRadius = 307 MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress) 308 309 // The expanding view can/should be hidden once it is completely covered by the opening 310 // window. 311 state.visible = getProgress( 312 timings, 313 linearProgress, 314 timings.contentBeforeFadeOutDelay, 315 timings.contentBeforeFadeOutDuration 316 ) < 1 317 318 applyStateToWindowBackgroundLayer( 319 windowBackgroundLayer, 320 state, 321 linearProgress, 322 launchContainer, 323 drawHole 324 ) 325 controller.onLaunchAnimationProgress(state, progress, linearProgress) 326 } 327 328 animator.start() 329 return object : Animation { 330 override fun cancel() { 331 cancelled = true 332 animator.cancel() 333 } 334 } 335 } 336 337 /** Return whether we are expanding fully above the [launchContainer]. */ 338 internal fun isExpandingFullyAbove(launchContainer: View, endState: State): Boolean { 339 launchContainer.getLocationOnScreen(launchContainerLocation) 340 return endState.top <= launchContainerLocation[1] && 341 endState.bottom >= launchContainerLocation[1] + launchContainer.height && 342 endState.left <= launchContainerLocation[0] && 343 endState.right >= launchContainerLocation[0] + launchContainer.width 344 } 345 346 private fun applyStateToWindowBackgroundLayer( 347 drawable: GradientDrawable, 348 state: State, 349 linearProgress: Float, 350 launchContainer: View, 351 drawHole: Boolean 352 ) { 353 // Update position. 354 launchContainer.getLocationOnScreen(launchContainerLocation) 355 drawable.setBounds( 356 state.left - launchContainerLocation[0], 357 state.top - launchContainerLocation[1], 358 state.right - launchContainerLocation[0], 359 state.bottom - launchContainerLocation[1] 360 ) 361 362 // Update radius. 363 cornerRadii[0] = state.topCornerRadius 364 cornerRadii[1] = state.topCornerRadius 365 cornerRadii[2] = state.topCornerRadius 366 cornerRadii[3] = state.topCornerRadius 367 cornerRadii[4] = state.bottomCornerRadius 368 cornerRadii[5] = state.bottomCornerRadius 369 cornerRadii[6] = state.bottomCornerRadius 370 cornerRadii[7] = state.bottomCornerRadius 371 drawable.cornerRadii = cornerRadii 372 373 // We first fade in the background layer to hide the expanding view, then fade it out 374 // with SRC mode to draw a hole punch in the status bar and reveal the opening window. 375 val fadeInProgress = getProgress( 376 timings, 377 linearProgress, 378 timings.contentBeforeFadeOutDelay, 379 timings.contentBeforeFadeOutDuration 380 ) 381 if (fadeInProgress < 1) { 382 val alpha = 383 interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) 384 drawable.alpha = (alpha * 0xFF).roundToInt() 385 } else { 386 val fadeOutProgress = getProgress( 387 timings, 388 linearProgress, 389 timings.contentAfterFadeInDelay, 390 timings.contentAfterFadeInDuration 391 ) 392 val alpha = 393 1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress) 394 drawable.alpha = (alpha * 0xFF).roundToInt() 395 396 if (drawHole) { 397 drawable.setXfermode(SRC_MODE) 398 } 399 } 400 } 401 } 402