diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig index 38af57260f..9fb5b7b061 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -433,3 +433,14 @@ flag { description: "Enables tracking active gesture logs in ProtoLog" bug: "293182501" } + + +flag { + name: "coordinate_workspace_scale" + namespace: "launcher" + description: "Ensure that the workspace and hotseat scale doesn't conflict and transitions smoothly between launching and closing apps" + bug: "366403487" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java index a64936d287..acf74cdede 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -116,6 +116,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.ColorUtils; +import com.android.app.animation.Animations; import com.android.internal.jank.Cuj; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory; @@ -574,23 +575,45 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener } else { List viewsToAnimate = new ArrayList<>(); Workspace workspace = mLauncher.getWorkspace(); - workspace.forEachVisiblePage( - view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets())); + if (Flags.coordinateWorkspaceScale()) { + viewsToAnimate.add(workspace); + } else { + workspace.forEachVisiblePage( + view -> viewsToAnimate.add(((CellLayout) view).getShortcutsAndWidgets())); + } + Hotseat hotseat = mLauncher.getHotseat(); // Do not scale hotseat as a whole when taskbar is present, and scale QSB only if it's // not inline. if (mDeviceProfile.isTaskbarPresent) { if (!mDeviceProfile.isQsbInline) { - viewsToAnimate.add(mLauncher.getHotseat().getQsb()); + viewsToAnimate.add(hotseat.getQsb()); } } else { - viewsToAnimate.add(mLauncher.getHotseat()); + viewsToAnimate.add(hotseat); } viewsToAnimate.forEach(view -> { view.setLayerType(View.LAYER_TYPE_HARDWARE, null); - ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scales) + float[] scale = scales; + if (Flags.coordinateWorkspaceScale()) { + // Start the animation from the current value, instead of assuming the views are + // in their resting state, so interrupted animations merge seamlessly. + // TODO(b/367591368): ideally these animations would be refactored to be + // controlled centrally so each instances doesn't need to care about this + // coordination. + scale = new float[]{view.getScaleX(), scales[1]}; + + // Cancel any ongoing animations. This is necessary to avoid a conflict between + // e.g. the unfinished animation triggered when closing an app back to Home and + // this animation caused by a launch. + Animations.Companion.cancelOngoingAnimation(view); + // Make sure to cache the current animation, so it can be properly interrupted. + Animations.Companion.setOngoingAnimation(view, launcherAnimator); + } + + ObjectAnimator scaleAnim = ObjectAnimator.ofFloat(view, SCALE_PROPERTY, scale) .setDuration(CONTENT_SCALE_DURATION); scaleAnim.setInterpolator(DECELERATE_1_5); launcherAnimator.play(scaleAnim); @@ -600,6 +623,11 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener viewsToAnimate.forEach(view -> { SCALE_PROPERTY.set(view, 1f); view.setLayerType(View.LAYER_TYPE_NONE, null); + + if (Flags.coordinateWorkspaceScale()) { + // Reset the cached animation. + Animations.Companion.setOngoingAnimation(view, null /* animation */); + } }); mLauncher.resumeExpensiveViewUpdates(); }; diff --git a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt index f547a7fba1..a94d023076 100644 --- a/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt +++ b/quickstep/src/com/android/quickstep/util/ScalingWorkspaceRevealAnim.kt @@ -23,8 +23,10 @@ import android.graphics.RectF import android.view.View import android.view.animation.PathInterpolator import androidx.core.graphics.transform +import com.android.app.animation.Animations import com.android.app.animation.Interpolators import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.Flags import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WORKSPACE_STATE import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY @@ -44,9 +46,9 @@ import com.android.quickstep.views.RecentsView * the screen outwards radially. This is used in conjunction with the swipe up to home animation. */ class ScalingWorkspaceRevealAnim( - launcher: QuickstepLauncher, + private val launcher: QuickstepLauncher, siblingAnimation: RectFSpringAnim?, - windowTargetRect: RectF? + windowTargetRect: RectF?, ) { companion object { private const val FADE_DURATION_MS = 200L @@ -86,25 +88,40 @@ class ScalingWorkspaceRevealAnim( launcher.workspace.stateTransitionAnimation.setScrim( PropertySetter.NO_ANIM_PROPERTY_SETTER, LauncherState.BACKGROUND_APP, - setupConfig + setupConfig, ) val workspace = launcher.workspace val hotseat = launcher.hotseat + var fromSize = + if (Flags.coordinateWorkspaceScale()) { + // Interrupt the current animation, if any. + Animations.cancelOngoingAnimation(workspace) + Animations.cancelOngoingAnimation(hotseat) + + if (workspace.scaleX != MAX_SIZE) { + workspace.scaleX + } else { + MIN_SIZE + } + } else { + MIN_SIZE + } + // Scale the Workspace and Hotseat around the same pivot. workspace.setPivotToScaleWithSelf(hotseat) animation.addFloat( workspace, WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], - MIN_SIZE, + fromSize, MAX_SIZE, SCALE_INTERPOLATOR, ) animation.addFloat( hotseat, HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_WORKSPACE_STATE], - MIN_SIZE, + fromSize, MAX_SIZE, SCALE_INTERPOLATOR, ) @@ -116,13 +133,13 @@ class ScalingWorkspaceRevealAnim( animation.setViewAlpha( workspace, MAX_ALPHA, - Interpolators.clampToProgress(LINEAR, 0f, fadeClamp) + Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), ) hotseat.alpha = MIN_ALPHA animation.setViewAlpha( hotseat, MAX_ALPHA, - Interpolators.clampToProgress(LINEAR, 0f, fadeClamp) + Interpolators.clampToProgress(LINEAR, 0f, fadeClamp), ) val transitionConfig = StateAnimationConfig() @@ -137,7 +154,7 @@ class ScalingWorkspaceRevealAnim( launcher.workspace.stateTransitionAnimation.setScrim( animation, LauncherState.NORMAL, - transitionConfig + transitionConfig, ) // To avoid awkward jumps in icon position, we want the sibling animation to always be @@ -164,7 +181,7 @@ class ScalingWorkspaceRevealAnim( 1 / workspace.scaleX, 1 / workspace.scaleY, transformed.centerX(), - transformed.centerY() + transformed.centerY(), ) } ) @@ -183,6 +200,12 @@ class ScalingWorkspaceRevealAnim( Runnable { workspace.setLayerType(View.LAYER_TYPE_NONE, null) hotseat.setLayerType(View.LAYER_TYPE_NONE, null) + + if (Flags.coordinateWorkspaceScale()) { + // Reset the cached animations. + Animations.setOngoingAnimation(workspace, animation = null) + Animations.setOngoingAnimation(hotseat, animation = null) + } } ) ) @@ -193,6 +216,14 @@ class ScalingWorkspaceRevealAnim( } fun start() { - getAnimators().start() + val animators = getAnimators() + if (Flags.coordinateWorkspaceScale()) { + // Make sure to cache the current animation, so it can be properly interrupted. + // TODO(b/367591368): ideally these animations would be refactored to be controlled + // centrally so each instances doesn't need to care about this coordination. + Animations.setOngoingAnimation(launcher.workspace, animators) + Animations.setOngoingAnimation(launcher.hotseat, animators) + } + animators.start() } }