1 /*
2  * Copyright (C) 2016 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.policy;
18 
19 import android.animation.ArgbEvaluator;
20 import android.annotation.ColorInt;
21 import android.annotation.DrawableRes;
22 import android.annotation.NonNull;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Bitmap;
26 import android.graphics.BlurMaskFilter;
27 import android.graphics.BlurMaskFilter.Blur;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.ColorFilter;
31 import android.graphics.Paint;
32 import android.graphics.PixelFormat;
33 import android.graphics.PorterDuff;
34 import android.graphics.PorterDuff.Mode;
35 import android.graphics.PorterDuffColorFilter;
36 import android.graphics.Rect;
37 import android.graphics.drawable.AnimatedVectorDrawable;
38 import android.graphics.drawable.Drawable;
39 import android.util.FloatProperty;
40 import android.view.ContextThemeWrapper;
41 import android.view.View;
42 
43 import com.android.settingslib.Utils;
44 import com.android.systemui.R;
45 
46 /**
47  * Drawable for {@link KeyButtonView}s that supports tinting between two colors, rotation and shows
48  * a shadow. AnimatedVectorDrawable will only support tinting from intensities but has no support
49  * for shadows nor rotations.
50  */
51 public class KeyButtonDrawable extends Drawable {
52 
53     public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_ROTATE =
54         new FloatProperty<KeyButtonDrawable>("KeyButtonRotation") {
55             @Override
56             public void setValue(KeyButtonDrawable drawable, float degree) {
57                 drawable.setRotation(degree);
58             }
59 
60             @Override
61             public Float get(KeyButtonDrawable drawable) {
62                 return drawable.getRotation();
63             }
64         };
65 
66     public static final FloatProperty<KeyButtonDrawable> KEY_DRAWABLE_TRANSLATE_Y =
67         new FloatProperty<KeyButtonDrawable>("KeyButtonTranslateY") {
68             @Override
69             public void setValue(KeyButtonDrawable drawable, float y) {
70                 drawable.setTranslationY(y);
71             }
72 
73             @Override
74             public Float get(KeyButtonDrawable drawable) {
75                 return drawable.getTranslationY();
76             }
77         };
78 
79     private final Paint mIconPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
80     private final Paint mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
81     private final ShadowDrawableState mState;
82     private AnimatedVectorDrawable mAnimatedDrawable;
83     private final Callback mAnimatedDrawableCallback = new Callback() {
84         @Override
85         public void invalidateDrawable(@NonNull Drawable who) {
86             invalidateSelf();
87         }
88 
89         @Override
90         public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
91             scheduleSelf(what, when);
92         }
93 
94         @Override
95         public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
96             unscheduleSelf(what);
97         }
98     };
99 
KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor, boolean horizontalFlip, Color ovalBackgroundColor)100     public KeyButtonDrawable(Drawable d, @ColorInt int lightColor, @ColorInt int darkColor,
101             boolean horizontalFlip, Color ovalBackgroundColor) {
102         this(d, new ShadowDrawableState(lightColor, darkColor,
103                 d instanceof AnimatedVectorDrawable, horizontalFlip, ovalBackgroundColor));
104     }
105 
KeyButtonDrawable(Drawable d, ShadowDrawableState state)106     private KeyButtonDrawable(Drawable d, ShadowDrawableState state) {
107         mState = state;
108         if (d != null) {
109             mState.mBaseHeight = d.getIntrinsicHeight();
110             mState.mBaseWidth = d.getIntrinsicWidth();
111             mState.mChangingConfigurations = d.getChangingConfigurations();
112             mState.mChildState = d.getConstantState();
113         }
114         if (canAnimate()) {
115             mAnimatedDrawable = (AnimatedVectorDrawable) mState.mChildState.newDrawable().mutate();
116             mAnimatedDrawable.setCallback(mAnimatedDrawableCallback);
117             setDrawableBounds(mAnimatedDrawable);
118         }
119     }
120 
setDarkIntensity(float intensity)121     public void setDarkIntensity(float intensity) {
122         mState.mDarkIntensity = intensity;
123         final int color = (int) ArgbEvaluator.getInstance()
124                 .evaluate(intensity, mState.mLightColor, mState.mDarkColor);
125         updateShadowAlpha();
126         setColorFilter(new PorterDuffColorFilter(color, Mode.SRC_ATOP));
127     }
128 
setRotation(float degrees)129     public void setRotation(float degrees) {
130         if (canAnimate()) {
131             // AnimatedVectorDrawables will not support rotation
132             return;
133         }
134         if (mState.mRotateDegrees != degrees) {
135             mState.mRotateDegrees = degrees;
136             invalidateSelf();
137         }
138     }
139 
setTranslationX(float x)140     public void setTranslationX(float x) {
141         setTranslation(x, mState.mTranslationY);
142     }
143 
setTranslationY(float y)144     public void setTranslationY(float y) {
145         setTranslation(mState.mTranslationX, y);
146     }
147 
setTranslation(float x, float y)148     public void setTranslation(float x, float y) {
149         if (mState.mTranslationX != x || mState.mTranslationY != y) {
150             mState.mTranslationX = x;
151             mState.mTranslationY = y;
152             invalidateSelf();
153         }
154     }
155 
setShadowProperties(int x, int y, int size, int color)156     public void setShadowProperties(int x, int y, int size, int color) {
157         if (canAnimate()) {
158             // AnimatedVectorDrawables will not support shadows
159             return;
160         }
161         if (mState.mShadowOffsetX != x || mState.mShadowOffsetY != y
162                 || mState.mShadowSize != size || mState.mShadowColor != color) {
163             mState.mShadowOffsetX = x;
164             mState.mShadowOffsetY = y;
165             mState.mShadowSize = size;
166             mState.mShadowColor = color;
167             mShadowPaint.setColorFilter(
168                     new PorterDuffColorFilter(mState.mShadowColor, Mode.SRC_ATOP));
169             updateShadowAlpha();
170             invalidateSelf();
171         }
172     }
173 
174     @Override
setAlpha(int alpha)175     public void setAlpha(int alpha) {
176         mState.mAlpha = alpha;
177         mIconPaint.setAlpha(alpha);
178         updateShadowAlpha();
179         invalidateSelf();
180     }
181 
182     @Override
setColorFilter(ColorFilter colorFilter)183     public void setColorFilter(ColorFilter colorFilter) {
184         mIconPaint.setColorFilter(colorFilter);
185         if (mAnimatedDrawable != null) {
186             if (hasOvalBg()) {
187                 mAnimatedDrawable.setColorFilter(
188                         new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN));
189             } else {
190                 mAnimatedDrawable.setColorFilter(colorFilter);
191             }
192         }
193         invalidateSelf();
194     }
195 
getDarkIntensity()196     public float getDarkIntensity() {
197         return mState.mDarkIntensity;
198     }
199 
getRotation()200     public float getRotation() {
201         return mState.mRotateDegrees;
202     }
203 
getTranslationX()204     public float getTranslationX() {
205         return mState.mTranslationX;
206     }
207 
getTranslationY()208     public float getTranslationY() {
209         return mState.mTranslationY;
210     }
211 
212     @Override
getConstantState()213     public ConstantState getConstantState() {
214         return mState;
215     }
216 
217     @Override
getOpacity()218     public int getOpacity() {
219         return PixelFormat.TRANSLUCENT;
220     }
221 
222     @Override
getIntrinsicHeight()223     public int getIntrinsicHeight() {
224         return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
225     }
226 
227     @Override
getIntrinsicWidth()228     public int getIntrinsicWidth() {
229         return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
230     }
231 
canAnimate()232     public boolean canAnimate() {
233         return mState.mSupportsAnimation;
234     }
235 
startAnimation()236     public void startAnimation() {
237         if (mAnimatedDrawable != null) {
238             mAnimatedDrawable.start();
239         }
240     }
241 
resetAnimation()242     public void resetAnimation() {
243         if (mAnimatedDrawable != null) {
244             mAnimatedDrawable.reset();
245         }
246     }
247 
clearAnimationCallbacks()248     public void clearAnimationCallbacks() {
249         if (mAnimatedDrawable != null) {
250             mAnimatedDrawable.clearAnimationCallbacks();
251         }
252     }
253 
254     @Override
draw(Canvas canvas)255     public void draw(Canvas canvas) {
256         Rect bounds = getBounds();
257         if (bounds.isEmpty()) {
258             return;
259         }
260 
261         if (mAnimatedDrawable != null) {
262             mAnimatedDrawable.draw(canvas);
263         } else {
264             // If no cache or previous cached bitmap is hardware/software acceleration does not
265             // match the current canvas on draw then regenerate
266             boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
267             if (hwBitmapChanged) {
268                 mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
269             }
270             if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
271                 regenerateBitmapIconCache();
272             }
273             canvas.save();
274             canvas.translate(mState.mTranslationX, mState.mTranslationY);
275             canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);
276 
277             if (mState.mShadowSize > 0) {
278                 if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
279                     regenerateBitmapShadowCache();
280                 }
281 
282                 // Translate (with rotation offset) before drawing the shadow
283                 final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
284                 final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
285                         + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
286                 final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
287                         - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
288                 canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
289                         mShadowPaint);
290             }
291             canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
292             canvas.restore();
293         }
294     }
295 
296     @Override
canApplyTheme()297     public boolean canApplyTheme() {
298         return mState.canApplyTheme();
299     }
300 
getDrawableBackgroundColor()301     @ColorInt int getDrawableBackgroundColor() {
302         return mState.mOvalBackgroundColor.toArgb();
303     }
304 
hasOvalBg()305     boolean hasOvalBg() {
306         return mState.mOvalBackgroundColor != null;
307     }
308 
regenerateBitmapIconCache()309     private void regenerateBitmapIconCache() {
310         final int width = getIntrinsicWidth();
311         final int height = getIntrinsicHeight();
312         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
313         final Canvas canvas = new Canvas(bitmap);
314 
315         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
316         final Drawable d = mState.mChildState.newDrawable().mutate();
317         setDrawableBounds(d);
318         canvas.save();
319         if (mState.mHorizontalFlip) {
320             canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
321         }
322         d.draw(canvas);
323         canvas.restore();
324 
325         if (mState.mIsHardwareBitmap) {
326             bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
327         }
328         mState.mLastDrawnIcon = bitmap;
329     }
330 
regenerateBitmapShadowCache()331     private void regenerateBitmapShadowCache() {
332         if (mState.mShadowSize == 0) {
333             // No shadow
334             mState.mLastDrawnIcon = null;
335             return;
336         }
337 
338         final int width = getIntrinsicWidth();
339         final int height = getIntrinsicHeight();
340         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
341         Canvas canvas = new Canvas(bitmap);
342 
343         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
344         final Drawable d = mState.mChildState.newDrawable().mutate();
345         setDrawableBounds(d);
346         canvas.save();
347         if (mState.mHorizontalFlip) {
348             canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
349         }
350         d.draw(canvas);
351         canvas.restore();
352 
353         // Draws the shadow from original drawable
354         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
355         paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
356         int[] offset = new int[2];
357         final Bitmap shadow = bitmap.extractAlpha(paint, offset);
358         paint.setMaskFilter(null);
359         bitmap.eraseColor(Color.TRANSPARENT);
360         canvas.drawBitmap(shadow, offset[0], offset[1], paint);
361 
362         if (mState.mIsHardwareBitmap) {
363             bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
364         }
365         mState.mLastDrawnShadow = bitmap;
366     }
367 
368     /**
369      * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
370      * dark color and shadow should not be visible at the same time.
371      */
updateShadowAlpha()372     private void updateShadowAlpha() {
373         // Update the color from the original color's alpha as the max
374         int alpha = Color.alpha(mState.mShadowColor);
375         mShadowPaint.setAlpha(
376                 Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
377     }
378 
379     /**
380      * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
381      * @param d the drawable to set the bounds
382      */
setDrawableBounds(Drawable d)383     private void setDrawableBounds(Drawable d) {
384         final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
385         final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
386         d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
387                 getIntrinsicHeight() - offsetY);
388     }
389 
390     private static class ShadowDrawableState extends ConstantState {
391         int mChangingConfigurations;
392         int mBaseWidth;
393         int mBaseHeight;
394         float mRotateDegrees;
395         float mTranslationX;
396         float mTranslationY;
397         int mShadowOffsetX;
398         int mShadowOffsetY;
399         int mShadowSize;
400         int mShadowColor;
401         float mDarkIntensity;
402         int mAlpha;
403         boolean mHorizontalFlip;
404 
405         boolean mIsHardwareBitmap;
406         Bitmap mLastDrawnIcon;
407         Bitmap mLastDrawnShadow;
408         ConstantState mChildState;
409 
410         final int mLightColor;
411         final int mDarkColor;
412         final boolean mSupportsAnimation;
413         final Color mOvalBackgroundColor;
414 
ShadowDrawableState(@olorInt int lightColor, @ColorInt int darkColor, boolean animated, boolean horizontalFlip, Color ovalBackgroundColor)415         public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
416                 boolean animated, boolean horizontalFlip, Color ovalBackgroundColor) {
417             mLightColor = lightColor;
418             mDarkColor = darkColor;
419             mSupportsAnimation = animated;
420             mAlpha = 255;
421             mHorizontalFlip = horizontalFlip;
422             mOvalBackgroundColor = ovalBackgroundColor;
423         }
424 
425         @Override
newDrawable()426         public Drawable newDrawable() {
427             return new KeyButtonDrawable(null, this);
428         }
429 
430         @Override
getChangingConfigurations()431         public int getChangingConfigurations() {
432             return mChangingConfigurations;
433         }
434 
435         @Override
canApplyTheme()436         public boolean canApplyTheme() {
437             return true;
438         }
439     }
440 
441     /**
442      * Creates a KeyButtonDrawable with a shadow given its icon. The tint applied to the drawable
443      * is determined by the dark and light theme given by the context.
444      * @param ctx Context to get the drawable and determine the dark and light theme
445      * @param icon the icon resource id
446      * @param hasShadow if a shadow will appear with the drawable
447      * @param ovalBackgroundColor the color of the oval bg that will be drawn
448      * @return KeyButtonDrawable
449      */
create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow, Color ovalBackgroundColor)450     public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
451             boolean hasShadow, Color ovalBackgroundColor) {
452         final int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
453         final int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
454         Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
455         Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
456         return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow,
457                 ovalBackgroundColor);
458     }
459 
460     /**
461      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
462      * {@link #create(Context, int, boolean, boolean)}.
463      */
create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow)464     public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
465             boolean hasShadow) {
466         return create(ctx, icon, hasShadow, null /* ovalBackgroundColor */);
467     }
468 
469     /**
470      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
471      * {@link #create(Context, int, boolean, boolean)}.
472      */
create(Context lightContext, Context darkContext, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)473     public static KeyButtonDrawable create(Context lightContext, Context darkContext,
474             @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor) {
475         return create(lightContext,
476             Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor),
477             Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor),
478             iconResId, hasShadow, ovalBackgroundColor);
479     }
480 
481     /**
482      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
483      * {@link #create(Context, int, boolean, boolean)}.
484      */
create(Context context, @ColorInt int lightColor, @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)485     public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
486             @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow,
487             Color ovalBackgroundColor) {
488         final Resources res = context.getResources();
489         boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
490         Drawable d = context.getDrawable(iconResId);
491         final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor,
492                 isRtl && d.isAutoMirrored(), ovalBackgroundColor);
493         if (hasShadow) {
494             int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
495             int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
496             int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius);
497             int color = context.getColor(R.color.nav_key_button_shadow_color);
498             drawable.setShadowProperties(offsetX, offsetY, radius, color);
499         }
500         return drawable;
501     }
502 }
503