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