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 static java.lang.Float.isNaN; 20 21 import android.annotation.NonNull; 22 import android.content.Context; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.PorterDuff; 26 import android.graphics.PorterDuff.Mode; 27 import android.graphics.PorterDuffColorFilter; 28 import android.graphics.Rect; 29 import android.graphics.drawable.Drawable; 30 import android.os.Looper; 31 import android.util.AttributeSet; 32 import android.view.View; 33 34 import androidx.annotation.Nullable; 35 import androidx.core.graphics.ColorUtils; 36 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.internal.colorextraction.ColorExtractor; 40 41 import java.util.concurrent.Executor; 42 43 44 /** 45 * A view which can draw a scrim. This view maybe be used in multiple windows running on different 46 * threads, but is controlled by {@link com.android.systemui.statusbar.phone.ScrimController} so we 47 * need to be careful to synchronize when necessary. 48 */ 49 public class ScrimView extends View { 50 51 private final Object mColorLock = new Object(); 52 53 @GuardedBy("mColorLock") 54 private final ColorExtractor.GradientColors mColors; 55 // Used only for returning the colors 56 private final ColorExtractor.GradientColors mTmpColors = new ColorExtractor.GradientColors(); 57 private float mViewAlpha = 1.0f; 58 private Drawable mDrawable; 59 private PorterDuffColorFilter mColorFilter; 60 private int mTintColor; 61 private boolean mBlendWithMainColor = true; 62 private Runnable mChangeRunnable; 63 private Executor mChangeRunnableExecutor; 64 private Executor mExecutor; 65 private Looper mExecutorLooper; 66 @Nullable 67 private Rect mDrawableBounds; 68 ScrimView(Context context)69 public ScrimView(Context context) { 70 this(context, null); 71 } 72 ScrimView(Context context, AttributeSet attrs)73 public ScrimView(Context context, AttributeSet attrs) { 74 this(context, attrs, 0); 75 } 76 ScrimView(Context context, AttributeSet attrs, int defStyleAttr)77 public ScrimView(Context context, AttributeSet attrs, int defStyleAttr) { 78 this(context, attrs, defStyleAttr, 0); 79 } 80 ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)81 public ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 82 super(context, attrs, defStyleAttr, defStyleRes); 83 84 mDrawable = new ScrimDrawable(); 85 mDrawable.setCallback(this); 86 mColors = new ColorExtractor.GradientColors(); 87 mExecutorLooper = Looper.myLooper(); 88 mExecutor = Runnable::run; 89 executeOnExecutor(() -> { 90 updateColorWithTint(false); 91 }); 92 } 93 94 /** 95 * Needed for WM Shell, which has its own thread structure. 96 */ setExecutor(Executor executor, Looper looper)97 public void setExecutor(Executor executor, Looper looper) { 98 mExecutor = executor; 99 mExecutorLooper = looper; 100 } 101 102 @Override onDraw(Canvas canvas)103 protected void onDraw(Canvas canvas) { 104 if (mDrawable.getAlpha() > 0) { 105 mDrawable.draw(canvas); 106 } 107 } 108 109 @VisibleForTesting setDrawable(Drawable drawable)110 void setDrawable(Drawable drawable) { 111 executeOnExecutor(() -> { 112 mDrawable = drawable; 113 mDrawable.setCallback(this); 114 mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom()); 115 mDrawable.setAlpha((int) (255 * mViewAlpha)); 116 invalidate(); 117 }); 118 } 119 120 @Override invalidateDrawable(@onNull Drawable drawable)121 public void invalidateDrawable(@NonNull Drawable drawable) { 122 super.invalidateDrawable(drawable); 123 if (drawable == mDrawable) { 124 invalidate(); 125 } 126 } 127 128 @Override onLayout(boolean changed, int left, int top, int right, int bottom)129 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 130 super.onLayout(changed, left, top, right, bottom); 131 if (mDrawableBounds != null) { 132 mDrawable.setBounds(mDrawableBounds); 133 } else if (changed) { 134 mDrawable.setBounds(left, top, right, bottom); 135 invalidate(); 136 } 137 } 138 139 @Override setClickable(boolean clickable)140 public void setClickable(boolean clickable) { 141 executeOnExecutor(() -> { 142 super.setClickable(clickable); 143 }); 144 } 145 146 /** 147 * Sets the color of the scrim, without animating them. 148 */ setColors(@onNull ColorExtractor.GradientColors colors)149 public void setColors(@NonNull ColorExtractor.GradientColors colors) { 150 setColors(colors, false); 151 } 152 153 /** 154 * Sets the scrim colors, optionally animating them. 155 * @param colors The colors. 156 * @param animated If we should animate the transition. 157 */ setColors(@onNull ColorExtractor.GradientColors colors, boolean animated)158 public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) { 159 if (colors == null) { 160 throw new IllegalArgumentException("Colors cannot be null"); 161 } 162 executeOnExecutor(() -> { 163 synchronized (mColorLock) { 164 if (mColors.equals(colors)) { 165 return; 166 } 167 mColors.set(colors); 168 } 169 updateColorWithTint(animated); 170 }); 171 } 172 173 @VisibleForTesting getDrawable()174 Drawable getDrawable() { 175 return mDrawable; 176 } 177 178 /** 179 * Returns current scrim colors. 180 */ getColors()181 public ColorExtractor.GradientColors getColors() { 182 synchronized (mColorLock) { 183 mTmpColors.set(mColors); 184 } 185 return mTmpColors; 186 } 187 188 /** 189 * Applies tint to this view, without animations. 190 */ setTint(int color)191 public void setTint(int color) { 192 setTint(color, false); 193 } 194 195 /** 196 * The call to {@link #setTint} will blend with the main color, with the amount 197 * determined by the alpha of the tint. Set to false to avoid this blend. 198 */ setBlendWithMainColor(boolean blend)199 public void setBlendWithMainColor(boolean blend) { 200 mBlendWithMainColor = blend; 201 } 202 203 /** @return true if blending tint color with main color */ shouldBlendWithMainColor()204 public boolean shouldBlendWithMainColor() { 205 return mBlendWithMainColor; 206 } 207 208 /** 209 * Tints this view, optionally animating it. 210 * @param color The color. 211 * @param animated If we should animate. 212 */ setTint(int color, boolean animated)213 public void setTint(int color, boolean animated) { 214 executeOnExecutor(() -> { 215 if (mTintColor == color) { 216 return; 217 } 218 mTintColor = color; 219 updateColorWithTint(animated); 220 }); 221 } 222 updateColorWithTint(boolean animated)223 private void updateColorWithTint(boolean animated) { 224 if (mDrawable instanceof ScrimDrawable) { 225 // Optimization to blend colors and avoid a color filter 226 ScrimDrawable drawable = (ScrimDrawable) mDrawable; 227 float tintAmount = Color.alpha(mTintColor) / 255f; 228 229 int mainTinted = mTintColor; 230 if (mBlendWithMainColor) { 231 mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, tintAmount); 232 } 233 drawable.setColor(mainTinted, animated); 234 } else { 235 boolean hasAlpha = Color.alpha(mTintColor) != 0; 236 if (hasAlpha) { 237 PorterDuff.Mode targetMode = mColorFilter == null 238 ? Mode.SRC_OVER : mColorFilter.getMode(); 239 if (mColorFilter == null || mColorFilter.getColor() != mTintColor) { 240 mColorFilter = new PorterDuffColorFilter(mTintColor, targetMode); 241 } 242 } else { 243 mColorFilter = null; 244 } 245 246 mDrawable.setColorFilter(mColorFilter); 247 mDrawable.invalidateSelf(); 248 } 249 250 if (mChangeRunnable != null) { 251 mChangeRunnableExecutor.execute(mChangeRunnable); 252 } 253 } 254 getTint()255 public int getTint() { 256 return mTintColor; 257 } 258 259 @Override hasOverlappingRendering()260 public boolean hasOverlappingRendering() { 261 return false; 262 } 263 264 /** 265 * It might look counterintuitive to have another method to set the alpha instead of 266 * only using {@link #setAlpha(float)}. In this case we're in a hardware layer 267 * optimizing blend modes, so it makes sense. 268 * 269 * @param alpha Gradient alpha from 0 to 1. 270 */ setViewAlpha(float alpha)271 public void setViewAlpha(float alpha) { 272 if (isNaN(alpha)) { 273 throw new IllegalArgumentException("alpha cannot be NaN: " + alpha); 274 } 275 executeOnExecutor(() -> { 276 if (alpha != mViewAlpha) { 277 mViewAlpha = alpha; 278 279 mDrawable.setAlpha((int) (255 * alpha)); 280 if (mChangeRunnable != null) { 281 mChangeRunnableExecutor.execute(mChangeRunnable); 282 } 283 } 284 }); 285 } 286 getViewAlpha()287 public float getViewAlpha() { 288 return mViewAlpha; 289 } 290 291 /** 292 * Sets a callback that is invoked whenever the alpha, color, or tint change. 293 */ setChangeRunnable(Runnable changeRunnable, Executor changeRunnableExecutor)294 public void setChangeRunnable(Runnable changeRunnable, Executor changeRunnableExecutor) { 295 mChangeRunnable = changeRunnable; 296 mChangeRunnableExecutor = changeRunnableExecutor; 297 } 298 299 @Override canReceivePointerEvents()300 protected boolean canReceivePointerEvents() { 301 return false; 302 } 303 executeOnExecutor(Runnable r)304 private void executeOnExecutor(Runnable r) { 305 if (mExecutor == null || Looper.myLooper() == mExecutorLooper) { 306 r.run(); 307 } else { 308 mExecutor.execute(r); 309 } 310 } 311 312 /** 313 * Make bottom edge concave so overlap between layers is not visible for alphas between 0 and 1 314 */ enableBottomEdgeConcave(boolean clipScrim)315 public void enableBottomEdgeConcave(boolean clipScrim) { 316 if (mDrawable instanceof ScrimDrawable) { 317 ((ScrimDrawable) mDrawable).setBottomEdgeConcave(clipScrim); 318 } 319 } 320 321 /** 322 * The position of the bottom of the scrim, used for clipping. 323 * @see #enableBottomEdgeConcave(boolean) 324 */ setBottomEdgePosition(int y)325 public void setBottomEdgePosition(int y) { 326 if (mDrawable instanceof ScrimDrawable) { 327 ((ScrimDrawable) mDrawable).setBottomEdgePosition(y); 328 } 329 } 330 331 /** 332 * Enable view to have rounded corners. 333 */ enableRoundedCorners(boolean enabled)334 public void enableRoundedCorners(boolean enabled) { 335 if (mDrawable instanceof ScrimDrawable) { 336 ((ScrimDrawable) mDrawable).setRoundedCornersEnabled(enabled); 337 } 338 } 339 340 /** 341 * Set bounds for the view, all coordinates are absolute 342 */ setDrawableBounds(float left, float top, float right, float bottom)343 public void setDrawableBounds(float left, float top, float right, float bottom) { 344 if (mDrawableBounds == null) { 345 mDrawableBounds = new Rect(); 346 } 347 mDrawableBounds.set((int) left, (int) top, (int) right, (int) bottom); 348 mDrawable.setBounds(mDrawableBounds); 349 } 350 351 /** 352 * Corner radius of both concave or convex corners. 353 * @see #enableRoundedCorners(boolean) 354 * @see #enableBottomEdgeConcave(boolean) 355 */ setCornerRadius(int radius)356 public void setCornerRadius(int radius) { 357 if (mDrawable instanceof ScrimDrawable) { 358 ((ScrimDrawable) mDrawable).setRoundedCorners(radius); 359 } 360 } 361 } 362