diff --git a/quickstep/res/drawable/bg_bubble_dismiss_circle.xml b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml new file mode 100644 index 0000000000..b793eecdf9 --- /dev/null +++ b/quickstep/res/drawable/bg_bubble_dismiss_circle.xml @@ -0,0 +1,27 @@ + + + + + + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/ic_bubble_dismiss_white.xml b/quickstep/res/drawable/ic_bubble_dismiss_white.xml new file mode 100644 index 0000000000..b15111b821 --- /dev/null +++ b/quickstep/res/drawable/ic_bubble_dismiss_white.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index e4f65554a4..6140d14971 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -367,6 +367,13 @@ 3dp 1dp + + 96dp + 60dp + 24dp + 50dp + 548dp + diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 42cb29046f..cb9c329c77 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -90,6 +90,8 @@ import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.taskbar.bubbles.BubbleBarView; import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; +import com.android.launcher3.taskbar.bubbles.BubbleDismissController; +import com.android.launcher3.taskbar.bubbles.BubbleDragController; import com.android.launcher3.taskbar.bubbles.BubbleStashController; import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController; import com.android.launcher3.taskbar.overlay.TaskbarOverlayController; @@ -216,7 +218,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { new BubbleBarController(this, bubbleBarView), new BubbleBarViewController(this, bubbleBarView), new BubbleStashController(this), - new BubbleStashedHandleViewController(this, bubbleHandleView))); + new BubbleStashedHandleViewController(this, bubbleHandleView), + new BubbleDragController(this), + new BubbleDismissController(this, mDragLayer))); } // Construct controllers. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index eec334afbd..e93d4107e1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -95,6 +95,8 @@ public class BubbleBarView extends FrameLayout { private View.OnClickListener mOnClickListener; private final Rect mTempRect = new Rect(); + private float mRelativePivotX = 1f; + private float mRelativePivotY = 1f; // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the // collapsed state and 1 to the fully expanded state. @@ -109,6 +111,9 @@ public class BubbleBarView extends FrameLayout { @Nullable private Consumer mUpdateSelectedBubbleAfterCollapse; + @Nullable + private BubbleView mDraggedBubbleView; + public BubbleBarView(Context context) { this(context, null); } @@ -181,9 +186,10 @@ public class BubbleBarView extends FrameLayout { mBubbleBarBounds.right = right; mBubbleBarBounds.bottom = bottom; - // The bubble bar handle is aligned to the bottom edge of the screen so scale towards that. - setPivotX(getWidth()); - setPivotY(getHeight()); + // The bubble bar handle is aligned according to the relative pivot, + // by default it's aligned to the bottom edge of the screen so scale towards that + setPivotX(mRelativePivotX * getWidth()); + setPivotY(mRelativePivotY * getHeight()); // Position the views updateChildrenRenderNodeProperties(); @@ -198,6 +204,32 @@ public class BubbleBarView extends FrameLayout { return mBubbleBarBounds; } + /** + * 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. + * @param x relative X pivot value in range 0..1 + * @param y relative Y pivot value in range 0..1 + */ + public void setRelativePivot(float x, float y) { + mRelativePivotX = Float.max(Float.min(x, 1), 0); + mRelativePivotY = Float.max(Float.min(y, 1), 0); + requestLayout(); + } + + /** + * Get current relative pivot for X axis + */ + public float getRelativePivotX() { + return mRelativePivotX; + } + + /** + * Get current relative pivot for Y axis + */ + public float getRelativePivotY() { + return mRelativePivotY; + } + // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { @@ -254,9 +286,9 @@ public class BubbleBarView extends FrameLayout { // where the bubble will end up when the animation ends final float targetX = currentWidth - expandedWidth + expandedX; bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX); - // if we're fully expanded, set the z level to 0 + // if we're fully expanded, set the z level to 0 or to bubble elevation if dragged if (widthState == 1f) { - bv.setZ(0); + bv.setZ(bv == mDraggedBubbleView ? mBubbleElevation : 0); } // When we're expanded, we're not stacked so we're not behind the stack bv.setBehindStack(false, animate); @@ -328,6 +360,14 @@ public class BubbleBarView extends FrameLayout { updateArrowForSelected(/* shouldAnimate= */ true); } + /** + * Sets the dragged bubble view to correctly apply Z order. Dragged view should appear on top + */ + public void setDraggedBubble(@Nullable BubbleView view) { + mDraggedBubbleView = view; + requestLayout(); + } + /** * Update the arrow position to match the selected bubble. * diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index ca0c4cc206..2e3c701b82 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -24,6 +24,8 @@ import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.TaskbarActivityContext; @@ -54,6 +56,7 @@ public class BubbleBarViewController { // Initialized in init. private BubbleStashController mBubbleStashController; private BubbleBarController mBubbleBarController; + private BubbleDragController mBubbleDragController; private TaskbarStashController mTaskbarStashController; private TaskbarInsetsController mTaskbarInsetsController; private View.OnClickListener mBubbleClickListener; @@ -85,6 +88,7 @@ public class BubbleBarViewController { public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarController = bubbleControllers.bubbleBarController; + mBubbleDragController = bubbleControllers.bubbleDragController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; @@ -95,6 +99,7 @@ public class BubbleBarViewController { mBubbleBarScale.updateValue(1f); mBubbleClickListener = v -> onBubbleClicked(v); mBubbleBarClickListener = v -> setExpanded(true); + mBubbleDragController.setupBubbleBarView(mBarView); mBarView.setOnClickListener(mBubbleBarClickListener); mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() @@ -256,6 +261,7 @@ public class BubbleBarViewController { if (b != null) { mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize)); b.getView().setOnClickListener(mBubbleClickListener); + mBubbleDragController.setupBubbleView(b.getView()); } else { Log.w(TAG, "addBubble, bubble was null!"); } @@ -307,4 +313,41 @@ public class BubbleBarViewController { mBubbleStashController.showBubbleBar(true /* expand the bubbles */); } } + + /** + * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI + * to collapse the selected bubble while it's dragged. + * @param bubbleView dragged bubble view + */ + public void onDragStart(@NonNull BubbleView bubbleView) { + mBarView.setDraggedBubble(bubbleView); + if (bubbleView.getBubble() == null) return; + mSystemUiProxy.collapseWhileDragging(bubbleView.getBubble().getKey(), true /* collapse */); + } + + /** + * Removes the dragged bubble view in the bubble bar view, and notifies SystemUI + * to expand the selected bubble when dragging finished. + * @param bubbleView dragged bubble view + */ + public void onDragEnd(@NonNull BubbleView bubbleView) { + mBarView.setDraggedBubble(null); + if (bubbleView.getBubble() == null) return; + mSystemUiProxy.collapseWhileDragging(bubbleView.getBubble().getKey(), false /* collapse */); + } + + /** + * Called when bubble was dragged into the dismiss target. Notifies System + * @param bubble dismissed bubble item + */ + public void onDismissBubbleWhileDragging(@NonNull BubbleBarItem bubble) { + mSystemUiProxy.removeBubble(bubble.getKey()); + } + + /** + * Called when bubble stack was dragged into the dismiss target + */ + public void onDismissAllBubblesWhileDragging() { + mSystemUiProxy.removeAllBubbles(); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index 6417f3c585..c47427d4fb 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -27,6 +27,8 @@ public class BubbleControllers { public final BubbleBarViewController bubbleBarViewController; public final BubbleStashController bubbleStashController; public final BubbleStashedHandleViewController bubbleStashedHandleViewController; + public final BubbleDragController bubbleDragController; + public final BubbleDismissController bubbleDismissController; private final RunnableList mPostInitRunnables = new RunnableList(); @@ -39,11 +41,15 @@ public class BubbleControllers { BubbleBarController bubbleBarController, BubbleBarViewController bubbleBarViewController, BubbleStashController bubbleStashController, - BubbleStashedHandleViewController bubbleStashedHandleViewController) { + BubbleStashedHandleViewController bubbleStashedHandleViewController, + BubbleDragController bubbleDragController, + BubbleDismissController bubbleDismissController) { this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; this.bubbleStashController = bubbleStashController; this.bubbleStashedHandleViewController = bubbleStashedHandleViewController; + this.bubbleDragController = bubbleDragController; + this.bubbleDismissController = bubbleDismissController; } /** @@ -56,6 +62,8 @@ public class BubbleControllers { bubbleBarViewController.init(taskbarControllers, this); bubbleStashedHandleViewController.init(taskbarControllers, this); bubbleStashController.init(taskbarControllers, this); + bubbleDragController.init(/* bubbleControllers = */ this); + bubbleDismissController.init(/* bubbleControllers = */ this); mPostInitRunnables.executeAllAndDestroy(); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java new file mode 100644 index 0000000000..60633762b1 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2023 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; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.res.Resources; +import android.os.SystemProperties; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.dynamicanimation.animation.DynamicAnimation; + +import com.android.launcher3.R; +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.launcher3.taskbar.TaskbarDragLayer; +import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; + +/** + * Controls dismiss view presentation for the bubble bar dismiss functionality. + * Provides the dragged view snapping to the target dismiss area and animates it. + * When the dragged bubble/bubble stack is realised inside of the target area, it gets dismissed. + * + * @see BubbleDragController + */ +public class BubbleDismissController { + private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f; + public static final boolean ENABLE_FLING_TO_DISMISS_BUBBLE = + SystemProperties.getBoolean("persist.wm.debug.fling_to_dismiss_bubble", true); + private final TaskbarActivityContext mActivity; + private final TaskbarDragLayer mDragLayer; + @Nullable + private BubbleBarViewController mBubbleBarViewController; + + // Dismiss view that's attached to drag layer. It consists of the scrim view and the circular + // dismiss view used as a dismiss target. + @Nullable + private DismissView mDismissView; + + // The currently magnetized object, which is being dragged and will be attracted to the magnetic + // dismiss target. This is either the stack itself, or an individual bubble. + @Nullable + private MagnetizedObject mMagnetizedObject; + + // The MagneticTarget instance for our circular dismiss view. This is added to the + // MagnetizedObject instances for the stack and any dragged-out bubbles. + @Nullable + private MagnetizedObject.MagneticTarget mMagneticTarget; + @Nullable + private ValueAnimator mDismissAnimator; + + public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) { + mActivity = activity; + mDragLayer = dragLayer; + } + + /** + * Initializes dependencies when bubble controllers are created. + * Should be careful to only access things that were created in constructors for now, as some + * controllers may still be waiting for init(). + */ + public void init(@NonNull BubbleControllers bubbleControllers) { + mBubbleBarViewController = bubbleControllers.bubbleBarViewController; + } + + /** + * Setup the dismiss view and magnetized object that will be attracted to magnetic target. + * Should be called before handling events or showing/hiding dismiss view. + * @param magnetizedView the view to be pulled into target dismiss area + */ + public void setupDismissView(@NonNull View magnetizedView) { + setupDismissView(); + setupMagnetizedObject(magnetizedView); + } + + /** + * Handle the touch event and pass it to the magnetized object. + * It should be called after {@code setupDismissView} + */ + public boolean handleTouchEvent(@NonNull MotionEvent event) { + return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event); + } + + /** + * Show dismiss view with animation + * It should be called after {@code setupDismissView} + */ + public void showDismissView() { + if (mDismissView == null) return; + mDismissView.show(); + } + + /** + * Hide dismiss view with animation + * It should be called after {@code setupDismissView} + */ + public void hideDismissView() { + if (mDismissView == null) return; + mDismissView.hide(); + } + + /** + * Dismiss magnetized object when it's released in the dismiss target area + */ + private void dismissMagnetizedObject() { + if (mMagnetizedObject == null || mBubbleBarViewController == null) return; + if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleView) { + BubbleView bubbleView = (BubbleView) mMagnetizedObject.getUnderlyingObject(); + if (bubbleView.getBubble() != null) { + mBubbleBarViewController.onDismissBubbleWhileDragging(bubbleView.getBubble()); + } + } else if (mMagnetizedObject.getUnderlyingObject() instanceof BubbleBarView) { + mBubbleBarViewController.onDismissAllBubblesWhileDragging(); + } + cleanUpAnimatedViews(); + } + + /** + * Animate dismiss view when magnetized object gets stuck in the magnetic target + * @param view captured view + */ + private void animateDismissCaptured(@NonNull View view) { + cancelAnimations(); + mDismissAnimator = createDismissAnimator(view); + mDismissAnimator.start(); + } + + /** + * Animate dismiss view when magnetized object gets unstuck from the magnetic target + */ + private void animateDismissReleased() { + if (mDismissAnimator == null) return; + mDismissAnimator.removeAllListeners(); + mDismissAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + cancelAnimations(); + } + + @Override + public void onAnimationEnd(Animator animation) { + cancelAnimations(); + } + }); + mDismissAnimator.reverse(); + } + + /** + * Cancel and clear dismiss animations and reset view states + */ + private void cancelAnimations() { + if (mDismissAnimator == null) return; + ValueAnimator animator = mDismissAnimator; + mDismissAnimator = null; + animator.cancel(); + } + + /** + * Clean up views changed during animation + */ + private void cleanUpAnimatedViews() { + // Cancel animations + cancelAnimations(); + // Reset dismiss view + if (mDismissView != null) { + mDismissView.getCircle().setScaleX(1f); + mDismissView.getCircle().setScaleY(1f); + } + // Reset magnetized view + if (mMagnetizedObject != null) { + mMagnetizedObject.getUnderlyingObject().setAlpha(1f); + mMagnetizedObject.getUnderlyingObject().setScaleX(1f); + mMagnetizedObject.getUnderlyingObject().setScaleY(1f); + } + } + + private void setupDismissView() { + if (mDismissView != null) return; + mDismissView = new DismissView(mActivity.getApplicationContext()); + BubbleDismissViewUtils.setup(mDismissView); + mDragLayer.addView(mDismissView, /* index = */ 0, + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + setupMagneticTarget(mDismissView.getCircle()); + } + + private void setupMagneticTarget(@NonNull View view) { + int magneticFieldRadius = mActivity.getResources().getDimensionPixelSize( + R.dimen.bubblebar_dismiss_target_size); + mMagneticTarget = new MagnetizedObject.MagneticTarget(view, magneticFieldRadius); + } + + private void setupMagnetizedObject(@NonNull View magnetizedView) { + mMagnetizedObject = new MagnetizedObject<>(mActivity.getApplicationContext(), + magnetizedView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) { + @Override + public float getWidth(@NonNull View underlyingObject) { + return underlyingObject.getWidth(); + } + + @Override + public float getHeight(@NonNull View underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + }; + + mMagnetizedObject.setHapticsEnabled(true); + mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_BUBBLE); + mMagnetizedObject.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY); + if (mMagneticTarget != null) { + mMagnetizedObject.addTarget(mMagneticTarget); + } + mMagnetizedObject.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + animateDismissCaptured(magnetizedView); + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + animateDismissReleased(); + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + dismissMagnetizedObject(); + } + }); + } + + private ValueAnimator createDismissAnimator(@NonNull View magnetizedView) { + Resources resources = mActivity.getResources(); + int expandedSize = resources.getDimensionPixelSize(R.dimen.bubblebar_dismiss_target_size); + int collapsedSize = resources.getDimensionPixelSize( + R.dimen.bubblebar_dismiss_target_small_size); + float minScale = (float) collapsedSize / expandedSize; + ValueAnimator animator = ValueAnimator.ofFloat(1f, minScale); + animator.addUpdateListener(animation -> { + if (mDismissView == null) return; + final float animatedValue = (float) animation.getAnimatedValue(); + mDismissView.getCircle().setScaleX(animatedValue); + mDismissView.getCircle().setScaleY(animatedValue); + magnetizedView.setAlpha(animatedValue); + if (magnetizedView instanceof BubbleBarView) { + magnetizedView.setScaleX(animatedValue); + magnetizedView.setScaleY(animatedValue); + } + }); + return animator; + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt new file mode 100644 index 0000000000..4b235a97d2 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissViewExt.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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. + */ +@file:JvmName("BubbleDismissViewUtils") + +package com.android.launcher3.taskbar.bubbles + +import com.android.launcher3.R +import com.android.wm.shell.common.bubbles.DismissView + +/** + * Dismiss view is shared from WMShell. It requires setup with local resources. + * + * Usage: + * - Kotlin `dismissView.setup()` + * - Java `BubbleDismissViewUtils.setup(dismissView)` + */ +fun DismissView.setup() { + setup( + DismissView.Config( + targetSizeResId = R.dimen.bubblebar_dismiss_target_size, + iconSizeResId = R.dimen.bubblebar_dismiss_target_icon_size, + bottomMarginResId = R.dimen.bubblebar_dismiss_target_bottom_margin, + floatingGradientHeightResId = R.dimen.bubblebar_dismiss_floating_gradient_height, + floatingGradientColorResId = android.R.color.system_neutral1_900, + backgroundResId = R.drawable.bg_bubble_dismiss_circle, + iconResId = R.drawable.ic_bubble_dismiss_white + ) + ) +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java new file mode 100644 index 0000000000..28dc62c040 --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 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; + +import android.annotation.SuppressLint; +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.android.launcher3.taskbar.TaskbarActivityContext; +import com.android.wm.shell.common.bubbles.RelativeTouchListener; + +/** + * Controls bubble bar drag to dismiss interaction. + * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}. + * Supported interactions: + * - Drag a single bubble view into dismiss target to remove it. + * - Drag the bubble stack into dismiss target to remove all. + * Restores initial position of dragged view if released outside of the dismiss target. + */ +public class BubbleDragController { + private final TaskbarActivityContext mActivity; + private BubbleBarViewController mBubbleBarViewController; + private BubbleDismissController mBubbleDismissController; + + public BubbleDragController(TaskbarActivityContext activity) { + mActivity = activity; + } + + /** + * Initializes dependencies when bubble controllers are created. + * Should be careful to only access things that were created in constructors for now, as some + * controllers may still be waiting for init(). + */ + public void init(@NonNull BubbleControllers bubbleControllers) { + mBubbleBarViewController = bubbleControllers.bubbleBarViewController; + mBubbleDismissController = bubbleControllers.bubbleDismissController; + } + + /** + * Setup the bubble view for dragging and attach touch listener to it + */ + @SuppressLint("ClickableViewAccessibility") + public void setupBubbleView(@NonNull BubbleView bubbleView) { + // Don't setup dragging for overflow bubble view + if (bubbleView.getBubble() == null + || !(bubbleView.getBubble() instanceof BubbleBarBubble)) return; + bubbleView.setOnTouchListener(new BaseDragListener() { + @Override + protected void onDragStart() { + super.onDragStart(); + mBubbleBarViewController.onDragStart(bubbleView); + } + + @Override + protected void onDragEnd() { + super.onDragEnd(); + mBubbleBarViewController.onDragEnd(bubbleView); + } + }); + } + + /** + * Setup the bubble bar view for dragging and attach touch listener to it + */ + @SuppressLint("ClickableViewAccessibility") + public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) { + PointF initialRelativePivot = new PointF(); + bubbleBarView.setOnTouchListener(new BaseDragListener() { + @Override + public boolean onDown(@NonNull View view, @NonNull MotionEvent event) { + if (bubbleBarView.isExpanded()) return false; + // Setup dragging only when bubble bar is collapsed + return super.onDown(view, event); + } + + @Override + protected void onDragStart() { + super.onDragStart(); + initialRelativePivot.set(bubbleBarView.getRelativePivotX(), + bubbleBarView.getRelativePivotY()); + bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f); + } + + @Override + protected void onDragEnd() { + super.onDragEnd(); + bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y); + } + }); + } + + /** + * Base drag listener for handling a single bubble view or bubble bar view dragging. + * Controls dragging interaction and interacts with {@link BubbleDismissController} + * to coordinate dismiss view presentation. + * Lifecycle methods can be overridden do add extra setup/clean up steps + */ + private class BaseDragListener extends RelativeTouchListener { + private boolean mHandling; + private boolean mDragging; + + @Override + public boolean onDown(@NonNull View view, @NonNull MotionEvent event) { + mHandling = true; + mActivity.setTaskbarWindowFullscreen(true); + mBubbleDismissController.setupDismissView(view); + mBubbleDismissController.handleTouchEvent(event); + return true; + } + + @Override + public void onMove(@NonNull View view, @NonNull MotionEvent event, float viewInitialX, + float viewInitialY, float dx, float dy) { + if (!mHandling) return; + if (!mDragging) { + // Start dragging + mDragging = true; + onDragStart(); + } + if (!mBubbleDismissController.handleTouchEvent(event)) { + // Drag the view if not processed by dismiss controller + view.setTranslationX(viewInitialX + dx); + view.setTranslationY(viewInitialY + dy); + } + } + + @Override + public void onUp(@NonNull View view, @NonNull MotionEvent event, float viewInitialX, + float viewInitialY, float dx, float dy, float velX, float velY) { + onComplete(view, event, viewInitialX, viewInitialY); + } + + @Override + public void onCancel(@NonNull View view, @NonNull MotionEvent event, float viewInitialX, + float viewInitialY, float dx, float dy) { + onComplete(view, event, viewInitialX, viewInitialY); + } + + /** + * Prepares dismiss view for dragging. + * This method can be overridden to add extra setup on drag start + */ + protected void onDragStart() { + mBubbleDismissController.showDismissView(); + } + + /** + * Cleans up dismiss view after dragging. + * This method can be overridden to add extra setup on drag end + */ + protected void onDragEnd() { + mBubbleDismissController.hideDismissView(); + } + + /** + * Complete drag handling and clean up dependencies + */ + private void onComplete(@NonNull View view, @NonNull MotionEvent event, + float viewInitialX, float viewInitialY) { + if (!mHandling) return; + if (mDragging) { + // Stop dragging + mDragging = false; + view.setTranslationX(viewInitialX); + view.setTranslationY(viewInitialY); + onDragEnd(); + } + mBubbleDismissController.handleTouchEvent(event); + mActivity.setTaskbarWindowFullscreen(false); + mHandling = false; + } + } +} diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 60784f5854..3af9d5cb83 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -661,6 +661,31 @@ public class SystemUiProxy implements ISystemUiProxy { } } + /** + * Tells SysUI to remove the bubble with the provided key. + * @param key the key of the bubble to show. + */ + public void removeBubble(String key) { + if (mBubbles == null) return; + try { + mBubbles.removeBubble(key); + } catch (RemoteException e) { + Log.w(TAG, "Failed call removeBubble"); + } + } + + /** + * Tells SysUI to remove all bubbles. + */ + public void removeAllBubbles() { + if (mBubbles == null) return; + try { + mBubbles.removeAllBubbles(); + } catch (RemoteException e) { + Log.w(TAG, "Failed call removeAllBubbles"); + } + } + /** * Tells SysUI to collapse the bubbles. */ @@ -674,6 +699,21 @@ public class SystemUiProxy implements ISystemUiProxy { } } + /** + * Tells SysUI to collapse/expand selected bubble view while it's dragged. + * Should be called only when the bubble bar is expanded. + * @param bubbleKey the key of the bubble to collapse/expand + * @param collapse whether to collapse/expand selected bubble + */ + public void collapseWhileDragging(@Nullable String bubbleKey, boolean collapse) { + if (mBubbles == null) return; + try { + mBubbles.collapseWhileDragging(bubbleKey, collapse); + } catch (RemoteException e) { + Log.w(TAG, "Failed call collapseWhileDragging"); + } + } + // // Splitscreen //