1 /* 2 * Copyright (C) 2023 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.launcher3.taskbar.bubbles; 17 18 import android.annotation.Nullable; 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Canvas; 22 import android.graphics.Outline; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewOutlineProvider; 28 import android.widget.ImageView; 29 30 import androidx.constraintlayout.widget.ConstraintLayout; 31 32 import com.android.launcher3.R; 33 import com.android.launcher3.icons.DotRenderer; 34 import com.android.launcher3.icons.IconNormalizer; 35 import com.android.wm.shell.animation.Interpolators; 36 37 import java.util.EnumSet; 38 39 // TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share. 40 41 /** 42 * View that displays a bubble icon, along with an app badge on either the left or 43 * right side of the view. 44 */ 45 public class BubbleView extends ConstraintLayout { 46 47 public static final int DEFAULT_PATH_SIZE = 100; 48 49 /** 50 * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or 51 * another. If any of these flags are set, the dot will not be shown. 52 * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown. 53 */ 54 enum SuppressionFlag { 55 // TODO: (b/277815200) implement flyout 56 // Suppressed because the flyout is visible - it will morph into the dot via animation. 57 FLYOUT_VISIBLE, 58 // Suppressed because this bubble is behind others in the collapsed stack. 59 BEHIND_STACK, 60 } 61 62 private final EnumSet<SuppressionFlag> mSuppressionFlags = 63 EnumSet.noneOf(SuppressionFlag.class); 64 65 private final ImageView mBubbleIcon; 66 private final ImageView mAppIcon; 67 private final int mBubbleSize; 68 69 private DotRenderer mDotRenderer; 70 private DotRenderer.DrawParams mDrawParams; 71 private int mDotColor; 72 private Rect mTempBounds = new Rect(); 73 74 // Whether the dot is animating 75 private boolean mDotIsAnimating; 76 // What scale value the dot is animating to 77 private float mAnimatingToDotScale; 78 // The current scale value of the dot 79 private float mDotScale; 80 81 // TODO: (b/273310265) handle RTL 82 // Whether the bubbles are positioned on the left or right side of the screen 83 private boolean mOnLeft = false; 84 85 private BubbleBarItem mBubble; 86 BubbleView(Context context)87 public BubbleView(Context context) { 88 this(context, null); 89 } 90 BubbleView(Context context, AttributeSet attrs)91 public BubbleView(Context context, AttributeSet attrs) { 92 this(context, attrs, 0); 93 } 94 BubbleView(Context context, AttributeSet attrs, int defStyleAttr)95 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) { 96 this(context, attrs, defStyleAttr, 0); 97 } 98 BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)99 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, 100 int defStyleRes) { 101 super(context, attrs, defStyleAttr, defStyleRes); 102 // We manage positioning the badge ourselves 103 setLayoutDirection(LAYOUT_DIRECTION_LTR); 104 105 LayoutInflater.from(context).inflate(R.layout.bubble_view, this); 106 107 mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size); 108 mBubbleIcon = findViewById(R.id.icon_view); 109 mAppIcon = findViewById(R.id.app_icon_view); 110 111 mDrawParams = new DotRenderer.DrawParams(); 112 113 setFocusable(true); 114 setClickable(true); 115 setOutlineProvider(new ViewOutlineProvider() { 116 @Override 117 public void getOutline(View view, Outline outline) { 118 BubbleView.this.getOutline(outline); 119 } 120 }); 121 } 122 getOutline(Outline outline)123 private void getOutline(Outline outline) { 124 final int normalizedSize = IconNormalizer.getNormalizedCircleSize(mBubbleSize); 125 final int inset = (mBubbleSize - normalizedSize) / 2; 126 outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); 127 } 128 129 @Override dispatchDraw(Canvas canvas)130 public void dispatchDraw(Canvas canvas) { 131 super.dispatchDraw(canvas); 132 133 if (!shouldDrawDot()) { 134 return; 135 } 136 137 getDrawingRect(mTempBounds); 138 139 mDrawParams.dotColor = mDotColor; 140 mDrawParams.iconBounds = mTempBounds; 141 mDrawParams.leftAlign = mOnLeft; 142 mDrawParams.scale = mDotScale; 143 144 mDotRenderer.draw(canvas, mDrawParams); 145 } 146 147 /** Sets the bubble being rendered in this view. */ setBubble(BubbleBarBubble bubble)148 void setBubble(BubbleBarBubble bubble) { 149 mBubble = bubble; 150 mBubbleIcon.setImageBitmap(bubble.getIcon()); 151 mAppIcon.setImageBitmap(bubble.getBadge()); 152 mDotColor = bubble.getDotColor(); 153 mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE); 154 } 155 156 /** 157 * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles 158 * but does not represent app content, instead it shows recent bubbles that couldn't fit into 159 * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't 160 * come from an app. 161 */ setOverflow(BubbleBarOverflow overflow, Bitmap bitmap)162 void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) { 163 mBubble = overflow; 164 mBubbleIcon.setImageBitmap(bitmap); 165 mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge 166 } 167 168 /** Returns the bubble being rendered in this view. */ 169 @Nullable getBubble()170 BubbleBarItem getBubble() { 171 return mBubble; 172 } 173 updateDotVisibility(boolean animate)174 void updateDotVisibility(boolean animate) { 175 final float targetScale = shouldDrawDot() ? 1f : 0f; 176 if (animate) { 177 animateDotScale(); 178 } else { 179 mDotScale = targetScale; 180 mAnimatingToDotScale = targetScale; 181 invalidate(); 182 } 183 } 184 updateBadgeVisibility()185 void updateBadgeVisibility() { 186 if (mBubble instanceof BubbleBarOverflow) { 187 // The overflow bubble does not have a badge, so just bail. 188 return; 189 } 190 BubbleBarBubble bubble = (BubbleBarBubble) mBubble; 191 Bitmap appBadgeBitmap = bubble.getBadge(); 192 int translationX = mOnLeft 193 ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth()) 194 : 0; 195 mAppIcon.setTranslationX(translationX); 196 mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE); 197 } 198 199 /** Sets whether this bubble is in the stack & not the first bubble. **/ setBehindStack(boolean behindStack, boolean animate)200 void setBehindStack(boolean behindStack, boolean animate) { 201 if (behindStack) { 202 mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK); 203 } else { 204 mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK); 205 } 206 updateDotVisibility(animate); 207 updateBadgeVisibility(); 208 } 209 210 /** Whether this bubble is in the stack & not the first bubble. **/ isBehindStack()211 boolean isBehindStack() { 212 return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK); 213 } 214 215 /** Whether the dot indicating unseen content in a bubble should be shown. */ shouldDrawDot()216 private boolean shouldDrawDot() { 217 boolean bubbleHasUnseenContent = mBubble != null 218 && mBubble instanceof BubbleBarBubble 219 && mSuppressionFlags.isEmpty() 220 && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed(); 221 222 // Always render the dot if it's animating, since it could be animating out. Otherwise, show 223 // it if the bubble wants to show it, and we aren't suppressing it. 224 return bubbleHasUnseenContent || mDotIsAnimating; 225 } 226 227 /** How big the dot should be, fraction from 0 to 1. */ setDotScale(float fraction)228 private void setDotScale(float fraction) { 229 mDotScale = fraction; 230 invalidate(); 231 } 232 233 /** 234 * Animates the dot to the given scale. 235 */ animateDotScale()236 private void animateDotScale() { 237 float toScale = shouldDrawDot() ? 1f : 0f; 238 mDotIsAnimating = true; 239 240 // Don't restart the animation if we're already animating to the given value. 241 if (mAnimatingToDotScale == toScale || !shouldDrawDot()) { 242 mDotIsAnimating = false; 243 return; 244 } 245 246 mAnimatingToDotScale = toScale; 247 248 final boolean showDot = toScale > 0f; 249 250 // Do NOT wait until after animation ends to setShowDot 251 // to avoid overriding more recent showDot states. 252 clearAnimation(); 253 animate() 254 .setDuration(200) 255 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 256 .setUpdateListener((valueAnimator) -> { 257 float fraction = valueAnimator.getAnimatedFraction(); 258 fraction = showDot ? fraction : 1f - fraction; 259 setDotScale(fraction); 260 }).withEndAction(() -> { 261 setDotScale(showDot ? 1f : 0f); 262 mDotIsAnimating = false; 263 }).start(); 264 } 265 266 267 @Override toString()268 public String toString() { 269 String toString = mBubble != null ? mBubble.getKey() : "null"; 270 return "BubbleView{" + toString + "}"; 271 } 272 } 273