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