1 /* 2 * Copyright (C) 2014 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 17 package com.android.systemui.statusbar.notification.row; 18 19 import static com.android.systemui.util.ColorUtilKt.hexColorString; 20 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.graphics.Canvas; 24 import android.graphics.PorterDuff; 25 import android.graphics.drawable.Drawable; 26 import android.graphics.drawable.GradientDrawable; 27 import android.graphics.drawable.LayerDrawable; 28 import android.graphics.drawable.RippleDrawable; 29 import android.util.AttributeSet; 30 import android.view.View; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 35 import com.android.internal.util.ContrastColorUtil; 36 import com.android.settingslib.Utils; 37 import com.android.systemui.Dumpable; 38 import com.android.systemui.res.R; 39 import com.android.systemui.util.DrawableDumpKt; 40 41 import java.io.PrintWriter; 42 import java.util.Arrays; 43 44 /** 45 * A view that can be used for both the dimmed and normal background of an notification. 46 */ 47 public class NotificationBackgroundView extends View implements Dumpable { 48 49 private final boolean mDontModifyCorners; 50 private Drawable mBackground; 51 private int mClipTopAmount; 52 private int mClipBottomAmount; 53 private int mTintColor; 54 @Nullable private Integer mRippleColor; 55 private final float[] mCornerRadii = new float[8]; 56 private final float[] mFocusOverlayCornerRadii = new float[8]; 57 private float mFocusOverlayStroke = 0; 58 private boolean mBottomIsRounded; 59 private boolean mBottomAmountClips = true; 60 private int mActualHeight = -1; 61 private int mActualWidth = -1; 62 private boolean mExpandAnimationRunning; 63 private int mExpandAnimationWidth = -1; 64 private int mExpandAnimationHeight = -1; 65 private int mDrawableAlpha = 255; 66 private final ColorStateList mLightColoredStatefulColors; 67 private final ColorStateList mDarkColoredStatefulColors; 68 private final int mNormalColor; 69 NotificationBackgroundView(Context context, AttributeSet attrs)70 public NotificationBackgroundView(Context context, AttributeSet attrs) { 71 super(context, attrs); 72 mDontModifyCorners = getResources().getBoolean(R.bool.config_clipNotificationsToOutline); 73 mLightColoredStatefulColors = getResources().getColorStateList( 74 R.color.notification_state_color_light); 75 mDarkColoredStatefulColors = getResources().getColorStateList( 76 R.color.notification_state_color_dark); 77 mNormalColor = Utils.getColorAttrDefaultColor(mContext, 78 com.android.internal.R.attr.materialColorSurfaceContainerHigh); 79 mFocusOverlayStroke = getResources().getDimension(R.dimen.notification_focus_stroke_width); 80 } 81 82 @Override onDraw(Canvas canvas)83 protected void onDraw(Canvas canvas) { 84 if (mClipTopAmount + mClipBottomAmount < getActualHeight() || mExpandAnimationRunning) { 85 canvas.save(); 86 if (!mExpandAnimationRunning) { 87 canvas.clipRect(0, mClipTopAmount, getWidth(), 88 getActualHeight() - mClipBottomAmount); 89 } 90 draw(canvas, mBackground); 91 canvas.restore(); 92 } 93 } 94 draw(Canvas canvas, Drawable drawable)95 private void draw(Canvas canvas, Drawable drawable) { 96 if (drawable != null) { 97 int top = 0; 98 int bottom = getActualHeight(); 99 if (mBottomIsRounded 100 && mBottomAmountClips 101 && !mExpandAnimationRunning) { 102 bottom -= mClipBottomAmount; 103 } 104 final boolean isRtl = isLayoutRtl(); 105 final int width = getWidth(); 106 final int actualWidth = getActualWidth(); 107 108 int left = isRtl ? width - actualWidth : 0; 109 int right = isRtl ? width : actualWidth; 110 111 if (mExpandAnimationRunning) { 112 // Horizontally center this background view inside of the container 113 left = (int) ((width - actualWidth) / 2.0f); 114 right = (int) (left + actualWidth); 115 } 116 drawable.setBounds(left, top, right, bottom); 117 drawable.draw(canvas); 118 } 119 } 120 121 @Override verifyDrawable(Drawable who)122 protected boolean verifyDrawable(Drawable who) { 123 return super.verifyDrawable(who) || who == mBackground; 124 } 125 126 @Override drawableStateChanged()127 protected void drawableStateChanged() { 128 setState(getDrawableState()); 129 } 130 131 @Override drawableHotspotChanged(float x, float y)132 public void drawableHotspotChanged(float x, float y) { 133 if (mBackground != null) { 134 mBackground.setHotspot(x, y); 135 } 136 } 137 138 /** 139 * Stateful colors are colors that will overlay on the notification original color when one of 140 * hover states, pressed states or other similar states is activated. 141 */ setStatefulColors()142 private void setStatefulColors() { 143 if (mTintColor != mNormalColor) { 144 ColorStateList newColor = ContrastColorUtil.isColorDark(mTintColor) 145 ? mDarkColoredStatefulColors : mLightColoredStatefulColors; 146 ((GradientDrawable) getStatefulBackgroundLayer().mutate()).setColor(newColor); 147 } 148 } 149 150 /** 151 * Sets a background drawable. As we need to change our bounds independently of layout, we need 152 * the notion of a background independently of the regular View background.. 153 */ setCustomBackground(Drawable background)154 public void setCustomBackground(Drawable background) { 155 if (mBackground != null) { 156 mBackground.setCallback(null); 157 unscheduleDrawable(mBackground); 158 } 159 mBackground = background; 160 mRippleColor = null; 161 mBackground.mutate(); 162 if (mBackground != null) { 163 mBackground.setCallback(this); 164 setTint(mTintColor); 165 } 166 if (mBackground instanceof RippleDrawable) { 167 ((RippleDrawable) mBackground).setForceSoftware(true); 168 } 169 updateBackgroundRadii(); 170 invalidate(); 171 } 172 setCustomBackground(int drawableResId)173 public void setCustomBackground(int drawableResId) { 174 final Drawable d = mContext.getDrawable(drawableResId); 175 setCustomBackground(d); 176 } 177 getBaseBackgroundLayer()178 private Drawable getBaseBackgroundLayer() { 179 return ((LayerDrawable) mBackground).getDrawable(0); 180 } 181 getStatefulBackgroundLayer()182 private Drawable getStatefulBackgroundLayer() { 183 return ((LayerDrawable) mBackground).getDrawable(1); 184 } 185 setTint(int tintColor)186 public void setTint(int tintColor) { 187 Drawable baseLayer = getBaseBackgroundLayer(); 188 baseLayer.mutate().setTintMode(PorterDuff.Mode.SRC_ATOP); 189 baseLayer.setTint(tintColor); 190 mTintColor = tintColor; 191 setStatefulColors(); 192 invalidate(); 193 } 194 setActualHeight(int actualHeight)195 public void setActualHeight(int actualHeight) { 196 if (mExpandAnimationRunning) { 197 return; 198 } 199 mActualHeight = actualHeight; 200 invalidate(); 201 } 202 getActualHeight()203 private int getActualHeight() { 204 if (mExpandAnimationRunning && mExpandAnimationHeight > -1) { 205 return mExpandAnimationHeight; 206 } else if (mActualHeight > -1) { 207 return mActualHeight; 208 } 209 return getHeight(); 210 } 211 setActualWidth(int actualWidth)212 public void setActualWidth(int actualWidth) { 213 mActualWidth = actualWidth; 214 } 215 getActualWidth()216 private int getActualWidth() { 217 if (mExpandAnimationRunning && mExpandAnimationWidth > -1) { 218 return mExpandAnimationWidth; 219 } else if (mActualWidth > -1) { 220 return mActualWidth; 221 } 222 return getWidth(); 223 } 224 setClipTopAmount(int clipTopAmount)225 public void setClipTopAmount(int clipTopAmount) { 226 mClipTopAmount = clipTopAmount; 227 invalidate(); 228 } 229 setClipBottomAmount(int clipBottomAmount)230 public void setClipBottomAmount(int clipBottomAmount) { 231 mClipBottomAmount = clipBottomAmount; 232 invalidate(); 233 } 234 235 @Override hasOverlappingRendering()236 public boolean hasOverlappingRendering() { 237 238 // Prevents this view from creating a layer when alpha is animating. 239 return false; 240 } 241 setState(int[] drawableState)242 public void setState(int[] drawableState) { 243 if (mBackground != null && mBackground.isStateful()) { 244 mBackground.setState(drawableState); 245 } 246 } 247 setRippleColor(int color)248 public void setRippleColor(int color) { 249 if (mBackground instanceof RippleDrawable) { 250 RippleDrawable ripple = (RippleDrawable) mBackground; 251 ripple.setColor(ColorStateList.valueOf(color)); 252 mRippleColor = color; 253 } else { 254 mRippleColor = null; 255 } 256 } 257 setDrawableAlpha(int drawableAlpha)258 public void setDrawableAlpha(int drawableAlpha) { 259 mDrawableAlpha = drawableAlpha; 260 if (mExpandAnimationRunning) { 261 return; 262 } 263 mBackground.setAlpha(drawableAlpha); 264 } 265 266 /** 267 * Sets the current top and bottom radius for this background. 268 */ setRadius(float topRoundness, float bottomRoundness)269 public void setRadius(float topRoundness, float bottomRoundness) { 270 if (topRoundness == mCornerRadii[0] && bottomRoundness == mCornerRadii[4]) { 271 return; 272 } 273 mBottomIsRounded = bottomRoundness != 0.0f; 274 mCornerRadii[0] = topRoundness; 275 mCornerRadii[1] = topRoundness; 276 mCornerRadii[2] = topRoundness; 277 mCornerRadii[3] = topRoundness; 278 mCornerRadii[4] = bottomRoundness; 279 mCornerRadii[5] = bottomRoundness; 280 mCornerRadii[6] = bottomRoundness; 281 mCornerRadii[7] = bottomRoundness; 282 updateBackgroundRadii(); 283 } 284 setBottomAmountClips(boolean clips)285 public void setBottomAmountClips(boolean clips) { 286 if (clips != mBottomAmountClips) { 287 mBottomAmountClips = clips; 288 invalidate(); 289 } 290 } 291 updateBackgroundRadii()292 private void updateBackgroundRadii() { 293 if (mDontModifyCorners) { 294 return; 295 } 296 if (mBackground instanceof LayerDrawable layerDrawable) { 297 int numberOfLayers = layerDrawable.getNumberOfLayers(); 298 for (int i = 0; i < numberOfLayers; i++) { 299 GradientDrawable gradientDrawable = (GradientDrawable) layerDrawable.getDrawable(i); 300 gradientDrawable.setCornerRadii(mCornerRadii); 301 } 302 updateFocusOverlayRadii(layerDrawable); 303 } 304 } 305 updateFocusOverlayRadii(LayerDrawable background)306 private void updateFocusOverlayRadii(LayerDrawable background) { 307 GradientDrawable overlay = 308 (GradientDrawable) background.findDrawableByLayerId( 309 R.id.notification_focus_overlay); 310 for (int i = 0; i < mCornerRadii.length; i++) { 311 // in theory subtracting mFocusOverlayStroke/2 should be enough but notification 312 // background is still peeking a bit from below - probably due to antialiasing or 313 // overlay uneven scaling. So let's subtract full mFocusOverlayStroke to make sure the 314 // radius is a bit smaller and covers background corners fully 315 mFocusOverlayCornerRadii[i] = Math.max(0, mCornerRadii[i] - mFocusOverlayStroke); 316 } 317 overlay.setCornerRadii(mFocusOverlayCornerRadii); 318 } 319 320 /** Set the current expand animation size. */ setExpandAnimationSize(int width, int height)321 public void setExpandAnimationSize(int width, int height) { 322 mExpandAnimationHeight = height; 323 mExpandAnimationWidth = width; 324 invalidate(); 325 } 326 setExpandAnimationRunning(boolean running)327 public void setExpandAnimationRunning(boolean running) { 328 mExpandAnimationRunning = running; 329 if (mBackground instanceof LayerDrawable) { 330 GradientDrawable gradientDrawable = 331 (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0); 332 // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to 333 // spot during animation anyways. 334 gradientDrawable.setAntiAlias(!running); 335 } 336 if (!mExpandAnimationRunning) { 337 setDrawableAlpha(mDrawableAlpha); 338 } 339 invalidate(); 340 } 341 342 @Override dump(PrintWriter pw, @NonNull String[] args)343 public void dump(PrintWriter pw, @NonNull String[] args) { 344 pw.println("mDontModifyCorners: " + mDontModifyCorners); 345 pw.println("mClipTopAmount: " + mClipTopAmount); 346 pw.println("mClipBottomAmount: " + mClipBottomAmount); 347 pw.println("mCornerRadii: " + Arrays.toString(mCornerRadii)); 348 pw.println("mBottomIsRounded: " + mBottomIsRounded); 349 pw.println("mBottomAmountClips: " + mBottomAmountClips); 350 pw.println("mActualWidth: " + mActualWidth); 351 pw.println("mActualHeight: " + mActualHeight); 352 pw.println("mTintColor: " + hexColorString(mTintColor)); 353 pw.println("mRippleColor: " + hexColorString(mRippleColor)); 354 pw.println("mBackground: " + DrawableDumpKt.dumpToString(mBackground)); 355 } 356 357 /** create a concise dump of this view's colors */ toDumpString()358 public String toDumpString() { 359 return "<NotificationBackgroundView" 360 + " tintColor=" + hexColorString(mTintColor) 361 + " rippleColor=" + hexColorString(mRippleColor) 362 + " bgColor=" + DrawableDumpKt.getSolidColor(mBackground) 363 + ">"; 364 365 } 366 } 367