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