1 /* 2 * Copyright (C) 2021 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.scrim; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ValueAnimator; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.graphics.Canvas; 25 import android.graphics.ColorFilter; 26 import android.graphics.Paint; 27 import android.graphics.Path; 28 import android.graphics.PixelFormat; 29 import android.graphics.Rect; 30 import android.graphics.RectF; 31 import android.graphics.Xfermode; 32 import android.graphics.drawable.Drawable; 33 import android.view.animation.DecelerateInterpolator; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.graphics.ColorUtils; 37 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 38 39 /** 40 * Drawable used on SysUI scrims. 41 */ 42 public class ScrimDrawable extends Drawable { 43 private static final String TAG = "ScrimDrawable"; 44 45 private boolean mShouldUseLargeScreenSize; 46 private final Paint mPaint; 47 private final Path mPath = new Path(); 48 private final RectF mBoundsRectF = new RectF(); 49 50 private int mAlpha = 255; 51 private int mMainColor; 52 private ValueAnimator mColorAnimation; 53 private int mMainColorTo; 54 private float mCornerRadius; 55 private ConcaveInfo mConcaveInfo; 56 private int mBottomEdgePosition; 57 private float mBottomEdgeRadius = -1; 58 private boolean mCornerRadiusEnabled; 59 ScrimDrawable()60 public ScrimDrawable() { 61 mPaint = new Paint(); 62 mPaint.setStyle(Paint.Style.FILL); 63 mShouldUseLargeScreenSize = false; 64 } 65 66 /** 67 * Sets the background color. 68 * @param mainColor the color. 69 * @param animated if transition should be interpolated. 70 */ setColor(int mainColor, boolean animated)71 public void setColor(int mainColor, boolean animated) { 72 if (mainColor == mMainColorTo) { 73 return; 74 } 75 76 if (mColorAnimation != null && mColorAnimation.isRunning()) { 77 mColorAnimation.cancel(); 78 } 79 80 mMainColorTo = mainColor; 81 82 if (animated) { 83 final int mainFrom = mMainColor; 84 85 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 86 anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 87 anim.addUpdateListener(animation -> { 88 float ratio = (float) animation.getAnimatedValue(); 89 mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio); 90 invalidateSelf(); 91 }); 92 anim.addListener(new AnimatorListenerAdapter() { 93 @Override 94 public void onAnimationEnd(Animator animation, boolean isReverse) { 95 if (mColorAnimation == animation) { 96 mColorAnimation = null; 97 } 98 } 99 }); 100 anim.setInterpolator(new DecelerateInterpolator()); 101 anim.start(); 102 mColorAnimation = anim; 103 } else { 104 mMainColor = mainColor; 105 invalidateSelf(); 106 } 107 } 108 109 @Override setAlpha(int alpha)110 public void setAlpha(int alpha) { 111 if (alpha != mAlpha) { 112 mAlpha = alpha; 113 invalidateSelf(); 114 } 115 } 116 117 @Override getAlpha()118 public int getAlpha() { 119 return mAlpha; 120 } 121 122 @Override setXfermode(@ullable Xfermode mode)123 public void setXfermode(@Nullable Xfermode mode) { 124 mPaint.setXfermode(mode); 125 invalidateSelf(); 126 } 127 128 @Override setColorFilter(ColorFilter colorFilter)129 public void setColorFilter(ColorFilter colorFilter) { 130 mPaint.setColorFilter(colorFilter); 131 } 132 133 @Override getColorFilter()134 public ColorFilter getColorFilter() { 135 return mPaint.getColorFilter(); 136 } 137 138 @Override getOpacity()139 public int getOpacity() { 140 return PixelFormat.TRANSLUCENT; 141 } 142 setShouldUseLargeScreenSize(boolean v)143 public void setShouldUseLargeScreenSize(boolean v) { 144 mShouldUseLargeScreenSize = v; 145 } 146 147 /** 148 * Corner radius used by either concave or convex corners. 149 */ setRoundedCorners(float radius)150 public void setRoundedCorners(float radius) { 151 if (radius == mCornerRadius) { 152 return; 153 } 154 mCornerRadius = radius; 155 if (mConcaveInfo != null) { 156 mConcaveInfo.setCornerRadius(radius); 157 updatePath(); 158 } 159 invalidateSelf(); 160 } 161 162 /** 163 * If we should draw a rounded rect instead of a rect. 164 */ setRoundedCornersEnabled(boolean enabled)165 public void setRoundedCornersEnabled(boolean enabled) { 166 if (mCornerRadiusEnabled == enabled) { 167 return; 168 } 169 mCornerRadiusEnabled = enabled; 170 invalidateSelf(); 171 } 172 173 /** 174 * If we should draw a concave rounded rect instead of a rect. 175 */ setBottomEdgeConcave(boolean enabled)176 public void setBottomEdgeConcave(boolean enabled) { 177 if (enabled && mConcaveInfo != null) { 178 return; 179 } 180 if (!enabled) { 181 mConcaveInfo = null; 182 } else { 183 mConcaveInfo = new ConcaveInfo(); 184 mConcaveInfo.setCornerRadius(mCornerRadius); 185 } 186 invalidateSelf(); 187 } 188 189 /** 190 * Location of concave edge. 191 * @see #setBottomEdgeConcave(boolean) 192 */ setBottomEdgePosition(int y)193 public void setBottomEdgePosition(int y) { 194 if (mBottomEdgePosition == y) { 195 return; 196 } 197 mBottomEdgePosition = y; 198 if (mConcaveInfo == null) { 199 return; 200 } 201 updatePath(); 202 invalidateSelf(); 203 } 204 setBottomEdgeRadius(float radius)205 public void setBottomEdgeRadius(float radius) { 206 if (mBottomEdgeRadius != radius) { 207 mBottomEdgeRadius = radius; 208 invalidateSelf(); 209 } 210 } 211 212 @Override draw(@onNull Canvas canvas)213 public void draw(@NonNull Canvas canvas) { 214 mPaint.setColor(mMainColor); 215 mPaint.setAlpha(mAlpha); 216 if (mConcaveInfo != null) { 217 drawConcave(canvas); 218 } else if (mCornerRadiusEnabled && mCornerRadius > 0) { 219 float topEdgeRadius = mCornerRadius; 220 float bottomEdgeRadius = mBottomEdgeRadius == -1.0 ? mCornerRadius : mBottomEdgeRadius; 221 222 mBoundsRectF.set(getBounds()); 223 224 // When the back gesture causes the notification scrim to be scaled down, 225 // this offset "reveals" the rounded bottom edge as it "pulls away". 226 // We must *not* make this adjustment on largescreen shades (where the corner is sharp). 227 if (!mShouldUseLargeScreenSize && mBottomEdgeRadius != -1) { 228 mBoundsRectF.bottom -= bottomEdgeRadius; 229 } 230 231 // We need a box with rounded corners but its lower corners are not rounded on large 232 // screen devices in "portrait" orientation. 233 // Thus, we cannot draw a symmetric rounded rectangle via canvas.drawRoundRect() 234 // and must build a box with different corner radii at the top and at the bottom. 235 // Additionally, when the scrim is pushed to the very bottom of the screen, do not draw 236 // anything (drawing a rounded box with these specifications is not possible). 237 // TODO(b/271030611) perhaps this could be accomplished via Path.addRoundRect instead? 238 if (mBoundsRectF.bottom - mBoundsRectF.top > bottomEdgeRadius) { 239 mPath.reset(); 240 mPath.moveTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius); 241 mPath.cubicTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius, 242 mBoundsRectF.right, mBoundsRectF.top, 243 mBoundsRectF.right - topEdgeRadius, mBoundsRectF.top); 244 mPath.lineTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top); 245 mPath.cubicTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top, 246 mBoundsRectF.left, mBoundsRectF.top, 247 mBoundsRectF.left, mBoundsRectF.top + topEdgeRadius); 248 mPath.lineTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius); 249 mPath.cubicTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius, 250 mBoundsRectF.left, mBoundsRectF.bottom, 251 mBoundsRectF.left + bottomEdgeRadius, mBoundsRectF.bottom); 252 mPath.lineTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom); 253 mPath.cubicTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom, 254 mBoundsRectF.right, mBoundsRectF.bottom, 255 mBoundsRectF.right, mBoundsRectF.bottom - bottomEdgeRadius); 256 mPath.close(); 257 canvas.drawPath(mPath, mPaint); 258 } 259 } else { 260 canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, 261 getBounds().bottom, mPaint); 262 } 263 } 264 265 @Override onBoundsChange(Rect bounds)266 protected void onBoundsChange(Rect bounds) { 267 updatePath(); 268 } 269 drawConcave(Canvas canvas)270 private void drawConcave(Canvas canvas) { 271 canvas.clipOutPath(mConcaveInfo.mPath); 272 canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, 273 mBottomEdgePosition + mConcaveInfo.mPathOverlap, mPaint); 274 } 275 updatePath()276 private void updatePath() { 277 if (mConcaveInfo == null) { 278 return; 279 } 280 mConcaveInfo.mPath.reset(); 281 float top = mBottomEdgePosition; 282 float bottom = mBottomEdgePosition + mConcaveInfo.mPathOverlap; 283 mConcaveInfo.mPath.addRoundRect(getBounds().left, top, getBounds().right, bottom, 284 mConcaveInfo.mCornerRadii, Path.Direction.CW); 285 } 286 287 @VisibleForTesting getMainColor()288 public int getMainColor() { 289 return mMainColor; 290 } 291 292 private static class ConcaveInfo { 293 private float mPathOverlap; 294 private final float[] mCornerRadii; 295 private final Path mPath = new Path(); 296 ConcaveInfo()297 ConcaveInfo() { 298 mCornerRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; 299 } 300 setCornerRadius(float radius)301 public void setCornerRadius(float radius) { 302 mPathOverlap = radius; 303 mCornerRadii[0] = radius; 304 mCornerRadii[1] = radius; 305 mCornerRadii[2] = radius; 306 mCornerRadii[3] = radius; 307 } 308 } 309 } 310