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