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