Initial animation of new bubble bar bubble
This change animates a new bubble bar bubble. Currently only handles the case where the bubble is added when the bubble bar is stashed.
There's no synchronization with the stash handle yet either.
Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT
Bug: 280605790
Test: atest BubbleBarViewAnimatorTest
Change-Id: Ic3f5edc0dde204f871cf7e1288bd50620e6beebb
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 79fdeda..8eeb055 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar.bubbles
+import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
@@ -27,12 +28,10 @@
import com.android.launcher3.Utilities
import com.android.launcher3.Utilities.mapToRange
import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound
-import com.android.launcher3.taskbar.TaskbarActivityContext
import com.android.wm.shell.common.TriangleShape
/** Drawable for the background of the bubble bar. */
-class BubbleBarBackground(context: TaskbarActivityContext, private val backgroundHeight: Float) :
- Drawable() {
+class BubbleBarBackground(context: Context, private val backgroundHeight: Float) : Drawable() {
private val DARK_THEME_SHADOW_ALPHA = 51f
private val LIGHT_THEME_SHADOW_ALPHA = 25f
@@ -46,6 +45,7 @@
var arrowPositionX: Float = 0f
private set
+
private var showingArrow: Boolean = false
private var arrowDrawable: ShapeDrawable
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index a5da65f..4ca7c89 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -39,8 +39,6 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.SpringAnimationBuilder;
-import com.android.launcher3.taskbar.TaskbarActivityContext;
-import com.android.launcher3.views.ActivityContext;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import java.util.List;
@@ -159,8 +157,6 @@
public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- TaskbarActivityContext activityContext = ActivityContext.lookupContext(context);
-
setAlpha(0);
setVisibility(INVISIBLE);
mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
@@ -171,7 +167,7 @@
setClipToPadding(false);
- mBubbleBarBackground = new BubbleBarBackground(activityContext,
+ mBubbleBarBackground = new BubbleBarBackground(context,
getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
setBackgroundDrawable(mBubbleBarBackground);
@@ -379,6 +375,36 @@
return mRelativePivotY;
}
+ /** Prepares for animating a bubble while being stashed. */
+ public void prepareForAnimatingBubbleWhileStashed(String bubbleKey) {
+ // 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.
+ setBackground(null);
+ for (int i = 0; i < getChildCount(); i++) {
+ final BubbleView view = (BubbleView) getChildAt(i);
+ final String key = view.getBubble().getKey();
+ if (!bubbleKey.equals(key)) {
+ view.setVisibility(INVISIBLE);
+ }
+ }
+ setVisibility(VISIBLE);
+ setAlpha(1);
+ setTranslationY(0);
+ setScaleX(1);
+ setScaleY(1);
+ }
+
+ /** Resets the state after the bubble animation completed. */
+ public void onAnimatingBubbleCompleted() {
+ setBackground(mBubbleBarBackground);
+ for (int i = 0; i < getChildCount(); i++) {
+ final BubbleView view = (BubbleView) getChildAt(i);
+ view.setVisibility(VISIBLE);
+ view.setAlpha(1f);
+ }
+ }
+
// TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0f019a3..96d91ea 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -34,6 +34,7 @@
import com.android.launcher3.taskbar.TaskbarControllers;
import com.android.launcher3.taskbar.TaskbarInsetsController;
import com.android.launcher3.taskbar.TaskbarStashController;
+import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.SystemUiProxy;
@@ -81,6 +82,8 @@
private boolean mHiddenForNoBubbles = true;
private boolean mShouldShowEducation;
+ private BubbleBarViewAnimator mBubbleBarViewAnimator;
+
public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) {
mActivity = activity;
mBarView = barView;
@@ -113,6 +116,8 @@
mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) ->
mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()
);
+
+ mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController);
}
private void onBubbleClicked(View v) {
@@ -316,6 +321,12 @@
new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
+
+ boolean isStashedOrGone =
+ mBubbleStashController.isStashed() || mBarView.getVisibility() != VISIBLE;
+ if (b instanceof BubbleBarBubble && isStashedOrGone) {
+ mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b);
+ }
} else {
Log.w(TAG, "addBubble, bubble was null!");
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
index 6549ad6..bcdc718 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java
@@ -145,7 +145,7 @@
}
/** Sets the bubble being rendered in this view. */
- void setBubble(BubbleBarBubble bubble) {
+ public void setBubble(BubbleBarBubble bubble) {
mBubble = bubble;
mBubbleIcon.setImageBitmap(bubble.getIcon());
mAppIcon.setImageBitmap(bubble.getBadge());
@@ -159,7 +159,7 @@
* the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
* come from an app.
*/
- void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
+ public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow;
mBubbleIcon.setImageBitmap(bitmap);
mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
@@ -168,7 +168,7 @@
/** Returns the bubble being rendered in this view. */
@Nullable
- BubbleBarItem getBubble() {
+ public BubbleBarItem getBubble() {
return mBubble;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
new file mode 100644
index 0000000..bcb9f4d
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+import android.view.View
+import android.view.View.VISIBLE
+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.wm.shell.shared.animation.PhysicsAnimator
+
+/** Handles animations for bubble bar bubbles. */
+class BubbleBarViewAnimator
+@JvmOverloads
+constructor(
+ private val bubbleBarView: BubbleBarView,
+ private val bubbleStashController: BubbleStashController,
+ private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
+) {
+
+ private companion object {
+ /** 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
+ }
+
+ /** An interface for scheduling jobs. */
+ interface Scheduler {
+
+ /** Schedule the given [block] to run. */
+ fun post(block: () -> Unit)
+
+ /** Schedule the given [block] to start with a delay of [delayMillis]. */
+ fun postDelayed(delayMillis: Long, block: () -> Unit)
+ }
+
+ /** A [Scheduler] that uses a Handler to run jobs. */
+ private class HandlerScheduler(private val view: View) : Scheduler {
+
+ override fun post(block: () -> Unit) {
+ view.post(block)
+ }
+
+ override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+ view.postDelayed(block, delayMillis)
+ }
+ }
+
+ private val springConfig =
+ PhysicsAnimator.SpringConfig(
+ stiffness = SpringForce.STIFFNESS_LOW,
+ dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
+ )
+
+ /** Animates a bubble for the state where the bubble bar is stashed. */
+ fun animateBubbleInForStashed(b: BubbleBarBubble) {
+ val bubbleView = b.view
+ val animator = PhysicsAnimator.getInstance(bubbleView)
+ 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 hideAnimation = buildHideAnimation(animator)
+ scheduler.post(showAnimation)
+ scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
+ }
+
+ /** Returns a lambda that starts the animation that shows the new bubble. */
+ private fun buildShowAnimation(
+ bubbleView: BubbleView,
+ key: String,
+ animator: PhysicsAnimator<BubbleView>
+ ): () -> Unit = {
+ bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
+ animator.setDefaultSpringConfig(springConfig)
+ animator
+ .spring(DynamicAnimation.ALPHA, 1f)
+ .spring(DynamicAnimation.TRANSLATION_Y, BUBBLE_ANIMATION_TRANSLATION_Y)
+ bubbleView.alpha = 0f
+ bubbleView.visibility = VISIBLE
+ animator.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) {
+ if (bubbleStashController.isStashed) {
+ bubbleBarView.alpha = 0f
+ }
+ bubbleBarView.onAnimatingBubbleCompleted()
+ }
+ }
+ animator.start()
+ }
+}
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
new file mode 100644
index 0000000..c17aeaa
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.bubbles.animation
+
+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.INVISIBLE
+import android.view.View.VISIBLE
+import android.widget.FrameLayout
+import androidx.core.graphics.drawable.toBitmap
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.R
+import com.android.launcher3.taskbar.bubbles.BubbleBarBubble
+import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow
+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.PhysicsAnimatorTestUtils
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleBarViewAnimatorTest {
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val animatorScheduler = TestBubbleBarViewAnimatorScheduler()
+
+ @Before
+ fun setUp() {
+ PhysicsAnimatorTestUtils.prepareForTest()
+ }
+
+ @Test
+ fun animateBubbleInForStashed() {
+ lateinit var overflowView: BubbleView
+ lateinit var bubbleView: BubbleView
+ lateinit var bubble: BubbleBarBubble
+ val bubbleBarView = BubbleBarView(context)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleBarView.layoutParams = FrameLayout.LayoutParams(0, 0)
+ val inflater = LayoutInflater.from(context)
+
+ val bitmap = ColorDrawable(Color.WHITE).toBitmap(width = 20, height = 20)
+ overflowView =
+ inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+ overflowView.setOverflow(BubbleBarOverflow(overflowView), bitmap)
+ bubbleBarView.addView(overflowView)
+
+ val bubbleInfo = BubbleInfo("key", 0, null, null, 0, context.packageName, null, false)
+ bubbleView =
+ inflater.inflate(R.layout.bubblebar_item_view, bubbleBarView, false) as BubbleView
+ bubble =
+ BubbleBarBubble(bubbleInfo, bubbleView, bitmap, bitmap, Color.WHITE, Path(), "")
+ bubbleView.setBubble(bubble)
+ bubbleBarView.addView(bubbleView)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ val bubbleStashController = mock<BubbleStashController>()
+ whenever(bubbleStashController.isStashed).thenReturn(true)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble)
+ }
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ assertThat(overflowView.visibility).isEqualTo(INVISIBLE)
+ assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ DynamicAnimation.ALPHA,
+ DynamicAnimation.TRANSLATION_Y
+ )
+
+ assertThat(bubbleView.alpha).isEqualTo(1)
+ assertThat(bubbleView.translationY).isEqualTo(-50)
+
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ DynamicAnimation.ALPHA,
+ DynamicAnimation.TRANSLATION_Y
+ )
+
+ assertThat(bubbleView.alpha).isEqualTo(1)
+ assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleView.translationY).isEqualTo(0)
+ assertThat(bubbleBarView.alpha).isEqualTo(0)
+ assertThat(overflowView.alpha).isEqualTo(1)
+ assertThat(overflowView.visibility).isEqualTo(VISIBLE)
+ }
+
+ private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+
+ var delayedBlock: (() -> Unit)? = null
+ private set
+
+ override fun post(block: () -> Unit) {
+ block.invoke()
+ }
+
+ override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+ check(delayedBlock == null) { "there is already a pending block waiting to run" }
+ delayedBlock = block
+ }
+ }
+}