/* * 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.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.launcher3.Utilities.mapRange; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; import android.animation.Animator; import android.animation.AnimatorSet; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.app.animation.Interpolators; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarSharedState; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.taskbar.bubbles.BubbleBarLocationDropTarget.BubbleBarDragListener; import com.android.launcher3.taskbar.bubbles.animation.BubbleBarViewAnimator; import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutController; import com.android.launcher3.taskbar.bubbles.flyout.BubbleBarFlyoutPositioner; import com.android.launcher3.taskbar.bubbles.flyout.FlyoutCallbacks; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.SystemUiProxy; import com.android.wm.shell.Flags; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.DeviceConfig; import java.io.PrintWriter; import java.util.List; import java.util.Objects; import java.util.function.Consumer; /** * Controller for {@link BubbleBarView}. Manages the visibility of the bubble bar as well as * responding to changes in bubble state provided by BubbleBarController. */ public class BubbleBarViewController { private static final String TAG = "BubbleBarViewController"; private static final float APP_ICON_SMALL_DP = 44f; private static final float APP_ICON_MEDIUM_DP = 48f; private static final float APP_ICON_LARGE_DP = 52f; /** The dot size is defined as a percentage of the icon size. */ private static final float DOT_TO_BUBBLE_SIZE_RATIO = 0.228f; public static final int TASKBAR_FADE_IN_DURATION_MS = 150; public static final int TASKBAR_FADE_IN_DELAY_MS = 50; public static final int TASKBAR_FADE_OUT_DURATION_MS = 100; private final SystemUiProxy mSystemUiProxy; private final TaskbarActivityContext mActivity; private final BubbleBarView mBarView; private int mIconSize; private int mBubbleBarPadding; private final int mDragElevation; // Initialized in init. private BubbleStashController mBubbleStashController; private BubbleBarController mBubbleBarController; private BubbleDragController mBubbleDragController; private TaskbarStashController mTaskbarStashController; private TaskbarInsetsController mTaskbarInsetsController; private TaskbarViewPropertiesProvider mTaskbarViewPropertiesProvider; private View.OnClickListener mBubbleClickListener; private BubbleView.Controller mBubbleViewController; private BubbleBarOverflow mOverflowBubble; // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing private final MultiValueAlpha mBubbleBarAlpha; private final AnimatedFloat mBubbleBarBubbleAlpha = new AnimatedFloat(this::updateBubbleAlpha); private final AnimatedFloat mBubbleBarBackgroundAlpha = new AnimatedFloat( this::updateBackgroundAlpha); private final AnimatedFloat mBubbleBarScaleX = new AnimatedFloat(this::updateScaleX); private final AnimatedFloat mBubbleBarScaleY = new AnimatedFloat(this::updateScaleY); private final AnimatedFloat mBubbleBarBackgroundScaleX = new AnimatedFloat( this::updateBackgroundScaleX); private final AnimatedFloat mBubbleBarBackgroundScaleY = new AnimatedFloat( this::updateBackgroundScaleY); private final AnimatedFloat mBubbleBarTranslationY = new AnimatedFloat( this::updateTranslationY); private final AnimatedFloat mBubbleOffsetY = new AnimatedFloat( this::updateBubbleOffsetY); private final AnimatedFloat mBubbleBarPinning = new AnimatedFloat(pinningProgress -> { updateTranslationY(); setBubbleBarScaleAndPadding(pinningProgress); }); private final BubbleBarDragListener mDragListener = new BubbleBarDragListener() { @Override public void getBubbleBarLocationHitRect(@NonNull BubbleBarLocation bubbleBarLocation, Rect outRect) { Point screenSize = DisplayController.INSTANCE.get(mActivity).getInfo().currentSize; outRect.top = screenSize.y - mBubbleBarDropTargetSize; outRect.bottom = screenSize.y; if (bubbleBarLocation.isOnLeft(mBarView.isLayoutRtl())) { outRect.left = 0; outRect.right = mBubbleBarDropTargetSize; } else { outRect.left = screenSize.x - mBubbleBarDropTargetSize; outRect.right = screenSize.x; } } @Override public void onLauncherItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location, @NonNull ItemInfo itemInfo) { AbstractFloatingView.closeAllOpenViews(mActivity); if (itemInfo instanceof WorkspaceItemInfo) { ShortcutInfo shortcutInfo = ((WorkspaceItemInfo) itemInfo).getDeepShortcutInfo(); if (shortcutInfo != null) { mSystemUiProxy.showShortcutBubble(shortcutInfo, location); return; } } Intent itemIntent = itemInfo.getIntent(); if (itemIntent != null && itemIntent.getComponent() != null) { itemIntent.setPackage(itemIntent.getComponent().getPackageName()); mSystemUiProxy.showAppBubble(itemIntent, itemInfo.user, location); } } @Override public void onLauncherItemDraggedOutsideBubbleBarDropZone() { onItemDraggedOutsideBubbleBarDropZone(); mSystemUiProxy.showBubbleDropTarget(/* show = */ false); } @Override public void onLauncherItemDraggedOverBubbleBarDragZone( @NonNull BubbleBarLocation location) { onDragItemOverBubbleBarDragZone(location); mSystemUiProxy.showBubbleDropTarget(/* show = */ true, location); } @NonNull @Override public View getDropView() { return mBarView; } }; // Modified when swipe up is happening on the bubble bar or task bar. private float mBubbleBarSwipeUpTranslationY; // Modified when bubble bar is springing back into the stash handle. private float mBubbleBarStashTranslationY; // Minimum distance between the BubbleBar and the taskbar private final int mBubbleBarTaskbarMinDistance; // Whether the bar is hidden for a sysui state. private boolean mHiddenForSysui; // Whether the bar is hidden because there are no bubbles. private boolean mHiddenForNoBubbles = true; // Whether the bar is hidden when stashed private boolean mHiddenForStashed; private boolean mShouldShowEducation; public boolean mOverflowAdded; private boolean mWasStashedBeforeEnteringBubbleDragZone = false; /** This field is used solely to track the bubble bar location prior to the start of the drag */ private @Nullable BubbleBarLocation mBubbleBarDragLocation; private BubbleBarViewAnimator mBubbleBarViewAnimator; private final FrameLayout mBubbleBarContainer; private BubbleBarFlyoutController mBubbleBarFlyoutController; private BubbleBarPinController mBubbleBarPinController; private TaskbarSharedState mTaskbarSharedState; private final BubbleBarLocationDropTarget mBubbleBarLeftDropTarget; private final BubbleBarLocationDropTarget mBubbleBarRightDropTarget; private final TimeSource mTimeSource = System::currentTimeMillis; private final int mTaskbarTranslationDelta; private final int mBubbleBarDropTargetSize; @Nullable private BubbleBarBoundsChangeListener mBoundsChangeListener; public BubbleBarViewController(TaskbarActivityContext activity, BubbleBarView barView, FrameLayout bubbleBarContainer) { mActivity = activity; mBarView = barView; mBubbleBarContainer = bubbleBarContainer; mSystemUiProxy = SystemUiProxy.INSTANCE.get(mActivity); mBubbleBarAlpha = new MultiValueAlpha(mBarView, 1 /* num alpha channels */); Resources res = activity.getResources(); mIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); mBubbleBarTaskbarMinDistance = res.getDimensionPixelSize( R.dimen.bubblebar_transient_taskbar_min_distance); mDragElevation = res.getDimensionPixelSize(R.dimen.dragged_bubble_elevation); mTaskbarTranslationDelta = getBubbleBarTranslationDeltaForTaskbar(activity); if (DeviceConfig.isSmallTablet(mActivity)) { mBubbleBarDropTargetSize = res.getDimensionPixelSize(R.dimen.drag_zone_bubble_fold); } else { mBubbleBarDropTargetSize = res.getDimensionPixelSize(R.dimen.drag_zone_bubble_tablet); } mBubbleBarLeftDropTarget = new BubbleBarLocationDropTarget(BubbleBarLocation.LEFT, mDragListener); mBubbleBarRightDropTarget = new BubbleBarLocationDropTarget(BubbleBarLocation.RIGHT, mDragListener); } /** Initializes controller. */ public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers, TaskbarViewPropertiesProvider taskbarViewPropertiesProvider) { mTaskbarSharedState = controllers.getSharedState(); mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleBarController = bubbleControllers.bubbleBarController; mBubbleDragController = bubbleControllers.bubbleDragController; mBubbleBarPinController = bubbleControllers.bubbleBarPinController; mTaskbarStashController = controllers.taskbarStashController; mTaskbarInsetsController = controllers.taskbarInsetsController; mBubbleBarFlyoutController = new BubbleBarFlyoutController( mBubbleBarContainer, createFlyoutPositioner(), createFlyoutCallbacks()); mBubbleBarViewAnimator = new BubbleBarViewAnimator( mBarView, mBubbleStashController, mBubbleBarFlyoutController, createBubbleBarParentViewController(), mBubbleBarController::showExpandedView, () -> setHiddenForBubbles(false)); mTaskbarViewPropertiesProvider = taskbarViewPropertiesProvider; onBubbleBarConfigurationChanged(/* animate= */ false); mActivity.addOnDeviceProfileChangeListener( dp -> onBubbleBarConfigurationChanged(/* animate= */ true)); mBubbleBarScaleY.updateValue(1f); mBubbleClickListener = v -> onBubbleClicked((BubbleView) v); mBubbleDragController.setupBubbleBarView(mBarView); mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView); if (!Flags.enableOptionalBubbleOverflow()) { showOverflow(true); } if (!mBubbleStashController.isTransientTaskBar()) { // TODO(b/380274085) for transient taskbar mode, the click is also handled by the input // consumer. This check can be removed once b/380274085 is fixed. mBarView.setOnClickListener(v -> setExpanded(!mBarView.isExpanded())); } mBarView.addOnLayoutChangeListener( (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); if (mBoundsChangeListener != null) { mBoundsChangeListener.onBoundsChanged(); } }); float pinningValue = mActivity.isTransientTaskbar() ? PINNING_TRANSIENT : PINNING_PERSISTENT; mBubbleBarPinning.updateValue(pinningValue); mBarView.setController(new BubbleBarView.Controller() { @Override public float getBubbleBarTranslationY() { return mBubbleStashController.getBubbleBarTranslationY(); } @Override public void onBubbleBarTouched() { if (isAnimatingNewBubble()) { interruptAnimationForTouch(); } } @Override public void expandBubbleBar() { BubbleBarViewController.this.setExpanded( /* isExpanded= */ true, /* maybeShowEdu*/ true); } @Override public void dismissBubbleBar() { onDismissAllBubbles(); } @Override public void updateBubbleBarLocation(BubbleBarLocation location, @BubbleBarLocation.UpdateSource int source) { mBubbleBarController.updateBubbleBarLocation(location, source); } @Override public void setIsDragging(boolean dragging) { mBubbleBarContainer.setElevation(dragging ? mDragElevation : 0); } }); mBubbleViewController = new BubbleView.Controller() { @Override public BubbleBarLocation getBubbleBarLocation() { return BubbleBarViewController.this.getBubbleBarLocation(); } @Override public void dismiss(BubbleView bubble) { if (bubble.getBubble() != null) { notifySysUiBubbleDismissed(bubble.getBubble()); } onBubbleDismissed(bubble); } @Override public void collapse() { collapseBubbleBar(); } @Override public void updateBubbleBarLocation(BubbleBarLocation location, @BubbleBarLocation.UpdateSource int source) { mBubbleBarController.updateBubbleBarLocation(location, source); } }; } /** Adds bubble bar locations drop zones to the drag controller. */ public void addBubbleBarDropTargets(DragController dragController) { dragController.addDropTarget(mBubbleBarLeftDropTarget); dragController.addDropTarget(mBubbleBarRightDropTarget); } /** Removes bubble bar locations drop zones to the drag controller. */ public void removeBubbleBarDropTargets(DragController dragController) { dragController.removeDropTarget(mBubbleBarLeftDropTarget); dragController.removeDropTarget(mBubbleBarRightDropTarget); } /** Returns animated float property responsible for pinning transition animation. */ public AnimatedFloat getBubbleBarPinning() { return mBubbleBarPinning; } private BubbleBarFlyoutPositioner createFlyoutPositioner() { return new BubbleBarFlyoutPositioner() { @Override public boolean isOnLeft() { boolean shouldRevertLocation = mBarView.isShowingDropTarget() && isLocationUpdatedForDropTarget(); boolean isOnLeft = mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); return shouldRevertLocation != isOnLeft; } @Override public float getTargetTy() { return mBarView.getTranslationY() - mBarView.getHeight(); } @Override @NonNull public PointF getDistanceToCollapsedPosition() { // the flyout animates from the selected bubble dot. calculate the distance it needs // to translate itself to its starting position. PointF distanceToDotCenter = mBarView.getSelectedBubbleDotDistanceFromTopLeft(); // if we're gravitating left, return the distance between the top left corner of the // bubble bar and the bottom left corner of the dot. // if we're gravitating right, return the distance between the top right corner of // the bubble bar and the bottom right corner of the dot. float distanceX = isOnLeft() ? distanceToDotCenter.x - getCollapsedSize() / 2 : mBarView.getWidth() - distanceToDotCenter.x - getCollapsedSize() / 2; float distanceY = distanceToDotCenter.y + getCollapsedSize() / 2; return new PointF(distanceX, distanceY); } @Override public float getCollapsedSize() { return mIconSize * DOT_TO_BUBBLE_SIZE_RATIO; } @Override public int getCollapsedColor() { return mBarView.getSelectedBubbleDotColor(); } @Override public float getCollapsedElevation() { return mBarView.getBubbleElevation(); } @Override public float getDistanceToRevealTriangle() { return getDistanceToCollapsedPosition().y - mBarView.getPointerSize(); } }; } private FlyoutCallbacks createFlyoutCallbacks() { return new FlyoutCallbacks() { @Override public void flyoutClicked() { interruptAnimationForTouch(); setExpanded(/* isExpanded= */ true, /* maybeShowEdu*/ true); } }; } private BubbleBarParentViewHeightUpdateNotifier createBubbleBarParentViewController() { return new BubbleBarParentViewHeightUpdateNotifier() { @Override public void updateTopBoundary() { mActivity.setTaskbarWindowForAnimatingBubble(); } }; } private void onBubbleClicked(BubbleView bubbleView) { if (mBubbleBarPinning.isAnimating()) return; bubbleView.markSeen(); BubbleBarItem bubble = bubbleView.getBubble(); if (bubble == null) { Log.e(TAG, "bubble click listener, bubble was null"); } final String currentlySelected = mBubbleBarController.getSelectedBubbleKey(); if (mBarView.isExpanded() && Objects.equals(bubble.getKey(), currentlySelected)) { // Tapping the currently selected bubble while expanded collapses the view. collapseBubbleBar(); } else { mBubbleBarController.showAndSelectBubble(bubble); } } /** Interrupts the running animation for a touch event on the bubble bar or flyout. */ private void interruptAnimationForTouch() { mBubbleBarViewAnimator.interruptForTouch(); mBubbleStashController.onNewBubbleAnimationInterrupted(false, mBarView.getTranslationY()); } private void collapseBubbleBar() { setExpanded(false); mBubbleStashController.stashBubbleBar(); } /** Notifies that the stash state is changing. */ public void onStashStateChanging() { if (isAnimatingNewBubble()) { mBubbleBarViewAnimator.onStashStateChangingWhileAnimating(); } } /** Shows the education view if it was previously requested. */ private boolean maybeShowEduView() { if (mShouldShowEducation) { mShouldShowEducation = false; // Get the bubble bar bounds on screen Rect bounds = new Rect(); mBarView.getBoundsOnScreen(bounds); // Calculate user education reference position in Screen coordinates Point position = new Point(bounds.centerX(), bounds.top); // Show user education relative to the reference point mSystemUiProxy.showUserEducation(position); return true; } return false; } /** Notifies that the IME became visible. */ public void onImeVisible() { if (isAnimatingNewBubble()) { mBubbleBarViewAnimator.interruptForIme(); } } // // The below animators are exposed to BubbleStashController so it can manage the stashing // animation. // public MultiPropertyFactory getBubbleBarAlpha() { return mBubbleBarAlpha; } public AnimatedFloat getBubbleBarBubbleAlpha() { return mBubbleBarBubbleAlpha; } public AnimatedFloat getBubbleBarBackgroundAlpha() { return mBubbleBarBackgroundAlpha; } public AnimatedFloat getBubbleBarScaleX() { return mBubbleBarScaleX; } public AnimatedFloat getBubbleBarScaleY() { return mBubbleBarScaleY; } public AnimatedFloat getBubbleBarBackgroundScaleX() { return mBubbleBarBackgroundScaleX; } public AnimatedFloat getBubbleBarBackgroundScaleY() { return mBubbleBarBackgroundScaleY; } public AnimatedFloat getBubbleBarTranslationY() { return mBubbleBarTranslationY; } public AnimatedFloat getBubbleOffsetY() { return mBubbleOffsetY; } public float getBubbleBarCollapsedWidth() { return mBarView.collapsedWidth(); } public float getBubbleBarCollapsedHeight() { return mBarView.getBubbleBarCollapsedHeight(); } /** Returns the bubble bar arrow height.*/ public float getBubbleBarArrowHeight() { return mBarView.getArrowHeight(); } /** * @see BubbleBarView#getRelativePivotX() */ public float getBubbleBarRelativePivotX() { return mBarView.getRelativePivotX(); } /** * @see BubbleBarView#getRelativePivotY() */ public float getBubbleBarRelativePivotY() { return mBarView.getRelativePivotY(); } /** * @see BubbleBarView#setRelativePivot(float, float) */ public void setBubbleBarRelativePivot(float x, float y) { mBarView.setRelativePivot(x, y); } /** * Whether the bubble bar is visible or not. */ public boolean isBubbleBarVisible() { return mBarView.getVisibility() == VISIBLE; } /** Whether the bubble bar has bubbles. */ public boolean hasBubbles() { return mBarView.getBubbleChildCount() > 0; } /** * @return current {@link BubbleBarLocation} */ public BubbleBarLocation getBubbleBarLocation() { return mBarView.getBubbleBarLocation(); } /** * @return the max collapsed width for the bubble bar. */ public float getCollapsedWidthWithMaxVisibleBubbles() { return mBarView.getCollapsedWidthWithMaxVisibleBubbles(); } /** * @return {@code true} if bubble bar is on the left edge of the screen, {@code false} if on * the right */ public boolean isBubbleBarOnLeft() { return mBarView.getBubbleBarLocation().isOnLeft(mBarView.isLayoutRtl()); } /** * Update bar {@link BubbleBarLocation} */ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { mBarView.setBubbleBarLocation(bubbleBarLocation); } /** * Animate bubble bar to the given location. The location change is transient. It does not * update the state of the bubble bar. * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { mBarView.animateToBubbleBarLocation(bubbleBarLocation); } /** Return animator for animating bubble bar in. */ public Animator animateBubbleBarLocationIn(BubbleBarLocation fromLocation, BubbleBarLocation toLocation) { return mBarView.animateToBubbleBarLocationIn(fromLocation, toLocation); } /** Return animator for animating bubble bar out. */ public Animator animateBubbleBarLocationOut(BubbleBarLocation toLocation) { return mBarView.animateToBubbleBarLocationOut(toLocation); } /** 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) { mBubbleBarDragLocation = bubbleBarLocation; mBarView.showDropTarget(/* isDropTarget = */ true); mWasStashedBeforeEnteringBubbleDragZone = hasBubbles() && mBubbleStashController.isStashed(); if (mWasStashedBeforeEnteringBubbleDragZone) { // bubble bar is stashed - un-stash at drag location mBubbleStashController.showBubbleBarAtLocation( /* fromLocation = */ getBubbleBarLocation(), /* toLocation = */ mBubbleBarDragLocation ); } else if (hasBubbles()) { if (isLocationUpdatedForDropTarget()) { // bubble bar has bubbles and location is changed - animate bar to the opposite side animateBubbleBarLocation(bubbleBarLocation); } } else { // bubble bar has no bubbles flow just show the empty drop target mBubbleBarPinController.showDropTarget(bubbleBarLocation); } } /** * Returns {@code true} if location was updated after most recent * {@link #onDragItemOverBubbleBarDragZone}}. */ public boolean isLocationUpdatedForDropTarget() { if (mBubbleBarDragLocation == null) { return false; } boolean isRtl = mBarView.isLayoutRtl(); return getBubbleBarLocation().isOnLeft(isRtl) != mBubbleBarDragLocation.isOnLeft(isRtl); } /** * 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() { if (!isShowingDropTarget()) { return; } if (mWasStashedBeforeEnteringBubbleDragZone && mBubbleBarDragLocation != null) { // bubble bar was stashed - stash at original location mBubbleStashController.stashBubbleBarToLocation( /* fromLocation = */ mBubbleBarDragLocation, /* toLocation = */ getBubbleBarLocation() ); } else if (hasBubbles()) { if (isLocationUpdatedForDropTarget()) { // bubble bar has bubbles and location was changed - return to the original // location animateBubbleBarLocation(getBubbleBarLocation()); } } onItemDragCompleted(); } /** * 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 onItemDragCompleted() { mBarView.showDropTarget(/* isDropTarget = */ false); mBubbleBarPinController.hideDropTarget(); mWasStashedBeforeEnteringBubbleDragZone = false; mBubbleBarDragLocation = null; } /** * The bounds of the bubble bar. */ public Rect getBubbleBarBounds() { return mBarView.getBubbleBarBounds(); } /** Returns the bounds of the flyout view if it exists, or {@code null} otherwise. */ @Nullable public Rect getFlyoutBounds() { return mBubbleBarFlyoutController.getFlyoutBounds(); } /** Checks that bubble bar is visible and that the motion event is within bounds. */ public boolean isEventOverBubbleBar(MotionEvent event) { if (!isBubbleBarVisible()) return false; final Rect bounds = getBubbleBarBounds(); final int bubbleBarTopOnScreen = mBarView.getRestingTopPositionOnScreen(); final float x = event.getX(); return event.getRawY() >= bubbleBarTopOnScreen && x >= bounds.left && x <= bounds.right; } /** Whether a new bubble is animating. */ public boolean isAnimatingNewBubble() { return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.isAnimating(); } public boolean isNewBubbleAnimationRunningOrPending() { return mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimation(); } /** The horizontal margin of the bubble bar from the edge of the screen. */ public int getHorizontalMargin() { return mBarView.getHorizontalMargin(); } /** * When the bubble bar is not stashed, it can be collapsed (the icons are in a stack) or * expanded (the icons are in a row). This indicates whether the bubble bar is expanded. */ public boolean isExpanded() { return mBarView.isExpanded(); } /** * Whether the motion event is within the bounds of the bubble bar. */ public boolean isEventOverAnyItem(MotionEvent ev) { return mBarView.isEventOverAnyItem(ev); } // // Visibility of the bubble bar // /** * Returns whether the bubble bar is hidden because there are no bubbles. */ public boolean isHiddenForNoBubbles() { return mHiddenForNoBubbles; } /** Returns maximum height of the bubble bar with the flyout view. */ public int getBubbleBarWithFlyoutMaximumHeight() { if (!hasBubbles() && !isAnimatingNewBubble()) return 0; int bubbleBarTopOnHome = (int) (mBubbleStashController.getBubbleBarVerticalCenterForHome() + mBarView.getBubbleBarCollapsedHeight() / 2 + mBarView.getArrowHeight()); if (isAnimatingNewBubble()) { if (mTaskbarStashController.isInApp() && mBubbleStashController.getHasHandleView()) { // when animating a bubble in an app, the bubble bar will be higher than its // position on home float bubbleBarTopDistanceFromBottom = -mBubbleStashController.getBubbleBarTranslationYForTaskbar() + mBarView.getHeight(); return (int) bubbleBarTopDistanceFromBottom + mBubbleBarFlyoutController.getMaximumFlyoutHeight(); } return bubbleBarTopOnHome + mBubbleBarFlyoutController.getMaximumFlyoutHeight(); } else { return bubbleBarTopOnHome; } } /** * Sets whether the bubble bar should be hidden because there are no bubbles. */ public void setHiddenForBubbles(boolean hidden) { if (mHiddenForNoBubbles != hidden) { mHiddenForNoBubbles = hidden; if (hidden) { mBarView.dismiss(() -> { updateVisibilityForStateChange(); mBarView.setExpanded(false); adjustTaskbarAndHotseatToBubbleBarState(/* isBubbleBarExpanded= */ false); mActivity.bubbleBarVisibilityChanged(/* isVisible= */ false); }); } else { updateVisibilityForStateChange(); mActivity.bubbleBarVisibilityChanged(/* isVisible= */ true); } } } /** Sets a callback that updates the selected bubble after the bubble bar collapses. */ public void setUpdateSelectedBubbleAfterCollapse( Consumer updateSelectedBubbleAfterCollapse) { mBarView.setUpdateSelectedBubbleAfterCollapse(updateSelectedBubbleAfterCollapse); } /** Returns whether the bubble bar should be hidden because of the current sysui state. */ boolean isHiddenForSysui() { return mHiddenForSysui; } /** * Sets whether the bubble bar should be hidden due to SysUI state (e.g. on lockscreen). */ public void setHiddenForSysui(boolean hidden) { if (mHiddenForSysui != hidden) { mHiddenForSysui = hidden; updateVisibilityForStateChange(); } } /** Sets whether the bubble bar should be hidden due to stashed state */ public void setHiddenForStashed(boolean hidden) { if (mHiddenForStashed != hidden) { mHiddenForStashed = hidden; updateVisibilityForStateChange(); } } private void updateVisibilityForStateChange() { boolean hiddenForStashedAndNotAnimating = mHiddenForStashed && !mBubbleBarViewAnimator.isAnimating(); if (mHiddenForSysui || mHiddenForNoBubbles || hiddenForStashedAndNotAnimating) { //TODO(b/404870188) this visibility change cause search view drag misbehavior mBarView.setVisibility(INVISIBLE); } else { mBarView.setVisibility(VISIBLE); } } /** * Returns the translation X of the transient taskbar according to the bubble bar location * regardless of the current taskbar mode. */ public int getTransientTaskbarTranslationXForBubbleBar(BubbleBarLocation location) { int taskbarShift = 0; if (!isBubbleBarVisible() || mTaskbarViewPropertiesProvider == null) return taskbarShift; Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds(); if (taskbarViewBounds.isEmpty()) return taskbarShift; int actualDistance = getDistanceBetweenTransientTaskbarAndBubbleBar(location, taskbarViewBounds); if (actualDistance < mBubbleBarTaskbarMinDistance) { taskbarShift = mBubbleBarTaskbarMinDistance - actualDistance; if (!location.isOnLeft(mBarView.isLayoutRtl())) { taskbarShift = -taskbarShift; } } return taskbarShift; } private int getDistanceBetweenTransientTaskbarAndBubbleBar(BubbleBarLocation location, Rect taskbarViewBounds) { Resources res = mActivity.getResources(); DeviceProfile transientDp = mActivity.getTransientTaskbarDeviceProfile(); int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, transientDp); int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, transientDp); int transientWidthWithMargin = (int) (mBarView.getCollapsedWidthForIconSizeAndPadding( transientIconSize, transientPadding) + mBarView.getHorizontalMargin()); int distance; if (location.isOnLeft(mBarView.isLayoutRtl())) { distance = taskbarViewBounds.left - transientWidthWithMargin; } else { int displayWidth = res.getDisplayMetrics().widthPixels; int bubbleBarLeft = displayWidth - transientWidthWithMargin; distance = bubbleBarLeft - taskbarViewBounds.right; } return distance; } // // Modifying view related properties. // /** Notifies controller of configuration change, so bubble bar can be adjusted */ public void onBubbleBarConfigurationChanged(boolean animate) { int newIconSize; int newPadding; Resources res = mActivity.getResources(); if (mBubbleStashController.isBubblesShowingOnHome() || mBubbleStashController.isTransientTaskBar()) { newIconSize = getBubbleBarIconSizeFromDeviceProfile(res); newPadding = getBubbleBarPaddingFromDeviceProfile(res); } else { // the bubble bar is shown inside the persistent task bar, use preset sizes newIconSize = res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar); newPadding = res.getDimensionPixelSize( R.dimen.bubblebar_icon_spacing_persistent_taskbar); } updateBubbleBarIconSizeAndPadding(newIconSize, newPadding, animate); } private int getBubbleBarIconSizeFromDeviceProfile(Resources res) { return getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getDeviceProfile()); } private int getBubbleBarIconSizeFromDeviceProfile(Resources res, DeviceProfile deviceProfile) { DisplayMetrics dm = res.getDisplayMetrics(); float smallIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_SMALL_DP, dm); float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_MEDIUM_DP, dm); float smallMediumThreshold = (smallIconSize + mediumIconSize) / 2f; int taskbarIconSize = deviceProfile.taskbarIconSize; return taskbarIconSize <= smallMediumThreshold ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_size_small) : res.getDimensionPixelSize(R.dimen.bubblebar_icon_size); } private int getBubbleBarPaddingFromDeviceProfile(Resources res) { return getBubbleBarPaddingFromDeviceProfile(res, mActivity.getDeviceProfile()); } private int getBubbleBarPaddingFromDeviceProfile(Resources res, DeviceProfile deviceProfile) { DisplayMetrics dm = res.getDisplayMetrics(); float mediumIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_MEDIUM_DP, dm); float largeIconSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, APP_ICON_LARGE_DP, dm); float mediumLargeThreshold = (mediumIconSize + largeIconSize) / 2f; return deviceProfile.taskbarIconSize >= mediumLargeThreshold ? res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_large) : res.getDimensionPixelSize(R.dimen.bubblebar_icon_spacing); } private void updateBubbleBarIconSizeAndPadding(int iconSize, int padding, boolean animate) { if (mIconSize == iconSize && mBubbleBarPadding == padding) return; mIconSize = iconSize; mBubbleBarPadding = padding; if (animate) { mBarView.animateBubbleBarIconSize(iconSize, padding); } else { mBarView.setIconSizeAndPadding(iconSize, padding); } } /** * Sets the translation of the bubble bar during the swipe up gesture. */ public void setTranslationYForSwipe(float transY) { mBubbleBarSwipeUpTranslationY = transY; updateTranslationY(); } /** * Sets the translation of the bubble bar during the stash animation. */ public void setTranslationYForStash(float transY) { mBubbleBarStashTranslationY = transY; updateTranslationY(); } private void updateTranslationY() { mBarView.setTranslationY(mBubbleBarTranslationY.value + mBubbleBarSwipeUpTranslationY + mBubbleBarStashTranslationY + getBubbleBarTranslationYForTaskbarPinning()); } /** Computes translation y for taskbar pinning. */ private float getBubbleBarTranslationYForTaskbarPinning() { if (mTaskbarSharedState == null) return 0f; float pinningProgress = mBubbleBarPinning.value; if (mTaskbarSharedState.startTaskbarVariantIsTransient) { return mapRange(pinningProgress, /* min = */ 0f, mTaskbarTranslationDelta); } else { return mapRange(pinningProgress, -mTaskbarTranslationDelta, /* max = */ 0f); } } private void setBubbleBarScaleAndPadding(float pinningProgress) { Resources res = mActivity.getResources(); // determine icon scale for pinning int persistentIconSize = res.getDimensionPixelSize( R.dimen.bubblebar_icon_size_persistent_taskbar); int transientIconSize = getBubbleBarIconSizeFromDeviceProfile(res, mActivity.getTransientTaskbarDeviceProfile()); float pinningIconSize = mapRange(pinningProgress, transientIconSize, persistentIconSize); // determine bubble bar padding for pinning int persistentPadding = res.getDimensionPixelSize( R.dimen.bubblebar_icon_spacing_persistent_taskbar); int transientPadding = getBubbleBarPaddingFromDeviceProfile(res, mActivity.getTransientTaskbarDeviceProfile()); float pinningPadding = mapRange(pinningProgress, transientPadding, persistentPadding); mBarView.setIconSizeAndPaddingForPinning(pinningIconSize, pinningPadding); } /** * Calculates the vertical difference in the bubble bar positions for pinned and transient * taskbar modes. */ private int getBubbleBarTranslationDeltaForTaskbar(TaskbarActivityContext activity) { Resources res = activity.getResources(); int persistentBubbleSize = res .getDimensionPixelSize(R.dimen.bubblebar_icon_size_persistent_taskbar); int persistentSpacingSize = res .getDimensionPixelSize(R.dimen.bubblebar_icon_spacing_persistent_taskbar); int persistentBubbleBarSize = persistentBubbleSize + persistentSpacingSize * 2; int persistentTaskbarHeight = activity.getPersistentTaskbarDeviceProfile().taskbarHeight; int persistentBubbleBarY = (persistentTaskbarHeight - persistentBubbleBarSize) / 2; int transientBubbleBarY = activity.getTransientTaskbarDeviceProfile().taskbarBottomMargin; return transientBubbleBarY - persistentBubbleBarY; } private void updateScaleX(float scale) { mBarView.setScaleX(scale); } private void updateScaleY(float scale) { mBarView.setScaleY(scale); } private void updateBackgroundScaleX(float scale) { mBarView.setBackgroundScaleX(scale); } private void updateBackgroundScaleY(float scale) { mBarView.setBackgroundScaleY(scale); } private void updateBubbleAlpha(float alpha) { mBarView.setBubbleAlpha(alpha); } private void updateBubbleOffsetY(float transY) { mBarView.setBubbleOffsetY(transY); } private void updateBackgroundAlpha(float alpha) { mBarView.setBackgroundAlpha(alpha); } // // Manipulating the specific bubble views in the bar // /** * Removes the provided bubble from the bubble bar. */ public void removeBubble(BubbleBarBubble b) { if (b != null) { mBarView.removeBubble(b.getView()); b.getView().setController(null); } else { Log.w(TAG, "removeBubble, bubble was null!"); } } /** Adds a new bubble and removes an old bubble at the same time. */ public void addBubbleAndRemoveBubble(BubbleBarBubble addedBubble, BubbleBarBubble removedBubble, @Nullable BubbleBarBubble bubbleToSelect, boolean isExpanding, boolean suppressAnimation, boolean addOverflowToo) { BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), removedBubble.getView(), bubbleToSelectView, addOverflowToo ? () -> showOverflow(true) : null); addedBubble.getView().setOnClickListener(mBubbleClickListener); addedBubble.getView().setController(mBubbleViewController); removedBubble.getView().setController(null); mBubbleDragController.setupBubbleView(addedBubble.getView()); if (!suppressAnimation) { animateBubbleNotification(addedBubble, isExpanding, /* isUpdate= */ false); } } /** Whether the overflow view is added to the bubble bar. */ public boolean isOverflowAdded() { return mOverflowAdded; } /** Shows or hides the overflow view. */ public void showOverflow(boolean showOverflow) { if (mOverflowAdded == showOverflow) return; mOverflowAdded = showOverflow; if (mOverflowAdded) { mBarView.addBubble(mOverflowBubble.getView()); mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); mOverflowBubble.getView().setController(mBubbleViewController); } else { mBarView.removeBubble(mOverflowBubble.getView()); mOverflowBubble.getView().setOnClickListener(null); mOverflowBubble.getView().setController(null); } } /** Adds the overflow view to the bubble bar while animating a view away. */ public void addOverflowAndRemoveBubble(BubbleBarBubble removedBubble, @Nullable BubbleBarBubble bubbleToSelect) { if (mOverflowAdded) return; mOverflowAdded = true; BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView(), bubbleToSelectView, null /* onEndRunnable */); mOverflowBubble.getView().setOnClickListener(mBubbleClickListener); mOverflowBubble.getView().setController(mBubbleViewController); removedBubble.getView().setController(null); } /** Removes the overflow view to the bubble bar while animating a view in. */ public void removeOverflowAndAddBubble(BubbleBarBubble addedBubble, @Nullable BubbleBarBubble bubbleToSelect) { if (!mOverflowAdded) return; mOverflowAdded = false; BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView(), bubbleToSelectView, null /* onEndRunnable */); addedBubble.getView().setOnClickListener(mBubbleClickListener); addedBubble.getView().setController(mBubbleViewController); mOverflowBubble.getView().setController(null); } /** * Adds the provided bubble to the bubble bar. */ public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation, @Nullable BubbleBarBubble bubbleToSelect ) { if (b != null) { BubbleView bubbleToSelectView = bubbleToSelect == null ? null : bubbleToSelect.getView(); mBarView.addBubble(b.getView(), bubbleToSelectView); b.getView().setOnClickListener(mBubbleClickListener); mBubbleDragController.setupBubbleView(b.getView()); b.getView().setController(mBubbleViewController); if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) { // the bubble bar and handle are initialized as part of the first bubble animation. // if the animation is suppressed, immediately stash or show the bubble bar to // ensure they've been initialized. if (mTaskbarStashController.isInApp() && mBubbleStashController.isTransientTaskBar() && mTaskbarStashController.isStashed()) { mBubbleStashController.stashBubbleBarImmediate(); } else { mBubbleStashController.showBubbleBarImmediate(); } return; } animateBubbleNotification(bubble, isExpanding, /* isUpdate= */ false); } else { Log.w(TAG, "addBubble, bubble was null!"); } } /** Animates the bubble bar to notify the user about a bubble change. */ public void animateBubbleNotification(BubbleBarBubble bubble, boolean isExpanding, boolean isUpdate) { // if we're not already animating another bubble, update the dot visibility. otherwise the // the dot will be handled as part of the animation. if (!mBubbleBarViewAnimator.isAnimating()) { bubble.getView().updateDotVisibility( /* animate= */ !mBubbleStashController.isStashed()); } // if we're expanded, don't animate the bubble bar. if (isExpanded()) { return; } boolean isInApp = mTaskbarStashController.isInApp(); // if this is the first bubble, animate to the initial state. if (mBarView.getBubbleChildCount() == 1 && !isUpdate) { // 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. boolean animateForCollapsed = !mBubbleStashController.isStashed() || !mBubbleStashController.isTransientTaskBar(); if (animateForCollapsed) { mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding); return; } if (isInApp && mBubbleStashController.getHasHandleView()) { mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding); } } /** * Reorders the bubbles based on the provided list. */ public void reorderBubbles(List newOrder) { List viewList = newOrder.stream().filter(Objects::nonNull) .map(BubbleBarBubble::getView).toList(); mBarView.reorder(viewList); } /** * Updates the selected bubble. */ public void updateSelectedBubble(BubbleBarItem newlySelected) { mBarView.setSelectedBubble(newlySelected.getView()); } /** @see #setExpanded(boolean, boolean) */ public void setExpanded(boolean isExpanded) { setExpanded(isExpanded, /* maybeShowEdu= */ false); } /** * Sets whether the bubble bar should be expanded (not unstashed, but have the contents * within it expanded). This method notifies SystemUI that the bubble bar is expanded and * showing a selected bubble. This method should ONLY be called from UI events originating * from Launcher. * * @param isExpanded whether the bar should be expanded * @param maybeShowEdu whether we should show the edu view before expanding */ public void setExpanded(boolean isExpanded, boolean maybeShowEdu) { // if we're trying to expand try showing the edu view instead if (maybeShowEdu && isExpanded && !mBarView.isExpanded() && maybeShowEduView()) { return; } if (!mBubbleBarPinning.isAnimating() && isExpanded != mBarView.isExpanded()) { mBarView.setExpanded(isExpanded); adjustTaskbarAndHotseatToBubbleBarState(isExpanded); if (!isExpanded) { mSystemUiProxy.collapseBubbles(); } else { mBubbleBarController.showSelectedBubble(); mTaskbarStashController.updateAndAnimateTransientTaskbar(true /* stash */, false /* shouldBubblesFollow */); } } } /** * Hides the persistent taskbar if it is going to intersect with the expanded bubble bar if in * app or overview. */ private void adjustTaskbarAndHotseatToBubbleBarState(boolean isBubbleBarExpanded) { if (!mBubbleStashController.isBubblesShowingOnHome() && !mBubbleStashController.isTransientTaskBar()) { boolean hideTaskbar = isBubbleBarExpanded && isIntersectingTaskbar(); Animator taskbarAlphaAnimator = mTaskbarViewPropertiesProvider.getIconsAlpha() .animateToValue(hideTaskbar ? 0 : 1); taskbarAlphaAnimator.setDuration(hideTaskbar ? TASKBAR_FADE_OUT_DURATION_MS : TASKBAR_FADE_IN_DURATION_MS); if (!hideTaskbar) { taskbarAlphaAnimator.setStartDelay(TASKBAR_FADE_IN_DELAY_MS); } taskbarAlphaAnimator.setInterpolator(Interpolators.LINEAR); taskbarAlphaAnimator.start(); } } /** Return {@code true} if expanded bubble bar would intersect the taskbar. */ public boolean isIntersectingTaskbar() { if (mBarView.isExpanding() || mBarView.isExpanded()) { Rect taskbarViewBounds = mTaskbarViewPropertiesProvider.getTaskbarViewBounds(); return mBarView.getBubbleBarExpandedBounds().intersect(taskbarViewBounds); } else { return false; } } /** * Sets whether the bubble bar should be expanded. This method is used in response to UI events * from SystemUI. */ public void setExpandedFromSysui(boolean isExpanded) { if (isNewBubbleAnimationRunningOrPending() && isExpanded) { mBubbleBarViewAnimator.expandedWhileAnimating(); return; } if (!isExpanded) { mBubbleStashController.stashBubbleBar(); } else { mBubbleStashController.showBubbleBar(true /* expand the bubbles */); } } /** * Stores a request to show the education view for later processing when appropriate. * * @see #maybeShowEduView() */ public void prepareToShowEducation() { mShouldShowEducation = true; } /** * Updates the dragged bubble view in the bubble bar view, and notifies SystemUI * that a bubble is being dragged to dismiss. * * @param bubbleView dragged bubble view */ public void onBubbleDragStart(@NonNull BubbleView bubbleView) { if (bubbleView.getBubble() == null) return; mSystemUiProxy.startBubbleDrag(bubbleView.getBubble().getKey()); mBarView.setDraggedBubble(bubbleView); } /** * Notifies SystemUI to expand the selected bubble when the bubble is released. */ public void onBubbleDragRelease(BubbleBarLocation location) { mSystemUiProxy.stopBubbleDrag(location, mBarView.getRestingTopPositionOnScreen()); } /** Handle given bubble being dismissed */ public void onBubbleDismissed(BubbleView bubble) { mBubbleBarController.onBubbleDismissed(bubble); mBarView.removeBubble(bubble); } /** * Notifies {@link BubbleBarView} that drag and all animations are finished. */ public void onBubbleDragEnd() { mBarView.setDraggedBubble(null); } /** Notifies that dragging the bubble bar ended. */ public void onBubbleBarDragEnd() { // we may have changed the bubble bar translation Y value from the value it had at the // beginning of the drag, so update the translation Y animator state mBubbleBarTranslationY.updateValue(mBarView.getTranslationY()); } /** * Get translation for bubble bar when drag is released. * * @see BubbleBarView#getBubbleBarDragReleaseTranslation(PointF, BubbleBarLocation) */ public PointF getBubbleBarDragReleaseTranslation(PointF initialTranslation, BubbleBarLocation location) { return mBarView.getBubbleBarDragReleaseTranslation(initialTranslation, location); } /** * Get translation for bubble view when drag is released. * * @see BubbleBarView#getDraggedBubbleReleaseTranslation(PointF, BubbleBarLocation) */ public PointF getDraggedBubbleReleaseTranslation(PointF initialTranslation, BubbleBarLocation location) { if (location == mBarView.getBubbleBarLocation()) { return initialTranslation; } return mBarView.getDraggedBubbleReleaseTranslation(initialTranslation, location); } /** * Notify SystemUI that the given bubble has been dismissed. */ public void notifySysUiBubbleDismissed(@NonNull BubbleBarItem bubble) { mSystemUiProxy.dragBubbleToDismiss(bubble.getKey(), mTimeSource.currentTimeMillis()); } /** * Called when bubble stack was dismissed */ public void onDismissAllBubbles() { mSystemUiProxy.removeAllBubbles(); } /** Removes all existing bubble views */ public void removeAllBubbles() { mOverflowAdded = false; mBarView.removeAllViews(); } /** Returns the view index of the existing bubble */ public int bubbleViewIndex(View bubbleView) { return mBarView.indexOfChild(bubbleView); } /** * Set listener to be notified when bubble bar bounds have changed */ public void setBoundsChangeListener(@Nullable BubbleBarBoundsChangeListener listener) { mBoundsChangeListener = listener; } /** Called when the controller is destroyed. */ public void onDestroy() { adjustTaskbarAndHotseatToBubbleBarState(/*isBubbleBarExpanded = */false); } /** * Removes the bubble from the bubble bar and notifies sysui that the bubble should move to * full screen. */ public void moveDraggedBubbleToFullscreen(@NonNull BubbleView bubbleView, Point dropLocation) { if (bubbleView.getBubble() == null) { return; } String key = bubbleView.getBubble().getKey(); mSystemUiProxy.moveDraggedBubbleToFullscreen(key, dropLocation); onBubbleDismissed(bubbleView); } /** * Create an animator for showing or hiding bubbles when stashed state changes * * @param isStashed {@code true} when bubble bar should be stashed to the handle */ public Animator createRevealAnimatorForStashChange(boolean isStashed) { Rect stashedHandleBounds = new Rect(); mBubbleStashController.getHandleBounds(stashedHandleBounds); int childCount = mBarView.getChildCount(); float newChildWidth = (float) stashedHandleBounds.width() / childCount; AnimatorSet animatorSet = new AnimatorSet(); for (int i = 0; i < childCount; i++) { BubbleView child = (BubbleView) mBarView.getChildAt(i); animatorSet.play( createRevealAnimForBubble(child, isStashed, stashedHandleBounds, newChildWidth)); } return animatorSet; } private Animator createRevealAnimForBubble(BubbleView bubbleView, boolean isStashed, Rect stashedHandleBounds, float newWidth) { Rect viewBounds = new Rect(0, 0, bubbleView.getWidth(), bubbleView.getHeight()); int viewCenterY = viewBounds.centerY(); int halfHandleHeight = stashedHandleBounds.height() / 2; int widthDelta = Math.max(0, (int) (viewBounds.width() - newWidth) / 2); Rect stashedViewBounds = new Rect( viewBounds.left + widthDelta, viewCenterY - halfHandleHeight, viewBounds.right - widthDelta, viewCenterY + halfHandleHeight ); float viewRadius = 0f; // Use 0 to not clip the new message dot or the app icon float stashedRadius = stashedViewBounds.height() / 2f; return new RoundedRectRevealOutlineProvider(viewRadius, stashedRadius, viewBounds, stashedViewBounds).createRevealAnimator(bubbleView, !isStashed, 0); } /** * Listener to receive updates about bubble bar bounds changing */ public interface BubbleBarBoundsChangeListener { /** Called when bounds have changed */ void onBoundsChanged(); } /** Interface for getting the current timestamp. */ interface TimeSource { long currentTimeMillis(); } /** Dumps the state of BubbleBarViewController. */ public void dump(PrintWriter pw) { pw.println("Bubble bar view controller state:"); pw.println(" mHiddenForSysui: " + mHiddenForSysui); pw.println(" mHiddenForNoBubbles: " + mHiddenForNoBubbles); pw.println(" mHiddenForStashed: " + mHiddenForStashed); pw.println(" mShouldShowEducation: " + mShouldShowEducation); pw.println(" mBubbleBarTranslationY.value: " + mBubbleBarTranslationY.value); pw.println(" mBubbleBarSwipeUpTranslationY: " + mBubbleBarSwipeUpTranslationY); pw.println(" mOverflowAdded: " + mOverflowAdded); if (mBarView != null) { mBarView.dump(pw); } else { pw.println(" Bubble bar view is null!"); } } /** Interface for BubbleBarViewController to get the taskbar view properties. */ public interface TaskbarViewPropertiesProvider { /** Returns the bounds of the taskbar. */ Rect getTaskbarViewBounds(); /** Returns taskbar icons alpha */ MultiPropertyFactory.MultiProperty getIconsAlpha(); } }