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