• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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