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