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.Xfermode; 31 import android.graphics.drawable.Drawable; 32 import android.view.animation.DecelerateInterpolator; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.graphics.ColorUtils; 36 37 /** 38 * Drawable used on SysUI scrims. 39 */ 40 public class ScrimDrawable extends Drawable { 41 private static final String TAG = "ScrimDrawable"; 42 private static final long COLOR_ANIMATION_DURATION = 2000; 43 44 private final Paint mPaint; 45 private int mAlpha = 255; 46 private int mMainColor; 47 private ValueAnimator mColorAnimation; 48 private int mMainColorTo; 49 private float mCornerRadius; 50 private ConcaveInfo mConcaveInfo; 51 private int mBottomEdgePosition; 52 private boolean mCornerRadiusEnabled; 53 ScrimDrawable()54 public ScrimDrawable() { 55 mPaint = new Paint(); 56 mPaint.setStyle(Paint.Style.FILL); 57 } 58 59 /** 60 * Sets the background color. 61 * @param mainColor the color. 62 * @param animated if transition should be interpolated. 63 */ setColor(int mainColor, boolean animated)64 public void setColor(int mainColor, boolean animated) { 65 if (mainColor == mMainColorTo) { 66 return; 67 } 68 69 if (mColorAnimation != null && mColorAnimation.isRunning()) { 70 mColorAnimation.cancel(); 71 } 72 73 mMainColorTo = mainColor; 74 75 if (animated) { 76 final int mainFrom = mMainColor; 77 78 ValueAnimator anim = ValueAnimator.ofFloat(0, 1); 79 anim.setDuration(COLOR_ANIMATION_DURATION); 80 anim.addUpdateListener(animation -> { 81 float ratio = (float) animation.getAnimatedValue(); 82 mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio); 83 invalidateSelf(); 84 }); 85 anim.addListener(new AnimatorListenerAdapter() { 86 @Override 87 public void onAnimationEnd(Animator animation, boolean isReverse) { 88 if (mColorAnimation == animation) { 89 mColorAnimation = null; 90 } 91 } 92 }); 93 anim.setInterpolator(new DecelerateInterpolator()); 94 anim.start(); 95 mColorAnimation = anim; 96 } else { 97 mMainColor = mainColor; 98 invalidateSelf(); 99 } 100 } 101 102 @Override setAlpha(int alpha)103 public void setAlpha(int alpha) { 104 if (alpha != mAlpha) { 105 mAlpha = alpha; 106 invalidateSelf(); 107 } 108 } 109 110 @Override getAlpha()111 public int getAlpha() { 112 return mAlpha; 113 } 114 115 @Override setXfermode(@ullable Xfermode mode)116 public void setXfermode(@Nullable Xfermode mode) { 117 mPaint.setXfermode(mode); 118 invalidateSelf(); 119 } 120 121 @Override setColorFilter(ColorFilter colorFilter)122 public void setColorFilter(ColorFilter colorFilter) { 123 mPaint.setColorFilter(colorFilter); 124 } 125 126 @Override getColorFilter()127 public ColorFilter getColorFilter() { 128 return mPaint.getColorFilter(); 129 } 130 131 @Override getOpacity()132 public int getOpacity() { 133 return PixelFormat.TRANSLUCENT; 134 } 135 136 /** 137 * Corner radius used by either concave or convex corners. 138 */ setRoundedCorners(float radius)139 public void setRoundedCorners(float radius) { 140 if (radius == mCornerRadius) { 141 return; 142 } 143 mCornerRadius = radius; 144 if (mConcaveInfo != null) { 145 mConcaveInfo.setCornerRadius(radius); 146 updatePath(); 147 } 148 invalidateSelf(); 149 } 150 151 /** 152 * If we should draw a rounded rect instead of a rect. 153 */ setRoundedCornersEnabled(boolean enabled)154 public void setRoundedCornersEnabled(boolean enabled) { 155 if (mCornerRadiusEnabled == enabled) { 156 return; 157 } 158 mCornerRadiusEnabled = enabled; 159 invalidateSelf(); 160 } 161 162 /** 163 * If we should draw a concave rounded rect instead of a rect. 164 */ setBottomEdgeConcave(boolean enabled)165 public void setBottomEdgeConcave(boolean enabled) { 166 if (enabled && mConcaveInfo != null) { 167 return; 168 } 169 if (!enabled) { 170 mConcaveInfo = null; 171 } else { 172 mConcaveInfo = new ConcaveInfo(); 173 mConcaveInfo.setCornerRadius(mCornerRadius); 174 } 175 invalidateSelf(); 176 } 177 178 /** 179 * Location of concave edge. 180 * @see #setBottomEdgeConcave(boolean) 181 */ setBottomEdgePosition(int y)182 public void setBottomEdgePosition(int y) { 183 if (mBottomEdgePosition == y) { 184 return; 185 } 186 mBottomEdgePosition = y; 187 if (mConcaveInfo == null) { 188 return; 189 } 190 updatePath(); 191 invalidateSelf(); 192 } 193 194 @Override draw(@onNull Canvas canvas)195 public void draw(@NonNull Canvas canvas) { 196 mPaint.setColor(mMainColor); 197 mPaint.setAlpha(mAlpha); 198 if (mConcaveInfo != null) { 199 drawConcave(canvas); 200 } else if (mCornerRadiusEnabled && mCornerRadius > 0) { 201 canvas.drawRoundRect(getBounds().left, getBounds().top, getBounds().right, 202 getBounds().bottom + mCornerRadius, 203 /* x radius*/ mCornerRadius, /* y radius*/ mCornerRadius, mPaint); 204 } else { 205 canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, 206 getBounds().bottom, mPaint); 207 } 208 } 209 210 @Override onBoundsChange(Rect bounds)211 protected void onBoundsChange(Rect bounds) { 212 updatePath(); 213 } 214 drawConcave(Canvas canvas)215 private void drawConcave(Canvas canvas) { 216 canvas.clipOutPath(mConcaveInfo.mPath); 217 canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, 218 mBottomEdgePosition + mConcaveInfo.mPathOverlap, mPaint); 219 } 220 updatePath()221 private void updatePath() { 222 if (mConcaveInfo == null) { 223 return; 224 } 225 mConcaveInfo.mPath.reset(); 226 float top = mBottomEdgePosition; 227 float bottom = mBottomEdgePosition + mConcaveInfo.mPathOverlap; 228 mConcaveInfo.mPath.addRoundRect(getBounds().left, top, getBounds().right, bottom, 229 mConcaveInfo.mCornerRadii, Path.Direction.CW); 230 } 231 232 @VisibleForTesting getMainColor()233 public int getMainColor() { 234 return mMainColor; 235 } 236 237 private static class ConcaveInfo { 238 private float mPathOverlap; 239 private final float[] mCornerRadii; 240 private final Path mPath = new Path(); 241 ConcaveInfo()242 ConcaveInfo() { 243 mCornerRadii = new float[] {0, 0, 0, 0, 0, 0, 0, 0}; 244 } 245 setCornerRadius(float radius)246 public void setCornerRadius(float radius) { 247 mPathOverlap = radius; 248 mCornerRadii[0] = radius; 249 mCornerRadii[1] = radius; 250 mCornerRadii[2] = radius; 251 mCornerRadii[3] = radius; 252 } 253 } 254 } 255