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