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