• 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 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