• 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 com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
19 
20 import android.annotation.DrawableRes;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Outline;
27 import android.graphics.Path;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 import android.util.AttributeSet;
31 import android.util.PathParser;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewOutlineProvider;
35 import android.widget.ImageView;
36 
37 import androidx.constraintlayout.widget.ConstraintLayout;
38 
39 import com.android.launcher3.icons.DotRenderer;
40 import com.android.wm.shell.R;
41 import com.android.wm.shell.shared.animation.Interpolators;
42 
43 import java.util.EnumSet;
44 
45 /**
46  * View that displays an adaptive icon with an app-badge and a dot.
47  *
48  * Dot = a small colored circle that indicates whether this bubble has an unread update.
49  * Badge = the icon associated with the app that created this bubble, this will show work profile
50  * badge if appropriate.
51  */
52 public class BadgedImageView extends ConstraintLayout {
53 
54     /** Same value as Launcher3 dot code */
55     public static final float WHITE_SCRIM_ALPHA = 0.54f;
56     /** Same as value in Launcher3 IconShape */
57     public static final int DEFAULT_PATH_SIZE = 100;
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 final ImageView mBubbleIcon;
78     private final ImageView mAppIcon;
79 
80     private float mDotScale = 0f;
81     private float mAnimatingToDotScale = 0f;
82     private boolean mDotIsAnimating = false;
83 
84     private BubbleViewProvider mBubble;
85     private BubblePositioner mPositioner;
86     private boolean mBadgeOnLeft;
87     private boolean mDotOnLeft;
88     private DotRenderer mDotRenderer;
89     private DotRenderer.DrawParams mDrawParams;
90     private int mDotColor;
91 
92     private Rect mTempBounds = new Rect();
93 
BadgedImageView(Context context)94     public BadgedImageView(Context context) {
95         this(context, null);
96     }
97 
BadgedImageView(Context context, AttributeSet attrs)98     public BadgedImageView(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr)102     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) {
103         this(context, attrs, defStyleAttr, 0);
104     }
105 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)106     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr,
107             int defStyleRes) {
108         super(context, attrs, defStyleAttr, defStyleRes);
109         // We manage positioning the badge ourselves
110         setLayoutDirection(LAYOUT_DIRECTION_LTR);
111 
112         LayoutInflater.from(context).inflate(R.layout.badged_image_view, this);
113 
114         mBubbleIcon = findViewById(R.id.icon_view);
115         mAppIcon = findViewById(R.id.app_icon_view);
116 
117         final TypedArray ta = mContext.obtainStyledAttributes(attrs, new int[]{android.R.attr.src},
118                 defStyleAttr, defStyleRes);
119         mBubbleIcon.setImageResource(ta.getResourceId(0, 0));
120         ta.recycle();
121 
122         mDrawParams = new DotRenderer.DrawParams();
123 
124         setFocusable(true);
125         setClickable(true);
126         setOutlineProvider(new ViewOutlineProvider() {
127             @Override
128             public void getOutline(View view, Outline outline) {
129                 BadgedImageView.this.getOutline(outline);
130             }
131         });
132     }
133 
getOutline(Outline outline)134     private void getOutline(Outline outline) {
135         final int bubbleSize = mPositioner.getBubbleSize();
136         final int normalizedSize = Math.round(ICON_VISIBLE_AREA_FACTOR * bubbleSize);
137         final int inset = (bubbleSize - normalizedSize) / 2;
138         outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
139     }
140 
initialize(BubblePositioner positioner)141     public void initialize(BubblePositioner positioner) {
142         mPositioner = positioner;
143 
144         Path iconPath = PathParser.createPathFromPathData(
145                 getResources().getString(com.android.internal.R.string.config_icon_mask));
146         mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(),
147                 iconPath, DEFAULT_PATH_SIZE);
148     }
149 
showDotAndBadge(boolean onLeft)150     public void showDotAndBadge(boolean onLeft) {
151         removeDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
152         animateDotBadgePositions(onLeft);
153     }
154 
hideDotAndBadge(boolean onLeft)155     public void hideDotAndBadge(boolean onLeft) {
156         addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
157         mBadgeOnLeft = onLeft;
158         mDotOnLeft = onLeft;
159         hideBadge();
160     }
161 
162     /**
163      * Updates the view with provided info.
164      */
setRenderedBubble(BubbleViewProvider bubble)165     public void setRenderedBubble(BubbleViewProvider bubble) {
166         mBubble = bubble;
167         mBubbleIcon.setImageBitmap(bubble.getBubbleIcon());
168         mAppIcon.setImageBitmap(bubble.getAppBadge());
169         if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) {
170             hideBadge();
171         } else {
172             showBadge();
173         }
174         mDotColor = bubble.getDotColor();
175         drawDot(bubble.getDotPath());
176     }
177 
178     @Override
dispatchDraw(Canvas canvas)179     public void dispatchDraw(Canvas canvas) {
180         super.dispatchDraw(canvas);
181 
182         if (!shouldDrawDot()) {
183             return;
184         }
185 
186         getDrawingRect(mTempBounds);
187 
188         mDrawParams.dotColor = mDotColor;
189         mDrawParams.iconBounds = mTempBounds;
190         mDrawParams.leftAlign = mDotOnLeft;
191         mDrawParams.scale = mDotScale;
192 
193         mDotRenderer.draw(canvas, mDrawParams);
194     }
195 
196     /**
197      * Set drawable resource shown as the icon
198      */
setIconImageResource(@rawableRes int drawable)199     public void setIconImageResource(@DrawableRes int drawable) {
200         mBubbleIcon.setImageResource(drawable);
201     }
202 
203     /**
204      * Get icon drawable
205      */
getIconDrawable()206     public Drawable getIconDrawable() {
207         return mBubbleIcon.getDrawable();
208     }
209 
210     /** Adds a dot suppression flag, updating dot visibility if needed. */
addDotSuppressionFlag(SuppressionFlag flag)211     void addDotSuppressionFlag(SuppressionFlag flag) {
212         if (mDotSuppressionFlags.add(flag)) {
213             // Update dot visibility, and animate out if we're now behind the stack.
214             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */);
215         }
216     }
217 
218     /** Removes a dot suppression flag, updating dot visibility if needed. */
removeDotSuppressionFlag(SuppressionFlag flag)219     void removeDotSuppressionFlag(SuppressionFlag flag) {
220         if (mDotSuppressionFlags.remove(flag)) {
221             // Update dot visibility, animating if we're no longer behind the stack.
222             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK);
223         }
224     }
225 
226     /** Updates the visibility of the dot, animating if requested. */
updateDotVisibility(boolean animate)227     void updateDotVisibility(boolean animate) {
228         final float targetScale = shouldDrawDot() ? 1f : 0f;
229 
230         if (animate) {
231             animateDotScale(targetScale, null /* after */);
232         } else {
233             mDotScale = targetScale;
234             mAnimatingToDotScale = targetScale;
235             invalidate();
236         }
237     }
238 
239     /**
240      * @param iconPath The new icon path to use when calculating dot position.
241      */
drawDot(Path iconPath)242     void drawDot(Path iconPath) {
243         mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(),
244                 iconPath, DEFAULT_PATH_SIZE);
245         invalidate();
246     }
247 
248     /**
249      * How big the dot should be, fraction from 0 to 1.
250      */
setDotScale(float fraction)251     void setDotScale(float fraction) {
252         mDotScale = fraction;
253         invalidate();
254     }
255 
256     /**
257      * Whether decorations (badges or dots) are on the left.
258      */
getDotOnLeft()259     boolean getDotOnLeft() {
260         return mDotOnLeft;
261     }
262 
263     /**
264      * Return dot position relative to bubble view container bounds.
265      */
getDotCenter()266     float[] getDotCenter() {
267         float[] dotPosition;
268         if (mDotOnLeft) {
269             dotPosition = mDotRenderer.getLeftDotPosition();
270         } else {
271             dotPosition = mDotRenderer.getRightDotPosition();
272         }
273         getDrawingRect(mTempBounds);
274         float dotCenterX = mTempBounds.width() * dotPosition[0];
275         float dotCenterY = mTempBounds.height() * dotPosition[1];
276         return new float[]{dotCenterX, dotCenterY};
277     }
278 
279     /**
280      * The key for the {@link Bubble} associated with this view, if one exists.
281      */
282     @Nullable
getKey()283     public String getKey() {
284         return (mBubble != null) ? mBubble.getKey() : null;
285     }
286 
getDotColor()287     int getDotColor() {
288         return mDotColor;
289     }
290 
291     /** Sets the position of the dot and badge, animating them out and back in if requested. */
animateDotBadgePositions(boolean onLeft)292     void animateDotBadgePositions(boolean onLeft) {
293         if (onLeft != getDotOnLeft()) {
294             if (shouldDrawDot()) {
295                 animateDotScale(0f /* showDot */, () -> {
296                     mDotOnLeft = onLeft;
297                     invalidate();
298                     animateDotScale(1.0f, null /* after */);
299                 });
300             } else {
301                 mDotOnLeft = onLeft;
302             }
303         }
304         mBadgeOnLeft = onLeft;
305         // TODO animate badge
306         showBadge();
307     }
308 
309     /** Sets the position of the dot and badge. */
setDotBadgeOnLeft(boolean onLeft)310     void setDotBadgeOnLeft(boolean onLeft) {
311         mBadgeOnLeft = onLeft;
312         mDotOnLeft = onLeft;
313         invalidate();
314         showBadge();
315     }
316 
317     /** Whether to draw the dot in onDraw(). */
shouldDrawDot()318     private boolean shouldDrawDot() {
319         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
320         // it if the bubble wants to show it, and we aren't suppressing it.
321         return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty());
322     }
323 
324     /**
325      * Animates the dot to the given scale, running the optional callback when the animation ends.
326      */
animateDotScale(float toScale, @Nullable Runnable after)327     public void animateDotScale(float toScale, @Nullable Runnable after) {
328         mDotIsAnimating = true;
329 
330         // Don't restart the animation if we're already animating to the given value.
331         if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
332             mDotIsAnimating = false;
333             return;
334         }
335 
336         mAnimatingToDotScale = toScale;
337 
338         final boolean showDot = toScale > 0f;
339 
340         // Do NOT wait until after animation ends to setShowDot
341         // to avoid overriding more recent showDot states.
342         clearAnimation();
343         animate()
344                 .setDuration(200)
345                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
346                 .setUpdateListener((valueAnimator) -> {
347                     float fraction = valueAnimator.getAnimatedFraction();
348                     fraction = showDot ? fraction : 1f - fraction;
349                     setDotScale(fraction);
350                 }).withEndAction(() -> {
351                     setDotScale(showDot ? 1f : 0f);
352                     mDotIsAnimating = false;
353                     if (after != null) {
354                         after.run();
355                     }
356                 }).start();
357     }
358 
showBadge()359     void showBadge() {
360         Bitmap appBadgeBitmap = mBubble.getAppBadge();
361         final boolean showAppBadge = (mBubble instanceof Bubble)
362                 && ((Bubble) mBubble).showAppBadge();
363         if (appBadgeBitmap == null || !showAppBadge) {
364             mAppIcon.setVisibility(GONE);
365             return;
366         }
367 
368         int translationX;
369         if (mBadgeOnLeft) {
370             translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth());
371         } else {
372             translationX = 0;
373         }
374 
375         mAppIcon.setTranslationX(translationX);
376         mAppIcon.setVisibility(VISIBLE);
377     }
378 
hideBadge()379     void hideBadge() {
380         mAppIcon.setVisibility(GONE);
381     }
382 
383     @Override
toString()384     public String toString() {
385         return "BadgedImageView{" + mBubble + "}";
386     }
387 }
388