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