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