From a87abece5a50d2fb9fcad865adcc77a991ec44e5 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Thu, 18 Apr 2024 11:55:56 -0400 Subject: [PATCH] Handle taps on the animating bubble Make the animating bubble touchable and expand the bubble bar when tapping on it. Demo: http://recall/-/bJtug1HhvXkkeA4MQvIaiP/c4wBVtrT5YIgxL5kVuuoDU Flag: ACONFIG com.android.wm.shell.enable_bubble_bar DEVELOPMENT Bug: 280605846 Test: atest BubbleBarViewAnimatorTest Change-Id: I09281c2ebc9921750ada42dfc27e95d1ad0bd83b --- .../taskbar/TaskbarInsetsController.kt | 10 +++ .../taskbar/bubbles/BubbleBarView.java | 33 +++++++- .../bubbles/BubbleBarViewController.java | 15 ++++ .../bubbles/BubbleStashController.java | 17 +++- .../animation/BubbleBarViewAnimator.kt | 51 +++++++++-- .../animation/BubbleBarViewAnimatorTest.kt | 84 +++++++++++++++++-- 6 files changed, 195 insertions(+), 15 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index 8dc81cf461..47043d8c6c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -153,6 +153,16 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas 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 711ba6258f..c72fb15b90 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 @@ public class BubbleBarView extends FrameLayout { * 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 they are expanded in the bubble bar @@ -431,6 +433,30 @@ public class BubbleBarView extends FrameLayout { 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. + * + *

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. @@ -797,10 +823,15 @@ public class BubbleBarView extends FrameLayout { @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 aa1b4dfb40..de74576156 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -139,6 +139,15 @@ public class BubbleBarViewController { 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. @@ -214,6 +223,12 @@ public class BubbleBarViewController { 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 76d86de4e2..bea0af8394 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 @@ public class BubbleStashController { 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 class BubbleStashController { public PhysicsAnimator 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 2d8983fca7..da369448da 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( // 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 @@ constructor( 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 @@ constructor( // 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 @@ constructor( } } } + 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 @@ constructor( * 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 @@ constructor( } } animator.addEndListener { _, _, _, _, _, _, _ -> + animatingBubble = null bubbleView.alpha = 0f bubbleView.translationY = 0f bubbleView.scaleY = 1f @@ -237,9 +265,18 @@ constructor( 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 d90e0484e5..3d8484d2d1 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 @@ class BubbleBarViewAnimatorTest { 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() + 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 + } } }