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