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