1 /*
2  * Copyright (C) 2014 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.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.annotation.NonNull;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.Point;
28 import android.graphics.PorterDuff;
29 import android.graphics.PorterDuffColorFilter;
30 import android.graphics.PorterDuffXfermode;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.support.v4.graphics.ColorUtils;
34 import android.util.AttributeSet;
35 import android.util.Log;
36 import android.view.Display;
37 import android.view.View;
38 import android.view.WindowManager;
39 import android.view.animation.Interpolator;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.colorextraction.ColorExtractor;
43 import com.android.internal.colorextraction.drawable.GradientDrawable;
44 import com.android.settingslib.Utils;
45 import com.android.systemui.Dependency;
46 import com.android.systemui.statusbar.policy.ConfigurationController;
47 
48 /**
49  * A view which can draw a scrim
50  */
51 public class ScrimView extends View implements ConfigurationController.ConfigurationListener {
52     private static final String TAG = "ScrimView";
53     private final ColorExtractor.GradientColors mColors;
54     private int mDensity;
55     private boolean mDrawAsSrc;
56     private float mViewAlpha = 1.0f;
57     private ValueAnimator mAlphaAnimator;
58     private Rect mExcludedRect = new Rect();
59     private boolean mHasExcludedArea;
60     private Drawable mDrawable;
61     private PorterDuffColorFilter mColorFilter;
62     private int mTintColor;
63     private ValueAnimator.AnimatorUpdateListener mAlphaUpdateListener = animation -> {
64         if (mDrawable == null) {
65             Log.w(TAG, "Trying to animate null drawable");
66             return;
67         }
68         mDrawable.setAlpha((int) (255 * (float) animation.getAnimatedValue()));
69     };
70     private AnimatorListenerAdapter mClearAnimatorListener = new AnimatorListenerAdapter() {
71         @Override
72         public void onAnimationEnd(Animator animation) {
73             mAlphaAnimator = null;
74         }
75     };
76     private Runnable mChangeRunnable;
77     private int mCornerRadius;
78 
ScrimView(Context context)79     public ScrimView(Context context) {
80         this(context, null);
81     }
82 
ScrimView(Context context, AttributeSet attrs)83     public ScrimView(Context context, AttributeSet attrs) {
84         this(context, attrs, 0);
85     }
86 
ScrimView(Context context, AttributeSet attrs, int defStyleAttr)87     public ScrimView(Context context, AttributeSet attrs, int defStyleAttr) {
88         this(context, attrs, defStyleAttr, 0);
89     }
90 
ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)91     public ScrimView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
92         super(context, attrs, defStyleAttr, defStyleRes);
93 
94         mDrawable = new GradientDrawable(context);
95         mDrawable.setCallback(this);
96         mColors = new ColorExtractor.GradientColors();
97         updateScreenSize();
98         updateColorWithTint(false);
99         initView();
100         final Configuration currentConfig = mContext.getResources().getConfiguration();
101         mDensity = currentConfig.densityDpi;
102     }
103 
initView()104     private void initView() {
105         mCornerRadius = getResources().getDimensionPixelSize(
106                 Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
107     }
108 
109     @Override
onConfigurationChanged(Configuration newConfig)110     protected void onConfigurationChanged(Configuration newConfig) {
111         super.onConfigurationChanged(newConfig);
112         int densityDpi = newConfig.densityDpi;
113         if (mDensity != densityDpi) {
114             mDensity = densityDpi;
115             initView();
116         }
117     }
118 
119     @Override
onAttachedToWindow()120     protected void onAttachedToWindow() {
121         super.onAttachedToWindow();
122 
123         // We need to know about configuration changes to update the gradient size
124         // since it's independent from view bounds.
125         ConfigurationController config = Dependency.get(ConfigurationController.class);
126         config.addCallback(this);
127     }
128 
129     @Override
onDetachedFromWindow()130     protected void onDetachedFromWindow() {
131         super.onDetachedFromWindow();
132 
133         ConfigurationController config = Dependency.get(ConfigurationController.class);
134         config.removeCallback(this);
135     }
136 
137     @Override
onDraw(Canvas canvas)138     protected void onDraw(Canvas canvas) {
139         if (mDrawAsSrc || mDrawable.getAlpha() > 0) {
140             if (!mHasExcludedArea) {
141                 mDrawable.draw(canvas);
142             } else {
143                 if (mExcludedRect.top > 0) {
144                     canvas.save();
145                     canvas.clipRect(0, 0, getWidth(), mExcludedRect.top);
146                     mDrawable.draw(canvas);
147                     canvas.restore();
148                 }
149                 if (mExcludedRect.left > 0) {
150                     canvas.save();
151                     canvas.clipRect(0, mExcludedRect.top, mExcludedRect.left,
152                             mExcludedRect.bottom);
153                     mDrawable.draw(canvas);
154                     canvas.restore();
155                 }
156                 if (mExcludedRect.right < getWidth()) {
157                     canvas.save();
158                     canvas.clipRect(mExcludedRect.right, mExcludedRect.top, getWidth(),
159                             mExcludedRect.bottom);
160                     mDrawable.draw(canvas);
161                     canvas.restore();
162                 }
163                 if (mExcludedRect.bottom < getHeight()) {
164                     canvas.save();
165                     canvas.clipRect(0, mExcludedRect.bottom, getWidth(), getHeight());
166                     mDrawable.draw(canvas);
167                     canvas.restore();
168                 }
169                 // We also need to draw the rounded corners of the background
170                 canvas.save();
171                 canvas.clipRect(mExcludedRect.left, mExcludedRect.top,
172                         mExcludedRect.left + mCornerRadius, mExcludedRect.top + mCornerRadius);
173                 mDrawable.draw(canvas);
174                 canvas.restore();
175                 canvas.save();
176                 canvas.clipRect(mExcludedRect.right - mCornerRadius, mExcludedRect.top,
177                         mExcludedRect.right, mExcludedRect.top + mCornerRadius);
178                 mDrawable.draw(canvas);
179                 canvas.restore();
180                 canvas.save();
181                 canvas.clipRect(mExcludedRect.left, mExcludedRect.bottom - mCornerRadius,
182                         mExcludedRect.left + mCornerRadius, mExcludedRect.bottom);
183                 mDrawable.draw(canvas);
184                 canvas.restore();
185                 canvas.save();
186                 canvas.clipRect(mExcludedRect.right - mCornerRadius,
187                         mExcludedRect.bottom - mCornerRadius,
188                         mExcludedRect.right, mExcludedRect.bottom);
189                 mDrawable.draw(canvas);
190                 canvas.restore();
191             }
192         }
193     }
194 
setDrawable(Drawable drawable)195     public void setDrawable(Drawable drawable) {
196         mDrawable = drawable;
197         mDrawable.setCallback(this);
198         mDrawable.setBounds(getLeft(), getTop(), getRight(), getBottom());
199         mDrawable.setAlpha((int) (255 * mViewAlpha));
200         setDrawAsSrc(mDrawAsSrc);
201         updateScreenSize();
202         invalidate();
203     }
204 
205     @Override
invalidateDrawable(@onNull Drawable drawable)206     public void invalidateDrawable(@NonNull Drawable drawable) {
207         super.invalidateDrawable(drawable);
208         if (drawable == mDrawable) {
209             invalidate();
210         }
211     }
212 
setDrawAsSrc(boolean asSrc)213     public void setDrawAsSrc(boolean asSrc) {
214         mDrawAsSrc = asSrc;
215         PorterDuff.Mode mode = asSrc ? PorterDuff.Mode.SRC : PorterDuff.Mode.SRC_OVER;
216         mDrawable.setXfermode(new PorterDuffXfermode(mode));
217     }
218 
219     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)220     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
221         super.onLayout(changed, left, top, right, bottom);
222         if (changed) {
223             mDrawable.setBounds(left, top, right, bottom);
224             invalidate();
225         }
226     }
227 
setColors(@onNull ColorExtractor.GradientColors colors)228     public void setColors(@NonNull ColorExtractor.GradientColors colors) {
229         setColors(colors, false);
230     }
231 
setColors(@onNull ColorExtractor.GradientColors colors, boolean animated)232     public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
233         if (colors == null) {
234             throw new IllegalArgumentException("Colors cannot be null");
235         }
236         if (mColors.equals(colors)) {
237             return;
238         }
239         mColors.set(colors);
240         updateColorWithTint(animated);
241     }
242 
243     @VisibleForTesting
getDrawable()244     Drawable getDrawable() {
245         return mDrawable;
246     }
247 
getColors()248     public ColorExtractor.GradientColors getColors() {
249         return mColors;
250     }
251 
setTint(int color)252     public void setTint(int color) {
253         setTint(color, false);
254     }
255 
setTint(int color, boolean animated)256     public void setTint(int color, boolean animated) {
257         if (mTintColor == color) {
258             return;
259         }
260         mTintColor = color;
261         updateColorWithTint(animated);
262     }
263 
updateColorWithTint(boolean animated)264     private void updateColorWithTint(boolean animated) {
265         if (mDrawable instanceof GradientDrawable) {
266             // Optimization to blend colors and avoid a color filter
267             GradientDrawable drawable = (GradientDrawable) mDrawable;
268             float tintAmount = Color.alpha(mTintColor) / 255f;
269             int mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor,
270                     tintAmount);
271             int secondaryTinted = ColorUtils.blendARGB(mColors.getSecondaryColor(), mTintColor,
272                     tintAmount);
273             drawable.setColors(mainTinted, secondaryTinted, animated);
274         } else {
275             if (mColorFilter == null) {
276                 mColorFilter = new PorterDuffColorFilter(mTintColor, PorterDuff.Mode.SRC_OVER);
277             } else {
278                 mColorFilter.setColor(mTintColor);
279             }
280             mDrawable.setColorFilter(Color.alpha(mTintColor) == 0 ? null : mColorFilter);
281             mDrawable.invalidateSelf();
282         }
283 
284         if (mChangeRunnable != null) {
285             mChangeRunnable.run();
286         }
287     }
288 
getTint()289     public int getTint() {
290         return mTintColor;
291     }
292 
293     @Override
hasOverlappingRendering()294     public boolean hasOverlappingRendering() {
295         return false;
296     }
297 
298     /**
299      * It might look counterintuitive to have another method to set the alpha instead of
300      * only using {@link #setAlpha(float)}. In this case we're in a hardware layer
301      * optimizing blend modes, so it makes sense.
302      *
303      * @param alpha Gradient alpha from 0 to 1.
304      */
setViewAlpha(float alpha)305     public void setViewAlpha(float alpha) {
306         if (alpha != mViewAlpha) {
307             mViewAlpha = alpha;
308 
309             if (mAlphaAnimator != null) {
310                 mAlphaAnimator.cancel();
311             }
312 
313             mDrawable.setAlpha((int) (255 * alpha));
314             if (mChangeRunnable != null) {
315                 mChangeRunnable.run();
316             }
317         }
318     }
319 
getViewAlpha()320     public float getViewAlpha() {
321         return mViewAlpha;
322     }
323 
setExcludedArea(Rect area)324     public void setExcludedArea(Rect area) {
325         if (area == null) {
326             mHasExcludedArea = false;
327             invalidate();
328             return;
329         }
330 
331         int left = Math.max(area.left, 0);
332         int top = Math.max(area.top, 0);
333         int right = Math.min(area.right, getWidth());
334         int bottom = Math.min(area.bottom, getHeight());
335         mExcludedRect.set(left, top, right, bottom);
336         mHasExcludedArea = left < right && top < bottom;
337         invalidate();
338     }
339 
340     public void setChangeRunnable(Runnable changeRunnable) {
341         mChangeRunnable = changeRunnable;
342     }
343 
344     @Override
345     public void onConfigChanged(Configuration newConfig) {
346         updateScreenSize();
347     }
348 
349     private void updateScreenSize() {
350         if (mDrawable instanceof GradientDrawable) {
351             WindowManager wm = mContext.getSystemService(WindowManager.class);
352             if (wm == null) {
353                 Log.w(TAG, "Can't resize gradient drawable to fit the screen");
354                 return;
355             }
356             Display display = wm.getDefaultDisplay();
357             if (display != null) {
358                 Point size = new Point();
359                 display.getRealSize(size);
360                 ((GradientDrawable) mDrawable).setScreenSize(size.x, size.y);
361             }
362         }
363     }
364 }
365