From c7106806f13518f1f7cbef9ff3525838fe600178 Mon Sep 17 00:00:00 2001 From: mpodolian Date: Tue, 11 Feb 2025 15:39:42 -0800 Subject: [PATCH 1/2] Made the bubble bar react to drag events from the Shell. Added logic to show and hide the bubble bar drop target on Shell drag-and-drop events. Test: Manual. Drag a shortcut or an application icon from the taskbar to the bubble bar drop zone. Observe that if the bubble bar is showing bubbles, the background stroke color becomes non-transparent; if there are no bubbles, the drop zone appears. Drag the app icon to the opposite side and observe that the bubble bar location is animated or the drop zone appears on the dragged side. Drop the application icon or shortcut on any side and observe that a new bubble is added and immediately expanded. Additionally, the bubble bar remains at the drop location. expanded. Bug: 388894910 Flag: com.android.wm.shell.enable_create_any_bubble Change-Id: I581dddf2c76ac982a5c78bc32d94b79bf63fd2b9 --- .../taskbar/TaskbarDragController.java | 28 ++++++++- .../taskbar/bubbles/BubbleBarBackground.kt | 23 +++++++- .../taskbar/bubbles/BubbleBarController.java | 22 +++++-- .../taskbar/bubbles/BubbleBarView.java | 10 ++++ .../bubbles/BubbleBarViewController.java | 59 ++++++++++++++++++- 5 files changed, 132 insertions(+), 10 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index 3a83db2959..4b709ac92e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -51,6 +51,7 @@ import android.view.ViewRootImpl; import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.app.animation.Interpolators; import com.android.internal.logging.InstanceId; @@ -74,6 +75,7 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; +import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.util.DisplayController; @@ -511,6 +513,7 @@ public class TaskbarDragController extends DragController im return mIsSystemDragInProgress; } + @VisibleForTesting private void maybeOnDragEnd() { if (!isDragging()) { ((BubbleTextView) mDragObject.originalView).setIconDisabled(false); @@ -518,17 +521,38 @@ public class TaskbarDragController extends DragController im TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_DRAGGING, false); mActivity.onDragEnd(); if (mReturnAnimator == null) { + // If an item is dropped on the bubble bar, the bubble bar handles the drop, + // so it should not collapse along with the taskbar. + boolean droppedOnBubbleBar = notifyBubbleBarItemDropped(); // Upon successful drag, immediately stash taskbar. // Note, this must be done last to ensure no AutohideSuspendFlags are active, as // that will prevent us from stashing until the timeout. - mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); - + mControllers.taskbarStashController.updateAndAnimateTransientTaskbar( + /* stash = */ true, + /* shouldBubblesFollow = */ !droppedOnBubbleBar + ); mActivity.getStatsLogManager().logger().withItemInfo(mDragObject.dragInfo) .log(LAUNCHER_APP_LAUNCH_DRAGDROP); } } } + /** + * Exits the Bubble Bar drop target mode if applicable. + * + * @return {@code true} if drop target mode was active. + */ + private boolean notifyBubbleBarItemDropped() { + return mControllers.bubbleControllers.map(bc -> { + BubbleBarViewController bubbleBarViewController = bc.bubbleBarViewController; + boolean showingDropTarget = bubbleBarViewController.isShowingDropTarget(); + if (showingDropTarget) { + bubbleBarViewController.onItemDroppedInBubbleBarDragZone(); + } + return showingDropTarget; + }).orElse(false); + } + @Override protected void endDrag() { if (mDisallowGlobalDrag) { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt index 249773d3df..97be2e87b1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt @@ -44,10 +44,13 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float) private val arrowVisibleHeight: Float private val strokeAlpha: Int + private val strokeColor: Int + private val strokeColorDropTarget: Int private val shadowAlpha: Int private val shadowBlur: Float private val keyShadowDistance: Float private var arrowHeightFraction = 1f + private var isShowingDropTarget: Boolean = false var arrowPositionX: Float = 0f private set @@ -100,7 +103,9 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float) fillPaint.flags = Paint.ANTI_ALIAS_FLAG fillPaint.style = Paint.Style.FILL // configure stroke paint - strokePaint.color = context.getColor(R.color.taskbar_stroke) + strokeColor = context.getColor(R.color.taskbar_stroke) + strokeColorDropTarget = context.getColor(com.android.internal.R.color.system_primary_fixed) + strokePaint.color = strokeColor strokePaint.flags = Paint.ANTI_ALIAS_FLAG strokePaint.style = Paint.Style.STROKE strokePaint.strokeWidth = res.getDimension(R.dimen.transient_taskbar_stroke_width) @@ -235,9 +240,25 @@ class BubbleBarBackground(context: Context, private var backgroundHeight: Float) return max(0f, getScaledArrowHeight() - (arrowHeight - arrowVisibleHeight)) } + /** Set whether the background should show the drop target */ + fun showDropTarget(isDropTarget: Boolean) { + if (isShowingDropTarget == isDropTarget) { + return + } + isShowingDropTarget = isDropTarget + val strokeColor = if (isDropTarget) strokeColorDropTarget else strokeColor + val alpha = if (isDropTarget) DRAG_STROKE_ALPHA else strokeAlpha + strokePaint.color = strokeColor + strokePaint.alpha = alpha + invalidateSelf() + } + + fun isShowingDropTarget() = isShowingDropTarget + companion object { private const val DARK_THEME_STROKE_ALPHA = 51 private const val LIGHT_THEME_STROKE_ALPHA = 41 + private const val DRAG_STROKE_ALPHA = 255 private const val DARK_THEME_SHADOW_ALPHA = 51 private const val LIGHT_THEME_SHADOW_ALPHA = 25 } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java index 4e029e32be..5ddbe032f9 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java @@ -33,7 +33,8 @@ import android.os.Bundle; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Log; -import android.widget.Toast; + +import androidx.annotation.NonNull; import com.android.launcher3.taskbar.TaskbarSharedState; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; @@ -589,15 +590,24 @@ public class BubbleBarController extends IBubblesListener.Stub { } @Override - public void onDragItemOverBubbleBarDragZone(BubbleBarLocation location) { - //TODO(b/388894910): add meaningful implementation - MAIN_EXECUTOR.execute(() -> - Toast.makeText(mContext, "onDragItemOver " + location, Toast.LENGTH_SHORT).show()); + public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) { + MAIN_EXECUTOR.execute(() -> { + mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) { + mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation); + } + }); } @Override public void onItemDraggedOutsideBubbleBarDropZone() { - + MAIN_EXECUTOR.execute(() -> { + if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) { + BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation(); + mBubbleBarLocationListener.onBubbleBarLocationAnimated(original); + } + mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone(); + }); } /** Notifies WMShell to show the expanded view. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index c00112383b..d43ebe2977 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -536,6 +536,16 @@ public class BubbleBarView extends FrameLayout { return (float) (displayWidth - getWidth() - margin); } + /** Set whether the background should show the drop target */ + public void showDropTarget(boolean isDropTarget) { + mBubbleBarBackground.showDropTarget(isDropTarget); + } + + /** Returns whether the Bubble Bar is currently displaying a drop target. */ + public boolean isShowingDropTarget() { + return mBubbleBarBackground.isShowingDropTarget(); + } + /** * Animate bubble bar to the given location transiently. Does not modify the layout or the value * returned by {@link #getBubbleBarLocation()}. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 026f239152..b02b85fad4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -131,12 +131,13 @@ public class BubbleBarViewController { // Whether the bar is hidden when stashed private boolean mHiddenForStashed; private boolean mShouldShowEducation; - public boolean mOverflowAdded; + private boolean mIsLocationUpdatedForDropTarget = false; private BubbleBarViewAnimator mBubbleBarViewAnimator; private final FrameLayout mBubbleBarContainer; private BubbleBarFlyoutController mBubbleBarFlyoutController; + private BubbleBarPinController mBubbleBarPinController; private TaskbarSharedState mTaskbarSharedState; private final TimeSource mTimeSource = System::currentTimeMillis; private final int mTaskbarTranslationDelta; @@ -166,6 +167,7 @@ public class BubbleBarViewController { mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarController = bubbleControllers.bubbleBarController; mBubbleDragController = bubbleControllers.bubbleDragController; + mBubbleBarPinController = bubbleControllers.bubbleBarPinController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; mBubbleBarFlyoutController = new BubbleBarFlyoutController( @@ -524,6 +526,61 @@ public class BubbleBarViewController { mBarView.animateToBubbleBarLocation(bubbleBarLocation); } + /** Returns whether the Bubble Bar is currently displaying a drop target. */ + public boolean isShowingDropTarget() { + return mBarView.isShowingDropTarget(); + } + + /** + * Notifies the controller that a drag event is over the Bubble Bar drop zone. The controller + * will display the appropriate drop target and enter drop target mode. The controller will also + * update the return value of {@link #isLocationUpdatedForDropTarget()} to true if location was + * updated. + */ + public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) { + mBarView.showDropTarget(/* isDropTarget = */ true); + mIsLocationUpdatedForDropTarget = getBubbleBarLocation() != bubbleBarLocation; + if (mIsLocationUpdatedForDropTarget) { + animateBubbleBarLocation(bubbleBarLocation); + } + if (!hasBubbles()) { + mBubbleBarPinController.showDropTarget(bubbleBarLocation); + } + } + + /** + * Returns {@code true} if location was updated after most recent + * {@link #onDragItemOverBubbleBarDragZone}}. + */ + public boolean isLocationUpdatedForDropTarget() { + return mIsLocationUpdatedForDropTarget; + } + + /** + * Notifies the controller that the drag event is outside the Bubble Bar drop zone. + * This will hide the drop target zone if there are no bubbles or return the + * Bubble Bar to its original location. The controller will also exit drop target + * mode and reset the value returned from {@link #isLocationUpdatedForDropTarget()} to false. + */ + public void onItemDraggedOutsideBubbleBarDropZone() { + mBarView.showDropTarget(/* isDropTarget = */ false); + if (mIsLocationUpdatedForDropTarget) { + animateBubbleBarLocation(getBubbleBarLocation()); + } + mBubbleBarPinController.hideDropTarget(); + mIsLocationUpdatedForDropTarget = false; + } + + /** + * Notifies the controller that the drag has completed over the Bubble Bar drop zone. + * The controller will hide the drop target if there are no bubbles and exit drop target mode. + */ + public void onItemDroppedInBubbleBarDragZone() { + mBarView.showDropTarget(/* isDropTarget = */ false); + mBubbleBarPinController.hideDropTarget(); + mIsLocationUpdatedForDropTarget = false; + } + /** * The bounds of the bubble bar. */ From 39c4bf24482aeb85a7bc10874a7f7383619490d0 Mon Sep 17 00:00:00 2001 From: mpodolian Date: Thu, 13 Feb 2025 16:56:16 -0800 Subject: [PATCH 2/2] Handle new bubble notification while drag is in progress. Handle new bubble notification while drag is in progress. Updated logic to react to new bubble notifications arriving during drag operations before a drop occurs. Bug: 388894910 Flag: com.android.wm.shell.enable_create_any_bubble Test: BubbleBarViewAnimatorTest Test: Manual. Go to some application and have no bubbles. Start dragging the taskbar icon over the bubble bar drop zone. Trigger new bubble with the bubbles test application. Observe drop target is hidden and new bubble is animated in, followed by flyout. When flyout disappears, bubble bar remains on the screen. Release icon over the bubble bar. Observe newly added application bubble is expanded. Repeat same test, but drag the taskbar icon to the opposite side. Test: Manual. Go to some application, but have bubbles. Start dragging the taskbar icon over the bubble bar drop zone. Trigger new bubble with the bubbles test application. Observe new bubble is added and the flyout is shown. When flyout disappears, bubble bar remains on the screen. Release icon over the bubble bar. Observe newly added application bubble is expanded. Repeat same test, but drag the taskbar icon to the opposite side. Change-Id: I6fe1139ef56f4ad40d36a804016d23a9b34abf5f --- .../bubbles/BubbleBarViewController.java | 12 +++- .../animation/BubbleBarViewAnimator.kt | 30 +++++---- .../animation/BubbleBarViewAnimatorTest.kt | 66 +++++++++++++++++++ 3 files changed, 94 insertions(+), 14 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index b02b85fad4..b90a5b001a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -276,7 +276,10 @@ public class BubbleBarViewController { @Override public boolean isOnLeft() { - return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); + boolean shouldRevertLocation = + mBarView.isShowingDropTarget() && mIsLocationUpdatedForDropTarget; + boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); + return shouldRevertLocation != isOnLeft; } @Override @@ -1053,7 +1056,12 @@ public class BubbleBarViewController { boolean isInApp = mTaskbarStashController.isInApp(); // if this is the first bubble, animate to the initial state. if (mBarView.getBubbleChildCount() == 1 && !isUpdate) { - mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding); + // If a drop target is visible and the first bubble is added, hide the empty drop target + if (mBarView.isShowingDropTarget()) { + mBubbleBarPinController.hideDropTarget(); + } + mBubbleBarViewAnimator.animateToInitialState(bubble, isInApp, isExpanding, + mBarView.isShowingDropTarget()); return; } // if we're not stashed or we're in persistent taskbar, animate for collapsed state. 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 745c689541..30cfafe694 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -365,7 +365,12 @@ constructor( } /** Animates to the initial state of the bubble bar, when there are no previous bubbles. */ - fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) { + fun animateToInitialState( + b: BubbleBarBubble, + isInApp: Boolean, + isExpanding: Boolean, + isDragging: Boolean = false, + ) { val bubbleView = b.view val animator = PhysicsAnimator.getInstance(bubbleView) if (animator.isRunning()) animator.cancel() @@ -374,15 +379,12 @@ constructor( // bubble bar to the handle if we're in an app. val showAnimation = buildBubbleBarSpringInAnimation() val hideAnimation = - if (isInApp && !isExpanding) { + if (isInApp && !isExpanding && !isDragging) { buildBubbleBarToHandleAnimation() } else { Runnable { - moveToState(AnimatingBubble.State.ANIMATING_OUT) - bubbleBarFlyoutController.collapseFlyout { - onFlyoutRemoved() - clearAnimatingBubble() - } + collapseFlyoutAndUpdateState() + if (isDragging) return@Runnable bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } @@ -440,11 +442,7 @@ constructor( // first bounce the bubble bar and show the flyout. Then hide the flyout. val showAnimation = buildBubbleBarBounceAnimation() val hideAnimation = Runnable { - moveToState(AnimatingBubble.State.ANIMATING_OUT) - bubbleBarFlyoutController.collapseFlyout { - onFlyoutRemoved() - clearAnimatingBubble() - } + collapseFlyoutAndUpdateState() bubbleStashController.showBubbleBarImmediate() bubbleStashController.updateTaskbarTouchRegion() } @@ -454,6 +452,14 @@ constructor( scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) } + private fun collapseFlyoutAndUpdateState() { + moveToState(AnimatingBubble.State.ANIMATING_OUT) + bubbleBarFlyoutController.collapseFlyout { + onFlyoutRemoved() + clearAnimatingBubble() + } + } + /** * The bubble bar animation when it is collapsed is divided into 2 chained animations. The first * animation is a regular accelerate animation that moves the bubble bar upwards. When it ends 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 46b5659ce9..a456fb9c73 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 @@ -60,6 +60,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -572,6 +573,71 @@ class BubbleBarViewAnimatorTest { verify(bubbleStashController).stashBubbleBarImmediate() } + @Test + fun animateToInitialState_whileDragging_inApp() { + setUpBubbleBar() + setUpBubbleStashController() + whenever(bubbleStashController.bubbleBarTranslationY) + .thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR) + + val handle = View(context) + val handleAnimator = PhysicsAnimator.getInstance(handle) + whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator) + + val barAnimator = PhysicsAnimator.getInstance(bubbleBarView) + + var notifiedBubbleBarVisible = false + val onBubbleBarVisible = Runnable { notifiedBubbleBarVisible = true } + val animator = + BubbleBarViewAnimator( + bubbleBarView, + bubbleStashController, + flyoutController, + bubbleBarParentViewController, + onExpanded = emptyRunnable, + onBubbleBarVisible = onBubbleBarVisible, + animatorScheduler, + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + bubbleBarView.visibility = INVISIBLE + animator.animateToInitialState( + bubble, + isInApp = true, + isExpanding = false, + isDragging = true, + ) + } + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + barAnimator.assertIsNotRunning() + assertThat(animator.isAnimating).isTrue() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR) + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(1) + waitForFlyoutToShow() + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + waitForFlyoutToHide() + + InstrumentationRegistry.getInstrumentation().runOnMainSync {} + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + assertThat(bubbleBarParentViewController.timesInvoked).isEqualTo(2) + assertThat(animator.isAnimating).isFalse() + assertThat(bubbleBarView.alpha).isEqualTo(1) + assertThat(handle.translationY).isEqualTo(0) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(notifiedBubbleBarVisible).isTrue() + + verify(bubbleStashController, never()).stashBubbleBarImmediate() + } + @Test fun animateToInitialState_inApp_autoExpanding() { setUpBubbleBar()