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