diff --git a/quickstep/res/color/bubblebar_drop_target_bg_color.xml b/quickstep/res/color/bubblebar_drop_target_bg_color.xml new file mode 100644 index 0000000000..ca37c7ffff --- /dev/null +++ b/quickstep/res/color/bubblebar_drop_target_bg_color.xml @@ -0,0 +1,19 @@ + + + + \ No newline at end of file diff --git a/quickstep/res/drawable/bg_bubble_bar_drop_target.xml b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml new file mode 100644 index 0000000000..79e43187f4 --- /dev/null +++ b/quickstep/res/drawable/bg_bubble_bar_drop_target.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/quickstep/res/layout/bubble_bar_drop_target.xml b/quickstep/res/layout/bubble_bar_drop_target.xml new file mode 100644 index 0000000000..23f240c67d --- /dev/null +++ b/quickstep/res/layout/bubble_bar_drop_target.xml @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index 93ef735700..caa949eebf 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -417,6 +417,7 @@ 80dp 1dp + 2dp 90dp 50dp @@ -432,6 +433,11 @@ 24dp 50dp 548dp + 192dp + 242dp + + + 36dp diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index a3aa93adfd..8769f110a3 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -99,6 +99,7 @@ import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.AutohideSu import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback; import com.android.launcher3.taskbar.allapps.TaskbarAllAppsController; import com.android.launcher3.taskbar.bubbles.BubbleBarController; +import com.android.launcher3.taskbar.bubbles.BubbleBarPinController; import com.android.launcher3.taskbar.bubbles.BubbleBarView; import com.android.launcher3.taskbar.bubbles.BubbleBarViewController; import com.android.launcher3.taskbar.bubbles.BubbleControllers; @@ -255,7 +256,10 @@ public class TaskbarActivityContext extends BaseTaskbarContext { new BubbleStashController(this), new BubbleStashedHandleViewController(this, bubbleHandleView), new BubbleDragController(this), - new BubbleDismissController(this, mDragLayer))); + new BubbleDismissController(this, mDragLayer), + new BubbleBarPinController(this, mDragLayer, + () -> getDeviceProfile().getDisplayInfo().currentSize) + )); } // Construct controllers. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt index 79fdedaf17..8eeb0550ae 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt @@ -15,6 +15,7 @@ */ package com.android.launcher3.taskbar.bubbles +import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.ColorFilter @@ -27,12 +28,10 @@ import com.android.launcher3.R import com.android.launcher3.Utilities import com.android.launcher3.Utilities.mapToRange import com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound -import com.android.launcher3.taskbar.TaskbarActivityContext import com.android.wm.shell.common.TriangleShape /** Drawable for the background of the bubble bar. */ -class BubbleBarBackground(context: TaskbarActivityContext, private val backgroundHeight: Float) : - Drawable() { +class BubbleBarBackground(context: Context, private val backgroundHeight: Float) : Drawable() { private val DARK_THEME_SHADOW_ALPHA = 51f private val LIGHT_THEME_SHADOW_ALPHA = 25f @@ -46,6 +45,7 @@ class BubbleBarBackground(context: TaskbarActivityContext, private val backgroun var arrowPositionX: Float = 0f private set + private var showingArrow: Boolean = false private var arrowDrawable: ShapeDrawable diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt new file mode 100644 index 0000000000..8ed994966c --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarPinController.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.content.Context +import android.graphics.Point +import android.graphics.RectF +import android.view.Gravity.BOTTOM +import android.view.Gravity.LEFT +import android.view.Gravity.RIGHT +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.updateLayoutParams +import com.android.launcher3.R +import com.android.wm.shell.common.bubbles.BaseBubblePinController +import com.android.wm.shell.common.bubbles.BubbleBarLocation + +/** + * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar + */ +class BubbleBarPinController( + private val context: Context, + private val container: FrameLayout, + private val screenSizeProvider: () -> Point +) : BaseBubblePinController() { + + private lateinit var bubbleBarViewController: BubbleBarViewController + private lateinit var bubbleStashController: BubbleStashController + private var dropTargetView: View? = null + + fun init(bubbleControllers: BubbleControllers) { + bubbleBarViewController = bubbleControllers.bubbleBarViewController + bubbleStashController = bubbleControllers.bubbleStashController + } + + override fun getScreenCenterX(): Int { + return screenSizeProvider.invoke().x / 2 + } + + override fun getExclusionRect(): RectF { + val rect = + RectF( + 0f, + 0f, + context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_width), + context.resources.getDimension(R.dimen.bubblebar_dismiss_zone_height) + ) + val screenSize = screenSizeProvider.invoke() + val middleX = screenSize.x / 2 + // Center it around the bottom center of the screen + rect.offsetTo(middleX - rect.width() / 2, screenSize.y - rect.height()) + return rect + } + + override fun createDropTargetView(): View { + return LayoutInflater.from(context) + .inflate(R.layout.bubble_bar_drop_target, container, false) + .also { view -> + dropTargetView = view + container.addView(view) + } + } + + override fun getDropTargetView(): View? { + return dropTargetView + } + + override fun removeDropTargetView(view: View) { + container.removeView(view) + dropTargetView = null + } + + @SuppressLint("RtlHardcoded") + override fun updateLocation(location: BubbleBarLocation) { + val onLeft = location.isOnLeft(container.isLayoutRtl) + + val bounds = bubbleBarViewController.bubbleBarBounds + val horizontalMargin = bubbleBarViewController.horizontalMargin + dropTargetView?.updateLayoutParams { + width = bounds.width() + height = bounds.height() + gravity = BOTTOM or (if (onLeft) LEFT else RIGHT) + leftMargin = horizontalMargin + rightMargin = horizontalMargin + bottomMargin = -bubbleStashController.bubbleBarTranslationY.toInt() + } + } +} diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java index a5da65f660..4ca7c89127 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java @@ -39,8 +39,6 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.launcher3.R; import com.android.launcher3.anim.SpringAnimationBuilder; -import com.android.launcher3.taskbar.TaskbarActivityContext; -import com.android.launcher3.views.ActivityContext; import com.android.wm.shell.common.bubbles.BubbleBarLocation; import java.util.List; @@ -159,8 +157,6 @@ public class BubbleBarView extends FrameLayout { public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - TaskbarActivityContext activityContext = ActivityContext.lookupContext(context); - setAlpha(0); setVisibility(INVISIBLE); mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap); @@ -171,7 +167,7 @@ public class BubbleBarView extends FrameLayout { setClipToPadding(false); - mBubbleBarBackground = new BubbleBarBackground(activityContext, + mBubbleBarBackground = new BubbleBarBackground(context, getResources().getDimensionPixelSize(R.dimen.bubblebar_size)); setBackgroundDrawable(mBubbleBarBackground); @@ -379,6 +375,36 @@ public class BubbleBarView extends FrameLayout { return mRelativePivotY; } + /** Prepares for animating a bubble while being stashed. */ + public void prepareForAnimatingBubbleWhileStashed(String bubbleKey) { + // we're about to animate the new bubble in. the new bubble has already been added to this + // view, but we're currently stashed, so before we can start the animation we need make + // everything else in the bubble bar invisible, except for the bubble that's being animated. + setBackground(null); + for (int i = 0; i < getChildCount(); i++) { + final BubbleView view = (BubbleView) getChildAt(i); + final String key = view.getBubble().getKey(); + if (!bubbleKey.equals(key)) { + view.setVisibility(INVISIBLE); + } + } + setVisibility(VISIBLE); + setAlpha(1); + setTranslationY(0); + setScaleX(1); + setScaleY(1); + } + + /** Resets the state after the bubble animation completed. */ + public void onAnimatingBubbleCompleted() { + setBackground(mBubbleBarBackground); + for (int i = 0; i < getChildCount(); i++) { + final BubbleView view = (BubbleView) getChildAt(i); + view.setVisibility(VISIBLE); + view.setAlpha(1f); + } + } + // TODO: (b/280605790) animate it @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 0f019a3d40..96d91eae49 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -34,6 +34,7 @@ import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarStashController; +import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.SystemUiProxy; @@ -81,6 +82,8 @@ public class BubbleBarViewController { private boolean mHiddenForNoBubbles = true; private boolean mShouldShowEducation; + private BubbleBarViewAnimator mBubbleBarViewAnimator; + public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView) { mActivity = activity; mBarView = barView; @@ -113,6 +116,8 @@ public class BubbleBarViewController { mBarView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged() ); + + mBubbleBarViewAnimator = new BubbleBarViewAnimator(mBarView, mBubbleStashController); } private void onBubbleClicked(View v) { @@ -316,6 +321,12 @@ public class BubbleBarViewController { new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT)); b.getView().setOnClickListener(mBubbleClickListener); mBubbleDragController.setupBubbleView(b.getView()); + + boolean isStashedOrGone = + mBubbleStashController.isStashed() || mBarView.getVisibility() != VISIBLE; + if (b instanceof BubbleBarBubble && isStashedOrGone) { + mBubbleBarViewAnimator.animateBubbleInForStashed((BubbleBarBubble) b); + } } else { Log.w(TAG, "addBubble, bubble was null!"); } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java index c47427d4fb..90f1be36cb 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java @@ -29,6 +29,7 @@ public class BubbleControllers { public final BubbleStashedHandleViewController bubbleStashedHandleViewController; public final BubbleDragController bubbleDragController; public final BubbleDismissController bubbleDismissController; + public final BubbleBarPinController bubbleBarPinController; private final RunnableList mPostInitRunnables = new RunnableList(); @@ -43,13 +44,15 @@ public class BubbleControllers { BubbleStashController bubbleStashController, BubbleStashedHandleViewController bubbleStashedHandleViewController, BubbleDragController bubbleDragController, - BubbleDismissController bubbleDismissController) { + BubbleDismissController bubbleDismissController, + BubbleBarPinController bubbleBarPinController) { this.bubbleBarController = bubbleBarController; this.bubbleBarViewController = bubbleBarViewController; this.bubbleStashController = bubbleStashController; this.bubbleStashedHandleViewController = bubbleStashedHandleViewController; this.bubbleDragController = bubbleDragController; this.bubbleDismissController = bubbleDismissController; + this.bubbleBarPinController = bubbleBarPinController; } /** @@ -64,6 +67,7 @@ public class BubbleControllers { bubbleStashController.init(taskbarControllers, this); bubbleDragController.init(/* bubbleControllers = */ this); bubbleDismissController.init(/* bubbleControllers = */ this); + bubbleBarPinController.init(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 index 73c71c8edf..a40f33c595 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDismissController.java @@ -67,6 +67,9 @@ public class BubbleDismissController { @Nullable private BubbleDragAnimator mAnimator; + @Nullable + private Listener mListener; + public BubbleDismissController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) { mActivity = activity; mDragLayer = dragLayer; @@ -81,6 +84,13 @@ public class BubbleDismissController { mBubbleBarViewController = bubbleControllers.bubbleBarViewController; } + /** + * Set listener to be notified of dismiss events + */ + public void setListener(@Nullable Listener listener) { + mListener = listener; + } + /** * 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. @@ -189,6 +199,9 @@ public class BubbleDismissController { @NonNull MagnetizedObject draggedObject) { if (mAnimator == null) return; mAnimator.animateDismissCaptured(); + if (mListener != null) { + mListener.onStuckToDismissChanged(true /* stuck */); + } } @Override @@ -197,6 +210,9 @@ public class BubbleDismissController { float velX, float velY, boolean wasFlungOut) { if (mAnimator == null) return; mAnimator.animateDismissReleased(); + if (mListener != null) { + mListener.onStuckToDismissChanged(false /* stuck */); + } } @Override @@ -206,4 +222,10 @@ public class BubbleDismissController { } }); } + + /** Interface to receive updates about the dismiss state */ + public interface Listener { + /** Called when view is stuck or unstuck from dismiss target */ + void onStuckToDismissChanged(boolean stuck); + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java index 08fd681b4c..dab7d9d07a 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleDragController.java @@ -25,10 +25,11 @@ import android.view.ViewConfiguration; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.R; import com.android.launcher3.taskbar.TaskbarActivityContext; /** - * Controls bubble bar drag to dismiss interaction. + * Controls bubble bar drag interactions. * Interacts with {@link BubbleDismissController}, used by {@link BubbleBarViewController}. * Supported interactions: * - Drag a single bubble view into dismiss target to remove it. @@ -39,6 +40,7 @@ public class BubbleDragController { private final TaskbarActivityContext mActivity; private BubbleBarViewController mBubbleBarViewController; private BubbleDismissController mBubbleDismissController; + private BubbleBarPinController mBubbleBarPinController; public BubbleDragController(TaskbarActivityContext activity) { mActivity = activity; @@ -52,6 +54,12 @@ public class BubbleDragController { public void init(@NonNull BubbleControllers bubbleControllers) { mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleDismissController = bubbleControllers.bubbleDismissController; + mBubbleBarPinController = bubbleControllers.bubbleBarPinController; + mBubbleBarPinController.setListener(location -> { + // TODO(b/330585397): update bubble bar location in shell + }); + mBubbleDismissController.setListener( + stuck -> mBubbleBarPinController.setDropTargetHidden(stuck)); } /** @@ -88,6 +96,10 @@ public class BubbleDragController { @SuppressLint("ClickableViewAccessibility") public void setupBubbleBarView(@NonNull BubbleBarView bubbleBarView) { PointF initialRelativePivot = new PointF(); + final int restingElevation = bubbleBarView.getResources().getDimensionPixelSize( + R.dimen.bubblebar_elevation); + final int dragElevation = bubbleBarView.getResources().getDimensionPixelSize( + R.dimen.bubblebar_drag_elevation); bubbleBarView.setOnTouchListener(new BubbleTouchListener() { @Override protected boolean onTouchDown(@NonNull View view, @NonNull MotionEvent event) { @@ -102,12 +114,31 @@ public class BubbleDragController { // By default the bubble bar view pivot is in bottom right corner, while dragging // it should be centered in order to align it with the dismiss target view bubbleBarView.setRelativePivot(/* x = */ 0.5f, /* y = */ 0.5f); + bubbleBarView.setElevation(dragElevation); + mBubbleBarPinController.onDragStart( + bubbleBarView.getBubbleBarLocation().isOnLeft(bubbleBarView.isLayoutRtl())); + } + + @Override + protected void onDragUpdate(float x, float y) { + mBubbleBarPinController.onDragUpdate(x, y); + } + + @Override + protected void onDragRelease() { + mBubbleBarPinController.onDragEnd(); + } + + @Override + protected void onDragDismiss() { + mBubbleBarPinController.onDragEnd(); } @Override void onDragEnd() { // Restoring the initial pivot for the bubble bar view bubbleBarView.setRelativePivot(initialRelativePivot.x, initialRelativePivot.y); + bubbleBarView.setElevation(restingElevation); } }); } @@ -169,6 +200,13 @@ public class BubbleDragController { */ abstract void onDragStart(); + /** + * Called when bubble is dragged to new coordinates. + * Not called while bubble is stuck to the dismiss target. + */ + protected void onDragUpdate(float x, float y) { + } + /** * Called when the dragging interaction has ended and all the animations have completed */ @@ -232,8 +270,10 @@ public class BubbleDragController { * @param event the motion event */ protected void onTouchMove(@NonNull View view, @NonNull MotionEvent event) { - final float dx = event.getRawX() - mTouchDownLocation.x; - final float dy = event.getRawY() - mTouchDownLocation.y; + float rawX = event.getRawX(); + float rawY = event.getRawY(); + final float dx = rawX - mTouchDownLocation.x; + final float dy = rawY - mTouchDownLocation.y; switch (mState) { case TOUCHED: final boolean movedOut = Math.hypot(dx, dy) > mTouchSlop; @@ -244,7 +284,7 @@ public class BubbleDragController { } break; case DRAGGING: - drag(view, event, dx, dy); + drag(view, event, dx, dy, rawX, rawY); break; } } @@ -293,10 +333,12 @@ public class BubbleDragController { mBubbleDismissController.showDismissView(); } - private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy) { + private void drag(@NonNull View view, @NonNull MotionEvent event, float dx, float dy, + float x, float y) { if (mBubbleDismissController.handleTouchEvent(event)) return; view.setTranslationX(mViewInitialPosition.x + dx); view.setTranslationY(mViewInitialPosition.y + dy); + onDragUpdate(x, y); } private void stopDragging(@NonNull View view, @NonNull MotionEvent event) { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java index 6549ad6846..bcdc718955 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleView.java @@ -145,7 +145,7 @@ public class BubbleView extends ConstraintLayout { } /** Sets the bubble being rendered in this view. */ - void setBubble(BubbleBarBubble bubble) { + public void setBubble(BubbleBarBubble bubble) { mBubble = bubble; mBubbleIcon.setImageBitmap(bubble.getIcon()); mAppIcon.setImageBitmap(bubble.getBadge()); @@ -159,7 +159,7 @@ public class BubbleView extends ConstraintLayout { * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't * come from an app. */ - void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { + public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { mBubble = overflow; mBubbleIcon.setImageBitmap(bitmap); mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge @@ -168,7 +168,7 @@ public class BubbleView extends ConstraintLayout { /** Returns the bubble being rendered in this view. */ @Nullable - BubbleBarItem getBubble() { + public BubbleBarItem getBubble() { return mBubble; } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt new file mode 100644 index 0000000000..bcb9f4d31b --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 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.animation + +import android.view.View +import android.view.View.VISIBLE +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleStashController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.wm.shell.shared.animation.PhysicsAnimator + +/** Handles animations for bubble bar bubbles. */ +class BubbleBarViewAnimator +@JvmOverloads +constructor( + private val bubbleBarView: BubbleBarView, + private val bubbleStashController: BubbleStashController, + private val scheduler: Scheduler = HandlerScheduler(bubbleBarView) +) { + + private companion object { + /** The time to show the flyout. */ + const val FLYOUT_DELAY_MS: Long = 2500 + /** The translation Y the new bubble will animate to. */ + const val BUBBLE_ANIMATION_TRANSLATION_Y = -50f + } + + /** An interface for scheduling jobs. */ + interface Scheduler { + + /** Schedule the given [block] to run. */ + fun post(block: () -> Unit) + + /** Schedule the given [block] to start with a delay of [delayMillis]. */ + fun postDelayed(delayMillis: Long, block: () -> Unit) + } + + /** A [Scheduler] that uses a Handler to run jobs. */ + private class HandlerScheduler(private val view: View) : Scheduler { + + override fun post(block: () -> Unit) { + view.post(block) + } + + override fun postDelayed(delayMillis: Long, block: () -> Unit) { + view.postDelayed(block, delayMillis) + } + } + + private val springConfig = + PhysicsAnimator.SpringConfig( + stiffness = SpringForce.STIFFNESS_LOW, + dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY + ) + + /** Animates a bubble for the state where the bubble bar is stashed. */ + fun animateBubbleInForStashed(b: BubbleBarBubble) { + val bubbleView = b.view + val animator = PhysicsAnimator.getInstance(bubbleView) + if (animator.isRunning()) animator.cancel() + // the animation of a new bubble is divided into 2 parts. The first part shows the bubble + // and the second part hides it after a delay. + val showAnimation = buildShowAnimation(bubbleView, b.key, animator) + val hideAnimation = buildHideAnimation(animator) + scheduler.post(showAnimation) + scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation) + } + + /** Returns a lambda that starts the animation that shows the new bubble. */ + private fun buildShowAnimation( + bubbleView: BubbleView, + key: String, + animator: PhysicsAnimator + ): () -> Unit = { + bubbleBarView.prepareForAnimatingBubbleWhileStashed(key) + animator.setDefaultSpringConfig(springConfig) + animator + .spring(DynamicAnimation.ALPHA, 1f) + .spring(DynamicAnimation.TRANSLATION_Y, BUBBLE_ANIMATION_TRANSLATION_Y) + bubbleView.alpha = 0f + bubbleView.visibility = VISIBLE + animator.start() + } + + /** Returns a lambda that starts the animation that hides the new bubble. */ + private fun buildHideAnimation(animator: PhysicsAnimator): () -> Unit = { + animator.setDefaultSpringConfig(springConfig) + animator + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.TRANSLATION_Y, 0f) + .addEndListener { _, _, _, canceled, _, _, allRelevantPropertyAnimsEnded -> + if (!canceled && allRelevantPropertyAnimsEnded) { + if (bubbleStashController.isStashed) { + bubbleBarView.alpha = 0f + } + bubbleBarView.onAnimatingBubbleCompleted() + } + } + animator.start() + } +} 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 new file mode 100644 index 0000000000..c17aeaaf76 --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 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.animation + +import android.content.Context +import android.graphics.Color +import android.graphics.Path +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.View.INVISIBLE +import android.view.View.VISIBLE +import android.widget.FrameLayout +import androidx.core.graphics.drawable.toBitmap +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.R +import com.android.launcher3.taskbar.bubbles.BubbleBarBubble +import com.android.launcher3.taskbar.bubbles.BubbleBarOverflow +import com.android.launcher3.taskbar.bubbles.BubbleBarView +import com.android.launcher3.taskbar.bubbles.BubbleStashController +import com.android.launcher3.taskbar.bubbles.BubbleView +import com.android.wm.shell.common.bubbles.BubbleInfo +import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarViewAnimatorTest { + + private val context = ApplicationProvider.getApplicationContext() + private val animatorScheduler = TestBubbleBarViewAnimatorScheduler() + + @Before + fun setUp() { + PhysicsAnimatorTestUtils.prepareForTest() + } + + @Test + fun animateBubbleInForStashed() { + 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) + + 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) + + 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 animator = + BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + animator.animateBubbleInForStashed(bubble) + } + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + assertThat(overflowView.visibility).isEqualTo(INVISIBLE) + assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleView.visibility).isEqualTo(VISIBLE) + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + DynamicAnimation.ALPHA, + DynamicAnimation.TRANSLATION_Y + ) + + assertThat(bubbleView.alpha).isEqualTo(1) + assertThat(bubbleView.translationY).isEqualTo(-50) + + assertThat(animatorScheduler.delayedBlock).isNotNull() + InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!) + + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + + PhysicsAnimatorTestUtils.blockUntilAnimationsEnd( + DynamicAnimation.ALPHA, + DynamicAnimation.TRANSLATION_Y + ) + + assertThat(bubbleView.alpha).isEqualTo(1) + assertThat(bubbleView.visibility).isEqualTo(VISIBLE) + assertThat(bubbleView.translationY).isEqualTo(0) + assertThat(bubbleBarView.alpha).isEqualTo(0) + assertThat(overflowView.alpha).isEqualTo(1) + assertThat(overflowView.visibility).isEqualTo(VISIBLE) + } + + private class TestBubbleBarViewAnimatorScheduler : BubbleBarViewAnimator.Scheduler { + + var delayedBlock: (() -> Unit)? = null + private set + + override fun post(block: () -> Unit) { + block.invoke() + } + + override fun postDelayed(delayMillis: Long, block: () -> Unit) { + check(delayedBlock == null) { "there is already a pending block waiting to run" } + delayedBlock = block + } + } +}