Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c629fd8 authored by Mady Mellor's avatar Mady Mellor
Browse files

Support optional bubble overflow in bubble bar

This is similar to the animations that add / remove a bubble at the
same time -- the overflow is generally added when a bubble is removed.
The overflow is generally removed when a bubble is added (i.e. user
promotes a bubble out of the overflow).

There are a couple of additional cases:
- when bubbles are first added to the bar -- if there were saved
  bubbles in the overflow, the view should be added
- an app could cancel its bubbles / remove its shortcuts and not have
  any in the stack but could have some in the overflow & it could
  become empty without an addition.

Flag: com.android.wm.shell.enable_optional_bubble_overflow
Flag: com.android.wm.shell.enable_bubble_bar
Test: manual - add bubbles to the bubble bar for first time
             => observe there is no overflow
             - dismiss a bubble
             => observe the overflow is added, tap on it, tap on the
                bubble in it
             => observe that bubble is added & the overflow disappears
             - dismiss all the bubbles
             - add a bubble
             => observe the overflow is there & has the previously
                dismissed bubbles
             - cancel all the bubbles that are in the overflow via
               adb
             => observe the overflow is remvoed
Bug: 334175587
Change-Id: I2b6e855e65520b4b2b1fde7757d46f00a468b4a6
parent d9873c10
Loading
Loading
Loading
Loading
+26 −2
Original line number Diff line number Diff line
@@ -138,6 +138,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
        List<RemovedBubble> removedBubbles;
        List<String> bubbleKeysInOrder;
        Point expandedViewDropTargetSize;
        boolean showOverflow;
        boolean showOverflowChanged;

        // These need to be loaded in the background
        BubbleBarBubble addedBubble;
@@ -156,6 +158,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
            removedBubbles = update.removedBubbles;
            bubbleKeysInOrder = update.bubbleKeysInOrder;
            expandedViewDropTargetSize = update.expandedViewDropTargetSize;
            showOverflow = update.showOverflow;
            showOverflowChanged = update.showOverflowChanged;
        }
    }

@@ -271,7 +275,13 @@ public class BubbleBarController extends IBubblesListener.Stub {

        BubbleBarBubble bubbleToSelect = null;

        if (update.addedBubble != null && update.removedBubbles.size() == 1) {
        if (Flags.enableOptionalBubbleOverflow()
                && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null
                && update.removedBubbles.isEmpty()) {
            // A bubble was added from the overflow (& now it's empty / not showing)
            mBubbles.put(update.addedBubble.getKey(), update.addedBubble);
            mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble);
        } else if (update.addedBubble != null && update.removedBubbles.size() == 1) {
            // we're adding and removing a bubble at the same time. handle this as a single update.
            RemovedBubble removedBubble = update.removedBubbles.get(0);
            BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey());
@@ -285,11 +295,17 @@ public class BubbleBarController extends IBubblesListener.Stub {
                Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey());
            }
        } else {
            boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow()
                    && update.showOverflowChanged && update.showOverflow;
            if (!update.removedBubbles.isEmpty()) {
                for (int i = 0; i < update.removedBubbles.size(); i++) {
                    RemovedBubble removedBubble = update.removedBubbles.get(i);
                    BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey());
                    if (bubble != null) {
                    if (bubble != null && overflowNeedsToBeAdded) {
                        // First removal, show the overflow
                        overflowNeedsToBeAdded = false;
                        mBubbleBarViewController.addOverflowAndRemoveBubble(bubble);
                    } else if (bubble != null) {
                        mBubbleBarViewController.removeBubble(bubble);
                    } else {
                        Log.w(TAG, "trying to remove bubble that doesn't exist: "
@@ -302,6 +318,11 @@ public class BubbleBarController extends IBubblesListener.Stub {
                mBubbleBarViewController.addBubble(update.addedBubble, isExpanding,
                        suppressAnimation);
            }
            if (Flags.enableOptionalBubbleOverflow()
                    && update.showOverflowChanged
                    && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) {
                mBubbleBarViewController.showOverflow(update.showOverflow);
            }
        }

        // if a bubble was updated upstream, but removed before the update was received, add it back
@@ -333,6 +354,9 @@ public class BubbleBarController extends IBubblesListener.Stub {
                }
            }
        }
        if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) {
            mBubbleBarViewController.showOverflow(true);
        }

        // Adds and removals have happened, update visibility before any other visual changes.
        mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty());
+50 −32
Original line number Diff line number Diff line
@@ -715,11 +715,13 @@ public class BubbleBarView extends FrameLayout {
    public void addBubble(BubbleView bubble) {
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                Gravity.LEFT);
        final int index = bubble.isOverflow() ? getChildCount() : 0;

        if (isExpanded()) {
            // if we're expanded scale the new bubble in
            bubble.setScaleX(0f);
            bubble.setScaleY(0f);
            addView(bubble, 0, lp);
            addView(bubble, index, lp);
            bubble.showDotIfNeeded(/* animate= */ false);

            mBubbleAnimator = new BubbleAnimator(mIconSize, mExpandedBarIconsSpacing,
@@ -748,23 +750,33 @@ public class BubbleBarView extends FrameLayout {
            };
            mBubbleAnimator.animateNewBubble(indexOfChild(mSelectedBubbleView), listener);
        } else {
            addView(bubble, 0, lp);
            addView(bubble, index, lp);
        }
    }

    /** Add a new bubble and remove an old bubble from the bubble bar. */
    public void addBubbleAndRemoveBubble(View addedBubble, View removedBubble) {
    public void addBubbleAndRemoveBubble(BubbleView addedBubble, BubbleView removedBubble) {
        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) mIconSize, (int) mIconSize,
                Gravity.LEFT);
        boolean isOverflowSelected = mSelectedBubbleView.isOverflow();
        boolean removingOverflow = removedBubble.isOverflow();
        boolean addingOverflow = addedBubble.isOverflow();

        if (!isExpanded()) {
            removeView(removedBubble);
            addView(addedBubble, 0, lp);
            int index = addingOverflow ? getChildCount() : 0;
            addView(addedBubble, index, lp);
            return;
        }
        int index = addingOverflow ? getChildCount() : 0;
        addedBubble.setScaleX(0f);
        addedBubble.setScaleY(0f);
        addView(addedBubble, 0, lp);
        addView(addedBubble, index, lp);

        if (isOverflowSelected && removingOverflow) {
            // The added bubble will be selected
            mSelectedBubbleView = addedBubble;
        }
        int indexOfSelectedBubble = indexOfChild(mSelectedBubbleView);
        int indexOfBubbleToRemove = indexOfChild(removedBubble);

@@ -924,7 +936,7 @@ public class BubbleBarView extends FrameLayout {
        final float currentWidth = getWidth();
        final float expandedWidth = expandedWidth();
        final float collapsedWidth = collapsedWidth();
        int bubbleCount = getChildCount();
        int childCount = getChildCount();
        float viewBottom = mBubbleBarBounds.height() + (isExpanded() ? mPointerSize : 0);
        float bubbleBarAnimatedTop = viewBottom - getBubbleBarHeight();
        // When translating X & Y the scale is ignored, so need to deduct it from the translations
@@ -932,7 +944,7 @@ public class BubbleBarView extends FrameLayout {
        final boolean onLeft = bubbleBarLocation.isOnLeft(isLayoutRtl());
        // elevation state is opposite to widthState - when expanded all icons are flat
        float elevationState = (1 - widthState);
        for (int i = 0; i < bubbleCount; i++) {
        for (int i = 0; i < childCount; i++) {
            BubbleView bv = (BubbleView) getChildAt(i);
            if (bv == mDraggedBubbleView || bv == mDismissedByDragBubbleView) {
                // Skip the dragged bubble. Its translation is managed by the drag controller.
@@ -951,9 +963,9 @@ public class BubbleBarView extends FrameLayout {
            bv.setTranslationY(ty);

            // the position of the bubble when the bar is fully expanded
            final float expandedX = getExpandedBubbleTranslationX(i, bubbleCount, onLeft);
            final float expandedX = getExpandedBubbleTranslationX(i, childCount, onLeft);
            // the position of the bubble when the bar is fully collapsed
            final float collapsedX = getCollapsedBubbleTranslationX(i, bubbleCount, onLeft);
            final float collapsedX = getCollapsedBubbleTranslationX(i, childCount, onLeft);

            // slowly animate elevation while keeping correct Z ordering
            float fullElevationForChild = (MAX_BUBBLES * mBubbleElevation) - i;
@@ -981,13 +993,10 @@ public class BubbleBarView extends FrameLayout {
                final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
                final float targetX = collapsedX + collapsedBarShift;
                bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                // If we're fully collapsed, hide all bubbles except for the first 2. If there are
                // only 2 bubbles, hide the second bubble as well because it's the overflow.
                // If we're fully collapsed, hide all bubbles except for the first 2, excluding
                // the overflow.
                if (widthState == 0) {
                    if (i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
                        bv.setAlpha(0);
                    } else if (i == MAX_VISIBLE_BUBBLES_COLLAPSED - 1
                            && bubbleCount == MAX_VISIBLE_BUBBLES_COLLAPSED) {
                    if (bv.isOverflow() || i > MAX_VISIBLE_BUBBLES_COLLAPSED - 1) {
                        bv.setAlpha(0);
                    } else {
                        bv.setAlpha(1);
@@ -1043,22 +1052,26 @@ public class BubbleBarView extends FrameLayout {
        return translationX - getScaleIconShift();
    }

    private float getCollapsedBubbleTranslationX(int bubbleIndex, int bubbleCount, boolean onLeft) {
        if (bubbleIndex < 0 || bubbleIndex >= bubbleCount) {
    private float getCollapsedBubbleTranslationX(int bubbleIndex, int childCount, boolean onLeft) {
        if (bubbleIndex < 0 || bubbleIndex >= childCount) {
            return 0;
        }
        float translationX;
        if (onLeft) {
            // Shift the first bubble only if there are more bubbles in addition to overflow
            translationX = mBubbleBarPadding + (
                    bubbleIndex == 0 && bubbleCount > MAX_VISIBLE_BUBBLES_COLLAPSED
                            ? mIconOverlapAmount : 0);
            // Shift the first bubble only if there are more bubbles
            if (bubbleIndex == 0 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
                translationX = mIconOverlapAmount;
            } else {
            translationX = mBubbleBarPadding + (
                    bubbleIndex == 0 || bubbleCount <= MAX_VISIBLE_BUBBLES_COLLAPSED
                            ? 0 : mIconOverlapAmount);
                translationX = 0f;
            }
        } else {
            if (bubbleIndex == 1 && getBubbleChildCount() >= MAX_VISIBLE_BUBBLES_COLLAPSED) {
                translationX = mIconOverlapAmount;
            } else {
                translationX = 0f;
            }
        return translationX - getScaleIconShift();
        }
        return mBubbleBarPadding + translationX - getScaleIconShift();
    }

    /**
@@ -1256,15 +1269,20 @@ public class BubbleBarView extends FrameLayout {
    }

    private float collapsedWidth() {
        final int childCount = getChildCount();
        final int bubbleChildCount = getBubbleChildCount();
        final float horizontalPadding = 2 * mBubbleBarPadding;
        // If there are more than 2 bubbles, the first 2 should be visible when collapsed.
        // Otherwise just the first bubble should be visible because we don't show the overflow.
        return childCount > MAX_VISIBLE_BUBBLES_COLLAPSED
        // If there are more than 2 bubbles, the first 2 should be visible when collapsed,
        // excluding the overflow.
        return bubbleChildCount >= MAX_VISIBLE_BUBBLES_COLLAPSED
                ? getScaledIconSize() + mIconOverlapAmount + horizontalPadding
                : getScaledIconSize() + horizontalPadding;
    }

    /** Returns the child count excluding the overflow if it's present. */
    private int getBubbleChildCount() {
        return hasOverflow() ? getChildCount() - 1 : getChildCount();
    }

    private float getBubbleBarExpandedHeight() {
        return getBubbleBarCollapsedHeight() + mPointerSize;
    }
@@ -1303,8 +1321,8 @@ public class BubbleBarView extends FrameLayout {
        return mIsAnimatingNewBubble;
    }

    private boolean hasOverview() {
        // Overview is always the last bubble
    private boolean hasOverflow() {
        // Overflow is always the last bubble
        View lastChild = getChildAt(getChildCount() - 1);
        if (lastChild instanceof BubbleView bubbleView) {
            return bubbleView.getBubble() instanceof BubbleBarOverflow;
@@ -1336,7 +1354,7 @@ public class BubbleBarView extends FrameLayout {
        CharSequence contentDesc = firstChild != null ? firstChild.getContentDescription() : "";

        // Don't count overflow if it exists
        int bubbleCount = getChildCount() - (hasOverview() ? 1 : 0);
        int bubbleCount = getChildCount() - (hasOverflow() ? 1 : 0);
        if (bubbleCount > 1) {
            contentDesc = getResources().getString(R.string.bubble_bar_description_multiple_bubbles,
                    contentDesc, bubbleCount - 1);
+38 −6
Original line number Diff line number Diff line
@@ -92,6 +92,8 @@ public class BubbleBarViewController {
    private boolean mHiddenForNoBubbles = true;
    private boolean mShouldShowEducation;

    public boolean mOverflowAdded;

    private BubbleBarViewAnimator mBubbleBarViewAnimator;

    private final TimeSource mTimeSource = System::currentTimeMillis;
@@ -123,7 +125,6 @@ public class BubbleBarViewController {
        mBubbleBarClickListener = v -> expandBubbleBar();
        mBubbleDragController.setupBubbleBarView(mBarView);
        mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
        addOverflow();
        mBarView.setOnClickListener(mBubbleBarClickListener);
        mBarView.addOnLayoutChangeListener(
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -494,13 +495,44 @@ public class BubbleBarViewController {
        }
    }

    /**
     * Adds the overflow view to the bubble bar.
     */
    public void addOverflow() {
    /** 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) {
        if (mOverflowAdded) return;
        mOverflowAdded = true;
        mBarView.addBubbleAndRemoveBubble(mOverflowBubble.getView(), removedBubble.getView());
        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) {
        if (!mOverflowAdded) return;
        mOverflowAdded = false;
        mBarView.addBubbleAndRemoveBubble(addedBubble.getView(), mOverflowBubble.getView());
        addedBubble.getView().setOnClickListener(mBubbleClickListener);
        addedBubble.getView().setController(mBubbleViewController);
        mOverflowBubble.getView().setController(null);
    }

    /**
+7 −0
Original line number Diff line number Diff line
@@ -74,6 +74,7 @@ public class BubbleView extends ConstraintLayout {
    private boolean mOnLeft = false;

    private BubbleBarItem mBubble;
    private boolean mIsOverflow;

    private Bitmap mIcon;

@@ -271,12 +272,18 @@ public class BubbleView extends ConstraintLayout {
     */
    public void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
        mBubble = overflow;
        mIsOverflow = true;
        mIcon = bitmap;
        updateBubbleIcon();
        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
        setContentDescription(getResources().getString(R.string.bubble_bar_overflow_description));
    }

    /** Whether this view represents the overflow button. */
    public boolean isOverflow() {
        return mIsOverflow;
    }

    /** Returns the bubble being rendered in this view. */
    @Nullable
    public BubbleBarItem getBubble() {