1 /* <lambda>null2 * 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 18 package com.android.quickstep.util 19 20 import android.animation.Animator 21 import android.animation.AnimatorListenerAdapter 22 import android.animation.AnimatorSet 23 import android.animation.ObjectAnimator 24 import android.animation.ValueAnimator 25 import android.app.ActivityManager.RunningTaskInfo 26 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 27 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW 28 import android.content.Context 29 import android.graphics.Bitmap 30 import android.graphics.Rect 31 import android.graphics.RectF 32 import android.graphics.drawable.Drawable 33 import android.view.RemoteAnimationTarget 34 import android.view.SurfaceControl 35 import android.view.SurfaceControl.Transaction 36 import android.view.View 37 import android.view.WindowManager.TRANSIT_OPEN 38 import android.view.WindowManager.TRANSIT_TO_FRONT 39 import android.window.TransitionInfo 40 import android.window.TransitionInfo.Change 41 import android.window.WindowContainerToken 42 import androidx.annotation.VisibleForTesting 43 import com.android.app.animation.Interpolators 44 import com.android.launcher3.DeviceProfile 45 import com.android.launcher3.Flags.enableOverviewIconMenu 46 import com.android.launcher3.InsettableFrameLayout 47 import com.android.launcher3.QuickstepTransitionManager 48 import com.android.launcher3.R 49 import com.android.launcher3.Utilities 50 import com.android.launcher3.anim.PendingAnimation 51 import com.android.launcher3.apppairs.AppPairIcon 52 import com.android.launcher3.config.FeatureFlags 53 import com.android.launcher3.logging.StatsLogManager.EventEnum 54 import com.android.launcher3.model.data.WorkspaceItemInfo 55 import com.android.launcher3.statehandlers.DepthController 56 import com.android.launcher3.statemanager.StateManager 57 import com.android.launcher3.taskbar.TaskbarActivityContext 58 import com.android.launcher3.uioverrides.QuickstepLauncher 59 import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE 60 import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource 61 import com.android.launcher3.views.BaseDragLayer 62 import com.android.quickstep.TaskViewUtils 63 import com.android.quickstep.views.FloatingAppPairView 64 import com.android.quickstep.views.FloatingTaskView 65 import com.android.quickstep.views.GroupedTaskView 66 import com.android.quickstep.views.IconAppChipView 67 import com.android.quickstep.views.RecentsView 68 import com.android.quickstep.views.RecentsViewContainer 69 import com.android.quickstep.views.SplitInstructionsView 70 import com.android.quickstep.views.TaskThumbnailViewDeprecated 71 import com.android.quickstep.views.TaskView 72 import com.android.quickstep.views.TaskView.TaskContainer 73 import com.android.quickstep.views.TaskViewIcon 74 import com.android.wm.shell.shared.TransitionUtil 75 import java.util.Optional 76 import java.util.function.Supplier 77 78 /** 79 * Utils class to help run animations for initiating split screen from launcher. Will be expanded 80 * with future refactors. Works in conjunction with the state stored in [SplitSelectStateController] 81 */ 82 class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) { 83 companion object { 84 // Break this out into maybe enums? Abstractions into its own classes? Tbd. 85 data class SplitAnimInitProps( 86 val originalView: View, 87 val originalBitmap: Bitmap?, 88 val iconDrawable: Drawable, 89 val fadeWithThumbnail: Boolean, 90 val isStagedTask: Boolean, 91 val iconView: View? 92 ) 93 } 94 95 /** 96 * Returns different elements to animate for the initial split selection animation depending on 97 * the state of the surface from which the split was initiated 98 */ 99 fun getFirstAnimInitViews( 100 taskViewSupplier: Supplier<TaskView>, 101 splitSelectSourceSupplier: Supplier<SplitSelectSource?> 102 ): SplitAnimInitProps { 103 val splitSelectSource = splitSelectSourceSupplier.get() 104 if (!splitSelectStateController.isAnimateCurrentTaskDismissal) { 105 // Initiating from home 106 return SplitAnimInitProps( 107 splitSelectSource!!.view, 108 originalBitmap = null, 109 splitSelectSource.drawable, 110 fadeWithThumbnail = false, 111 isStagedTask = true, 112 iconView = null 113 ) 114 } else if (splitSelectStateController.isDismissingFromSplitPair) { 115 // Initiating split from overview, but on a split pair 116 val taskView = taskViewSupplier.get() 117 for (container: TaskContainer in taskView.taskContainers) { 118 if (container.task.getKey().getId() == splitSelectStateController.initialTaskId) { 119 val drawable = getDrawable(container.iconView, splitSelectSource) 120 return SplitAnimInitProps( 121 container.thumbnailViewDeprecated, 122 container.thumbnailViewDeprecated.thumbnail, 123 drawable!!, 124 fadeWithThumbnail = true, 125 isStagedTask = true, 126 iconView = container.iconView.asView() 127 ) 128 } 129 } 130 throw IllegalStateException( 131 "Attempting to init split from existing split pair " + 132 "without a valid taskIdAttributeContainer" 133 ) 134 } else { 135 // Initiating split from overview on fullscreen task TaskView 136 val taskView = taskViewSupplier.get() 137 taskView.taskContainers.first().let { 138 val drawable = getDrawable(it.iconView, splitSelectSource) 139 return SplitAnimInitProps( 140 it.thumbnailViewDeprecated, 141 it.thumbnailViewDeprecated.thumbnail, 142 drawable!!, 143 fadeWithThumbnail = true, 144 isStagedTask = true, 145 iconView = it.iconView.asView() 146 ) 147 } 148 } 149 } 150 151 /** 152 * Returns the drawable that's provided in iconView, however if that is null it falls back to 153 * the drawable that's in splitSelectSource. TaskView's icon drawable can be null if the 154 * TaskView is scrolled far enough off screen 155 * 156 * @return [Drawable] 157 */ 158 fun getDrawable(iconView: TaskViewIcon, splitSelectSource: SplitSelectSource?): Drawable? { 159 if (iconView.drawable == null && splitSelectSource != null) { 160 return splitSelectSource.drawable 161 } 162 return iconView.drawable 163 } 164 165 /** 166 * When selecting first app from split pair, second app's thumbnail remains. This animates the 167 * second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying it 168 * with [TaskThumbnailViewDeprecated]'s splashView. Adds animations to the provided builder. 169 * Note: The app that **was not** selected as the first split app should be the container that's 170 * passed through. 171 * 172 * @param builder Adds animation to this 173 * @param taskIdAttributeContainer container of the app that **was not** selected 174 * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair 175 * (opposite of that representing [taskIdAttributeContainer]) 176 */ 177 fun addInitialSplitFromPair( 178 taskIdAttributeContainer: TaskContainer, 179 builder: PendingAnimation, 180 deviceProfile: DeviceProfile, 181 taskViewWidth: Int, 182 taskViewHeight: Int, 183 isPrimaryTaskSplitting: Boolean 184 ) { 185 val thumbnail = taskIdAttributeContainer.thumbnailViewDeprecated 186 val iconView: View = taskIdAttributeContainer.iconView.asView() 187 builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailViewDeprecated.SPLASH_ALPHA, 1f)) 188 thumbnail.setShowSplashForSplitSelection(true) 189 // With the new `IconAppChipView`, we always want to keep the chip pinned to the 190 // top left of the task / thumbnail. 191 if (enableOverviewIconMenu()) { 192 builder.add( 193 ObjectAnimator.ofFloat( 194 (iconView as IconAppChipView).splitTranslationX, 195 MULTI_PROPERTY_VALUE, 196 0f 197 ) 198 ) 199 builder.add( 200 ObjectAnimator.ofFloat(iconView.splitTranslationY, MULTI_PROPERTY_VALUE, 0f) 201 ) 202 } 203 if (deviceProfile.isLeftRightSplit) { 204 // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0 205 val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f 206 val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width 207 builder.add( 208 ObjectAnimator.ofFloat( 209 thumbnail, 210 TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, 211 centerThumbnailTranslationX 212 ) 213 ) 214 if (!enableOverviewIconMenu()) { 215 // icons are anchored from Gravity.END, so need to use negative translation 216 val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f 217 builder.add( 218 ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, -centerIconTranslationX) 219 ) 220 } 221 builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX)) 222 223 // Reset other dimensions 224 // TODO(b/271468547), can't set Y translate to 0, need to account for top space 225 thumbnail.scaleY = 1f 226 val translateYResetVal: Float = 227 if (!isPrimaryTaskSplitting) 0f 228 else deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() 229 builder.add( 230 ObjectAnimator.ofFloat( 231 thumbnail, 232 TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, 233 translateYResetVal 234 ) 235 ) 236 } else { 237 val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx 238 // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0 239 // primary thumbnail has layout margin above it, so secondary thumbnail needs to take 240 // that into account. We should migrate to only using translations otherwise this 241 // asymmetry causes problems.. 242 243 // Icon defaults to center | horizontal, we add additional translation for split 244 var centerThumbnailTranslationY: Float 245 246 // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary 247 // thumbnail needs to take that into account. We should migrate to only using 248 // translations otherwise this asymmetry causes problems.. 249 if (isPrimaryTaskSplitting) { 250 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f 251 centerThumbnailTranslationY += 252 deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat() 253 } else { 254 centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f 255 } 256 val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height 257 builder.add( 258 ObjectAnimator.ofFloat( 259 thumbnail, 260 TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_Y, 261 centerThumbnailTranslationY 262 ) 263 ) 264 265 if (!enableOverviewIconMenu()) { 266 // icons are anchored from Gravity.END, so need to use negative translation 267 builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X, 0f)) 268 } 269 builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY)) 270 271 // Reset other dimensions 272 thumbnail.scaleX = 1f 273 builder.add( 274 ObjectAnimator.ofFloat( 275 thumbnail, 276 TaskThumbnailViewDeprecated.SPLIT_SELECT_TRANSLATE_X, 277 0f 278 ) 279 ) 280 } 281 } 282 283 /** 284 * Creates and returns a fullscreen scrim to fade in behind the split confirm animation, and 285 * adds it to the provided [pendingAnimation]. 286 */ 287 fun addScrimBehindAnim( 288 pendingAnimation: PendingAnimation, 289 container: RecentsViewContainer, 290 context: Context 291 ): View { 292 val scrim = View(context) 293 val recentsView = container.getOverviewPanel<RecentsView<*, *>>() 294 val dp: DeviceProfile = container.getDeviceProfile() 295 // Add it before/under the most recently added first floating taskView 296 val firstAddedSplitViewIndex: Int = 297 container 298 .getDragLayer() 299 .indexOfChild(recentsView.splitSelectController.firstFloatingTaskView) 300 container.getDragLayer().addView(scrim, firstAddedSplitViewIndex) 301 // Make the scrim fullscreen 302 val lp = scrim.layoutParams as InsettableFrameLayout.LayoutParams 303 lp.topMargin = 0 304 lp.height = dp.heightPx 305 lp.width = dp.widthPx 306 307 scrim.alpha = 0f 308 scrim.setBackgroundColor( 309 container.asContext().resources.getColor(R.color.taskbar_background_dark) 310 ) 311 val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet) as SplitToConfirmTimings 312 pendingAnimation.setViewAlpha( 313 scrim, 314 1f, 315 Interpolators.clampToProgress( 316 timings.backingScrimFadeInterpolator, 317 timings.backingScrimFadeInStartOffset, 318 timings.backingScrimFadeInEndOffset 319 ) 320 ) 321 322 return scrim 323 } 324 325 /** Does not play any animation if user is not currently in split selection state. */ 326 fun playPlaceholderDismissAnim(container: RecentsViewContainer, splitDismissEvent: EventEnum) { 327 if (!splitSelectStateController.isSplitSelectActive) { 328 return 329 } 330 331 val anim = createPlaceholderDismissAnim(container, splitDismissEvent, null /*duration*/) 332 anim.start() 333 } 334 335 /** 336 * Returns [AnimatorSet] which slides initial split placeholder view offscreen and logs an event 337 * for why split is being dismissed 338 */ 339 fun createPlaceholderDismissAnim( 340 container: RecentsViewContainer, 341 splitDismissEvent: EventEnum, 342 duration: Long? 343 ): AnimatorSet { 344 val animatorSet = AnimatorSet() 345 duration?.let { animatorSet.duration = it } 346 val recentsView: RecentsView<*, *> = container.getOverviewPanel() 347 val floatingTask: FloatingTaskView = 348 splitSelectStateController.firstFloatingTaskView ?: return animatorSet 349 350 // We are in split selection state currently, transitioning to another state 351 val dragLayer: BaseDragLayer<*> = container.dragLayer 352 val onScreenRectF = RectF() 353 Utilities.getBoundsForViewInDragLayer( 354 dragLayer, 355 floatingTask, 356 Rect(0, 0, floatingTask.width, floatingTask.height), 357 false, 358 null, 359 onScreenRectF 360 ) 361 // Get the part of the floatingTask that intersects with the DragLayer (i.e. the 362 // on-screen portion) 363 onScreenRectF.intersect( 364 dragLayer.left.toFloat(), 365 dragLayer.top.toFloat(), 366 dragLayer.right.toFloat(), 367 dragLayer.bottom.toFloat() 368 ) 369 animatorSet.play( 370 ObjectAnimator.ofFloat( 371 floatingTask, 372 FloatingTaskView.PRIMARY_TRANSLATE_OFFSCREEN, 373 recentsView.pagedOrientationHandler.getFloatingTaskOffscreenTranslationTarget( 374 floatingTask, 375 onScreenRectF, 376 floatingTask.stagePosition, 377 container.deviceProfile 378 ) 379 ) 380 ) 381 animatorSet.addListener( 382 object : AnimatorListenerAdapter() { 383 override fun onAnimationEnd(animation: Animator) { 384 splitSelectStateController.resetState() 385 safeRemoveViewFromDragLayer( 386 container, 387 splitSelectStateController.splitInstructionsView 388 ) 389 } 390 } 391 ) 392 splitSelectStateController.logExitReason(splitDismissEvent) 393 return animatorSet 394 } 395 396 /** 397 * Returns a [PendingAnimation] to animate in the chip to instruct a user to select a second app 398 * for splitscreen 399 */ 400 fun getShowSplitInstructionsAnim(container: RecentsViewContainer): PendingAnimation { 401 safeRemoveViewFromDragLayer(container, splitSelectStateController.splitInstructionsView) 402 val splitInstructionsView = SplitInstructionsView.getSplitInstructionsView(container) 403 splitSelectStateController.splitInstructionsView = splitInstructionsView 404 val timings = AnimUtils.getDeviceOverviewToSplitTimings(container.deviceProfile.isTablet) 405 val anim = PendingAnimation(100 /*duration */) 406 splitInstructionsView.alpha = 0f 407 anim.setViewAlpha( 408 splitInstructionsView, 409 1f, 410 Interpolators.clampToProgress( 411 Interpolators.LINEAR, 412 timings.instructionsContainerFadeInStartOffset, 413 timings.instructionsContainerFadeInEndOffset 414 ) 415 ) 416 anim.addFloat( 417 splitInstructionsView, 418 SplitInstructionsView.UNFOLD, 419 0.1f, 420 1f, 421 Interpolators.clampToProgress( 422 Interpolators.EMPHASIZED_DECELERATE, 423 timings.instructionsUnfoldStartOffset, 424 timings.instructionsUnfoldEndOffset 425 ) 426 ) 427 return anim 428 } 429 430 /** Removes the split instructions view from [launcher] drag layer. */ 431 fun removeSplitInstructionsView(container: RecentsViewContainer) { 432 safeRemoveViewFromDragLayer(container, splitSelectStateController.splitInstructionsView) 433 } 434 435 /** 436 * Animates the first placeholder view to fullscreen and launches its task. 437 * 438 * TODO(b/276361926): Remove the [resetCallback] option once contextual launches 439 */ 440 fun playAnimPlaceholderToFullscreen( 441 container: RecentsViewContainer, 442 view: View, 443 resetCallback: Optional<Runnable> 444 ) { 445 val stagedTaskView = view as FloatingTaskView 446 447 val isTablet: Boolean = container.deviceProfile.isTablet 448 val duration = 449 if (isTablet) SplitAnimationTimings.TABLET_CONFIRM_DURATION 450 else SplitAnimationTimings.PHONE_CONFIRM_DURATION 451 452 val pendingAnimation = PendingAnimation(duration.toLong()) 453 val firstTaskStartingBounds = Rect() 454 val firstTaskEndingBounds = Rect() 455 456 stagedTaskView.getBoundsOnScreen(firstTaskStartingBounds) 457 container.dragLayer.getBoundsOnScreen(firstTaskEndingBounds) 458 splitSelectStateController.setLaunchingFirstAppFullscreen() 459 460 stagedTaskView.addConfirmAnimation( 461 pendingAnimation, 462 RectF(firstTaskStartingBounds), 463 firstTaskEndingBounds, 464 false /* fadeWithThumbnail */, 465 true /* isStagedTask */ 466 ) 467 468 pendingAnimation.addEndListener { 469 splitSelectStateController.launchInitialAppFullscreen { 470 if (FeatureFlags.enableSplitContextually()) { 471 splitSelectStateController.resetState() 472 } else if (resetCallback.isPresent) { 473 resetCallback.get().run() 474 } 475 } 476 } 477 478 pendingAnimation.buildAnim().start() 479 } 480 481 /** 482 * Called when launching a specific pair of apps, e.g. when tapping a pair of apps in Overview, 483 * or launching an app pair from its Home icon. Selects the appropriate launch animation and 484 * plays it. 485 */ 486 fun playSplitLaunchAnimation( 487 launchingTaskView: GroupedTaskView?, 488 launchingIconView: AppPairIcon?, 489 initialTaskId: Int, 490 secondTaskId: Int, 491 apps: Array<RemoteAnimationTarget>?, 492 wallpapers: Array<RemoteAnimationTarget>?, 493 nonApps: Array<RemoteAnimationTarget>?, 494 stateManager: StateManager<*, *>, 495 depthController: DepthController?, 496 info: TransitionInfo?, 497 t: Transaction?, 498 finishCallback: Runnable 499 ) { 500 if (info == null && t == null) { 501 // (Legacy animation) Tapping a split tile in Overview 502 // TODO (b/315490678): Ensure that this works with app pairs flow 503 check(apps != null && wallpapers != null && nonApps != null) { 504 "trying to call composeRecentsSplitLaunchAnimatorLegacy, but encountered an " + 505 "unexpected null" 506 } 507 508 composeRecentsSplitLaunchAnimatorLegacy( 509 launchingTaskView, 510 initialTaskId, 511 secondTaskId, 512 apps, 513 wallpapers, 514 nonApps, 515 stateManager, 516 depthController, 517 finishCallback 518 ) 519 520 return 521 } 522 523 if (launchingTaskView != null) { 524 // Tapping a split tile in Overview 525 check(info != null && t != null) { 526 "trying to launch a GroupedTaskView, but encountered an unexpected null" 527 } 528 529 composeRecentsSplitLaunchAnimator( 530 launchingTaskView, 531 stateManager, 532 depthController, 533 info, 534 t, 535 finishCallback 536 ) 537 } else if (launchingIconView != null) { 538 // Tapping an app pair icon 539 check(info != null && t != null) { 540 "trying to launch an app pair icon, but encountered an unexpected null" 541 } 542 val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info) 543 if (appPairLaunchingAppIndex == -1) { 544 // Launch split app pair animation 545 composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) 546 } else { 547 composeFullscreenIconSplitLaunchAnimator( 548 launchingIconView, 549 info, 550 t, 551 finishCallback, 552 appPairLaunchingAppIndex 553 ) 554 } 555 } else { 556 // Fallback case: simple fade-in animation 557 check(info != null && t != null) { 558 "trying to call composeFadeInSplitLaunchAnimator, but encountered an " + 559 "unexpected null" 560 } 561 562 composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback) 563 } 564 } 565 566 /** 567 * When the user taps a split tile in Overview, this will play the tasks' launch animation from 568 * the position of the tapped tile. 569 */ 570 @VisibleForTesting 571 fun composeRecentsSplitLaunchAnimator( 572 launchingTaskView: GroupedTaskView, 573 stateManager: StateManager<*, *>, 574 depthController: DepthController?, 575 info: TransitionInfo, 576 t: Transaction, 577 finishCallback: Runnable 578 ) { 579 TaskViewUtils.composeRecentsSplitLaunchAnimator( 580 launchingTaskView, 581 stateManager, 582 depthController, 583 info, 584 t, 585 finishCallback 586 ) 587 } 588 589 /** 590 * LEGACY VERSION: When the user taps a split tile in Overview, this will play the tasks' launch 591 * animation from the position of the tapped tile. 592 */ 593 @VisibleForTesting 594 fun composeRecentsSplitLaunchAnimatorLegacy( 595 launchingTaskView: GroupedTaskView?, 596 initialTaskId: Int, 597 secondTaskId: Int, 598 apps: Array<RemoteAnimationTarget>, 599 wallpapers: Array<RemoteAnimationTarget>, 600 nonApps: Array<RemoteAnimationTarget>, 601 stateManager: StateManager<*, *>, 602 depthController: DepthController?, 603 finishCallback: Runnable 604 ) { 605 TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy( 606 launchingTaskView, 607 initialTaskId, 608 secondTaskId, 609 apps, 610 wallpapers, 611 nonApps, 612 stateManager, 613 depthController, 614 finishCallback 615 ) 616 } 617 618 /** 619 * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise 620 * the integer index corresponding to [launchingIconView]'s contents for the single app to be 621 * animated 622 */ 623 fun hasChangesForBothAppPairs( 624 launchingIconView: AppPairIcon, 625 transitionInfo: TransitionInfo 626 ): Int { 627 val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName 628 val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName 629 var launchFullscreenAppIndex = -1 630 for (change in transitionInfo.changes) { 631 val taskInfo: RunningTaskInfo = change.taskInfo ?: continue 632 if ( 633 TransitionUtil.isOpeningType(change.mode) && 634 taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN 635 ) { 636 val baseIntent = taskInfo.baseIntent.component?.packageName 637 if (baseIntent == intent1) { 638 if (launchFullscreenAppIndex > -1) { 639 launchFullscreenAppIndex = -1 640 break 641 } 642 launchFullscreenAppIndex = 0 643 } else if (baseIntent == intent2) { 644 if (launchFullscreenAppIndex > -1) { 645 launchFullscreenAppIndex = -1 646 break 647 } 648 launchFullscreenAppIndex = 1 649 } 650 } 651 } 652 return launchFullscreenAppIndex 653 } 654 655 /** 656 * When the user taps an app pair icon to launch split, this will play the tasks' launch 657 * animation from the position of the icon. 658 * 659 * To find the root shell leash that we want to fade in, we do the following: The Changes we 660 * receive in transitionInfo are structured like this 661 * 662 * Root (grandparent) 663 * | 664 * |--> Split Root 1 (left/top side parent) (WINDOWING_MODE_MULTI_WINDOW) 665 * | | 666 * | --> App 1 (left/top side child) (WINDOWING_MODE_MULTI_WINDOW) 667 * |--> Divider 668 * |--> Split Root 2 (right/bottom side parent) (WINDOWING_MODE_MULTI_WINDOW) 669 * | 670 * --> App 2 (right/bottom side child) (WINDOWING_MODE_MULTI_WINDOW) 671 * 672 * We want to animate the Root (grandparent) so that it affects both apps and the divider. To do 673 * this, we find one of the nodes with WINDOWING_MODE_MULTI_WINDOW (one of the left-side ones, 674 * for simplicity) and traverse the tree until we find the grandparent. 675 * 676 * This function is only called when we are animating the app pair in from scratch. It is NOT 677 * called when we are animating in from an existing visible TaskView tile or an app that is 678 * already on screen. 679 */ 680 @VisibleForTesting 681 fun composeIconSplitLaunchAnimator( 682 launchingIconView: AppPairIcon, 683 transitionInfo: TransitionInfo, 684 t: Transaction, 685 finishCallback: Runnable 686 ) { 687 // If launching an app pair from Taskbar inside of an app context (no access to Launcher), 688 // use the scale-up animation 689 if (launchingIconView.context is TaskbarActivityContext) { 690 composeScaleUpLaunchAnimation( 691 transitionInfo, 692 t, 693 finishCallback, 694 WINDOWING_MODE_MULTI_WINDOW 695 ) 696 return 697 } 698 699 // Else we are in Launcher and can launch with the full icon stretch-and-split animation. 700 val launcher = QuickstepLauncher.getLauncher(launchingIconView.context) 701 val dp = launcher.deviceProfile 702 703 // Create an AnimatorSet that will run both shell and launcher transitions together 704 val launchAnimation = AnimatorSet() 705 var rootCandidate: Change? = null 706 707 for (change in transitionInfo.changes) { 708 val taskInfo: RunningTaskInfo = change.taskInfo ?: continue 709 710 // TODO (b/316490565): Replace this logic when SplitBounds is available to 711 // startAnimation() and we can know the precise taskIds of launching tasks. 712 // Find a change that has WINDOWING_MODE_MULTI_WINDOW. 713 if ( 714 taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && 715 (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) 716 ) { 717 // Check if it is a left/top app. 718 val isLeftTopApp = 719 (dp.isLeftRightSplit && change.endAbsBounds.left == 0) || 720 (!dp.isLeftRightSplit && change.endAbsBounds.top == 0) 721 if (isLeftTopApp) { 722 // Found one! 723 rootCandidate = change 724 break 725 } 726 } 727 } 728 729 // If we could not find a proper root candidate, something went wrong. 730 check(rootCandidate != null) { "Could not find a split root candidate" } 731 732 // Find the place where our left/top app window meets the divider (used for the 733 // launcher side animation) 734 val dividerPos = 735 if (dp.isLeftRightSplit) rootCandidate.endAbsBounds.right 736 else rootCandidate.endAbsBounds.bottom 737 738 // Recurse up the tree until parent is null, then we've found our root. 739 var parentToken: WindowContainerToken? = rootCandidate.parent 740 while (parentToken != null) { 741 rootCandidate = transitionInfo.getChange(parentToken) ?: break 742 parentToken = rootCandidate.parent 743 } 744 745 // Make sure nothing weird happened, like getChange() returning null. 746 check(rootCandidate != null) { "Failed to find a root leash" } 747 748 // Create a new floating view in Launcher, positioned above the launching icon 749 val drawableArea = launchingIconView.iconDrawableArea 750 val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context) 751 val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context) 752 appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) 753 appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) 754 755 val floatingView = 756 FloatingAppPairView.getFloatingAppPairView( 757 launcher, 758 drawableArea, 759 appIcon1, 760 appIcon2, 761 dividerPos 762 ) 763 floatingView.bringToFront() 764 765 launchAnimation.play( 766 getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate) 767 ) 768 launchAnimation.start() 769 } 770 771 /** 772 * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate a 773 * single fullscreen icon + background instead of for a pair 774 */ 775 @VisibleForTesting 776 fun composeFullscreenIconSplitLaunchAnimator( 777 launchingIconView: AppPairIcon, 778 transitionInfo: TransitionInfo, 779 t: Transaction, 780 finishCallback: Runnable, 781 launchFullscreenIndex: Int 782 ) { 783 // If launching an app pair from Taskbar inside of an app context (no access to Launcher), 784 // use the scale-up animation 785 if (launchingIconView.context is TaskbarActivityContext) { 786 composeScaleUpLaunchAnimation( 787 transitionInfo, 788 t, 789 finishCallback, 790 WINDOWING_MODE_FULLSCREEN 791 ) 792 return 793 } 794 795 // Else we are in Launcher and can launch with the full icon stretch-and-split animation. 796 val launcher = QuickstepLauncher.getLauncher(launchingIconView.context) 797 val dp = launcher.deviceProfile 798 799 // Create an AnimatorSet that will run both shell and launcher transitions together 800 val launchAnimation = AnimatorSet() 801 802 val appInfo = 803 launchingIconView.info.getContents()[launchFullscreenIndex] as WorkspaceItemInfo 804 val intentToLaunch = appInfo.intent.component?.packageName 805 var rootCandidate: Change? = null 806 for (change in transitionInfo.changes) { 807 val taskInfo: RunningTaskInfo = change.taskInfo ?: continue 808 val baseIntent = taskInfo.baseIntent.component?.packageName 809 if ( 810 TransitionUtil.isOpeningType(change.mode) && 811 taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN && 812 baseIntent == intentToLaunch 813 ) { 814 rootCandidate = change 815 } 816 } 817 818 // If we could not find a proper root candidate, something went wrong. 819 check(rootCandidate != null) { "Could not find a split root candidate" } 820 821 // Recurse up the tree until parent is null, then we've found our root. 822 var parentToken: WindowContainerToken? = rootCandidate.parent 823 while (parentToken != null) { 824 rootCandidate = transitionInfo.getChange(parentToken) ?: break 825 parentToken = rootCandidate.parent 826 } 827 828 // Make sure nothing weird happened, like getChange() returning null. 829 check(rootCandidate != null) { "Failed to find a root leash" } 830 831 // Create a new floating view in Launcher, positioned above the launching icon 832 val drawableArea = launchingIconView.iconDrawableArea 833 val appIcon = appInfo.newIcon(launchingIconView.context) 834 appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) 835 836 val floatingView = 837 FloatingAppPairView.getFloatingAppPairView( 838 launcher, 839 drawableArea, 840 appIcon, 841 null /*appIcon2*/, 842 0 /*dividerPos*/ 843 ) 844 floatingView.bringToFront() 845 launchAnimation.play( 846 getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, rootCandidate) 847 ) 848 launchAnimation.start() 849 } 850 851 private fun getIconLaunchValueAnimator( 852 t: Transaction, 853 dp: com.android.launcher3.DeviceProfile, 854 finishCallback: Runnable, 855 launcher: QuickstepLauncher, 856 floatingView: FloatingAppPairView, 857 rootCandidate: Change 858 ): ValueAnimator { 859 val progressUpdater = ValueAnimator.ofFloat(0f, 1f) 860 val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) 861 progressUpdater.setDuration(timings.getDuration().toLong()) 862 progressUpdater.interpolator = Interpolators.LINEAR 863 864 // Shell animation: the apps are revealed toward end of the launch animation 865 progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> 866 val progress = 867 Interpolators.clampToProgress( 868 Interpolators.LINEAR, 869 valueAnimator.animatedFraction, 870 timings.appRevealStartOffset, 871 timings.appRevealEndOffset 872 ) 873 874 // Set the alpha of the shell layer (2 apps + divider) 875 t.setAlpha(rootCandidate.leash, progress) 876 t.apply() 877 } 878 879 progressUpdater.addUpdateListener( 880 object : MultiValueUpdateListener() { 881 var mDx = 882 FloatProp( 883 floatingView.startingPosition.left, 884 dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, 885 Interpolators.clampToProgress( 886 timings.getStagedRectXInterpolator(), 887 timings.stagedRectSlideStartOffset, 888 timings.stagedRectSlideEndOffset 889 ) 890 ) 891 var mDy = 892 FloatProp( 893 floatingView.startingPosition.top, 894 dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, 895 Interpolators.clampToProgress( 896 Interpolators.EMPHASIZED, 897 timings.stagedRectSlideStartOffset, 898 timings.stagedRectSlideEndOffset 899 ) 900 ) 901 var mScaleX = 902 FloatProp( 903 1f /* start */, 904 dp.widthPx / floatingView.startingPosition.width(), 905 Interpolators.clampToProgress( 906 Interpolators.EMPHASIZED, 907 timings.stagedRectSlideStartOffset, 908 timings.stagedRectSlideEndOffset 909 ) 910 ) 911 var mScaleY = 912 FloatProp( 913 1f /* start */, 914 dp.heightPx / floatingView.startingPosition.height(), 915 Interpolators.clampToProgress( 916 Interpolators.EMPHASIZED, 917 timings.stagedRectSlideStartOffset, 918 timings.stagedRectSlideEndOffset 919 ) 920 ) 921 922 override fun onUpdate(percent: Float, initOnly: Boolean) { 923 floatingView.progress = percent 924 floatingView.x = mDx.value 925 floatingView.y = mDy.value 926 floatingView.scaleX = mScaleX.value 927 floatingView.scaleY = mScaleY.value 928 floatingView.invalidate() 929 } 930 } 931 ) 932 progressUpdater.addListener( 933 object : AnimatorListenerAdapter() { 934 override fun onAnimationEnd(animation: Animator) { 935 safeRemoveViewFromDragLayer(launcher, floatingView) 936 finishCallback.run() 937 } 938 } 939 ) 940 941 return progressUpdater 942 } 943 944 /** 945 * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when 946 * there is no visible associated tile to expand from. [windowingMode] helps determine whether 947 * we are looking for a split or a single fullscreen [Change] 948 */ 949 @VisibleForTesting 950 fun composeScaleUpLaunchAnimation( 951 transitionInfo: TransitionInfo, 952 t: Transaction, 953 finishCallback: Runnable, 954 windowingMode: Int 955 ) { 956 val launchAnimation = AnimatorSet() 957 val progressUpdater = ValueAnimator.ofFloat(0f, 1f) 958 progressUpdater.setDuration(QuickstepTransitionManager.APP_LAUNCH_DURATION) 959 progressUpdater.interpolator = Interpolators.EMPHASIZED 960 961 var rootCandidate: Change? = null 962 963 for (change in transitionInfo.changes) { 964 val taskInfo: RunningTaskInfo = change.taskInfo ?: continue 965 966 // TODO (b/316490565): Replace this logic when SplitBounds is available to 967 // startAnimation() and we can know the precise taskIds of launching tasks. 968 if ( 969 taskInfo.windowingMode == windowingMode && 970 (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) 971 ) { 972 // Found one! 973 rootCandidate = change 974 break 975 } 976 } 977 978 // If we could not find a proper root candidate, something went wrong. 979 check(rootCandidate != null) { "Could not find a split root candidate" } 980 981 // Recurse up the tree until parent is null, then we've found our root. 982 var parentToken: WindowContainerToken? = rootCandidate.parent 983 while (parentToken != null) { 984 rootCandidate = transitionInfo.getChange(parentToken) ?: break 985 parentToken = rootCandidate.parent 986 } 987 988 // Make sure nothing weird happened, like getChange() returning null. 989 check(rootCandidate != null) { "Failed to find a root leash" } 990 991 // Starting position is a 34% size tile centered in the middle of the screen. 992 // Ending position is the full device screen. 993 val screenBounds = rootCandidate.endAbsBounds 994 val startingScale = 0.34f 995 val startX = 996 screenBounds.left + 997 ((screenBounds.right - screenBounds.left) * ((1 - startingScale) / 2f)) 998 val startY = 999 screenBounds.top + 1000 ((screenBounds.bottom - screenBounds.top) * ((1 - startingScale) / 2f)) 1001 val endX = screenBounds.left 1002 val endY = screenBounds.top 1003 1004 progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> 1005 val progress = valueAnimator.animatedFraction 1006 1007 val x = startX + ((endX - startX) * progress) 1008 val y = startY + ((endY - startY) * progress) 1009 val scale = startingScale + ((1 - startingScale) * progress) 1010 1011 t.setPosition(rootCandidate.leash, x, y) 1012 t.setScale(rootCandidate.leash, scale, scale) 1013 t.setAlpha(rootCandidate.leash, progress) 1014 t.apply() 1015 } 1016 1017 // When animation ends, run finishCallback 1018 progressUpdater.addListener( 1019 object : AnimatorListenerAdapter() { 1020 override fun onAnimationEnd(animation: Animator) { 1021 finishCallback.run() 1022 } 1023 } 1024 ) 1025 1026 launchAnimation.play(progressUpdater) 1027 launchAnimation.start() 1028 } 1029 1030 /** 1031 * If we are launching split screen without any special animation from a starting View, we 1032 * simply fade in the starting apps and fade out launcher. 1033 */ 1034 @VisibleForTesting 1035 fun composeFadeInSplitLaunchAnimator( 1036 initialTaskId: Int, 1037 secondTaskId: Int, 1038 transitionInfo: TransitionInfo, 1039 t: Transaction, 1040 finishCallback: Runnable 1041 ) { 1042 var splitRoot1: Change? = null 1043 var splitRoot2: Change? = null 1044 val openingTargets = ArrayList<SurfaceControl>() 1045 for (change in transitionInfo.changes) { 1046 val taskInfo: RunningTaskInfo = change.taskInfo ?: continue 1047 val taskId = taskInfo.taskId 1048 val mode = change.mode 1049 1050 // Find the target tasks' root tasks since those are the split stages that need to 1051 // be animated (the tasks themselves are children and thus inherit animation). 1052 if (taskId == initialTaskId || taskId == secondTaskId) { 1053 check(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { 1054 "Expected task to be showing, but it is $mode" 1055 } 1056 } 1057 1058 if (taskId == initialTaskId) { 1059 splitRoot1 = change 1060 val parentToken1 = change.parent 1061 if (parentToken1 != null) { 1062 splitRoot1 = transitionInfo.getChange(parentToken1) ?: change 1063 } 1064 1065 if (splitRoot1?.leash != null) { 1066 openingTargets.add(splitRoot1.leash) 1067 } 1068 } 1069 1070 if (taskId == secondTaskId) { 1071 splitRoot2 = change 1072 val parentToken2 = change.parent 1073 if (parentToken2 != null) { 1074 splitRoot2 = transitionInfo.getChange(parentToken2) ?: change 1075 } 1076 1077 if (splitRoot2?.leash != null) { 1078 openingTargets.add(splitRoot2.leash) 1079 } 1080 } 1081 } 1082 1083 if (splitRoot1 != null) { 1084 // Set the highest level split root alpha; we could technically use the parent of 1085 // either splitRoot1 or splitRoot2 1086 val parentToken = splitRoot1.parent 1087 var rootLayer: Change? = null 1088 if (parentToken != null) { 1089 rootLayer = transitionInfo.getChange(parentToken) 1090 } 1091 if (rootLayer != null && rootLayer.leash != null) { 1092 openingTargets.add(rootLayer.leash) 1093 } 1094 } 1095 1096 val animTransaction = Transaction() 1097 val animator = ValueAnimator.ofFloat(0f, 1f) 1098 animator.setDuration(QuickstepTransitionManager.SPLIT_LAUNCH_DURATION.toLong()) 1099 animator.addUpdateListener { valueAnimator: ValueAnimator -> 1100 val progress = 1101 Interpolators.clampToProgress( 1102 Interpolators.LINEAR, 1103 valueAnimator.animatedFraction, 1104 0.8f, 1105 1f 1106 ) 1107 for (leash in openingTargets) { 1108 animTransaction.setAlpha(leash, progress) 1109 } 1110 animTransaction.apply() 1111 } 1112 1113 animator.addListener( 1114 object : AnimatorListenerAdapter() { 1115 override fun onAnimationStart(animation: Animator) { 1116 for (leash in openingTargets) { 1117 animTransaction.show(leash).setAlpha(leash, 0.0f) 1118 } 1119 animTransaction.apply() 1120 } 1121 1122 override fun onAnimationEnd(animation: Animator) { 1123 finishCallback.run() 1124 } 1125 } 1126 ) 1127 1128 t.apply() 1129 animator.start() 1130 } 1131 1132 private fun safeRemoveViewFromDragLayer(container: RecentsViewContainer, view: View?) { 1133 if (view != null) { 1134 container.dragLayer.removeView(view) 1135 } 1136 } 1137 } 1138