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