diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index c27e9f1287..711ba6258f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -96,6 +96,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 @@ -457,6 +459,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. @@ -477,6 +480,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); @@ -521,6 +527,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