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.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Matrix; 26 import android.graphics.Path; 27 import android.graphics.drawable.AdaptiveIconDrawable; 28 import android.graphics.drawable.BitmapDrawable; 29 import android.graphics.drawable.ColorDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.graphics.drawable.Icon; 32 import android.graphics.drawable.InsetDrawable; 33 import android.util.AttributeSet; 34 import android.util.PathParser; 35 import android.widget.FrameLayout; 36 37 import com.android.internal.graphics.ColorUtils; 38 import com.android.launcher3.icons.ShadowGenerator; 39 import com.android.systemui.Interpolators; 40 import com.android.systemui.R; 41 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 42 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 43 44 /** 45 * A floating object on the screen that can post message updates. 46 */ 47 public class BubbleView extends FrameLayout { 48 49 private static final int DARK_ICON_ALPHA = 180; 50 private static final double ICON_MIN_CONTRAST = 4.1; 51 private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY; 52 // Same value as Launcher3 badge code 53 private static final float WHITE_SCRIM_ALPHA = 0.54f; 54 private Context mContext; 55 56 private BadgedImageView mBadgedImageView; 57 private int mBadgeColor; 58 private int mIconInset; 59 private Drawable mUserBadgedAppIcon; 60 61 // mBubbleIconFactory cannot be static because it depends on Context. 62 private BubbleIconFactory mBubbleIconFactory; 63 64 private boolean mSuppressDot; 65 66 private Bubble mBubble; 67 BubbleView(Context context)68 public BubbleView(Context context) { 69 this(context, null); 70 } 71 BubbleView(Context context, AttributeSet attrs)72 public BubbleView(Context context, AttributeSet attrs) { 73 this(context, attrs, 0); 74 } 75 BubbleView(Context context, AttributeSet attrs, int defStyleAttr)76 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) { 77 this(context, attrs, defStyleAttr, 0); 78 } 79 BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)80 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 81 super(context, attrs, defStyleAttr, defStyleRes); 82 mContext = context; 83 mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset); 84 } 85 86 @Override onFinishInflate()87 protected void onFinishInflate() { 88 super.onFinishInflate(); 89 mBadgedImageView = findViewById(R.id.bubble_image); 90 } 91 92 @Override onAttachedToWindow()93 protected void onAttachedToWindow() { 94 super.onAttachedToWindow(); 95 } 96 97 /** 98 * Populates this view with a bubble. 99 * <p> 100 * This should only be called when a new bubble is being set on the view, updates to the 101 * current bubble should use {@link #update(Bubble)}. 102 * 103 * @param bubble the bubble to display in this view. 104 */ setBubble(Bubble bubble)105 public void setBubble(Bubble bubble) { 106 mBubble = bubble; 107 } 108 109 /** 110 * The {@link NotificationEntry} associated with this view, if one exists. 111 */ 112 @Nullable getEntry()113 public NotificationEntry getEntry() { 114 return mBubble != null ? mBubble.getEntry() : null; 115 } 116 117 /** 118 * The key for the {@link NotificationEntry} associated with this view, if one exists. 119 */ 120 @Nullable getKey()121 public String getKey() { 122 return (mBubble != null) ? mBubble.getKey() : null; 123 } 124 125 /** 126 * Updates the UI based on the bubble, updates badge and animates messages as needed. 127 */ update(Bubble bubble)128 public void update(Bubble bubble) { 129 mBubble = bubble; 130 updateViews(); 131 } 132 133 /** 134 * @param factory Factory for creating normalized bubble icons. 135 */ setBubbleIconFactory(BubbleIconFactory factory)136 public void setBubbleIconFactory(BubbleIconFactory factory) { 137 mBubbleIconFactory = factory; 138 } 139 setAppIcon(Drawable appIcon)140 public void setAppIcon(Drawable appIcon) { 141 mUserBadgedAppIcon = appIcon; 142 } 143 144 /** 145 * @return the {@link ExpandableNotificationRow} view to display notification content when the 146 * bubble is expanded. 147 */ 148 @Nullable getRowView()149 public ExpandableNotificationRow getRowView() { 150 return (mBubble != null) ? mBubble.getEntry().getRow() : null; 151 } 152 153 /** Changes the dot's visibility to match the bubble view's state. */ updateDotVisibility(boolean animate)154 void updateDotVisibility(boolean animate) { 155 updateDotVisibility(animate, null /* after */); 156 } 157 158 /** 159 * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the 160 * flyout is visible or animating, to hide the dot until the flyout visually transforms into it. 161 */ setSuppressDot(boolean suppressDot, boolean animate)162 void setSuppressDot(boolean suppressDot, boolean animate) { 163 mSuppressDot = suppressDot; 164 updateDotVisibility(animate); 165 } 166 167 /** Sets the position of the 'new' dot, animating it out and back in if requested. */ setDotPosition(boolean onLeft, boolean animate)168 void setDotPosition(boolean onLeft, boolean animate) { 169 if (animate && onLeft != mBadgedImageView.getDotOnLeft() && shouldShowDot()) { 170 animateDot(false /* showDot */, () -> { 171 mBadgedImageView.setDotOnLeft(onLeft); 172 animateDot(true /* showDot */, null); 173 }); 174 } else { 175 mBadgedImageView.setDotOnLeft(onLeft); 176 } 177 } 178 getDotCenter()179 float[] getDotCenter() { 180 float[] unscaled = mBadgedImageView.getDotCenter(); 181 return new float[]{unscaled[0], unscaled[1]}; 182 } 183 getDotPositionOnLeft()184 boolean getDotPositionOnLeft() { 185 return mBadgedImageView.getDotOnLeft(); 186 } 187 188 /** 189 * Changes the dot's visibility to match the bubble view's state, running the provided callback 190 * after animation if requested. 191 */ updateDotVisibility(boolean animate, Runnable after)192 private void updateDotVisibility(boolean animate, Runnable after) { 193 final boolean showDot = shouldShowDot(); 194 if (animate) { 195 animateDot(showDot, after); 196 } else { 197 mBadgedImageView.setShowDot(showDot); 198 mBadgedImageView.setDotScale(showDot ? 1f : 0f); 199 } 200 } 201 202 /** 203 * Animates the badge to show or hide. 204 */ animateDot(boolean showDot, Runnable after)205 private void animateDot(boolean showDot, Runnable after) { 206 if (mBadgedImageView.isShowingDot() == showDot) { 207 return; 208 } 209 // Do NOT wait until after animation ends to setShowDot 210 // to avoid overriding more recent showDot states. 211 mBadgedImageView.setShowDot(showDot); 212 mBadgedImageView.clearAnimation(); 213 mBadgedImageView.animate().setDuration(200) 214 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 215 .setUpdateListener((valueAnimator) -> { 216 float fraction = valueAnimator.getAnimatedFraction(); 217 fraction = showDot ? fraction : 1f - fraction; 218 mBadgedImageView.setDotScale(fraction); 219 }).withEndAction(() -> { 220 mBadgedImageView.setDotScale(showDot ? 1f : 0f); 221 if (after != null) { 222 after.run(); 223 } 224 }).start(); 225 } 226 updateViews()227 void updateViews() { 228 if (mBubble == null || mBubbleIconFactory == null) { 229 return; 230 } 231 // Update icon. 232 Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata(); 233 Notification n = mBubble.getEntry().notification.getNotification(); 234 Icon ic = metadata.getIcon(); 235 boolean needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP; 236 237 Drawable iconDrawable = ic.loadDrawable(mContext); 238 if (needsTint) { 239 iconDrawable = buildIconWithTint(iconDrawable, n.color); 240 } 241 Bitmap bubbleIcon = mBubbleIconFactory.createBadgedIconBitmap(iconDrawable, 242 null /* user */, 243 true /* shrinkNonAdaptiveIcons */).icon; 244 245 // Give it a shadow 246 Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(mUserBadgedAppIcon, 247 1f, mBubbleIconFactory.getBadgeSize()); 248 Canvas c = new Canvas(); 249 ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize()); 250 c.setBitmap(userBadgedBitmap); 251 shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); 252 253 mBubbleIconFactory.badgeWithDrawable(bubbleIcon, 254 new BitmapDrawable(mContext.getResources(), userBadgedBitmap)); 255 mBadgedImageView.setImageBitmap(bubbleIcon); 256 257 // Update badge. 258 int badgeColor = determineDominateColor(iconDrawable, n.color); 259 mBadgeColor = badgeColor; 260 mBadgedImageView.setDotColor(badgeColor); 261 262 // Update dot. 263 Path iconPath = PathParser.createPathFromPathData( 264 getResources().getString(com.android.internal.R.string.config_icon_mask)); 265 Matrix matrix = new Matrix(); 266 float scale = mBubbleIconFactory.getNormalizer().getScale(iconDrawable, 267 null /* outBounds */, null /* path */, null /* outMaskShape */); 268 float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f; 269 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 270 radius /* pivot y */); 271 iconPath.transform(matrix); 272 mBadgedImageView.drawDot(iconPath); 273 274 animateDot(shouldShowDot(), null /* after */); 275 } 276 shouldShowDot()277 boolean shouldShowDot() { 278 return mBubble.showBubbleDot() && !mSuppressDot; 279 } 280 getBadgeColor()281 int getBadgeColor() { 282 return mBadgeColor; 283 } 284 buildIconWithTint(Drawable iconDrawable, int backgroundColor)285 private AdaptiveIconDrawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) { 286 iconDrawable = checkTint(iconDrawable, backgroundColor); 287 InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset); 288 ColorDrawable background = new ColorDrawable(backgroundColor); 289 return new AdaptiveIconDrawable(background, foreground); 290 } 291 checkTint(Drawable iconDrawable, int backgroundColor)292 private Drawable checkTint(Drawable iconDrawable, int backgroundColor) { 293 backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */); 294 if (backgroundColor == Color.TRANSPARENT) { 295 // ColorUtils throws exception when background is translucent. 296 backgroundColor = DEFAULT_BACKGROUND_COLOR; 297 } 298 iconDrawable.setTint(Color.WHITE); 299 double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor); 300 if (contrastRatio < ICON_MIN_CONTRAST) { 301 int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA); 302 iconDrawable.setTint(dark); 303 } 304 return iconDrawable; 305 } 306 determineDominateColor(Drawable d, int defaultTint)307 private int determineDominateColor(Drawable d, int defaultTint) { 308 // XXX: should we pull from the drawable, app icon, notif tint? 309 return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA); 310 } 311 } 312