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