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 mOnLeft;
86 
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         mOnLeft = onLeft;
157         hideBadge();
158     }
159 
160     /**
161      * Updates the view with provided info.
162      */
setRenderedBubble(BubbleViewProvider bubble)163     public void setRenderedBubble(BubbleViewProvider bubble) {
164         mBubble = bubble;
165         mBubbleIcon.setImageBitmap(bubble.getBubbleIcon());
166         mAppIcon.setImageBitmap(bubble.getAppBadge());
167         if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) {
168             hideBadge();
169         } else {
170             showBadge();
171         }
172         mDotColor = bubble.getDotColor();
173         drawDot(bubble.getDotPath());
174     }
175 
176     @Override
dispatchDraw(Canvas canvas)177     public void dispatchDraw(Canvas canvas) {
178         super.dispatchDraw(canvas);
179 
180         if (!shouldDrawDot()) {
181             return;
182         }
183 
184         getDrawingRect(mTempBounds);
185 
186         mDrawParams.dotColor = mDotColor;
187         mDrawParams.iconBounds = mTempBounds;
188         mDrawParams.leftAlign = mOnLeft;
189         mDrawParams.scale = mDotScale;
190 
191         mDotRenderer.draw(canvas, mDrawParams);
192     }
193 
194     /**
195      * Set drawable resource shown as the icon
196      */
setIconImageResource(@rawableRes int drawable)197     public void setIconImageResource(@DrawableRes int drawable) {
198         mBubbleIcon.setImageResource(drawable);
199     }
200 
201     /**
202      * Get icon drawable
203      */
getIconDrawable()204     public Drawable getIconDrawable() {
205         return mBubbleIcon.getDrawable();
206     }
207 
208     /** Adds a dot suppression flag, updating dot visibility if needed. */
addDotSuppressionFlag(SuppressionFlag flag)209     void addDotSuppressionFlag(SuppressionFlag flag) {
210         if (mDotSuppressionFlags.add(flag)) {
211             // Update dot visibility, and animate out if we're now behind the stack.
212             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */);
213         }
214     }
215 
216     /** Removes a dot suppression flag, updating dot visibility if needed. */
removeDotSuppressionFlag(SuppressionFlag flag)217     void removeDotSuppressionFlag(SuppressionFlag flag) {
218         if (mDotSuppressionFlags.remove(flag)) {
219             // Update dot visibility, animating if we're no longer behind the stack.
220             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK);
221         }
222     }
223 
224     /** Updates the visibility of the dot, animating if requested. */
updateDotVisibility(boolean animate)225     void updateDotVisibility(boolean animate) {
226         final float targetScale = shouldDrawDot() ? 1f : 0f;
227 
228         if (animate) {
229             animateDotScale(targetScale, null /* after */);
230         } else {
231             mDotScale = targetScale;
232             mAnimatingToDotScale = targetScale;
233             invalidate();
234         }
235     }
236 
237     /**
238      * @param iconPath The new icon path to use when calculating dot position.
239      */
drawDot(Path iconPath)240     void drawDot(Path iconPath) {
241         mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(),
242                 iconPath, DEFAULT_PATH_SIZE);
243         invalidate();
244     }
245 
246     /**
247      * How big the dot should be, fraction from 0 to 1.
248      */
setDotScale(float fraction)249     void setDotScale(float fraction) {
250         mDotScale = fraction;
251         invalidate();
252     }
253 
254     /**
255      * Whether decorations (badges or dots) are on the left.
256      */
getDotOnLeft()257     boolean getDotOnLeft() {
258         return mOnLeft;
259     }
260 
261     /**
262      * Return dot position relative to bubble view container bounds.
263      */
getDotCenter()264     float[] getDotCenter() {
265         float[] dotPosition;
266         if (mOnLeft) {
267             dotPosition = mDotRenderer.getLeftDotPosition();
268         } else {
269             dotPosition = mDotRenderer.getRightDotPosition();
270         }
271         getDrawingRect(mTempBounds);
272         float dotCenterX = mTempBounds.width() * dotPosition[0];
273         float dotCenterY = mTempBounds.height() * dotPosition[1];
274         return new float[]{dotCenterX, dotCenterY};
275     }
276 
277     /**
278      * The key for the {@link Bubble} associated with this view, if one exists.
279      */
280     @Nullable
getKey()281     public String getKey() {
282         return (mBubble != null) ? mBubble.getKey() : null;
283     }
284 
getDotColor()285     int getDotColor() {
286         return mDotColor;
287     }
288 
289     /** Sets the position of the dot and badge, animating them out and back in if requested. */
animateDotBadgePositions(boolean onLeft)290     void animateDotBadgePositions(boolean onLeft) {
291         mOnLeft = onLeft;
292 
293         if (onLeft != getDotOnLeft() && shouldDrawDot()) {
294             animateDotScale(0f /* showDot */, () -> {
295                 invalidate();
296                 animateDotScale(1.0f, null /* after */);
297             });
298         }
299         // TODO animate badge
300         showBadge();
301 
302     }
303 
304     /** Sets the position of the dot and badge. */
setDotBadgeOnLeft(boolean onLeft)305     void setDotBadgeOnLeft(boolean onLeft) {
306         mOnLeft = onLeft;
307         invalidate();
308         showBadge();
309     }
310 
311     /** Whether to draw the dot in onDraw(). */
shouldDrawDot()312     private boolean shouldDrawDot() {
313         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
314         // it if the bubble wants to show it, and we aren't suppressing it.
315         return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty());
316     }
317 
318     /**
319      * Animates the dot to the given scale, running the optional callback when the animation ends.
320      */
animateDotScale(float toScale, @Nullable Runnable after)321     private void animateDotScale(float toScale, @Nullable Runnable after) {
322         mDotIsAnimating = true;
323 
324         // Don't restart the animation if we're already animating to the given value.
325         if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
326             mDotIsAnimating = false;
327             return;
328         }
329 
330         mAnimatingToDotScale = toScale;
331 
332         final boolean showDot = toScale > 0f;
333 
334         // Do NOT wait until after animation ends to setShowDot
335         // to avoid overriding more recent showDot states.
336         clearAnimation();
337         animate()
338                 .setDuration(200)
339                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
340                 .setUpdateListener((valueAnimator) -> {
341                     float fraction = valueAnimator.getAnimatedFraction();
342                     fraction = showDot ? fraction : 1f - fraction;
343                     setDotScale(fraction);
344                 }).withEndAction(() -> {
345                     setDotScale(showDot ? 1f : 0f);
346                     mDotIsAnimating = false;
347                     if (after != null) {
348                         after.run();
349                     }
350                 }).start();
351     }
352 
showBadge()353     void showBadge() {
354         Bitmap appBadgeBitmap = mBubble.getAppBadge();
355         if (appBadgeBitmap == null) {
356             mAppIcon.setVisibility(GONE);
357             return;
358         }
359 
360         int translationX;
361         if (mOnLeft) {
362             translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth());
363         } else {
364             translationX = 0;
365         }
366 
367         mAppIcon.setTranslationX(translationX);
368         mAppIcon.setVisibility(VISIBLE);
369     }
370 
hideBadge()371     void hideBadge() {
372         mAppIcon.setVisibility(GONE);
373     }
374 
375     @Override
toString()376     public String toString() {
377         return "BadgedImageView{" + mBubble + "}";
378     }
379 }
380