• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.wm.shell.bubbles;
17 
18 import static android.graphics.Paint.ANTI_ALIAS_FLAG;
19 import static android.graphics.Paint.DITHER_FLAG;
20 import static android.graphics.Paint.FILTER_BITMAP_FLAG;
21 
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Outline;
27 import android.graphics.Paint;
28 import android.graphics.PaintFlagsDrawFilter;
29 import android.graphics.Path;
30 import android.graphics.Rect;
31 import android.util.AttributeSet;
32 import android.util.PathParser;
33 import android.view.View;
34 import android.view.ViewOutlineProvider;
35 import android.widget.ImageView;
36 
37 import com.android.launcher3.icons.DotRenderer;
38 import com.android.launcher3.icons.IconNormalizer;
39 import com.android.wm.shell.animation.Interpolators;
40 
41 import java.util.EnumSet;
42 
43 /**
44  * View that displays an adaptive icon with an app-badge and a dot.
45  *
46  * Dot = a small colored circle that indicates whether this bubble has an unread update.
47  * Badge = the icon associated with the app that created this bubble, this will show work profile
48  * badge if appropriate.
49  */
50 public class BadgedImageView extends ImageView {
51 
52     /** Same value as Launcher3 dot code */
53     public static final float WHITE_SCRIM_ALPHA = 0.54f;
54     /** Same as value in Launcher3 IconShape */
55     public static final int DEFAULT_PATH_SIZE = 100;
56     /** Same as value in Launcher3 BaseIconFactory */
57     private static final float ICON_BADGE_SCALE = 0.444f;
58 
59     /**
60      * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of
61      * these flags are set, the dot will not be shown even if {@link Bubble#showDot()} returns true.
62      */
63     enum SuppressionFlag {
64         // Suppressed because the flyout is visible - it will morph into the dot via animation.
65         FLYOUT_VISIBLE,
66         // Suppressed because this bubble is behind others in the collapsed stack.
67         BEHIND_STACK,
68     }
69 
70     /**
71      * Start by suppressing the dot because the flyout is visible - most bubbles are added with a
72      * flyout, so this is a reasonable default.
73      */
74     private final EnumSet<SuppressionFlag> mDotSuppressionFlags =
75             EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE);
76 
77     private float mDotScale = 0f;
78     private float mAnimatingToDotScale = 0f;
79     private boolean mDotIsAnimating = false;
80 
81     private BubbleViewProvider mBubble;
82     private BubblePositioner mPositioner;
83     private boolean mOnLeft;
84 
85     private DotRenderer mDotRenderer;
86     private DotRenderer.DrawParams mDrawParams;
87     private int mDotColor;
88 
89     private Paint mPaint = new Paint(ANTI_ALIAS_FLAG);
90     private Rect mTempBounds = new Rect();
91 
BadgedImageView(Context context)92     public BadgedImageView(Context context) {
93         this(context, null);
94     }
95 
BadgedImageView(Context context, AttributeSet attrs)96     public BadgedImageView(Context context, AttributeSet attrs) {
97         this(context, attrs, 0);
98     }
99 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr)100     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) {
101         this(context, attrs, defStyleAttr, 0);
102     }
103 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)104     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr,
105             int defStyleRes) {
106         super(context, attrs, defStyleAttr, defStyleRes);
107         mDrawParams = new DotRenderer.DrawParams();
108 
109         setFocusable(true);
110         setClickable(true);
111         setOutlineProvider(new ViewOutlineProvider() {
112             @Override
113             public void getOutline(View view, Outline outline) {
114                 BadgedImageView.this.getOutline(outline);
115             }
116         });
117     }
118 
getOutline(Outline outline)119     private void getOutline(Outline outline) {
120         final int bubbleSize = mPositioner.getBubbleSize();
121         final int normalizedSize = IconNormalizer.getNormalizedCircleSize(bubbleSize);
122         final int inset = (bubbleSize - normalizedSize) / 2;
123         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
124     }
125 
initialize(BubblePositioner positioner)126     public void initialize(BubblePositioner positioner) {
127         mPositioner = positioner;
128 
129         Path iconPath = PathParser.createPathFromPathData(
130                 getResources().getString(com.android.internal.R.string.config_icon_mask));
131         mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(),
132                 iconPath, DEFAULT_PATH_SIZE);
133     }
134 
showDotAndBadge(boolean onLeft)135     public void showDotAndBadge(boolean onLeft) {
136         removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
137         animateDotBadgePositions(onLeft);
138 
139     }
140 
hideDotAndBadge(boolean onLeft)141     public void hideDotAndBadge(boolean onLeft) {
142         addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
143         mOnLeft = onLeft;
144         hideBadge();
145     }
146 
147     /**
148      * Updates the view with provided info.
149      */
setRenderedBubble(BubbleViewProvider bubble)150     public void setRenderedBubble(BubbleViewProvider bubble) {
151         mBubble = bubble;
152         if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) {
153             hideBadge();
154         } else {
155             showBadge();
156         }
157         mDotColor = bubble.getDotColor();
158         drawDot(bubble.getDotPath());
159     }
160 
161     @Override
onDraw(Canvas canvas)162     public void onDraw(Canvas canvas) {
163         super.onDraw(canvas);
164 
165         if (!shouldDrawDot()) {
166             return;
167         }
168 
169         getDrawingRect(mTempBounds);
170 
171         mDrawParams.color = mDotColor;
172         mDrawParams.iconBounds = mTempBounds;
173         mDrawParams.leftAlign = mOnLeft;
174         mDrawParams.scale = mDotScale;
175 
176         mDotRenderer.draw(canvas, mDrawParams);
177     }
178 
179     /** Adds a dot suppression flag, updating dot visibility if needed. */
addDotSuppressionFlag(SuppressionFlag flag)180     void addDotSuppressionFlag(SuppressionFlag flag) {
181         if (mDotSuppressionFlags.add(flag)) {
182             // Update dot visibility, and animate out if we're now behind the stack.
183             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */);
184         }
185     }
186 
187     /** Removes a dot suppression flag, updating dot visibility if needed. */
removeDotSuppressionFlag(SuppressionFlag flag)188     void removeDotSuppressionFlag(SuppressionFlag flag) {
189         if (mDotSuppressionFlags.remove(flag)) {
190             // Update dot visibility, animating if we're no longer behind the stack.
191             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK);
192         }
193     }
194 
195     /** Updates the visibility of the dot, animating if requested. */
updateDotVisibility(boolean animate)196     void updateDotVisibility(boolean animate) {
197         final float targetScale = shouldDrawDot() ? 1f : 0f;
198 
199         if (animate) {
200             animateDotScale(targetScale, null /* after */);
201         } else {
202             mDotScale = targetScale;
203             mAnimatingToDotScale = targetScale;
204             invalidate();
205         }
206     }
207 
208     /**
209      * @param iconPath The new icon path to use when calculating dot position.
210      */
drawDot(Path iconPath)211     void drawDot(Path iconPath) {
212         mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(),
213                 iconPath, DEFAULT_PATH_SIZE);
214         invalidate();
215     }
216 
217     /**
218      * How big the dot should be, fraction from 0 to 1.
219      */
setDotScale(float fraction)220     void setDotScale(float fraction) {
221         mDotScale = fraction;
222         invalidate();
223     }
224 
225     /**
226      * Whether decorations (badges or dots) are on the left.
227      */
getDotOnLeft()228     boolean getDotOnLeft() {
229         return mOnLeft;
230     }
231 
232     /**
233      * Return dot position relative to bubble view container bounds.
234      */
getDotCenter()235     float[] getDotCenter() {
236         float[] dotPosition;
237         if (mOnLeft) {
238             dotPosition = mDotRenderer.getLeftDotPosition();
239         } else {
240             dotPosition = mDotRenderer.getRightDotPosition();
241         }
242         getDrawingRect(mTempBounds);
243         float dotCenterX = mTempBounds.width() * dotPosition[0];
244         float dotCenterY = mTempBounds.height() * dotPosition[1];
245         return new float[]{dotCenterX, dotCenterY};
246     }
247 
248     /**
249      * The key for the {@link Bubble} associated with this view, if one exists.
250      */
251     @Nullable
getKey()252     public String getKey() {
253         return (mBubble != null) ? mBubble.getKey() : null;
254     }
255 
getDotColor()256     int getDotColor() {
257         return mDotColor;
258     }
259 
260     /** Sets the position of the dot and badge, animating them out and back in if requested. */
animateDotBadgePositions(boolean onLeft)261     void animateDotBadgePositions(boolean onLeft) {
262         mOnLeft = onLeft;
263 
264         if (onLeft != getDotOnLeft() && shouldDrawDot()) {
265             animateDotScale(0f /* showDot */, () -> {
266                 invalidate();
267                 animateDotScale(1.0f, null /* after */);
268             });
269         }
270         // TODO animate badge
271         showBadge();
272 
273     }
274 
275     /** Sets the position of the dot and badge. */
setDotBadgeOnLeft(boolean onLeft)276     void setDotBadgeOnLeft(boolean onLeft) {
277         mOnLeft = onLeft;
278         invalidate();
279         showBadge();
280     }
281 
282 
283     /** Whether to draw the dot in onDraw(). */
shouldDrawDot()284     private boolean shouldDrawDot() {
285         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
286         // it if the bubble wants to show it, and we aren't suppressing it.
287         return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty());
288     }
289 
290     /**
291      * Animates the dot to the given scale, running the optional callback when the animation ends.
292      */
animateDotScale(float toScale, @Nullable Runnable after)293     private void animateDotScale(float toScale, @Nullable Runnable after) {
294         mDotIsAnimating = true;
295 
296         // Don't restart the animation if we're already animating to the given value.
297         if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
298             mDotIsAnimating = false;
299             return;
300         }
301 
302         mAnimatingToDotScale = toScale;
303 
304         final boolean showDot = toScale > 0f;
305 
306         // Do NOT wait until after animation ends to setShowDot
307         // to avoid overriding more recent showDot states.
308         clearAnimation();
309         animate()
310                 .setDuration(200)
311                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
312                 .setUpdateListener((valueAnimator) -> {
313                     float fraction = valueAnimator.getAnimatedFraction();
314                     fraction = showDot ? fraction : 1f - fraction;
315                     setDotScale(fraction);
316                 }).withEndAction(() -> {
317                     setDotScale(showDot ? 1f : 0f);
318                     mDotIsAnimating = false;
319                     if (after != null) {
320                         after.run();
321                     }
322                 }).start();
323     }
324 
showBadge()325     void showBadge() {
326         Bitmap badge = mBubble.getAppBadge();
327         if (badge == null) {
328             setImageBitmap(mBubble.getBubbleIcon());
329             return;
330         }
331         Canvas bubbleCanvas = new Canvas();
332         Bitmap noBadgeBubble = mBubble.getBubbleIcon();
333         Bitmap bubble = noBadgeBubble.copy(noBadgeBubble.getConfig(), /* isMutable */ true);
334 
335         bubbleCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
336         bubbleCanvas.setBitmap(bubble);
337         final int bubbleSize = bubble.getWidth();
338         final int badgeSize = (int) (ICON_BADGE_SCALE * bubbleSize);
339         Rect dest = new Rect();
340         if (mOnLeft) {
341             dest.set(0, bubbleSize - badgeSize, badgeSize, bubbleSize);
342         } else {
343             dest.set(bubbleSize - badgeSize, bubbleSize - badgeSize, bubbleSize, bubbleSize);
344         }
345         bubbleCanvas.drawBitmap(badge, null /* src */, dest, mPaint);
346         bubbleCanvas.setBitmap(null);
347         setImageBitmap(bubble);
348     }
349 
hideBadge()350     void hideBadge() {
351         setImageBitmap(mBubble.getBubbleIcon());
352     }
353 }
354