diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java index b91f51270c..d531e2c5a8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java @@ -52,6 +52,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; @@ -75,6 +76,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; @@ -517,6 +519,7 @@ public class TaskbarDragController extends DragController im return mIsSystemDragInProgress; } + @VisibleForTesting private void maybeOnDragEnd() { if (!isDragging()) { ((BubbleTextView) mDragObject.originalView).setIconDisabled(false); @@ -524,17 +527,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..b90a5b001a 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( @@ -274,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 @@ -524,6 +529,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. */ @@ -996,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()