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