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