• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.taskbar.bubbles;
17 
18 import android.annotation.Nullable;
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Outline;
23 import android.graphics.Rect;
24 import android.util.AttributeSet;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewOutlineProvider;
28 import android.widget.ImageView;
29 
30 import androidx.constraintlayout.widget.ConstraintLayout;
31 
32 import com.android.launcher3.R;
33 import com.android.launcher3.icons.DotRenderer;
34 import com.android.launcher3.icons.IconNormalizer;
35 import com.android.wm.shell.animation.Interpolators;
36 
37 import java.util.EnumSet;
38 
39 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
40 
41 /**
42  * View that displays a bubble icon, along with an app badge on either the left or
43  * right side of the view.
44  */
45 public class BubbleView extends ConstraintLayout {
46 
47     public static final int DEFAULT_PATH_SIZE = 100;
48 
49     /**
50      * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
51      * another. If any of these flags are set, the dot will not be shown.
52      * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
53      */
54     enum SuppressionFlag {
55         // TODO: (b/277815200) implement flyout
56         // Suppressed because the flyout is visible - it will morph into the dot via animation.
57         FLYOUT_VISIBLE,
58         // Suppressed because this bubble is behind others in the collapsed stack.
59         BEHIND_STACK,
60     }
61 
62     private final EnumSet<SuppressionFlag> mSuppressionFlags =
63             EnumSet.noneOf(SuppressionFlag.class);
64 
65     private final ImageView mBubbleIcon;
66     private final ImageView mAppIcon;
67     private final int mBubbleSize;
68 
69     private DotRenderer mDotRenderer;
70     private DotRenderer.DrawParams mDrawParams;
71     private int mDotColor;
72     private Rect mTempBounds = new Rect();
73 
74     // Whether the dot is animating
75     private boolean mDotIsAnimating;
76     // What scale value the dot is animating to
77     private float mAnimatingToDotScale;
78     // The current scale value of the dot
79     private float mDotScale;
80 
81     // TODO: (b/273310265) handle RTL
82     // Whether the bubbles are positioned on the left or right side of the screen
83     private boolean mOnLeft = false;
84 
85     private BubbleBarItem mBubble;
86 
BubbleView(Context context)87     public BubbleView(Context context) {
88         this(context, null);
89     }
90 
BubbleView(Context context, AttributeSet attrs)91     public BubbleView(Context context, AttributeSet attrs) {
92         this(context, attrs, 0);
93     }
94 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr)95     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
96         this(context, attrs, defStyleAttr, 0);
97     }
98 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)99     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr,
100             int defStyleRes) {
101         super(context, attrs, defStyleAttr, defStyleRes);
102         // We manage positioning the badge ourselves
103         setLayoutDirection(LAYOUT_DIRECTION_LTR);
104 
105         LayoutInflater.from(context).inflate(R.layout.bubble_view, this);
106 
107         mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size);
108         mBubbleIcon = findViewById(R.id.icon_view);
109         mAppIcon = findViewById(R.id.app_icon_view);
110 
111         mDrawParams = new DotRenderer.DrawParams();
112 
113         setFocusable(true);
114         setClickable(true);
115         setOutlineProvider(new ViewOutlineProvider() {
116             @Override
117             public void getOutline(View view, Outline outline) {
118                 BubbleView.this.getOutline(outline);
119             }
120         });
121     }
122 
getOutline(Outline outline)123     private void getOutline(Outline outline) {
124         final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize);
125         final int inset = (mBubbleSize - normalizedSize) / 2;
126         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
127     }
128 
129     @Override
dispatchDraw(Canvas canvas)130     public void dispatchDraw(Canvas canvas) {
131         super.dispatchDraw(canvas);
132 
133         if (!shouldDrawDot()) {
134             return;
135         }
136 
137         getDrawingRect(mTempBounds);
138 
139         mDrawParams.dotColor = mDotColor;
140         mDrawParams.iconBounds = mTempBounds;
141         mDrawParams.leftAlign = mOnLeft;
142         mDrawParams.scale = mDotScale;
143 
144         mDotRenderer.draw(canvas, mDrawParams);
145     }
146 
147     /** Sets the bubble being rendered in this view. */
setBubble(BubbleBarBubble bubble)148     void setBubble(BubbleBarBubble bubble) {
149         mBubble = bubble;
150         mBubbleIcon.setImageBitmap(bubble.getIcon());
151         mAppIcon.setImageBitmap(bubble.getBadge());
152         mDotColor = bubble.getDotColor();
153         mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
154     }
155 
156     /**
157      * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
158      * but does not represent app content, instead it shows recent bubbles that couldn't fit into
159      * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
160      * come from an app.
161      */
setOverflow(BubbleBarOverflow overflow, Bitmap bitmap)162     void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
163         mBubble = overflow;
164         mBubbleIcon.setImageBitmap(bitmap);
165         mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
166     }
167 
168     /** Returns the bubble being rendered in this view. */
169     @Nullable
getBubble()170     BubbleBarItem getBubble() {
171         return mBubble;
172     }
173 
updateDotVisibility(boolean animate)174     void updateDotVisibility(boolean animate) {
175         final float targetScale = shouldDrawDot() ? 1f : 0f;
176         if (animate) {
177             animateDotScale();
178         } else {
179             mDotScale = targetScale;
180             mAnimatingToDotScale = targetScale;
181             invalidate();
182         }
183     }
184 
updateBadgeVisibility()185     void updateBadgeVisibility() {
186         if (mBubble instanceof BubbleBarOverflow) {
187             // The overflow bubble does not have a badge, so just bail.
188             return;
189         }
190         BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
191         Bitmap appBadgeBitmap = bubble.getBadge();
192         int translationX = mOnLeft
193                 ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
194                 : 0;
195         mAppIcon.setTranslationX(translationX);
196         mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
197     }
198 
199     /** Sets whether this bubble is in the stack & not the first bubble. **/
setBehindStack(boolean behindStack, boolean animate)200     void setBehindStack(boolean behindStack, boolean animate) {
201         if (behindStack) {
202             mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
203         } else {
204             mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
205         }
206         updateDotVisibility(animate);
207         updateBadgeVisibility();
208     }
209 
210     /** Whether this bubble is in the stack & not the first bubble. **/
isBehindStack()211     boolean isBehindStack() {
212         return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
213     }
214 
215     /** Whether the dot indicating unseen content in a bubble should be shown. */
shouldDrawDot()216     private boolean shouldDrawDot() {
217         boolean bubbleHasUnseenContent = mBubble != null
218                 && mBubble instanceof BubbleBarBubble
219                 && mSuppressionFlags.isEmpty()
220                 && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();
221 
222         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
223         // it if the bubble wants to show it, and we aren't suppressing it.
224         return bubbleHasUnseenContent || mDotIsAnimating;
225     }
226 
227     /** How big the dot should be, fraction from 0 to 1. */
setDotScale(float fraction)228     private void setDotScale(float fraction) {
229         mDotScale = fraction;
230         invalidate();
231     }
232 
233     /**
234      * Animates the dot to the given scale.
235      */
animateDotScale()236     private void animateDotScale() {
237         float toScale = shouldDrawDot() ? 1f : 0f;
238         mDotIsAnimating = true;
239 
240         // Don't restart the animation if we're already animating to the given value.
241         if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
242             mDotIsAnimating = false;
243             return;
244         }
245 
246         mAnimatingToDotScale = toScale;
247 
248         final boolean showDot = toScale > 0f;
249 
250         // Do NOT wait until after animation ends to setShowDot
251         // to avoid overriding more recent showDot states.
252         clearAnimation();
253         animate()
254                 .setDuration(200)
255                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
256                 .setUpdateListener((valueAnimator) -> {
257                     float fraction = valueAnimator.getAnimatedFraction();
258                     fraction = showDot ? fraction : 1f - fraction;
259                     setDotScale(fraction);
260                 }).withEndAction(() -> {
261                     setDotScale(showDot ? 1f : 0f);
262                     mDotIsAnimating = false;
263                 }).start();
264     }
265 
266 
267     @Override
toString()268     public String toString() {
269         String toString = mBubble != null ? mBubble.getKey() : "null";
270         return "BubbleView{" + toString + "}";
271     }
272 }
273