1 /* 2 * Copyright (C) 2023 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.wm.shell.windowdecor 18 19 import android.animation.Animator 20 import android.animation.AnimatorSet 21 import android.animation.ObjectAnimator 22 import android.view.View 23 import android.view.View.ALPHA 24 import android.view.View.SCALE_X 25 import android.view.View.SCALE_Y 26 import android.view.View.TRANSLATION_Y 27 import android.view.View.TRANSLATION_Z 28 import android.view.ViewGroup 29 import android.view.accessibility.AccessibilityEvent 30 import android.widget.Button 31 import androidx.core.animation.doOnEnd 32 import androidx.core.view.children 33 import com.android.wm.shell.R 34 import com.android.wm.shell.shared.animation.Interpolators 35 36 /** Animates the Handle Menu opening. */ 37 class HandleMenuAnimator( 38 private val handleMenu: View, 39 private val menuWidth: Int, 40 private val captionHeight: Float 41 ) { 42 companion object { 43 // Open animation constants 44 private const val MENU_Y_TRANSLATION_OPEN_DURATION: Long = 150 45 private const val HEADER_NONFREEFORM_SCALE_OPEN_DURATION: Long = 150 46 private const val HEADER_FREEFORM_SCALE_OPEN_DURATION: Long = 217 47 private const val HEADER_ELEVATION_OPEN_DURATION: Long = 83 48 private const val HEADER_CONTENT_ALPHA_OPEN_DURATION: Long = 100 49 private const val BODY_SCALE_OPEN_DURATION: Long = 180 50 private const val BODY_ALPHA_OPEN_DURATION: Long = 150 51 private const val BODY_ELEVATION_OPEN_DURATION: Long = 83 52 private const val BODY_CONTENT_ALPHA_OPEN_DURATION: Long = 167 53 54 private const val ELEVATION_OPEN_DELAY: Long = 33 55 private const val HEADER_CONTENT_ALPHA_OPEN_DELAY: Long = 67 56 private const val BODY_SCALE_OPEN_DELAY: Long = 50 57 private const val BODY_ALPHA_OPEN_DELAY: Long = 133 58 59 private const val HALF_INITIAL_SCALE: Float = 0.5f 60 private const val NONFREEFORM_HEADER_INITIAL_SCALE_X: Float = 0.6f 61 private const val NONFREEFORM_HEADER_INITIAL_SCALE_Y: Float = 0.05f 62 63 // Close animation constants 64 private const val HEADER_CLOSE_DELAY: Long = 20 65 private const val HEADER_CLOSE_DURATION: Long = 50 66 private const val HEADER_CONTENT_OPACITY_CLOSE_DELAY: Long = 25 67 private const val HEADER_CONTENT_OPACITY_CLOSE_DURATION: Long = 25 68 private const val BODY_CLOSE_DURATION: Long = 50 69 } 70 71 private val animators: MutableList<Animator> = mutableListOf() 72 private var runningAnimation: AnimatorSet? = null 73 74 private val appInfoPill: ViewGroup = handleMenu.requireViewById(R.id.app_info_pill) 75 private val windowingPill: ViewGroup = handleMenu.requireViewById(R.id.windowing_pill) 76 private val moreActionsPill: ViewGroup = handleMenu.requireViewById(R.id.more_actions_pill) 77 private val openInAppOrBrowserPill: ViewGroup = 78 handleMenu.requireViewById(R.id.open_in_app_or_browser_pill) 79 80 /** Animates the opening of the handle menu. */ animateOpennull81 fun animateOpen() { 82 prepareMenuForAnimation() 83 appInfoPillExpand() 84 animateAppInfoPillOpen() 85 animateWindowingPillOpen() 86 animateMoreActionsPillOpen() 87 animateOpenInAppOrBrowserPill() 88 runAnimations { 89 appInfoPill.post { 90 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( 91 AccessibilityEvent.TYPE_VIEW_FOCUSED) 92 } 93 } 94 } 95 96 /** 97 * Animates the opening of the handle menu. The caption handle in full screen and split screen 98 * will expand until it assumes the shape of the app info pill. Then, the other two pills will 99 * appear. 100 */ animateCaptionHandleExpandToOpennull101 fun animateCaptionHandleExpandToOpen() { 102 prepareMenuForAnimation() 103 captionHandleExpandIntoAppInfoPill() 104 animateAppInfoPillOpen() 105 animateWindowingPillOpen() 106 animateMoreActionsPillOpen() 107 animateOpenInAppOrBrowserPill() 108 runAnimations { 109 appInfoPill.post { 110 appInfoPill.requireViewById<View>(R.id.collapse_menu_button).sendAccessibilityEvent( 111 AccessibilityEvent.TYPE_VIEW_FOCUSED) 112 } 113 } 114 } 115 116 /** 117 * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then, 118 * the app info pill will collapse into the shape of the caption handle in full screen and split 119 * screen. 120 * 121 * @param after runs after the animation finishes. 122 */ animateCollapseIntoHandleClosenull123 fun animateCollapseIntoHandleClose(after: () -> Unit) { 124 appInfoCollapseToHandle() 125 animateAppInfoPillFadeOut() 126 windowingPillClose() 127 moreActionsPillClose() 128 openInAppOrBrowserPillClose() 129 runAnimations(after) 130 } 131 132 /** 133 * Animates the closing of the handle menu. The windowing and more actions pill vanish. Then, 134 * the app info pill will collapse into the shape of the caption handle in full screen and split 135 * screen. 136 * 137 * @param after runs after animation finishes. 138 * 139 */ animateClosenull140 fun animateClose(after: () -> Unit) { 141 appInfoPillCollapse() 142 animateAppInfoPillFadeOut() 143 windowingPillClose() 144 moreActionsPillClose() 145 openInAppOrBrowserPillClose() 146 runAnimations(after) 147 } 148 149 /** 150 * Prepares the handle menu for animation. Presets the opacity of necessary menu components. 151 * Presets pivots of handle menu and body pills for scaling animation. 152 */ prepareMenuForAnimationnull153 private fun prepareMenuForAnimation() { 154 // Preset opacity 155 appInfoPill.children.forEach { it.alpha = 0f } 156 windowingPill.alpha = 0f 157 moreActionsPill.alpha = 0f 158 openInAppOrBrowserPill.alpha = 0f 159 160 // Setup pivots. 161 handleMenu.pivotX = menuWidth / 2f 162 handleMenu.pivotY = 0f 163 164 windowingPill.pivotX = menuWidth / 2f 165 windowingPill.pivotY = appInfoPill.measuredHeight.toFloat() 166 167 moreActionsPill.pivotX = menuWidth / 2f 168 moreActionsPill.pivotY = appInfoPill.measuredHeight.toFloat() 169 170 openInAppOrBrowserPill.pivotX = menuWidth / 2f 171 openInAppOrBrowserPill.pivotY = appInfoPill.measuredHeight.toFloat() 172 } 173 animateAppInfoPillOpennull174 private fun animateAppInfoPillOpen() { 175 // Header Elevation Animation 176 animators += 177 ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Z, 1f).apply { 178 startDelay = ELEVATION_OPEN_DELAY 179 duration = HEADER_ELEVATION_OPEN_DURATION 180 } 181 182 // Content Opacity Animation 183 appInfoPill.children.forEach { 184 animators += 185 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { 186 startDelay = HEADER_CONTENT_ALPHA_OPEN_DELAY 187 duration = HEADER_CONTENT_ALPHA_OPEN_DURATION 188 } 189 } 190 } 191 captionHandleExpandIntoAppInfoPillnull192 private fun captionHandleExpandIntoAppInfoPill() { 193 // Header scaling animation 194 animators += 195 ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X, 1f) 196 .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION } 197 198 animators += 199 ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y, 1f) 200 .apply { duration = HEADER_NONFREEFORM_SCALE_OPEN_DURATION } 201 202 // Downward y-translation animation 203 val yStart: Float = -captionHeight / 2 204 animators += 205 ObjectAnimator.ofFloat(handleMenu, TRANSLATION_Y, yStart, 0f).apply { 206 duration = MENU_Y_TRANSLATION_OPEN_DURATION 207 } 208 } 209 appInfoPillExpandnull210 private fun appInfoPillExpand() { 211 // Header scaling animation 212 animators += 213 ObjectAnimator.ofFloat(appInfoPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { 214 duration = HEADER_FREEFORM_SCALE_OPEN_DURATION 215 } 216 217 animators += 218 ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { 219 duration = HEADER_FREEFORM_SCALE_OPEN_DURATION 220 } 221 } 222 animateWindowingPillOpennull223 private fun animateWindowingPillOpen() { 224 // Windowing X & Y Scaling Animation 225 animators += 226 ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { 227 startDelay = BODY_SCALE_OPEN_DELAY 228 duration = BODY_SCALE_OPEN_DURATION 229 } 230 231 animators += 232 ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { 233 startDelay = BODY_SCALE_OPEN_DELAY 234 duration = BODY_SCALE_OPEN_DURATION 235 } 236 237 // Windowing Opacity Animation 238 animators += 239 ObjectAnimator.ofFloat(windowingPill, ALPHA, 1f).apply { 240 startDelay = BODY_ALPHA_OPEN_DELAY 241 duration = BODY_ALPHA_OPEN_DURATION 242 } 243 244 // Windowing Elevation Animation 245 animators += 246 ObjectAnimator.ofFloat(windowingPill, TRANSLATION_Z, 1f).apply { 247 startDelay = ELEVATION_OPEN_DELAY 248 duration = BODY_ELEVATION_OPEN_DURATION 249 } 250 251 // Windowing Content Opacity Animation 252 windowingPill.children.forEach { 253 animators += 254 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { 255 startDelay = BODY_ALPHA_OPEN_DELAY 256 duration = BODY_CONTENT_ALPHA_OPEN_DURATION 257 interpolator = Interpolators.FAST_OUT_SLOW_IN 258 } 259 } 260 } 261 animateMoreActionsPillOpennull262 private fun animateMoreActionsPillOpen() { 263 // More Actions X & Y Scaling Animation 264 animators += 265 ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { 266 startDelay = BODY_SCALE_OPEN_DELAY 267 duration = BODY_SCALE_OPEN_DURATION 268 } 269 270 animators += 271 ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { 272 startDelay = BODY_SCALE_OPEN_DELAY 273 duration = BODY_SCALE_OPEN_DURATION 274 } 275 276 // More Actions Opacity Animation 277 animators += 278 ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 1f).apply { 279 startDelay = BODY_ALPHA_OPEN_DELAY 280 duration = BODY_ALPHA_OPEN_DURATION 281 } 282 283 // More Actions Elevation Animation 284 animators += 285 ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Z, 1f).apply { 286 startDelay = ELEVATION_OPEN_DELAY 287 duration = BODY_ELEVATION_OPEN_DURATION 288 } 289 290 // More Actions Content Opacity Animation 291 moreActionsPill.children.forEach { 292 animators += 293 ObjectAnimator.ofFloat(it, ALPHA, 1f).apply { 294 startDelay = BODY_ALPHA_OPEN_DELAY 295 duration = BODY_CONTENT_ALPHA_OPEN_DURATION 296 interpolator = Interpolators.FAST_OUT_SLOW_IN 297 } 298 } 299 } 300 animateOpenInAppOrBrowserPillnull301 private fun animateOpenInAppOrBrowserPill() { 302 // Open in Browser X & Y Scaling Animation 303 animators += 304 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE, 1f).apply { 305 startDelay = BODY_SCALE_OPEN_DELAY 306 duration = BODY_SCALE_OPEN_DURATION 307 } 308 309 animators += 310 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE, 1f).apply { 311 startDelay = BODY_SCALE_OPEN_DELAY 312 duration = BODY_SCALE_OPEN_DURATION 313 } 314 315 // Open in Browser Opacity Animation 316 animators += 317 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 1f).apply { 318 startDelay = BODY_ALPHA_OPEN_DELAY 319 duration = BODY_ALPHA_OPEN_DURATION 320 } 321 322 // Open in Browser Elevation Animation 323 animators += 324 ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Z, 1f).apply { 325 startDelay = ELEVATION_OPEN_DELAY 326 duration = BODY_ELEVATION_OPEN_DURATION 327 } 328 329 // Open in Browser Button Opacity Animation 330 val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button) 331 animators += 332 ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { 333 startDelay = BODY_ALPHA_OPEN_DELAY 334 duration = BODY_CONTENT_ALPHA_OPEN_DURATION 335 interpolator = Interpolators.FAST_OUT_SLOW_IN 336 } 337 } 338 appInfoPillCollapsenull339 private fun appInfoPillCollapse() { 340 // Header scaling animation 341 animators += 342 ObjectAnimator.ofFloat(appInfoPill, SCALE_X, 0f).apply { 343 startDelay = HEADER_CLOSE_DELAY 344 duration = HEADER_CLOSE_DURATION 345 } 346 347 animators += 348 ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, 0f).apply { 349 startDelay = HEADER_CLOSE_DELAY 350 duration = HEADER_CLOSE_DURATION 351 } 352 } 353 appInfoCollapseToHandlenull354 private fun appInfoCollapseToHandle() { 355 // Header X & Y Scaling Animation 356 animators += 357 ObjectAnimator.ofFloat(appInfoPill, SCALE_X, NONFREEFORM_HEADER_INITIAL_SCALE_X).apply { 358 startDelay = HEADER_CLOSE_DELAY 359 duration = HEADER_CLOSE_DURATION 360 } 361 362 animators += 363 ObjectAnimator.ofFloat(appInfoPill, SCALE_Y, NONFREEFORM_HEADER_INITIAL_SCALE_Y).apply { 364 startDelay = HEADER_CLOSE_DELAY 365 duration = HEADER_CLOSE_DURATION 366 } 367 // Upward y-translation animation 368 val yStart: Float = -captionHeight / 2 369 animators += 370 ObjectAnimator.ofFloat(appInfoPill, TRANSLATION_Y, yStart).apply { 371 startDelay = HEADER_CLOSE_DELAY 372 duration = HEADER_CLOSE_DURATION 373 } 374 } 375 animateAppInfoPillFadeOutnull376 private fun animateAppInfoPillFadeOut() { 377 // Header Content Opacity Animation 378 appInfoPill.children.forEach { 379 animators += 380 ObjectAnimator.ofFloat(it, ALPHA, 0f).apply { 381 startDelay = HEADER_CONTENT_OPACITY_CLOSE_DELAY 382 duration = HEADER_CONTENT_OPACITY_CLOSE_DURATION 383 } 384 } 385 } 386 windowingPillClosenull387 private fun windowingPillClose() { 388 // Windowing X & Y Scaling Animation 389 animators += 390 ObjectAnimator.ofFloat(windowingPill, SCALE_X, HALF_INITIAL_SCALE).apply { 391 duration = BODY_CLOSE_DURATION 392 } 393 394 animators += 395 ObjectAnimator.ofFloat(windowingPill, SCALE_Y, HALF_INITIAL_SCALE).apply { 396 duration = BODY_CLOSE_DURATION 397 } 398 399 // windowing Animation 400 animators += 401 ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply { 402 duration = BODY_CLOSE_DURATION 403 } 404 405 animators += 406 ObjectAnimator.ofFloat(windowingPill, ALPHA, 0f).apply { 407 duration = BODY_CLOSE_DURATION 408 } 409 } 410 moreActionsPillClosenull411 private fun moreActionsPillClose() { 412 // More Actions X & Y Scaling Animation 413 animators += 414 ObjectAnimator.ofFloat(moreActionsPill, SCALE_X, HALF_INITIAL_SCALE).apply { 415 duration = BODY_CLOSE_DURATION 416 } 417 418 animators += 419 ObjectAnimator.ofFloat(moreActionsPill, SCALE_Y, HALF_INITIAL_SCALE).apply { 420 duration = BODY_CLOSE_DURATION 421 } 422 423 // More Actions Opacity Animation 424 animators += 425 ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply { 426 duration = BODY_CLOSE_DURATION 427 } 428 429 animators += 430 ObjectAnimator.ofFloat(moreActionsPill, ALPHA, 0f).apply { 431 duration = BODY_CLOSE_DURATION 432 } 433 434 // upward more actions pill y-translation animation 435 val yStart: Float = -captionHeight / 2 436 animators += 437 ObjectAnimator.ofFloat(moreActionsPill, TRANSLATION_Y, yStart).apply { 438 duration = BODY_CLOSE_DURATION 439 } 440 } 441 openInAppOrBrowserPillClosenull442 private fun openInAppOrBrowserPillClose() { 443 // Open in Browser X & Y Scaling Animation 444 animators += 445 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_X, HALF_INITIAL_SCALE).apply { 446 duration = BODY_CLOSE_DURATION 447 } 448 449 animators += 450 ObjectAnimator.ofFloat(openInAppOrBrowserPill, SCALE_Y, HALF_INITIAL_SCALE).apply { 451 duration = BODY_CLOSE_DURATION 452 } 453 454 // Open in Browser Opacity Animation 455 animators += 456 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { 457 duration = BODY_CLOSE_DURATION 458 } 459 460 animators += 461 ObjectAnimator.ofFloat(openInAppOrBrowserPill, ALPHA, 0f).apply { 462 duration = BODY_CLOSE_DURATION 463 } 464 465 // Upward Open in Browser y-translation Animation 466 val yStart: Float = -captionHeight / 2 467 animators += 468 ObjectAnimator.ofFloat(openInAppOrBrowserPill, TRANSLATION_Y, yStart).apply { 469 duration = BODY_CLOSE_DURATION 470 } 471 } 472 473 /** 474 * Runs the list of hide animators concurrently. 475 * 476 * @param after runs after animation finishes. 477 */ runAnimationsnull478 private fun runAnimations(after: (() -> Unit)? = null) { 479 runningAnimation?.apply { 480 // Remove all listeners, so that the after function isn't triggered upon cancel. 481 removeAllListeners() 482 // If an animation runs while running animation is triggered, gracefully cancel. 483 cancel() 484 } 485 486 runningAnimation = AnimatorSet().apply { 487 playTogether(animators) 488 animators.clear() 489 doOnEnd { 490 after?.invoke() 491 runningAnimation = null 492 } 493 start() 494 } 495 } 496 } 497