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