1 /*
2  * Copyright (C) 2018 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.systemui.bubbles;
17 
18 import android.annotation.Nullable;
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Path;
22 import android.graphics.Rect;
23 import android.util.AttributeSet;
24 import android.util.PathParser;
25 import android.widget.ImageView;
26 
27 import com.android.launcher3.icons.DotRenderer;
28 import com.android.systemui.Interpolators;
29 import com.android.systemui.R;
30 
31 import java.util.EnumSet;
32 
33 /**
34  * View that displays an adaptive icon with an app-badge and a dot.
35  *
36  * Dot = a small colored circle that indicates whether this bubble has an unread update.
37  * Badge = the icon associated with the app that created this bubble, this will show work profile
38  * badge if appropriate.
39  */
40 public class BadgedImageView extends ImageView {
41 
42     /** Same value as Launcher3 dot code */
43     public static final float WHITE_SCRIM_ALPHA = 0.54f;
44     /** Same as value in Launcher3 IconShape */
45     public static final int DEFAULT_PATH_SIZE = 100;
46 
47     /**
48      * Flags that suppress the visibility of the 'new' dot, for one reason or another. If any of
49      * these flags are set, the dot will not be shown even if {@link Bubble#showDot()} returns true.
50      */
51     enum SuppressionFlag {
52         // Suppressed because the flyout is visible - it will morph into the dot via animation.
53         FLYOUT_VISIBLE,
54         // Suppressed because this bubble is behind others in the collapsed stack.
55         BEHIND_STACK,
56     }
57 
58     /**
59      * Start by suppressing the dot because the flyout is visible - most bubbles are added with a
60      * flyout, so this is a reasonable default.
61      */
62     private final EnumSet<SuppressionFlag> mDotSuppressionFlags =
63             EnumSet.of(SuppressionFlag.FLYOUT_VISIBLE);
64 
65     private float mDotScale = 0f;
66     private float mAnimatingToDotScale = 0f;
67     private boolean mDotIsAnimating = false;
68 
69     private BubbleViewProvider mBubble;
70 
71     private int mBubbleBitmapSize;
72     private DotRenderer mDotRenderer;
73     private DotRenderer.DrawParams mDrawParams;
74     private boolean mOnLeft;
75 
76     private int mDotColor;
77 
78     private Rect mTempBounds = new Rect();
79 
BadgedImageView(Context context)80     public BadgedImageView(Context context) {
81         this(context, null);
82     }
83 
BadgedImageView(Context context, AttributeSet attrs)84     public BadgedImageView(Context context, AttributeSet attrs) {
85         this(context, attrs, 0);
86     }
87 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr)88     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) {
89         this(context, attrs, defStyleAttr, 0);
90     }
91 
BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)92     public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr,
93             int defStyleRes) {
94         super(context, attrs, defStyleAttr, defStyleRes);
95         mBubbleBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_bitmap_size);
96         mDrawParams = new DotRenderer.DrawParams();
97 
98         Path iconPath = PathParser.createPathFromPathData(
99                 getResources().getString(com.android.internal.R.string.config_icon_mask));
100         mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE);
101 
102         setFocusable(true);
103         setClickable(true);
104     }
105 
106     /**
107      * Updates the view with provided info.
108      */
setRenderedBubble(BubbleViewProvider bubble)109     public void setRenderedBubble(BubbleViewProvider bubble) {
110         mBubble = bubble;
111         setImageBitmap(bubble.getBadgedImage());
112         mDotColor = bubble.getDotColor();
113         drawDot(bubble.getDotPath());
114     }
115 
116     @Override
onDraw(Canvas canvas)117     public void onDraw(Canvas canvas) {
118         super.onDraw(canvas);
119 
120         if (!shouldDrawDot()) {
121             return;
122         }
123 
124         getDrawingRect(mTempBounds);
125 
126         mDrawParams.color = mDotColor;
127         mDrawParams.iconBounds = mTempBounds;
128         mDrawParams.leftAlign = mOnLeft;
129         mDrawParams.scale = mDotScale;
130 
131         mDotRenderer.draw(canvas, mDrawParams);
132     }
133 
134     /** Adds a dot suppression flag, updating dot visibility if needed. */
addDotSuppressionFlag(SuppressionFlag flag)135     void addDotSuppressionFlag(SuppressionFlag flag) {
136         if (mDotSuppressionFlags.add(flag)) {
137             // Update dot visibility, and animate out if we're now behind the stack.
138             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */);
139         }
140     }
141 
142     /** Removes a dot suppression flag, updating dot visibility if needed. */
removeDotSuppressionFlag(SuppressionFlag flag)143     void removeDotSuppressionFlag(SuppressionFlag flag) {
144         if (mDotSuppressionFlags.remove(flag)) {
145             // Update dot visibility, animating if we're no longer behind the stack.
146             updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK);
147         }
148     }
149 
150     /** Updates the visibility of the dot, animating if requested. */
updateDotVisibility(boolean animate)151     void updateDotVisibility(boolean animate) {
152         final float targetScale = shouldDrawDot() ? 1f : 0f;
153 
154         if (animate) {
155             animateDotScale(targetScale, null /* after */);
156         } else {
157             mDotScale = targetScale;
158             mAnimatingToDotScale = targetScale;
159             invalidate();
160         }
161     }
162 
163     /**
164      * Set whether the dot should appear on left or right side of the view.
165      */
setDotOnLeft(boolean onLeft)166     void setDotOnLeft(boolean onLeft) {
167         mOnLeft = onLeft;
168         invalidate();
169     }
170 
171     /**
172      * @param iconPath The new icon path to use when calculating dot position.
173      */
drawDot(Path iconPath)174     void drawDot(Path iconPath) {
175         mDotRenderer = new DotRenderer(mBubbleBitmapSize, iconPath, DEFAULT_PATH_SIZE);
176         invalidate();
177     }
178 
179     /**
180      * How big the dot should be, fraction from 0 to 1.
181      */
setDotScale(float fraction)182     void setDotScale(float fraction) {
183         mDotScale = fraction;
184         invalidate();
185     }
186 
187     /**
188      * Whether decorations (badges or dots) are on the left.
189      */
getDotOnLeft()190     boolean getDotOnLeft() {
191         return mOnLeft;
192     }
193 
194     /**
195      * Return dot position relative to bubble view container bounds.
196      */
getDotCenter()197     float[] getDotCenter() {
198         float[] dotPosition;
199         if (mOnLeft) {
200             dotPosition = mDotRenderer.getLeftDotPosition();
201         } else {
202             dotPosition = mDotRenderer.getRightDotPosition();
203         }
204         getDrawingRect(mTempBounds);
205         float dotCenterX = mTempBounds.width() * dotPosition[0];
206         float dotCenterY = mTempBounds.height() * dotPosition[1];
207         return new float[]{dotCenterX, dotCenterY};
208     }
209 
210     /**
211      * The key for the {@link Bubble} associated with this view, if one exists.
212      */
213     @Nullable
getKey()214     public String getKey() {
215         return (mBubble != null) ? mBubble.getKey() : null;
216     }
217 
getDotColor()218     int getDotColor() {
219         return mDotColor;
220     }
221 
222     /** Sets the position of the 'new' dot, animating it out and back in if requested. */
setDotPositionOnLeft(boolean onLeft, boolean animate)223     void setDotPositionOnLeft(boolean onLeft, boolean animate) {
224         if (animate && onLeft != getDotOnLeft() && shouldDrawDot()) {
225             animateDotScale(0f /* showDot */, () -> {
226                 setDotOnLeft(onLeft);
227                 animateDotScale(1.0f, null /* after */);
228             });
229         } else {
230             setDotOnLeft(onLeft);
231         }
232     }
233 
getDotPositionOnLeft()234     boolean getDotPositionOnLeft() {
235         return getDotOnLeft();
236     }
237 
238     /** Whether to draw the dot in onDraw(). */
shouldDrawDot()239     private boolean shouldDrawDot() {
240         // Always render the dot if it's animating, since it could be animating out. Otherwise, show
241         // it if the bubble wants to show it, and we aren't suppressing it.
242         return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty());
243     }
244 
245     /**
246      * Animates the dot to the given scale, running the optional callback when the animation ends.
247      */
animateDotScale(float toScale, @Nullable Runnable after)248     private void animateDotScale(float toScale, @Nullable Runnable after) {
249         mDotIsAnimating = true;
250 
251         // Don't restart the animation if we're already animating to the given value.
252         if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
253             mDotIsAnimating = false;
254             return;
255         }
256 
257         mAnimatingToDotScale = toScale;
258 
259         final boolean showDot = toScale > 0f;
260 
261         // Do NOT wait until after animation ends to setShowDot
262         // to avoid overriding more recent showDot states.
263         clearAnimation();
264         animate()
265                 .setDuration(200)
266                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
267                 .setUpdateListener((valueAnimator) -> {
268                     float fraction = valueAnimator.getAnimatedFraction();
269                     fraction = showDot ? fraction : 1f - fraction;
270                     setDotScale(fraction);
271                 }).withEndAction(() -> {
272                     setDotScale(showDot ? 1f : 0f);
273                     mDotIsAnimating = false;
274                     if (after != null) {
275                         after.run();
276                     }
277                 }).start();
278     }
279 }
280