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 mOnLeft; 86 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 mOnLeft = onLeft; 157 hideBadge(); 158 } 159 160 /** 161 * Updates the view with provided info. 162 */ setRenderedBubble(BubbleViewProvider bubble)163 public void setRenderedBubble(BubbleViewProvider bubble) { 164 mBubble = bubble; 165 mBubbleIcon.setImageBitmap(bubble.getBubbleIcon()); 166 mAppIcon.setImageBitmap(bubble.getAppBadge()); 167 if (mDotSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK)) { 168 hideBadge(); 169 } else { 170 showBadge(); 171 } 172 mDotColor = bubble.getDotColor(); 173 drawDot(bubble.getDotPath()); 174 } 175 176 @Override dispatchDraw(Canvas canvas)177 public void dispatchDraw(Canvas canvas) { 178 super.dispatchDraw(canvas); 179 180 if (!shouldDrawDot()) { 181 return; 182 } 183 184 getDrawingRect(mTempBounds); 185 186 mDrawParams.dotColor = mDotColor; 187 mDrawParams.iconBounds = mTempBounds; 188 mDrawParams.leftAlign = mOnLeft; 189 mDrawParams.scale = mDotScale; 190 191 mDotRenderer.draw(canvas, mDrawParams); 192 } 193 194 /** 195 * Set drawable resource shown as the icon 196 */ setIconImageResource(@rawableRes int drawable)197 public void setIconImageResource(@DrawableRes int drawable) { 198 mBubbleIcon.setImageResource(drawable); 199 } 200 201 /** 202 * Get icon drawable 203 */ getIconDrawable()204 public Drawable getIconDrawable() { 205 return mBubbleIcon.getDrawable(); 206 } 207 208 /** Adds a dot suppression flag, updating dot visibility if needed. */ addDotSuppressionFlag(SuppressionFlag flag)209 void addDotSuppressionFlag(SuppressionFlag flag) { 210 if (mDotSuppressionFlags.add(flag)) { 211 // Update dot visibility, and animate out if we're now behind the stack. 212 updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK /* animate */); 213 } 214 } 215 216 /** Removes a dot suppression flag, updating dot visibility if needed. */ removeDotSuppressionFlag(SuppressionFlag flag)217 void removeDotSuppressionFlag(SuppressionFlag flag) { 218 if (mDotSuppressionFlags.remove(flag)) { 219 // Update dot visibility, animating if we're no longer behind the stack. 220 updateDotVisibility(flag == SuppressionFlag.BEHIND_STACK); 221 } 222 } 223 224 /** Updates the visibility of the dot, animating if requested. */ updateDotVisibility(boolean animate)225 void updateDotVisibility(boolean animate) { 226 final float targetScale = shouldDrawDot() ? 1f : 0f; 227 228 if (animate) { 229 animateDotScale(targetScale, null /* after */); 230 } else { 231 mDotScale = targetScale; 232 mAnimatingToDotScale = targetScale; 233 invalidate(); 234 } 235 } 236 237 /** 238 * @param iconPath The new icon path to use when calculating dot position. 239 */ drawDot(Path iconPath)240 void drawDot(Path iconPath) { 241 mDotRenderer = new DotRenderer(mPositioner.getBubbleSize(), 242 iconPath, DEFAULT_PATH_SIZE); 243 invalidate(); 244 } 245 246 /** 247 * How big the dot should be, fraction from 0 to 1. 248 */ setDotScale(float fraction)249 void setDotScale(float fraction) { 250 mDotScale = fraction; 251 invalidate(); 252 } 253 254 /** 255 * Whether decorations (badges or dots) are on the left. 256 */ getDotOnLeft()257 boolean getDotOnLeft() { 258 return mOnLeft; 259 } 260 261 /** 262 * Return dot position relative to bubble view container bounds. 263 */ getDotCenter()264 float[] getDotCenter() { 265 float[] dotPosition; 266 if (mOnLeft) { 267 dotPosition = mDotRenderer.getLeftDotPosition(); 268 } else { 269 dotPosition = mDotRenderer.getRightDotPosition(); 270 } 271 getDrawingRect(mTempBounds); 272 float dotCenterX = mTempBounds.width() * dotPosition[0]; 273 float dotCenterY = mTempBounds.height() * dotPosition[1]; 274 return new float[]{dotCenterX, dotCenterY}; 275 } 276 277 /** 278 * The key for the {@link Bubble} associated with this view, if one exists. 279 */ 280 @Nullable getKey()281 public String getKey() { 282 return (mBubble != null) ? mBubble.getKey() : null; 283 } 284 getDotColor()285 int getDotColor() { 286 return mDotColor; 287 } 288 289 /** Sets the position of the dot and badge, animating them out and back in if requested. */ animateDotBadgePositions(boolean onLeft)290 void animateDotBadgePositions(boolean onLeft) { 291 mOnLeft = onLeft; 292 293 if (onLeft != getDotOnLeft() && shouldDrawDot()) { 294 animateDotScale(0f /* showDot */, () -> { 295 invalidate(); 296 animateDotScale(1.0f, null /* after */); 297 }); 298 } 299 // TODO animate badge 300 showBadge(); 301 302 } 303 304 /** Sets the position of the dot and badge. */ setDotBadgeOnLeft(boolean onLeft)305 void setDotBadgeOnLeft(boolean onLeft) { 306 mOnLeft = onLeft; 307 invalidate(); 308 showBadge(); 309 } 310 311 /** Whether to draw the dot in onDraw(). */ shouldDrawDot()312 private boolean shouldDrawDot() { 313 // Always render the dot if it's animating, since it could be animating out. Otherwise, show 314 // it if the bubble wants to show it, and we aren't suppressing it. 315 return mDotIsAnimating || (mBubble.showDot() && mDotSuppressionFlags.isEmpty()); 316 } 317 318 /** 319 * Animates the dot to the given scale, running the optional callback when the animation ends. 320 */ animateDotScale(float toScale, @Nullable Runnable after)321 private void animateDotScale(float toScale, @Nullable Runnable after) { 322 mDotIsAnimating = true; 323 324 // Don't restart the animation if we're already animating to the given value. 325 if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { 326 mDotIsAnimating = false; 327 return; 328 } 329 330 mAnimatingToDotScale = toScale; 331 332 final boolean showDot = toScale > 0f; 333 334 // Do NOT wait until after animation ends to setShowDot 335 // to avoid overriding more recent showDot states. 336 clearAnimation(); 337 animate() 338 .setDuration(200) 339 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 340 .setUpdateListener((valueAnimator) -> { 341 float fraction = valueAnimator.getAnimatedFraction(); 342 fraction = showDot ? fraction : 1f - fraction; 343 setDotScale(fraction); 344 }).withEndAction(() -> { 345 setDotScale(showDot ? 1f : 0f); 346 mDotIsAnimating = false; 347 if (after != null) { 348 after.run(); 349 } 350 }).start(); 351 } 352 showBadge()353 void showBadge() { 354 Bitmap appBadgeBitmap = mBubble.getAppBadge(); 355 if (appBadgeBitmap == null) { 356 mAppIcon.setVisibility(GONE); 357 return; 358 } 359 360 int translationX; 361 if (mOnLeft) { 362 translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth()); 363 } else { 364 translationX = 0; 365 } 366 367 mAppIcon.setTranslationX(translationX); 368 mAppIcon.setVisibility(VISIBLE); 369 } 370 hideBadge()371 void hideBadge() { 372 mAppIcon.setVisibility(GONE); 373 } 374 375 @Override toString()376 public String toString() { 377 return "BadgedImageView{" + mBubble + "}"; 378 } 379 } 380