Merge "Handle taps on the animating bubble" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
index 6163dad..95c4e25 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt
@@ -153,6 +153,16 @@
context.deviceProfile.widthPx,
windowLayoutParams.height
)
+
+ // if there's an animating bubble add it to the touch region so that it's clickable
+ val animatingBubbleBounds =
+ controllers.bubbleControllers
+ .getOrNull()
+ ?.bubbleBarViewController
+ ?.animatingBubbleBounds
+ if (animatingBubbleBounds != null) {
+ defaultTouchableRegion.op(animatingBubbleBounds, Region.Op.UNION)
+ }
}
// Pre-calculate insets for different providers across different rotations for this gravity
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index db069d5..308e4ce 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -104,6 +104,8 @@
* updates the bounds and accounts for translation.
*/
private final Rect mBubbleBarBounds = new Rect();
+ /** The bounds of the animating bubble in the coordinate space of the BubbleBarView. */
+ private final Rect mAnimatingBubbleBounds = new Rect();
// The amount the bubbles overlap when they are stacked in the bubble bar
private final float mIconOverlapAmount;
// The spacing between the bubbles when bubble bar is expanded
@@ -460,6 +462,30 @@
return mBubbleBarBounds;
}
+ /** Returns the bounds of the animating bubble, or {@code null} if no bubble is animating. */
+ @Nullable
+ public Rect getAnimatingBubbleBounds() {
+ if (mIsAnimatingNewBubble) {
+ return mAnimatingBubbleBounds;
+ }
+ return null;
+ }
+
+ /**
+ * Updates the animating bubble bounds. This should be called when the bubble is fully animated
+ * in so that we can include it in taskbar touchable region.
+ *
+ * <p>The bounds are adjusted to the coordinate space of BubbleBarView so that it can be used
+ * by taskbar.
+ */
+ public void updateAnimatingBubbleBounds(int left, int top, int width, int height) {
+ Rect bubbleBarBounds = getBubbleBarBounds();
+ mAnimatingBubbleBounds.left = bubbleBarBounds.left + left;
+ mAnimatingBubbleBounds.top = bubbleBarBounds.top + top;
+ mAnimatingBubbleBounds.right = mAnimatingBubbleBounds.left + width;
+ mAnimatingBubbleBounds.bottom = mAnimatingBubbleBounds.top + height;
+ }
+
/**
* Set bubble bar relative pivot value for X and Y, applied as a fraction of view width/height
* respectively. If the value is not in range of 0 to 1 it will be normalized.
@@ -852,10 +878,15 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (!mIsBarExpanded) {
+ if (!mIsBarExpanded && !mIsAnimatingNewBubble) {
// When the bar is collapsed, all taps on it should expand it.
return true;
}
return super.onInterceptTouchEvent(ev);
}
+
+ /** Whether a new bubble is currently animating. */
+ public boolean isAnimatingNewBubble() {
+ return mIsAnimatingNewBubble;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 0e62eaf..be9a94d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -138,6 +138,15 @@
if (bubble == null) {
Log.e(TAG, "bubble click listener, bubble was null");
}
+
+ if (mBarView.isAnimatingNewBubble()) {
+ mBubbleBarViewAnimator.onBubbleClickedWhileAnimating();
+ mBubbleStashController.showBubbleBarImmediate();
+ setExpanded(true);
+ mBubbleBarController.showAndSelectBubble(bubble);
+ return;
+ }
+
final String currentlySelected = mBubbleBarController.getSelectedBubbleKey();
if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) {
// Tapping the currently selected bubble while expanded collapses the view.
@@ -213,6 +222,12 @@
return mBarView.getBubbleBarBounds();
}
+ /** The bounds of the animating bubble, or {@code null} if no bubble is animating. */
+ @Nullable
+ public Rect getAnimatingBubbleBounds() {
+ return mBarView.getAnimatingBubbleBounds();
+ }
+
/** The horizontal margin of the bubble bar from the edge of the screen. */
public int getHorizontalMargin() {
return mBarView.getHorizontalMargin();
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
index 76d86de..bea0af8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashController.java
@@ -347,7 +347,7 @@
hotseatCellHeight - mUnstashedHeight) / 2;
}
- float getBubbleBarTranslationY() {
+ public float getBubbleBarTranslationY() {
// If we're on home, adjust the translation so the bubble bar aligns with hotseat.
// Otherwise we're either showing in an app or in overview. In either case adjust it so
// the bubble bar aligns with the taskbar.
@@ -374,4 +374,19 @@
public PhysicsAnimator<View> getStashedHandlePhysicsAnimator() {
return mHandleViewController.getPhysicsAnimator();
}
+
+ /** Notifies taskbar that it should update its touchable region. */
+ public void updateTaskbarTouchRegion() {
+ mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged();
+ }
+
+ /** Shows the bubble bar immediately without animation. */
+ public void showBubbleBarImmediate() {
+ mHandleViewController.setTranslationYForSwipe(0);
+ mIconTranslationYForStash.updateValue(getBubbleBarTranslationY());
+ mIconAlphaForStash.setValue(1);
+ mIconScaleForStash.updateValue(1);
+ mIsStashed = false;
+ onIsStashedChanged();
+ }
}
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 2d8983f..da36944 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt
@@ -35,6 +35,8 @@
private val scheduler: Scheduler = HandlerScheduler(bubbleBarView)
) {
+ private var animatingBubble: AnimatingBubble? = null
+
private companion object {
/** The time to show the flyout. */
const val FLYOUT_DELAY_MS: Long = 2500
@@ -54,26 +56,40 @@
const val BUBBLE_ANIMATION_STASH_HANDLE_TRANSLATION_Y = -20f
}
+ /** Wrapper around the animating bubble with its show and hide animations. */
+ private data class AnimatingBubble(
+ val bubbleView: BubbleView,
+ val showAnimation: Runnable,
+ val hideAnimation: Runnable
+ )
+
/** An interface for scheduling jobs. */
interface Scheduler {
/** Schedule the given [block] to run. */
- fun post(block: () -> Unit)
+ fun post(block: Runnable)
/** Schedule the given [block] to start with a delay of [delayMillis]. */
- fun postDelayed(delayMillis: Long, block: () -> Unit)
+ fun postDelayed(delayMillis: Long, block: Runnable)
+
+ /** Cancel the given [block] if it hasn't started yet. */
+ fun cancel(block: Runnable)
}
/** A [Scheduler] that uses a Handler to run jobs. */
private class HandlerScheduler(private val view: View) : Scheduler {
- override fun post(block: () -> Unit) {
+ override fun post(block: Runnable) {
view.post(block)
}
- override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+ override fun postDelayed(delayMillis: Long, block: Runnable) {
view.postDelayed(block, delayMillis)
}
+
+ override fun cancel(block: Runnable) {
+ view.removeCallbacks(block)
+ }
}
private val springConfig =
@@ -91,6 +107,7 @@
// and the second part hides it after a delay.
val showAnimation = buildShowAnimation(bubbleView, b.key)
val hideAnimation = buildHideAnimation(bubbleView)
+ animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
scheduler.post(showAnimation)
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
}
@@ -113,7 +130,7 @@
private fun buildShowAnimation(
bubbleView: BubbleView,
key: String,
- ): () -> Unit = {
+ ) = Runnable {
bubbleBarView.prepareForAnimatingBubbleWhileStashed(key)
// calculate the initial translation x the bubble should have in order to align it with the
// stash handle.
@@ -140,7 +157,7 @@
// 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
+ target.alpha = 1 - fraction
}
ty >= totalTranslationY -> {
// this is the second leg of the animation. the handle should be completely
@@ -173,6 +190,16 @@
}
}
}
+ animator.addEndListener { _, _, _, _, _, _, _ ->
+ // the bubble is now fully settled in. make it touchable
+ bubbleBarView.updateAnimatingBubbleBounds(
+ bubbleView.left,
+ bubbleView.top,
+ bubbleView.width,
+ bubbleView.height
+ )
+ bubbleStashController.updateTaskbarTouchRegion()
+ }
animator.start()
}
@@ -189,7 +216,7 @@
* 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 = {
+ private fun buildHideAnimation(bubbleView: BubbleView) = Runnable {
// 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
@@ -230,6 +257,7 @@
}
}
animator.addEndListener { _, _, _, _, _, _, _ ->
+ animatingBubble = null
bubbleView.alpha = 0f
bubbleView.translationY = 0f
bubbleView.scaleY = 1f
@@ -237,9 +265,18 @@
bubbleBarView.alpha = 0f
}
bubbleBarView.onAnimatingBubbleCompleted()
+ bubbleStashController.updateTaskbarTouchRegion()
}
animator.start()
}
+
+ /** Handles clicking on the animating bubble while the animation is still playing. */
+ fun onBubbleClickedWhileAnimating() {
+ val hideAnimation = animatingBubble?.hideAnimation ?: return
+ scheduler.cancel(hideAnimation)
+ bubbleBarView.onAnimatingBubbleCompleted()
+ animatingBubble = null
+ }
}
/** The X position in screen coordinates of the center of the bubble. */
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 d90e048..3d8484d 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
@@ -130,18 +130,90 @@
assertThat(handle.translationY).isEqualTo(0)
}
- private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+ @Test
+ fun animateBubbleInForStashed_tapAnimatingBubble() {
+ 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)
- var delayedBlock: (() -> Unit)? = null
- private set
+ 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)
- override fun post(block: () -> Unit) {
- block.invoke()
+ 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 handle = View(context)
+ val handleAnimator = PhysicsAnimator.getInstance(handle)
+ whenever(bubbleStashController.stashedHandlePhysicsAnimator).thenReturn(handleAnimator)
+
+ val animator =
+ BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ animator.animateBubbleInForStashed(bubble)
}
- override fun postDelayed(delayMillis: Long, block: () -> Unit) {
+ // 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)
+ assertThat(bubbleView.alpha).isEqualTo(1)
+ assertThat(bubbleView.translationY).isEqualTo(-20)
+ assertThat(bubbleView.scaleY).isEqualTo(1)
+
+ // verify the hide bubble animation is pending
+ assertThat(animatorScheduler.delayedBlock).isNotNull()
+
+ animator.onBubbleClickedWhileAnimating()
+
+ assertThat(animatorScheduler.delayedBlock).isNull()
+ assertThat(overflowView.visibility).isEqualTo(VISIBLE)
+ assertThat(overflowView.alpha).isEqualTo(1)
+ assertThat(bubbleView.alpha).isEqualTo(1)
+ assertThat(bubbleView.visibility).isEqualTo(VISIBLE)
+ assertThat(bubbleBarView.background).isNotNull()
+ assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
+ }
+
+ private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler {
+
+ var delayedBlock: Runnable? = null
+ private set
+
+ override fun post(block: Runnable) {
+ block.run()
+ }
+
+ override fun postDelayed(delayMillis: Long, block: Runnable) {
check(delayedBlock == null) { "there is already a pending block waiting to run" }
delayedBlock = block
}
+
+ override fun cancel(block: Runnable) {
+ check(delayedBlock == block) { "the pending block does not match the canceled block" }
+ delayedBlock = null
+ }
}
}