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

Commit c299ad64 authored by Mady Mellor's avatar Mady Mellor
Browse files

Show / hide the "update" dot on bubbles in bubble bar

Updates BubbleView to include logic to render the update dot on a
bubble. This only shows for BubbleBarBubbles, not the overflow.

We only show the dot (and the badge) when:
- the bubble has new content / appropriate flags set
- AND the bubbles are expanded
  OR on the first bubble when bubbles are collapsed
- AND when the flyout is not animating (this bit doesn't exist yet)

If a bubble has a dot and is opened, the dot will animate away.
To do this, we update the flags set on a bubble.

The flag needs to be set on WMShell side as well as Launcher side.
When a bubble is shown by WMShell, it automatically updates the flag.
This CL adds code to update the flag on Launcher side when we call
into WMShell to show the bubble.

Test: manual
Bug: 269670235
Change-Id: I32f652effa9a73c567981aa5a2a5864e9c3c0c66
parent 1812924a
Loading
Loading
Loading
Loading
+13 −4
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_S

import android.annotation.BinderThread;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
@@ -319,9 +320,11 @@ public class BubbleBarController extends IBubblesListener.Stub {
        mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());

        if (update.updatedBubble != null) {
            // TODO: (b/269670235) handle updates:
            //  (1) if content / icons change -- requires reload & add back in place
            //  (2) if showing update dot changes -- tell the view to hide / show the dot
            // Updates mean the dot state may have changed; any other changes were updated in
            // the populateBubble step.
            BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
            // If we're not stashed, we're visible so animate
            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
        }
        if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
            // Create the new list
@@ -366,7 +369,13 @@ public class BubbleBarController extends IBubblesListener.Stub {
        if (getSelectedBubbleKey() != null) {
            int[] bubbleBarCoords = mBarView.getLocationOnScreen();
            if (mSelectedBubble instanceof BubbleBarBubble) {
                // TODO (b/269670235): hide the update dot on the view if needed.
                // Because we've visited this bubble, we should suppress the notification.
                // This is updated on WMShell side when we show the bubble, but that update isn't
                // passed to launcher, instead we apply it directly here.
                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
                info.setFlags(
                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
            }
            mSystemUiProxy.showBubble(getSelectedBubbleKey(),
                    bubbleBarCoords[0], bubbleBarCoords[1]);
+5 −6
Original line number Diff line number Diff line
@@ -234,6 +234,7 @@ public class BubbleBarView extends FrameLayout {
        final float collapsedWidth = collapsedWidth();
        int bubbleCount = getChildCount();
        final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
        final boolean animate = getVisibility() == VISIBLE;
        for (int i = 0; i < bubbleCount; i++) {
            BubbleView bv = (BubbleView) getChildAt(i);
            bv.setTranslationY(ty);
@@ -251,16 +252,14 @@ public class BubbleBarView extends FrameLayout {
                if (widthState == 1f) {
                    bv.setZ(0);
                }
                bv.showBadge();
                // When we're expanded, we're not stacked so we're not behind the stack
                bv.setBehindStack(false, animate);
            } else {
                final float targetX = currentWidth - collapsedWidth + collapsedX;
                bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
                if (i > 0) {
                    bv.hideBadge();
                } else {
                    bv.showBadge();
                }
                // If we're not the first bubble we're behind the stack
                bv.setBehindStack(i > 0, animate);
            }
        }

+145 −20
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package com.android.launcher3.taskbar.bubbles;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,10 +30,13 @@ import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;

import com.android.launcher3.R;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;

import java.util.EnumSet;

// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
// TODO: (b/269670235) currently this doesn't show the 'update dot'

/**
 * View that displays a bubble icon, along with an app badge on either the left or
@@ -39,14 +44,42 @@ import com.android.launcher3.icons.IconNormalizer;
 */
public class BubbleView extends ConstraintLayout {

    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
    public static final int DEFAULT_PATH_SIZE = 100;

    /**
     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
     * another. If any of these flags are set, the dot will not be shown.
     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
     */
    enum SuppressionFlag {
        // TODO: (b/277815200) implement flyout
        // Suppressed because the flyout is visible - it will morph into the dot via animation.
        FLYOUT_VISIBLE,
        // Suppressed because this bubble is behind others in the collapsed stack.
        BEHIND_STACK,
    }

    private final EnumSet<SuppressionFlag> mSuppressionFlags =
            EnumSet.noneOf(SuppressionFlag.class);

    private final ImageView mBubbleIcon;
    private final ImageView mAppIcon;
    private final int mBubbleSize;

    private DotRenderer mDotRenderer;
    private DotRenderer.DrawParams mDrawParams;
    private int mDotColor;
    private Rect mTempBounds = new Rect();

    // Whether the dot is animating
    private boolean mDotIsAnimating;
    // What scale value the dot is animating to
    private float mAnimatingToDotScale;
    // The current scale value of the dot
    private float mDotScale;

    // TODO: (b/273310265) handle RTL
    // Whether the bubbles are positioned on the left or right side of the screen
    private boolean mOnLeft = false;

    private BubbleBarItem mBubble;
@@ -75,6 +108,8 @@ public class BubbleView extends ConstraintLayout {
        mBubbleIcon = findViewById(R.id.icon_view);
        mAppIcon = findViewById(R.id.app_icon_view);

        mDrawParams = new DotRenderer.DrawParams();

        setFocusable(true);
        setClickable(true);
        setOutlineProvider(new ViewOutlineProvider() {
@@ -91,17 +126,43 @@ public class BubbleView extends ConstraintLayout {
        outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        if (!shouldDrawDot()) {
            return;
        }

        getDrawingRect(mTempBounds);

        mDrawParams.dotColor = mDotColor;
        mDrawParams.iconBounds = mTempBounds;
        mDrawParams.leftAlign = mOnLeft;
        mDrawParams.scale = mDotScale;

        mDotRenderer.draw(canvas, mDrawParams);
    }

    /** Sets the bubble being rendered in this view. */
    void setBubble(BubbleBarBubble bubble) {
        mBubble = bubble;
        mBubbleIcon.setImageBitmap(bubble.getIcon());
        mAppIcon.setImageBitmap(bubble.getBadge());
        mDotColor = bubble.getDotColor();
        mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
    }

    /**
     * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
     * but does not represent app content, instead it shows recent bubbles that couldn't fit into
     * 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) {
        mBubble = overflow;
        mBubbleIcon.setImageBitmap(bitmap);
        hideBadge();
        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
    }

    /** Returns the bubble being rendered in this view. */
@@ -110,38 +171,102 @@ public class BubbleView extends ConstraintLayout {
        return mBubble;
    }

    /** Shows the app badge on this bubble. */
    void showBadge() {
    void updateDotVisibility(boolean animate) {
        final float targetScale = shouldDrawDot() ? 1f : 0f;
        if (animate) {
            animateDotScale();
        } else {
            mDotScale = targetScale;
            mAnimatingToDotScale = targetScale;
            invalidate();
        }
    }

    void updateBadgeVisibility() {
        if (mBubble instanceof BubbleBarOverflow) {
            // The overflow bubble does not have a badge, so just bail.
            return;
        }
        BubbleBarBubble bubble = (BubbleBarBubble) mBubble;

        Bitmap appBadgeBitmap = bubble.getBadge();
        if (appBadgeBitmap == null) {
            mAppIcon.setVisibility(GONE);
            return;
        int translationX = mOnLeft
                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
                : 0;
        mAppIcon.setTranslationX(translationX);
        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
    }

        int translationX;
        if (mOnLeft) {
            translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
    /** Sets whether this bubble is in the stack & not the first bubble. **/
    void setBehindStack(boolean behindStack, boolean animate) {
        if (behindStack) {
            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
        } else {
            translationX = 0;
            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
        }
        updateDotVisibility(animate);
        updateBadgeVisibility();
    }

        mAppIcon.setTranslationX(translationX);
        mAppIcon.setVisibility(VISIBLE);
    /** Whether this bubble is in the stack & not the first bubble. **/
    boolean isBehindStack() {
        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
    }

    /** Hides the app badge on this bubble. */
    void hideBadge() {
        mAppIcon.setVisibility(GONE);
    /** Whether the dot indicating unseen content in a bubble should be shown. */
    private boolean shouldDrawDot() {
        boolean bubbleHasUnseenContent = mBubble != null
                && mBubble instanceof BubbleBarBubble
                && mSuppressionFlags.isEmpty()
                && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();

        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
        // it if the bubble wants to show it, and we aren't suppressing it.
        return bubbleHasUnseenContent || mDotIsAnimating;
    }

    /** How big the dot should be, fraction from 0 to 1. */
    private void setDotScale(float fraction) {
        mDotScale = fraction;
        invalidate();
    }

    /**
     * Animates the dot to the given scale.
     */
    private void animateDotScale() {
        float toScale = shouldDrawDot() ? 1f : 0f;
        mDotIsAnimating = true;

        // Don't restart the animation if we're already animating to the given value.
        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
            mDotIsAnimating = false;
            return;
        }

        mAnimatingToDotScale = toScale;

        final boolean showDot = toScale > 0f;

        // Do NOT wait until after animation ends to setShowDot
        // to avoid overriding more recent showDot states.
        clearAnimation();
        animate()
                .setDuration(200)
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                .setUpdateListener((valueAnimator) -> {
                    float fraction = valueAnimator.getAnimatedFraction();
                    fraction = showDot ? fraction : 1f - fraction;
                    setDotScale(fraction);
                }).withEndAction(() -> {
                    setDotScale(showDot ? 1f : 0f);
                    mDotIsAnimating = false;
                }).start();
    }


    @Override
    public String toString() {
        return "BubbleView{" + mBubble + "}";
        String toString = mBubble != null ? mBubble.getKey() : "null";
        return "BubbleView{" + toString + "}";
    }
}