• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
setVisible(boolean visible, boolean restart)175     public boolean setVisible(boolean visible, boolean restart) {
176         boolean changed = super.setVisible(visible, restart);
177         if (changed) {
178             // End any existing animations when the visibility changes
179             jumpToCurrentState();
180         }
181         return changed;
182     }
183 
184     @Override
jumpToCurrentState()185     public void jumpToCurrentState() {
186         super.jumpToCurrentState();
187         if (mAnimatedDrawable != null) {
188             mAnimatedDrawable.jumpToCurrentState();
189         }
190     }
191 
192     @Override
setAlpha(int alpha)193     public void setAlpha(int alpha) {
194         mState.mAlpha = alpha;
195         mIconPaint.setAlpha(alpha);
196         updateShadowAlpha();
197         invalidateSelf();
198     }
199 
200     @Override
setColorFilter(ColorFilter colorFilter)201     public void setColorFilter(ColorFilter colorFilter) {
202         mIconPaint.setColorFilter(colorFilter);
203         if (mAnimatedDrawable != null) {
204             if (hasOvalBg()) {
205                 mAnimatedDrawable.setColorFilter(
206                         new PorterDuffColorFilter(mState.mLightColor, PorterDuff.Mode.SRC_IN));
207             } else {
208                 mAnimatedDrawable.setColorFilter(colorFilter);
209             }
210         }
211         invalidateSelf();
212     }
213 
getDarkIntensity()214     public float getDarkIntensity() {
215         return mState.mDarkIntensity;
216     }
217 
getRotation()218     public float getRotation() {
219         return mState.mRotateDegrees;
220     }
221 
getTranslationX()222     public float getTranslationX() {
223         return mState.mTranslationX;
224     }
225 
getTranslationY()226     public float getTranslationY() {
227         return mState.mTranslationY;
228     }
229 
230     @Override
getConstantState()231     public ConstantState getConstantState() {
232         return mState;
233     }
234 
235     @Override
getOpacity()236     public int getOpacity() {
237         return PixelFormat.TRANSLUCENT;
238     }
239 
240     @Override
getIntrinsicHeight()241     public int getIntrinsicHeight() {
242         return mState.mBaseHeight + (mState.mShadowSize + Math.abs(mState.mShadowOffsetY)) * 2;
243     }
244 
245     @Override
getIntrinsicWidth()246     public int getIntrinsicWidth() {
247         return mState.mBaseWidth + (mState.mShadowSize + Math.abs(mState.mShadowOffsetX)) * 2;
248     }
249 
canAnimate()250     public boolean canAnimate() {
251         return mState.mSupportsAnimation;
252     }
253 
startAnimation()254     public void startAnimation() {
255         if (mAnimatedDrawable != null) {
256             mAnimatedDrawable.start();
257         }
258     }
259 
resetAnimation()260     public void resetAnimation() {
261         if (mAnimatedDrawable != null) {
262             mAnimatedDrawable.reset();
263         }
264     }
265 
clearAnimationCallbacks()266     public void clearAnimationCallbacks() {
267         if (mAnimatedDrawable != null) {
268             mAnimatedDrawable.clearAnimationCallbacks();
269         }
270     }
271 
272     @Override
draw(Canvas canvas)273     public void draw(Canvas canvas) {
274         Rect bounds = getBounds();
275         if (bounds.isEmpty()) {
276             return;
277         }
278 
279         if (mAnimatedDrawable != null) {
280             mAnimatedDrawable.draw(canvas);
281         } else {
282             // If no cache or previous cached bitmap is hardware/software acceleration does not
283             // match the current canvas on draw then regenerate
284             boolean hwBitmapChanged = mState.mIsHardwareBitmap != canvas.isHardwareAccelerated();
285             if (hwBitmapChanged) {
286                 mState.mIsHardwareBitmap = canvas.isHardwareAccelerated();
287             }
288             if (mState.mLastDrawnIcon == null || hwBitmapChanged) {
289                 regenerateBitmapIconCache();
290             }
291             canvas.save();
292             canvas.translate(mState.mTranslationX, mState.mTranslationY);
293             canvas.rotate(mState.mRotateDegrees, getIntrinsicWidth() / 2, getIntrinsicHeight() / 2);
294 
295             if (mState.mShadowSize > 0) {
296                 if (mState.mLastDrawnShadow == null || hwBitmapChanged) {
297                     regenerateBitmapShadowCache();
298                 }
299 
300                 // Translate (with rotation offset) before drawing the shadow
301                 final float radians = (float) (mState.mRotateDegrees * Math.PI / 180);
302                 final float shadowOffsetX = (float) (Math.sin(radians) * mState.mShadowOffsetY
303                         + Math.cos(radians) * mState.mShadowOffsetX) - mState.mTranslationX;
304                 final float shadowOffsetY = (float) (Math.cos(radians) * mState.mShadowOffsetY
305                         - Math.sin(radians) * mState.mShadowOffsetX) - mState.mTranslationY;
306                 canvas.drawBitmap(mState.mLastDrawnShadow, shadowOffsetX, shadowOffsetY,
307                         mShadowPaint);
308             }
309             canvas.drawBitmap(mState.mLastDrawnIcon, null, bounds, mIconPaint);
310             canvas.restore();
311         }
312     }
313 
314     @Override
canApplyTheme()315     public boolean canApplyTheme() {
316         return mState.canApplyTheme();
317     }
318 
getDrawableBackgroundColor()319     @ColorInt int getDrawableBackgroundColor() {
320         return mState.mOvalBackgroundColor.toArgb();
321     }
322 
hasOvalBg()323     boolean hasOvalBg() {
324         return mState.mOvalBackgroundColor != null;
325     }
326 
regenerateBitmapIconCache()327     private void regenerateBitmapIconCache() {
328         final int width = getIntrinsicWidth();
329         final int height = getIntrinsicHeight();
330         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
331         final Canvas canvas = new Canvas(bitmap);
332 
333         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
334         final Drawable d = mState.mChildState.newDrawable().mutate();
335         setDrawableBounds(d);
336         canvas.save();
337         if (mState.mHorizontalFlip) {
338             canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
339         }
340         d.draw(canvas);
341         canvas.restore();
342 
343         if (mState.mIsHardwareBitmap) {
344             bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
345         }
346         mState.mLastDrawnIcon = bitmap;
347     }
348 
regenerateBitmapShadowCache()349     private void regenerateBitmapShadowCache() {
350         if (mState.mShadowSize == 0) {
351             // No shadow
352             mState.mLastDrawnIcon = null;
353             return;
354         }
355 
356         final int width = getIntrinsicWidth();
357         final int height = getIntrinsicHeight();
358         Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
359         Canvas canvas = new Canvas(bitmap);
360 
361         // Call mutate, so that the pixel allocation by the underlying vector drawable is cleared.
362         final Drawable d = mState.mChildState.newDrawable().mutate();
363         setDrawableBounds(d);
364         canvas.save();
365         if (mState.mHorizontalFlip) {
366             canvas.scale(-1f, 1f, width * 0.5f, height * 0.5f);
367         }
368         d.draw(canvas);
369         canvas.restore();
370 
371         // Draws the shadow from original drawable
372         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
373         paint.setMaskFilter(new BlurMaskFilter(mState.mShadowSize, Blur.NORMAL));
374         int[] offset = new int[2];
375         final Bitmap shadow = bitmap.extractAlpha(paint, offset);
376         paint.setMaskFilter(null);
377         bitmap.eraseColor(Color.TRANSPARENT);
378         canvas.drawBitmap(shadow, offset[0], offset[1], paint);
379 
380         if (mState.mIsHardwareBitmap) {
381             bitmap = bitmap.copy(Bitmap.Config.HARDWARE, false);
382         }
383         mState.mLastDrawnShadow = bitmap;
384     }
385 
386     /**
387      * Set the alpha of the shadow. As dark intensity increases, drop the alpha of the shadow since
388      * dark color and shadow should not be visible at the same time.
389      */
updateShadowAlpha()390     private void updateShadowAlpha() {
391         // Update the color from the original color's alpha as the max
392         int alpha = Color.alpha(mState.mShadowColor);
393         mShadowPaint.setAlpha(
394                 Math.round(alpha * (mState.mAlpha / 255f) * (1 - mState.mDarkIntensity)));
395     }
396 
397     /**
398      * Prevent shadow clipping by offsetting the drawable bounds by the shadow and its offset
399      * @param d the drawable to set the bounds
400      */
setDrawableBounds(Drawable d)401     private void setDrawableBounds(Drawable d) {
402         final int offsetX = mState.mShadowSize + Math.abs(mState.mShadowOffsetX);
403         final int offsetY = mState.mShadowSize + Math.abs(mState.mShadowOffsetY);
404         d.setBounds(offsetX, offsetY, getIntrinsicWidth() - offsetX,
405                 getIntrinsicHeight() - offsetY);
406     }
407 
408     private static class ShadowDrawableState extends ConstantState {
409         int mChangingConfigurations;
410         int mBaseWidth;
411         int mBaseHeight;
412         float mRotateDegrees;
413         float mTranslationX;
414         float mTranslationY;
415         int mShadowOffsetX;
416         int mShadowOffsetY;
417         int mShadowSize;
418         int mShadowColor;
419         float mDarkIntensity;
420         int mAlpha;
421         boolean mHorizontalFlip;
422 
423         boolean mIsHardwareBitmap;
424         Bitmap mLastDrawnIcon;
425         Bitmap mLastDrawnShadow;
426         ConstantState mChildState;
427 
428         final int mLightColor;
429         final int mDarkColor;
430         final boolean mSupportsAnimation;
431         final Color mOvalBackgroundColor;
432 
ShadowDrawableState(@olorInt int lightColor, @ColorInt int darkColor, boolean animated, boolean horizontalFlip, Color ovalBackgroundColor)433         public ShadowDrawableState(@ColorInt int lightColor, @ColorInt int darkColor,
434                 boolean animated, boolean horizontalFlip, Color ovalBackgroundColor) {
435             mLightColor = lightColor;
436             mDarkColor = darkColor;
437             mSupportsAnimation = animated;
438             mAlpha = 255;
439             mHorizontalFlip = horizontalFlip;
440             mOvalBackgroundColor = ovalBackgroundColor;
441         }
442 
443         @Override
newDrawable()444         public Drawable newDrawable() {
445             return new KeyButtonDrawable(null, this);
446         }
447 
448         @Override
getChangingConfigurations()449         public int getChangingConfigurations() {
450             return mChangingConfigurations;
451         }
452 
453         @Override
canApplyTheme()454         public boolean canApplyTheme() {
455             return true;
456         }
457     }
458 
459     /**
460      * Creates a KeyButtonDrawable with a shadow given its icon. The tint applied to the drawable
461      * is determined by the dark and light theme given by the context.
462      * @param ctx Context to get the drawable and determine the dark and light theme
463      * @param icon the icon resource id
464      * @param hasShadow if a shadow will appear with the drawable
465      * @param ovalBackgroundColor the color of the oval bg that will be drawn
466      * @return KeyButtonDrawable
467      */
create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow, Color ovalBackgroundColor)468     public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
469             boolean hasShadow, Color ovalBackgroundColor) {
470         final int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
471         final int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
472         Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
473         Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
474         return KeyButtonDrawable.create(lightContext, darkContext, icon, hasShadow,
475                 ovalBackgroundColor);
476     }
477 
478     /**
479      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
480      * {@link #create(Context, int, boolean, boolean)}.
481      */
create(@onNull Context ctx, @DrawableRes int icon, boolean hasShadow)482     public static KeyButtonDrawable create(@NonNull Context ctx, @DrawableRes int icon,
483             boolean hasShadow) {
484         return create(ctx, icon, hasShadow, null /* ovalBackgroundColor */);
485     }
486 
487     /**
488      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
489      * {@link #create(Context, int, boolean, boolean)}.
490      */
create(Context lightContext, Context darkContext, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)491     public static KeyButtonDrawable create(Context lightContext, Context darkContext,
492             @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor) {
493         return create(lightContext,
494             Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor),
495             Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor),
496             iconResId, hasShadow, ovalBackgroundColor);
497     }
498 
499     /**
500      * Creates a KeyButtonDrawable with a shadow given its icon. For more information, see
501      * {@link #create(Context, int, boolean, boolean)}.
502      */
create(Context context, @ColorInt int lightColor, @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow, Color ovalBackgroundColor)503     public static KeyButtonDrawable create(Context context, @ColorInt int lightColor,
504             @ColorInt int darkColor, @DrawableRes int iconResId, boolean hasShadow,
505             Color ovalBackgroundColor) {
506         final Resources res = context.getResources();
507         boolean isRtl = res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
508         Drawable d = context.getDrawable(iconResId);
509         final KeyButtonDrawable drawable = new KeyButtonDrawable(d, lightColor, darkColor,
510                 isRtl && d.isAutoMirrored(), ovalBackgroundColor);
511         if (hasShadow) {
512             int offsetX = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_x);
513             int offsetY = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_offset_y);
514             int radius = res.getDimensionPixelSize(R.dimen.nav_key_button_shadow_radius);
515             int color = context.getColor(R.color.nav_key_button_shadow_color);
516             drawable.setShadowProperties(offsetX, offsetY, radius, color);
517         }
518         return drawable;
519     }
520 }
521