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.ArrayUtils; 36 import com.android.systemui.Dumpable; 37 import com.android.systemui.R; 38 39 import java.io.PrintWriter; 40 import java.util.Arrays; 41 42 /** 43 * A view that can be used for both the dimmed and normal background of an notification. 44 */ 45 public class NotificationBackgroundView extends View implements Dumpable { 46 47 private final boolean mDontModifyCorners; 48 private Drawable mBackground; 49 private int mClipTopAmount; 50 private int mClipBottomAmount; 51 private int mTintColor; 52 @Nullable private Integer mRippleColor; 53 private final float[] mCornerRadii = new float[8]; 54 private boolean mBottomIsRounded; 55 private boolean mBottomAmountClips = true; 56 private int mActualHeight = -1; 57 private int mActualWidth = -1; 58 private boolean mExpandAnimationRunning; 59 private int mExpandAnimationWidth = -1; 60 private int mExpandAnimationHeight = -1; 61 private int mDrawableAlpha = 255; 62 private boolean mIsPressedAllowed; 63 NotificationBackgroundView(Context context, AttributeSet attrs)64 public NotificationBackgroundView(Context context, AttributeSet attrs) { 65 super(context, attrs); 66 mDontModifyCorners = getResources().getBoolean( 67 R.bool.config_clipNotificationsToOutline); 68 } 69 70 @Override onDraw(Canvas canvas)71 protected void onDraw(Canvas canvas) { 72 if (mClipTopAmount + mClipBottomAmount < getActualHeight() || mExpandAnimationRunning) { 73 canvas.save(); 74 if (!mExpandAnimationRunning) { 75 canvas.clipRect(0, mClipTopAmount, getWidth(), 76 getActualHeight() - mClipBottomAmount); 77 } 78 draw(canvas, mBackground); 79 canvas.restore(); 80 } 81 } 82 draw(Canvas canvas, Drawable drawable)83 private void draw(Canvas canvas, Drawable drawable) { 84 if (drawable != null) { 85 int top = 0; 86 int bottom = getActualHeight(); 87 if (mBottomIsRounded 88 && mBottomAmountClips 89 && !mExpandAnimationRunning) { 90 bottom -= mClipBottomAmount; 91 } 92 final boolean isRtl = isLayoutRtl(); 93 final int width = getWidth(); 94 final int actualWidth = getActualWidth(); 95 96 int left = isRtl ? width - actualWidth : 0; 97 int right = isRtl ? width : actualWidth; 98 99 if (mExpandAnimationRunning) { 100 // Horizontally center this background view inside of the container 101 left = (int) ((width - actualWidth) / 2.0f); 102 right = (int) (left + actualWidth); 103 } 104 drawable.setBounds(left, top, right, bottom); 105 drawable.draw(canvas); 106 } 107 } 108 109 @Override verifyDrawable(Drawable who)110 protected boolean verifyDrawable(Drawable who) { 111 return super.verifyDrawable(who) || who == mBackground; 112 } 113 114 @Override drawableStateChanged()115 protected void drawableStateChanged() { 116 setState(getDrawableState()); 117 } 118 119 @Override drawableHotspotChanged(float x, float y)120 public void drawableHotspotChanged(float x, float y) { 121 if (mBackground != null) { 122 mBackground.setHotspot(x, y); 123 } 124 } 125 126 /** 127 * Sets a background drawable. As we need to change our bounds independently of layout, we need 128 * the notion of a background independently of the regular View background.. 129 */ setCustomBackground(Drawable background)130 public void setCustomBackground(Drawable background) { 131 if (mBackground != null) { 132 mBackground.setCallback(null); 133 unscheduleDrawable(mBackground); 134 } 135 mBackground = background; 136 mRippleColor = null; 137 mBackground.mutate(); 138 if (mBackground != null) { 139 mBackground.setCallback(this); 140 setTint(mTintColor); 141 } 142 if (mBackground instanceof RippleDrawable) { 143 ((RippleDrawable) mBackground).setForceSoftware(true); 144 } 145 updateBackgroundRadii(); 146 invalidate(); 147 } 148 setCustomBackground(int drawableResId)149 public void setCustomBackground(int drawableResId) { 150 final Drawable d = mContext.getDrawable(drawableResId); 151 setCustomBackground(d); 152 } 153 setTint(int tintColor)154 public void setTint(int tintColor) { 155 if (tintColor != 0) { 156 mBackground.setColorFilter(tintColor, PorterDuff.Mode.SRC_ATOP); 157 } else { 158 mBackground.clearColorFilter(); 159 } 160 mTintColor = tintColor; 161 invalidate(); 162 } 163 setActualHeight(int actualHeight)164 public void setActualHeight(int actualHeight) { 165 if (mExpandAnimationRunning) { 166 return; 167 } 168 mActualHeight = actualHeight; 169 invalidate(); 170 } 171 getActualHeight()172 private int getActualHeight() { 173 if (mExpandAnimationRunning && mExpandAnimationHeight > -1) { 174 return mExpandAnimationHeight; 175 } else if (mActualHeight > -1) { 176 return mActualHeight; 177 } 178 return getHeight(); 179 } 180 setActualWidth(int actualWidth)181 public void setActualWidth(int actualWidth) { 182 mActualWidth = actualWidth; 183 } 184 getActualWidth()185 private int getActualWidth() { 186 if (mExpandAnimationRunning && mExpandAnimationWidth > -1) { 187 return mExpandAnimationWidth; 188 } else if (mActualWidth > -1) { 189 return mActualWidth; 190 } 191 return getWidth(); 192 } 193 setClipTopAmount(int clipTopAmount)194 public void setClipTopAmount(int clipTopAmount) { 195 mClipTopAmount = clipTopAmount; 196 invalidate(); 197 } 198 setClipBottomAmount(int clipBottomAmount)199 public void setClipBottomAmount(int clipBottomAmount) { 200 mClipBottomAmount = clipBottomAmount; 201 invalidate(); 202 } 203 204 @Override hasOverlappingRendering()205 public boolean hasOverlappingRendering() { 206 207 // Prevents this view from creating a layer when alpha is animating. 208 return false; 209 } 210 setState(int[] drawableState)211 public void setState(int[] drawableState) { 212 if (mBackground != null && mBackground.isStateful()) { 213 if (!mIsPressedAllowed) { 214 drawableState = ArrayUtils.removeInt(drawableState, 215 com.android.internal.R.attr.state_pressed); 216 } 217 mBackground.setState(drawableState); 218 } 219 } 220 setRippleColor(int color)221 public void setRippleColor(int color) { 222 if (mBackground instanceof RippleDrawable) { 223 RippleDrawable ripple = (RippleDrawable) mBackground; 224 ripple.setColor(ColorStateList.valueOf(color)); 225 mRippleColor = color; 226 } else { 227 mRippleColor = null; 228 } 229 } 230 setDrawableAlpha(int drawableAlpha)231 public void setDrawableAlpha(int drawableAlpha) { 232 mDrawableAlpha = drawableAlpha; 233 if (mExpandAnimationRunning) { 234 return; 235 } 236 mBackground.setAlpha(drawableAlpha); 237 } 238 239 /** 240 * Sets the current top and bottom radius for this background. 241 */ setRadius(float topRoundness, float bottomRoundness)242 public void setRadius(float topRoundness, float bottomRoundness) { 243 if (topRoundness == mCornerRadii[0] && bottomRoundness == mCornerRadii[4]) { 244 return; 245 } 246 mBottomIsRounded = bottomRoundness != 0.0f; 247 mCornerRadii[0] = topRoundness; 248 mCornerRadii[1] = topRoundness; 249 mCornerRadii[2] = topRoundness; 250 mCornerRadii[3] = topRoundness; 251 mCornerRadii[4] = bottomRoundness; 252 mCornerRadii[5] = bottomRoundness; 253 mCornerRadii[6] = bottomRoundness; 254 mCornerRadii[7] = bottomRoundness; 255 updateBackgroundRadii(); 256 } 257 setBottomAmountClips(boolean clips)258 public void setBottomAmountClips(boolean clips) { 259 if (clips != mBottomAmountClips) { 260 mBottomAmountClips = clips; 261 invalidate(); 262 } 263 } 264 updateBackgroundRadii()265 private void updateBackgroundRadii() { 266 if (mDontModifyCorners) { 267 return; 268 } 269 if (mBackground instanceof LayerDrawable) { 270 GradientDrawable gradientDrawable = 271 (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0); 272 gradientDrawable.setCornerRadii(mCornerRadii); 273 } 274 } 275 276 /** Set the current expand animation size. */ setExpandAnimationSize(int width, int height)277 public void setExpandAnimationSize(int width, int height) { 278 mExpandAnimationHeight = height; 279 mExpandAnimationWidth = width; 280 invalidate(); 281 } 282 setExpandAnimationRunning(boolean running)283 public void setExpandAnimationRunning(boolean running) { 284 mExpandAnimationRunning = running; 285 if (mBackground instanceof LayerDrawable) { 286 GradientDrawable gradientDrawable = 287 (GradientDrawable) ((LayerDrawable) mBackground).getDrawable(0); 288 // Speed optimization: disable AA if transfer mode is not SRC_OVER. AA is not easy to 289 // spot during animation anyways. 290 gradientDrawable.setAntiAlias(!running); 291 } 292 if (!mExpandAnimationRunning) { 293 setDrawableAlpha(mDrawableAlpha); 294 } 295 invalidate(); 296 } 297 setPressedAllowed(boolean allowed)298 public void setPressedAllowed(boolean allowed) { 299 mIsPressedAllowed = allowed; 300 } 301 302 @Override dump(PrintWriter pw, @NonNull String[] args)303 public void dump(PrintWriter pw, @NonNull String[] args) { 304 pw.println("mDontModifyCorners: " + mDontModifyCorners); 305 pw.println("mClipTopAmount: " + mClipTopAmount); 306 pw.println("mClipBottomAmount: " + mClipBottomAmount); 307 pw.println("mCornerRadii: " + Arrays.toString(mCornerRadii)); 308 pw.println("mBottomIsRounded: " + mBottomIsRounded); 309 pw.println("mBottomAmountClips: " + mBottomAmountClips); 310 pw.println("mActualWidth: " + mActualWidth); 311 pw.println("mActualHeight: " + mActualHeight); 312 pw.println("mTintColor: " + hexColorString(mTintColor)); 313 pw.println("mRippleColor: " + hexColorString(mRippleColor)); 314 pw.println("mBackground: " + mBackground); 315 } 316 } 317