Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e79120af authored by Liran Binyamin's avatar Liran Binyamin
Browse files

Update new bubble animation

This change is a rewrite of the new bubble animation. Previously we chained the handle animation and the bubble animation so that they run sequentially.
That caused the transition from the handle to the bubble to be jarring.

We now use a single spring animation along the y axis that starts from the position of the handle to the position of where the bubble will end up.
The animation is split into 3 logical parts, where initially the bubble animates out, then the bubble starts animating in, and the last part is the overshoot of the spring animation, which is used to mark the bubble fully visible and ensure that from that point on, the bubble is only moving but doesn't change in scale or transparency as the bounce effect is playing.
Using a single animation path allows for a smooth transition from the handle to the bubble view.

Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/hHNUBdNJPiWi9gMbqy45UJ

Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT
Bug: 280605846
Test: atest BubbleBarViewAnimatorTest
Change-Id: Ic1d3244574b8500d4aad2e9c718e61c1c34bd82a
parent adbdfe2a
Loading
Loading
Loading
Loading
+5 −35
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.animation.AnimatorSet;
import android.annotation.Nullable;
import android.view.InsetsController;
import android.view.MotionEvent;
import android.view.View;

import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.StashedHandleViewController;
@@ -32,6 +33,7 @@ import com.android.launcher3.taskbar.TaskbarInsetsController;
import com.android.launcher3.taskbar.TaskbarStashController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.animation.PhysicsAnimator;

/**
 * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to
@@ -51,15 +53,6 @@ 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.
@@ -73,7 +66,6 @@ public class BubbleStashController {
    private AnimatedFloat mIconScaleForStash;
    private AnimatedFloat mIconTranslationYForStash;
    private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha;
    private AnimatedFloat mBubbleStashedHandleTranslationY;

    private boolean mRequestedStashState;
    private boolean mRequestedExpandedState;
@@ -105,7 +97,6 @@ public class BubbleStashController {

        mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get(
                StashedHandleViewController.ALPHA_INDEX_STASHED);
        mBubbleStashedHandleTranslationY = mHandleViewController.getStashedHandleTranslationY();

        mStashedHeight = mHandleViewController.getStashedHeight();
        mUnstashedHeight = mHandleViewController.getUnstashedHeight();
@@ -379,29 +370,8 @@ public class BubbleStashController {
        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;
    /** Returns the [PhysicsAnimator] for the stashed handle view. */
    public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
        return mHandleViewController.getPhysicsAnimator();
    }
}
+7 −18
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ 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;
@@ -40,6 +39,7 @@ import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.animation.PhysicsAnimator;

/**
 * Handles properties/data collection, then passes the results to our stashed handle View to render.
@@ -59,12 +59,6 @@ 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();

@@ -129,6 +123,11 @@ public class BubbleStashedHandleViewController {
                updateBounds(mBarViewController.getBubbleBarLocation()));
    }

    /** Returns the [PhysicsAnimator] for the stashed handle view. */
    public PhysicsAnimator<View> getPhysicsAnimator() {
        return PhysicsAnimator.getInstance(mStashedHandleView);
    }

    private void updateBounds(BubbleBarLocation bubbleBarLocation) {
        // As more bubbles get added, the icon bounds become larger. To ensure a consistent
        // handle bar position, we pin it to the edge of the screen.
@@ -238,21 +237,11 @@ 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) {
        mSwipeUpTranslationY = transY;
        updateTranslationY();
    }

    private void updateTranslationY() {
        mStashedHandleView.setTranslationY(mStashedHandleTranslationY.value + mSwipeUpTranslationY);
        mStashedHandleView.setTranslationY(transY);
    }

    /**
+120 −54
Original line number Diff line number Diff line
@@ -18,16 +18,12 @@ 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. */
@@ -43,17 +39,19 @@ 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_FINAL_TRANSLATION_Y = -50f
        const val BUBBLE_ANIMATION_BUBBLE_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
        const val BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET = 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
        /**
         * The distance the stashed handle will travel as it gets hidden as part of the new bubble
         * animation.
         */
        // TODO(liranb): calculate this based on the position of the views
        const val BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y = -20f
    }

    /** An interface for scheduling jobs. */
@@ -91,7 +89,7 @@ constructor(
        if (animator.isRunning()) animator.cancel()
        // 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 showAnimation = buildShowAnimation(bubbleView, b.key)
        val hideAnimation = buildHideAnimation(bubbleView)
        scheduler.post(showAnimation)
        scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
@@ -100,71 +98,139 @@ constructor(
    /**
     * 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.
     * Visually, the animation is divided into 2 parts. The stash handle starts animating up and
     * fading out and then the bubble starts animating up and fading in.
     *
     * To make the transition from the handle to the bubble smooth, the positions and movement of
     * the 2 views must be synchronized. To do that we use a single spring path along the Y axis,
     * starting from the handle's position to the eventual bubble's position. The path is split into
     * 3 parts.
     * 1. In the first part, we only animate the handle.
     * 1. In the second part the handle is fully hidden, and the bubble is animating in.
     * 1. The third part is the overshoot of the spring animation, where we make the bubble fully
     *    visible which helps avoiding further updates when we re-enter the second part.
     */
    private fun buildShowAnimation(
        bubbleView: BubbleView,
        key: String,
        bubbleAnimator: PhysicsAnimator<BubbleView>
    ): () -> Unit = {
        bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
        // 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)
        bubbleAnimator.setDefaultSpringConfig(springConfig)
        bubbleAnimator
            .spring(DynamicAnimation.ALPHA, 1f)
            .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
        // 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)

        // this is the total distance that both the stashed handle and the bubble will be traveling
        val totalTranslationY =
            BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
        val animator = bubbleStashController.stashedHandlePhysicsAnimator
        animator.setDefaultSpringConfig(springConfig)
        animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
        animator.addUpdateListener { target, values ->
            val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
            when {
                ty >= BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y -> {
                    // we're in the first leg of the animation. only animate the handle. the bubble
                    // remains hidden during this part of the animation

                    // map the path [0, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y] to [0,1]
                    val fraction = ty / BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
                    target.alpha = 1 - fraction / 2
                }
                ty >= totalTranslationY -> {
                    // this is the second leg of the animation. the handle should be completely
                    // hidden and the bubble should start animating in.
                    // it's possible that we're re-entering this leg because this is a spring
                    // animation, so only set the alpha and scale for the bubble if we didn't
                    // already fully animate in.
                    target.alpha = 0f
                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
                    if (bubbleView.alpha != 1f) {
                        // map the path
                        // [BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y, totalTranslationY]
                        // to [0, 1]
                        val fraction =
                            (ty - BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y) /
                                BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y
                        bubbleView.alpha = fraction
                        bubbleView.scaleY =
                            BUBBLE_ANIMATION_INITIAL_SCALE_Y +
                                (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
                    }
                }
                else -> {
                    // we're past the target animated value, set the alpha and scale for the bubble
                    // so that it's fully visible and no longer changing, but keep moving it along
                    // the animation path
                    bubbleView.alpha = 1f
                    bubbleView.scaleY = 1f
                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
                }
            }
        stashedHandleAnimation.start()
        }
        animator.start()
    }

    /**
     * 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.
     * Similarly to the show animation, this is visually 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.
     *
     * This is a spring animation that goes along the same path of the show animation in the
     * opposite order, and is split into 3 parts:
     * 1. In the first part the bubble animates out.
     * 1. In the second part the bubble is fully hidden and the handle animates in.
     * 1. The third part is the overshoot. The handle is made fully visible.
     */
    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
            )
        // this is the total distance that both the stashed handle and the bubble will be traveling
        val totalTranslationY =
            BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y + BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
        val animator = bubbleStashController.stashedHandlePhysicsAnimator
        animator.setDefaultSpringConfig(springConfig)
        animator.spring(DynamicAnimation.TRANSLATION_Y, 0f)
        animator.addUpdateListener { target, values ->
            val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
            when {
                ty <= BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y -> {
                    // this is the first leg of the animation. only animate the bubble. the handle
                    // is hidden during this part
                    bubbleView.translationY = ty + BUBBLE_ANIMATION_TRANSLATION_Y_OFFSET
                    // map the path
                    // [totalTranslationY, BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y]
                    // to [0, 1]
                    val fraction = (totalTranslationY - ty) / BUBBLE_ANIMATION_BUBBLE_TRANSLATION_Y
                    bubbleView.alpha = 1 - fraction / 2
                    bubbleView.scaleY = 1 - (1 - BUBBLE_ANIMATION_INITIAL_SCALE_Y) * fraction
                }
                ty <= 0 -> {
                    // this is the second part of the animation. make the bubble invisible and
                    // start fading in the handle, but don't update the alpha if it's already fully
                    // visible
                    bubbleView.alpha = 0f
                    if (target.alpha != 1f) {
                        // map the path [BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y, 0] to [0, 1]
                        val fraction =
                            (BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y - ty) /
                                BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y
                        target.alpha = fraction
                    }
                }
                else -> {
                    // we reached the target value. set the alpha of the handle to 1
                    target.alpha = 1f
                }
            }
        }
        animator.addEndListener { _, _, _, _, _, _, _ ->
            bubbleView.alpha = 0f
            stashAnimation.start()
            bubbleView.translationY = 0f
            bubbleView.scaleY = 1f
            if (bubbleStashController.isStashed) {
@@ -172,7 +238,7 @@ constructor(
            }
            bubbleBarView.onAnimatingBubbleCompleted()
        }
        hideBubbleAnimation.start()
        animator.start()
    }
}

+15 −57
Original line number Diff line number Diff line
@@ -16,19 +16,15 @@

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
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
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
@@ -42,16 +38,13 @@ import com.android.launcher3.taskbar.bubbles.BubbleBarView
import com.android.launcher3.taskbar.bubbles.BubbleStashController
import com.android.launcher3.taskbar.bubbles.BubbleView
import com.android.wm.shell.common.bubbles.BubbleInfo
import com.android.wm.shell.shared.animation.PhysicsAnimator
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
@@ -61,10 +54,6 @@ class BubbleBarViewAnimatorTest {
    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()

    companion object {
        @JvmField @ClassRule val animatorTestRule = AnimatorTestRule()
    }

    @Before
    fun setUp() {
        PhysicsAnimatorTestUtils.prepareForTest()
@@ -99,14 +88,9 @@ class BubbleBarViewAnimatorTest {
        val bubbleStashController = mock<BubbleStashController>()
        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 handle = View(context)
        val handleAnimator = PhysicsAnimator.getInstance(handle)
        whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)

        val animator =
            BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
@@ -115,44 +99,26 @@ 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)

        // let the animation start and wait for it to complete
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)

        assertThat(handle.alpha).isEqualTo(0)
        assertThat(handle.translationY).isEqualTo(-70)
        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.SCALE_Y,
        )

        assertThat(bubbleView.alpha).isEqualTo(1)
        assertThat(bubbleView.translationY).isEqualTo(-50)
        assertThat(bubbleView.translationY).isEqualTo(-20)
        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)
        }

        assertThat(showHandleAnimationStarted).isTrue()
        // let the animation start and wait for it to complete
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
        PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)

        assertThat(bubbleView.alpha).isEqualTo(1)
        assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
@@ -160,16 +126,8 @@ class BubbleBarViewAnimatorTest {
        assertThat(bubbleBarView.alpha).isEqualTo(0)
        assertThat(overflowView.alpha).isEqualTo(1)
        assertThat(overflowView.visibility).isEqualTo(VISIBLE)
    }

    private fun AnimatorSet.doOnStart(onStart: () -> Unit) {
        addListener(
            object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animator: Animator) {
                    onStart()
                }
            }
        )
        assertThat(handle.alpha).isEqualTo(1)
        assertThat(handle.translationY).isEqualTo(0)
    }

    private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {