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

Commit 98bbc2b4 authored by Liran Binyamin's avatar Liran Binyamin Committed by Android (Google) Code Review
Browse files

Merge "Animate the stash handle for new bubbles" into main

parents 8c14bef1 597ced30
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -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();
+42 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
+27 −5
Original line number Diff line number Diff line
@@ -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<View> getStashedHandleAlpha() {
        return mTaskbarStashedHandleAlpha;
        return mStashedHandleAlpha;
    }

    /** Returns the x position of the center of the stashed handle. */
    public float getStashedHandleCenterX() {
        return mStashedHandleBounds.exactCenterX();
    }

    /**
+89 −22
Original line number Diff line number Diff line
@@ -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<BubbleView>
        bubbleAnimator: PhysicsAnimator<BubbleView>
    ): () -> 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<BubbleView>): () -> Unit = {
        animator.setDefaultSpringConfig(springConfig)
        animator
            .spring(DynamicAnimation.ALPHA, 0f)
            .spring(DynamicAnimation.TRANSLATION_Y, 0f)
            .addEndListener { _, _, _, canceled, _, _, allRelevantPropertyAnimsEnded ->
                if (!canceled && allRelevantPropertyAnimsEnded) {
    /**
     * 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
            }
            bubbleBarView.onAnimatingBubbleCompleted()
        }
        hideBubbleAnimation.start()
    }
        animator.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
    }
+54 −7
Original line number Diff line number Diff line
@@ -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<Context>()
    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<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 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