From 597ced303339206edbd775e0b1f29066c61e5219 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Tue, 2 Apr 2024 15:44:26 -0400 Subject: [PATCH] Animate the stash handle for new bubbles The stash handle now animates out before the new bubble animates in, and animates back in after the new bubble is hidden. There's still some additional polish needed :) Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/z30Ob1rcDUkNEphLAQsgV Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT Bug: 280605846 Test: atest BubbleBarViewAnimatorTest Change-Id: I03449286b01ec96de9834e24a707652ddbe49fb0 --- .../taskbar/bubbles/BubbleBarView.java | 12 ++ .../bubbles/BubbleStashController.java | 42 +++++++ .../BubbleStashedHandleViewController.java | 32 ++++- .../animation/BubbleBarViewAnimator.kt | 111 ++++++++++++++---- .../animation/BubbleBarViewAnimatorTest.kt | 61 ++++++++-- 5 files changed, 224 insertions(+), 34 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index 4ca7c89127..11445deb90 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -94,6 +94,8 @@ public class BubbleBarView extends FrameLayout { private final BubbleBarBackground mBubbleBarBackground; + private boolean mIsAnimatingNewBubble = false; + /** * The current bounds of all the bubble bar. Note that these bounds may not account for * translation. The bounds should be retrieved using {@link #getBubbleBarBounds()} which @@ -377,6 +379,7 @@ public class BubbleBarView extends FrameLayout { /** Prepares for animating a bubble while being stashed. */ public void prepareForAnimatingBubbleWhileStashed(String bubbleKey) { + mIsAnimatingNewBubble = true; // we're about to animate the new bubble in. the new bubble has already been added to this // view, but we're currently stashed, so before we can start the animation we need make // everything else in the bubble bar invisible, except for the bubble that's being animated. @@ -397,6 +400,9 @@ public class BubbleBarView extends FrameLayout { /** Resets the state after the bubble animation completed. */ public void onAnimatingBubbleCompleted() { + mIsAnimatingNewBubble = false; + // setting the background triggers relayout so no need to explicitly invalidate after the + // animation setBackground(mBubbleBarBackground); for (int i = 0; i < getChildCount(); i++) { final BubbleView view = (BubbleView) getChildAt(i); @@ -441,6 +447,12 @@ public class BubbleBarView extends FrameLayout { * on the expanded state. */ private void updateChildrenRenderNodeProperties() { + if (mIsAnimatingNewBubble) { + // don't update bubbles if a new bubble animation is playing. + // the bubble bar will redraw itself via onLayout after the animation. + return; + } + final float widthState = (float) mWidthAnimator.getAnimatedValue(); final float currentWidth = getWidth(); final float expandedWidth = expandedWidth(); diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java index e25e58653a..61a2e2222d 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java @@ -51,6 +51,15 @@ public class BubbleStashController { */ private static final float STASHED_BAR_SCALE = 0.5f; + /** The duration of hiding and showing the stashed handle as part of a new bubble animation. */ + private static final long NEW_BUBBLE_HANDLE_ANIMATION_DURATION_MS = 200; + + /** The translation Y value the handle animates to when hiding it for a new bubble. */ + private static final int NEW_BUBBLE_HIDE_HANDLE_ANIMATION_TRANSLATION_Y = -20; + + /** The alpha value the handle animates to when hiding it for a new bubble. */ + public static final float NEW_BUBBLE_HIDE_HANDLE_ANIMATION_ALPHA = 0.5f; + protected final TaskbarActivityContext mActivity; // Initialized in init. @@ -64,6 +73,7 @@ public class BubbleStashController { private AnimatedFloat mIconScaleForStash; private AnimatedFloat mIconTranslationYForStash; private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha; + private AnimatedFloat mBubbleStashedHandleTranslationY; private boolean mRequestedStashState; private boolean mRequestedExpandedState; @@ -95,6 +105,7 @@ public class BubbleStashController { mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get( StashedHandleViewController.ALPHA_INDEX_STASHED); + mBubbleStashedHandleTranslationY = mHandleViewController.getStashedHandleTranslationY(); mStashedHeight = mHandleViewController.getStashedHeight(); mUnstashedHeight = mHandleViewController.getUnstashedHeight(); @@ -362,4 +373,35 @@ public class BubbleStashController { public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { mHandleViewController.setBubbleBarLocation(bubbleBarLocation); } + + /** Returns the x position of the center of the stashed handle. */ + public float getStashedHandleCenterX() { + return mHandleViewController.getStashedHandleCenterX(); + } + + /** Returns the animation for hiding the handle before a new bubble animates in. */ + public AnimatorSet buildHideHandleAnimationForNewBubble() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + mBubbleStashedHandleTranslationY.animateToValue( + NEW_BUBBLE_HIDE_HANDLE_ANIMATION_TRANSLATION_Y), + mBubbleStashedHandleAlpha.animateToValue(NEW_BUBBLE_HIDE_HANDLE_ANIMATION_ALPHA)); + animatorSet.setDuration(NEW_BUBBLE_HANDLE_ANIMATION_DURATION_MS); + return animatorSet; + } + + /** Sets the alpha value of the stashed handle. */ + public void setStashAlpha(float alpha) { + mBubbleStashedHandleAlpha.setValue(alpha); + } + + /** Returns the animation for showing the handle after a new bubble animated in. */ + public AnimatorSet buildShowHandleAnimationForNewBubble() { + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether( + mBubbleStashedHandleTranslationY.animateToValue(0), + mBubbleStashedHandleAlpha.animateToValue(1)); + animatorSet.setDuration(NEW_BUBBLE_HANDLE_ANIMATION_DURATION_MS); + return animatorSet; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java index f64517af21..2a5912a5c2 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java @@ -29,6 +29,7 @@ import android.view.View; import android.view.ViewOutlineProvider; import com.android.launcher3.R; +import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.taskbar.StashedHandleView; @@ -47,7 +48,7 @@ public class BubbleStashedHandleViewController { private final TaskbarActivityContext mActivity; private final StashedHandleView mStashedHandleView; - private final MultiValueAlpha mTaskbarStashedHandleAlpha; + private final MultiValueAlpha mStashedHandleAlpha; // Initialized in init. private BubbleBarViewController mBarViewController; @@ -58,6 +59,12 @@ public class BubbleStashedHandleViewController { private int mStashedHandleWidth; private int mStashedHandleHeight; + private final AnimatedFloat mStashedHandleTranslationY = + new AnimatedFloat(this::updateTranslationY); + + // Modified when swipe up is happening on the stashed handle or task bar. + private float mSwipeUpTranslationY; + // The bounds we want to clip to in the settled state when showing the stashed handle. private final Rect mStashedHandleBounds = new Rect(); @@ -75,7 +82,7 @@ public class BubbleStashedHandleViewController { StashedHandleView stashedHandleView) { mActivity = activity; mStashedHandleView = stashedHandleView; - mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1); + mStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, 1); } public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { @@ -93,7 +100,7 @@ public class BubbleStashedHandleViewController { R.dimen.transient_taskbar_bottom_margin); mStashedHandleView.getLayoutParams().height = mBarSize + bottomMargin; - mTaskbarStashedHandleAlpha.get(0).setValue(0); + mStashedHandleAlpha.get(0).setValue(0); mStashedTaskbarHeight = resources.getDimensionPixelSize( R.dimen.bubblebar_stashed_size); @@ -231,18 +238,33 @@ public class BubbleStashedHandleViewController { } } + /** Returns an animator for translation Y. */ + public AnimatedFloat getStashedHandleTranslationY() { + return mStashedHandleTranslationY; + } + /** * Sets the translation of the stashed handle during the swipe up gesture. */ public void setTranslationYForSwipe(float transY) { - mStashedHandleView.setTranslationY(transY); + mSwipeUpTranslationY = transY; + updateTranslationY(); + } + + private void updateTranslationY() { + mStashedHandleView.setTranslationY(mStashedHandleTranslationY.value + mSwipeUpTranslationY); } /** * Used by {@link BubbleStashController} to animate the handle when stashing or un stashing. */ public MultiPropertyFactory getStashedHandleAlpha() { - return mTaskbarStashedHandleAlpha; + return mStashedHandleAlpha; + } + + /** Returns the x position of the center of the stashed handle. */ + public float getStashedHandleCenterX() { + return mStashedHandleBounds.exactCenterX(); } /** diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt index bcb9f4d31b..1db51037de 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -18,12 +18,16 @@ package com.android.launcher3.taskbar.bubbles.animation import android.view.View import android.view.View.VISIBLE +import androidx.core.animation.AnimatorSet +import androidx.core.animation.ObjectAnimator +import androidx.core.animation.doOnEnd import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.launcher3.taskbar.bubbles.BubbleBarBubble import com.android.launcher3.taskbar.bubbles.BubbleBarView import com.android.launcher3.taskbar.bubbles.BubbleStashController import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.systemui.util.doOnEnd import com.android.wm.shell.shared.animation.PhysicsAnimator /** Handles animations for bubble bar bubbles. */ @@ -39,7 +43,17 @@ constructor( /** The time to show the flyout. */ const val FLYOUT_DELAY_MS: Long = 2500 /** The translation Y the new bubble will animate to. */ - const val BUBBLE_ANIMATION_TRANSLATION_Y = -50f + const val BUBBLE_ANIMATION_FINAL_TRANSLATION_Y = -50f + /** The initial translation Y value the new bubble is set to before the animation starts. */ + // TODO(liranb): get rid of this and calculate this based on the y-distance between the + // bubble and the stash handle. + const val BUBBLE_ANIMATION_INITIAL_TRANSLATION_Y = 50f + /** The initial scale Y value that the new bubble is set to before the animation starts. */ + const val BUBBLE_ANIMATION_INITIAL_SCALE_Y = 0.3f + /** The initial alpha value that the new bubble is set to before the animation starts. */ + const val BUBBLE_ANIMATION_INITIAL_ALPHA = 0.5f + /** The duration of the hide bubble animation. */ + const val HIDE_BUBBLE_ANIMATION_DURATION_MS = 250L } /** An interface for scheduling jobs. */ @@ -78,41 +92,94 @@ constructor( // the animation of a new bubble is divided into 2 parts. The first part shows the bubble // and the second part hides it after a delay. val showAnimation = buildShowAnimation(bubbleView, b.key, animator) - val hideAnimation = buildHideAnimation(animator) + val hideAnimation = buildHideAnimation(bubbleView) scheduler.post(showAnimation) scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } - /** Returns a lambda that starts the animation that shows the new bubble. */ + /** + * Returns a lambda that starts the animation that shows the new bubble. + * + * The animation is divided into 2 parts. First the stash handle starts animating up and fades + * out. When it ends the bubble starts fading in. The bubble and stashed handle are aligned to + * give the impression of the stash handle morphing into the bubble. + */ private fun buildShowAnimation( bubbleView: BubbleView, key: String, - animator: PhysicsAnimator + bubbleAnimator: PhysicsAnimator ): () -> Unit = { + // calculate the initial translation x the bubble should have in order to align it with the + // stash handle. + val initialTranslationX = + bubbleStashController.stashedHandleCenterX - bubbleView.centerXOnScreen bubbleBarView.prepareForAnimatingBubbleWhileStashed(key) - animator.setDefaultSpringConfig(springConfig) - animator + bubbleAnimator.setDefaultSpringConfig(springConfig) + bubbleAnimator .spring(DynamicAnimation.ALPHA, 1f) - .spring(DynamicAnimation.TRANSLATION_Y, BUBBLE_ANIMATION_TRANSLATION_Y) + .spring(DynamicAnimation.TRANSLATION_Y, BUBBLE_ANIMATION_FINAL_TRANSLATION_Y) + .spring(DynamicAnimation.SCALE_Y, 1f) + // prepare the bubble for the animation bubbleView.alpha = 0f + bubbleView.translationX = initialTranslationX + bubbleView.translationY = BUBBLE_ANIMATION_INITIAL_TRANSLATION_Y + bubbleView.scaleY = BUBBLE_ANIMATION_INITIAL_SCALE_Y bubbleView.visibility = VISIBLE - animator.start() + // start the stashed handle animation. when it ends, start the bubble animation. + val stashedHandleAnimation = bubbleStashController.buildHideHandleAnimationForNewBubble() + stashedHandleAnimation.doOnEnd { + bubbleView.alpha = BUBBLE_ANIMATION_INITIAL_ALPHA + bubbleAnimator.start() + bubbleStashController.setStashAlpha(0f) + } + stashedHandleAnimation.start() } - /** Returns a lambda that starts the animation that hides the new bubble. */ - private fun buildHideAnimation(animator: PhysicsAnimator): () -> Unit = { - animator.setDefaultSpringConfig(springConfig) - animator - .spring(DynamicAnimation.ALPHA, 0f) - .spring(DynamicAnimation.TRANSLATION_Y, 0f) - .addEndListener { _, _, _, canceled, _, _, allRelevantPropertyAnimsEnded -> - if (!canceled && allRelevantPropertyAnimsEnded) { - if (bubbleStashController.isStashed) { - bubbleBarView.alpha = 0f - } - bubbleBarView.onAnimatingBubbleCompleted() - } + /** + * Returns a lambda that starts the animation that hides the new bubble. + * + * Similarly to the show animation, this is divided into 2 parts. We first animate the bubble + * out, and then animate the stash handle in. At the end of the animation we reset the values of + * the bubble. + */ + private fun buildHideAnimation(bubbleView: BubbleView): () -> Unit = { + val stashAnimation = bubbleStashController.buildShowHandleAnimationForNewBubble() + val alphaAnimator = + ObjectAnimator.ofFloat(bubbleView, View.ALPHA, BUBBLE_ANIMATION_INITIAL_ALPHA) + val translationYAnimator = + ObjectAnimator.ofFloat( + bubbleView, + View.TRANSLATION_Y, + BUBBLE_ANIMATION_INITIAL_TRANSLATION_Y + ) + val scaleYAnimator = + ObjectAnimator.ofFloat(bubbleView, View.SCALE_Y, BUBBLE_ANIMATION_INITIAL_SCALE_Y) + val hideBubbleAnimation = AnimatorSet() + hideBubbleAnimation.playTogether(alphaAnimator, translationYAnimator, scaleYAnimator) + hideBubbleAnimation.duration = HIDE_BUBBLE_ANIMATION_DURATION_MS + hideBubbleAnimation.doOnEnd { + // the bubble is now hidden, start the stash handle animation and reset bubble + // properties + bubbleStashController.setStashAlpha( + BubbleStashController.NEW_BUBBLE_HIDE_HANDLE_ANIMATION_ALPHA + ) + bubbleView.alpha = 0f + stashAnimation.start() + bubbleView.translationY = 0f + bubbleView.scaleY = 1f + if (bubbleStashController.isStashed) { + bubbleBarView.alpha = 0f } - animator.start() + bubbleBarView.onAnimatingBubbleCompleted() + } + hideBubbleAnimation.start() } } + +/** The X position in screen coordinates of the center of the bubble. */ +private val BubbleView.centerXOnScreen: Float + get() { + val screenCoordinates = IntArray(2) + getLocationOnScreen(screenCoordinates) + return screenCoordinates[0] + width / 2f + } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt index c17aeaaf76..b478efac93 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -16,6 +16,9 @@ package com.android.launcher3.taskbar.bubbles.animation +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet import android.content.Context import android.graphics.Color import android.graphics.Path @@ -24,6 +27,8 @@ import android.view.LayoutInflater import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.core.animation.doOnEnd import androidx.core.graphics.drawable.toBitmap import androidx.dynamicanimation.animation.DynamicAnimation import androidx.test.core.app.ApplicationProvider @@ -39,10 +44,14 @@ import com.android.launcher3.taskbar.bubbles.BubbleView import com.android.wm.shell.common.bubbles.BubbleInfo import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit import org.junit.Before +import org.junit.ClassRule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @@ -52,6 +61,10 @@ class BubbleBarViewAnimatorTest { private val context = ApplicationProvider.getApplicationContext() private val animatorScheduler = TestBubbleBarViewAnimatorScheduler() + companion object { + @JvmField @ClassRule val animatorTestRule = AnimatorTestRule() + } + @Before fun setUp() { PhysicsAnimatorTestUtils.prepareForTest() @@ -86,6 +99,15 @@ class BubbleBarViewAnimatorTest { val bubbleStashController = mock() whenever(bubbleStashController.isStashed).thenReturn(true) + val semaphore = Semaphore(0) + val hideHandleAnimator = AnimatorSet() + hideHandleAnimator.duration = 0 + whenever(bubbleStashController.buildHideHandleAnimationForNewBubble()) + .thenReturn(hideHandleAnimator) + // add an end listener to the hide handle animation. we add it when the animation starts + // to ensure that it gets called after all other end listeners. + hideHandleAnimator.doOnStart { hideHandleAnimator.doOnEnd { semaphore.release() } } + val animator = BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) @@ -93,29 +115,44 @@ class BubbleBarViewAnimatorTest { animator.animateBubbleInForStashed(bubble) } + // wait for the stash handle animation to complete + assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue() + // stash handle animation finished. verify that the stash handle is now hidden + verify(bubbleStashController).setStashAlpha(0f) + InstrumentationRegistry.getInstrumentation().waitForIdleSync() assertThat(overflowView.visibility).isEqualTo(INVISIBLE) assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) assertThat(bubbleView.visibility).isEqualTo(VISIBLE) + // wait for the show bubble animation to complete PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( DynamicAnimation.ALPHA, - DynamicAnimation.TRANSLATION_Y + DynamicAnimation.TRANSLATION_Y, + DynamicAnimation.SCALE_Y, ) assertThat(bubbleView.alpha).isEqualTo(1) assertThat(bubbleView.translationY).isEqualTo(-50) + assertThat(bubbleView.scaleY).isEqualTo(1) + val showHandleAnimator = AnimatorSet() + showHandleAnimator.duration = 0 + whenever(bubbleStashController.buildShowHandleAnimationForNewBubble()) + .thenReturn(showHandleAnimator) + var showHandleAnimationStarted = false + showHandleAnimator.doOnStart { showHandleAnimationStarted = true } + + // execute the hide bubble animation assertThat(animatorScheduler.delayedBlock).isNotNull() InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + // finish the hide bubble animation + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animatorTestRule.advanceTimeBy(250) + } - InstrumentationRegistry.getInstrumentation().waitForIdleSync() - - PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( - DynamicAnimation.ALPHA, - DynamicAnimation.TRANSLATION_Y - ) + assertThat(showHandleAnimationStarted).isTrue() assertThat(bubbleView.alpha).isEqualTo(1) assertThat(bubbleView.visibility).isEqualTo(VISIBLE) @@ -125,6 +162,16 @@ class BubbleBarViewAnimatorTest { assertThat(overflowView.visibility).isEqualTo(VISIBLE) } + private fun AnimatorSet.doOnStart(onStart: () -> Unit) { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + onStart() + } + } + ) + } + private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { var delayedBlock: (() -> Unit)? = null