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