• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.systemui.bubbles;
18 
19 import android.annotation.Nullable;
20 import android.app.Notification;
21 import android.content.Context;
22 import android.graphics.Color;
23 import android.graphics.drawable.AdaptiveIconDrawable;
24 import android.graphics.drawable.ColorDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.Icon;
27 import android.graphics.drawable.InsetDrawable;
28 import android.util.AttributeSet;
29 import android.widget.FrameLayout;
30 
31 import com.android.internal.graphics.ColorUtils;
32 import com.android.systemui.Interpolators;
33 import com.android.systemui.R;
34 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
36 
37 /**
38  * A floating object on the screen that can post message updates.
39  */
40 public class BubbleView extends FrameLayout {
41     private static final String TAG = "BubbleView";
42 
43     private static final int DARK_ICON_ALPHA = 180;
44     private static final double ICON_MIN_CONTRAST = 4.1;
45     private static final int DEFAULT_BACKGROUND_COLOR =  Color.LTGRAY;
46     // Same value as Launcher3 badge code
47     private static final float WHITE_SCRIM_ALPHA = 0.54f;
48     private Context mContext;
49 
50     private BadgedImageView mBadgedImageView;
51     private int mBadgeColor;
52     private int mPadding;
53     private int mIconInset;
54 
55     private boolean mSuppressDot = false;
56 
57     private NotificationEntry mEntry;
58 
BubbleView(Context context)59     public BubbleView(Context context) {
60         this(context, null);
61     }
62 
BubbleView(Context context, AttributeSet attrs)63     public BubbleView(Context context, AttributeSet attrs) {
64         this(context, attrs, 0);
65     }
66 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr)67     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
68         this(context, attrs, defStyleAttr, 0);
69     }
70 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)71     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
72         super(context, attrs, defStyleAttr, defStyleRes);
73         mContext = context;
74         // XXX: can this padding just be on the view and we look it up?
75         mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding);
76         mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
77     }
78 
79     @Override
onFinishInflate()80     protected void onFinishInflate() {
81         super.onFinishInflate();
82         mBadgedImageView = findViewById(R.id.bubble_image);
83     }
84 
85     @Override
onAttachedToWindow()86     protected void onAttachedToWindow() {
87         super.onAttachedToWindow();
88     }
89 
90     /**
91      * Populates this view with a notification.
92      * <p>
93      * This should only be called when a new notification is being set on the view, updates to the
94      * current notification should use {@link #update(NotificationEntry)}.
95      *
96      * @param entry the notification to display as a bubble.
97      */
setNotif(NotificationEntry entry)98     public void setNotif(NotificationEntry entry) {
99         mEntry = entry;
100         updateViews();
101     }
102 
103     /**
104      * The {@link NotificationEntry} associated with this view, if one exists.
105      */
106     @Nullable
getEntry()107     public NotificationEntry getEntry() {
108         return mEntry;
109     }
110 
111     /**
112      * The key for the {@link NotificationEntry} associated with this view, if one exists.
113      */
114     @Nullable
getKey()115     public String getKey() {
116         return (mEntry != null) ? mEntry.key : null;
117     }
118 
119     /**
120      * Updates the UI based on the entry, updates badge and animates messages as needed.
121      */
update(NotificationEntry entry)122     public void update(NotificationEntry entry) {
123         mEntry = entry;
124         updateViews();
125     }
126 
127     /**
128      * @return the {@link ExpandableNotificationRow} view to display notification content when the
129      * bubble is expanded.
130      */
131     @Nullable
getRowView()132     public ExpandableNotificationRow getRowView() {
133         return (mEntry != null) ? mEntry.getRow() : null;
134     }
135 
136     /** Changes the dot's visibility to match the bubble view's state. */
updateDotVisibility(boolean animate)137     void updateDotVisibility(boolean animate) {
138         updateDotVisibility(animate, null /* after */);
139     }
140 
141 
142     /**
143      * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
144      * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
145      */
setSuppressDot(boolean suppressDot, boolean animate)146     void setSuppressDot(boolean suppressDot, boolean animate) {
147         mSuppressDot = suppressDot;
148         updateDotVisibility(animate);
149     }
150 
151     /** Sets the position of the 'new' dot, animating it out and back in if requested. */
setDotPosition(boolean onLeft, boolean animate)152     void setDotPosition(boolean onLeft, boolean animate) {
153         if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) {
154             animateDot(false /* showDot */, () -> {
155                 mBadgedImageView.setDotPosition(onLeft);
156                 animateDot(true /* showDot */, null);
157             });
158         } else {
159             mBadgedImageView.setDotPosition(onLeft);
160         }
161     }
162 
getDotPositionOnLeft()163     boolean getDotPositionOnLeft() {
164         return mBadgedImageView.getDotPosition();
165     }
166 
167     /**
168      * Changes the dot's visibility to match the bubble view's state, running the provided callback
169      * after animation if requested.
170      */
updateDotVisibility(boolean animate, Runnable after)171     private void updateDotVisibility(boolean animate, Runnable after) {
172         boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot;
173 
174         if (animate) {
175             animateDot(showDot, after);
176         } else {
177             mBadgedImageView.setShowDot(showDot);
178         }
179     }
180 
181     /**
182      * Animates the badge to show or hide.
183      */
animateDot(boolean showDot, Runnable after)184     private void animateDot(boolean showDot, Runnable after) {
185         if (mBadgedImageView.isShowingDot() != showDot) {
186             if (showDot) {
187                 mBadgedImageView.setShowDot(true);
188             }
189 
190             mBadgedImageView.clearAnimation();
191             mBadgedImageView.animate().setDuration(200)
192                     .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
193                     .setUpdateListener((valueAnimator) -> {
194                         float fraction = valueAnimator.getAnimatedFraction();
195                         fraction = showDot ? fraction : 1f - fraction;
196                         mBadgedImageView.setDotScale(fraction);
197                     }).withEndAction(() -> {
198                         if (!showDot) {
199                             mBadgedImageView.setShowDot(false);
200                         }
201 
202                         if (after != null) {
203                             after.run();
204                         }
205             }).start();
206         }
207     }
208 
updateViews()209     void updateViews() {
210         if (mEntry == null) {
211             return;
212         }
213         Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
214         Notification n = mEntry.notification.getNotification();
215         Icon ic;
216         boolean needsTint;
217         if (metadata != null) {
218             ic = metadata.getIcon();
219             needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP;
220         } else {
221             needsTint = n.getLargeIcon() == null;
222             ic = needsTint ? n.getSmallIcon() : n.getLargeIcon();
223         }
224         Drawable iconDrawable = ic.loadDrawable(mContext);
225         if (needsTint) {
226             mBadgedImageView.setImageDrawable(buildIconWithTint(iconDrawable, n.color));
227         } else {
228             mBadgedImageView.setImageDrawable(iconDrawable);
229         }
230         int badgeColor = determineDominateColor(iconDrawable, n.color);
231         mBadgeColor = badgeColor;
232         mBadgedImageView.setDotColor(badgeColor);
233         animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */);
234     }
235 
getBadgeColor()236     int getBadgeColor() {
237         return mBadgeColor;
238     }
239 
buildIconWithTint(Drawable iconDrawable, int backgroundColor)240     private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) {
241         iconDrawable = checkTint(iconDrawable, backgroundColor);
242         InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset);
243         ColorDrawable background = new ColorDrawable(backgroundColor);
244         return new AdaptiveIconDrawable(background, foreground);
245     }
246 
checkTint(Drawable iconDrawable, int backgroundColor)247     private Drawable checkTint(Drawable iconDrawable, int backgroundColor) {
248         backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */);
249         if (backgroundColor == Color.TRANSPARENT) {
250             // ColorUtils throws exception when background is translucent.
251             backgroundColor = DEFAULT_BACKGROUND_COLOR;
252         }
253         iconDrawable.setTint(Color.WHITE);
254         double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor);
255         if (contrastRatio < ICON_MIN_CONTRAST) {
256             int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA);
257             iconDrawable.setTint(dark);
258         }
259         return iconDrawable;
260     }
261 
determineDominateColor(Drawable d, int defaultTint)262     private int determineDominateColor(Drawable d, int defaultTint) {
263         // XXX: should we pull from the drawable, app icon, notif tint?
264         return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
265     }
266 }
267